Merge remote-tracking branch 'origin/master'

This commit is contained in:
2026-02-13 20:58:38 +03:00
parent 572bab4134
commit 7edcd446d0
6 changed files with 1245 additions and 0 deletions

View File

@@ -0,0 +1,591 @@
<template>
<q-page v-if="canUpdateUser" class="rdp-list-page">
<div class="rdp-filter-bar">
<div class="rdp-filter-row">
<q-input
v-model="store.filters.search"
class="rdp-filter-input rdp-search"
dense
filled
clearable
debounce="300"
label="Arama (Rol / Departman)"
>
<template #append>
<q-icon name="search" />
</template>
</q-input>
<div class="rdp-filter-actions">
<q-btn
label="Temizle"
icon="clear"
color="grey-7"
flat
:disable="store.loading"
@click="clearFilters"
/>
<q-btn
label="Yenile"
icon="refresh"
color="primary"
:loading="store.loading"
@click="store.fetchRows"
/>
</div>
<div class="rdp-config-menus">
<q-btn-dropdown
color="secondary"
outline
icon="view_module"
label="Modüller"
:auto-close="false"
>
<q-list dense class="rdp-menu-list">
<q-item clickable @click="selectAllModules">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-separator />
<q-item
v-for="m in store.modules"
:key="m.value"
clickable
@click="onModuleRowClick(m.value)"
>
<q-item-section avatar>
<q-checkbox
:model-value="isModuleSelected(m.value)"
dense
@update:model-value="(v) => toggleModule(m.value, v)"
@click.stop
/>
</q-item-section>
<q-item-section>{{ m.label }}</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<q-btn-dropdown
color="secondary"
outline
icon="tune"
:label="`Aksiyonlar (${activeModuleLabel})`"
:disable="!activeModuleCode"
:auto-close="false"
>
<q-list dense class="rdp-menu-list">
<q-item clickable @click="selectAllActionsForActive">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-separator />
<q-item
v-for="a in actionsForActiveModule"
:key="`${activeModuleCode}:${a}`"
clickable
>
<q-item-section avatar>
<q-checkbox
:model-value="isActionSelected(activeModuleCode, a)"
dense
@update:model-value="(v) => toggleAction(activeModuleCode, a, v)"
@click.stop
/>
</q-item-section>
<q-item-section>{{ actionLabel(a) }}</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
<div class="rdp-summary">
<span>Toplam Kayıt: <strong>{{ store.totalCount }}</strong></span>
</div>
</div>
</div>
<q-table
title="Rol + Departman Yetki Setleri"
class="rdp-table"
flat
bordered
dense
row-key="row_key"
:rows="tableRows"
:columns="columns"
:loading="store.loading"
no-data-label="Kayıt bulunamadı"
:rows-per-page-options="[0]"
hide-bottom
>
<template #body-cell="props">
<q-td :props="props" :class="props.col.classes">
<template v-if="props.col.name === 'open'">
<div class="text-center">
<q-btn
icon="open_in_new"
color="primary"
flat
round
dense
@click="openEditor(props.row)"
>
<q-tooltip>Yetki setini aç</q-tooltip>
</q-btn>
</div>
</template>
<template v-else-if="isPermissionColumn(props.col.name)">
<div class="text-center">
<q-checkbox
:model-value="Boolean(props.value)"
disable
dense
/>
</div>
</template>
<template v-else>
{{ props.value }}
</template>
</q-td>
</template>
</q-table>
<q-banner v-if="store.error" class="bg-red text-white q-mt-sm">
Hata: {{ store.error }}
</q-banner>
</q-page>
<q-page v-else class="q-pa-md flex flex-center">
<div class="text-negative text-subtitle1">
Bu modüle erişim yetkiniz yok.
</div>
</q-page>
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useQuasar } from 'quasar'
import { usePermission } from 'src/composables/usePermission'
import { useRoleDeptPermissionListStore } from 'src/stores/RoleDeptPermissionListStore'
const router = useRouter()
const $q = useQuasar()
const store = useRoleDeptPermissionListStore()
const { canUpdate } = usePermission()
const canUpdateUser = canUpdate('user')
const selectedModules = ref([])
const selectedActionsByModule = ref({})
const activeModuleCode = ref('')
const actionLabelMap = {
update: 'Güncelleme',
view: 'Görüntüleme',
insert: 'Ekleme',
export: ıktı',
write: 'Yazma',
read: 'Okuma',
delete: 'Silme',
login: 'Giriş',
refresh: 'Yenileme',
'user.update': 'Kullanıcı Güncelle'
}
const fixedColumns = [
{
name: 'open',
label: '',
field: 'open',
align: 'center',
sortable: false,
classes: 'freeze-col freeze-1',
headerClasses: 'freeze-col freeze-1',
style: 'width:56px; min-width:56px; max-width:56px',
headerStyle: 'width:56px; min-width:56px; max-width:56px'
},
{
name: 'role_title',
label: 'Rol',
field: 'role_title',
align: 'left',
sortable: true,
classes: 'freeze-col freeze-2',
headerClasses: 'freeze-col freeze-2',
style: 'width:220px; min-width:220px; max-width:220px',
headerStyle: 'width:220px; min-width:220px; max-width:220px'
},
{
name: 'department_title',
label: 'Departman',
field: 'department_title',
align: 'left',
sortable: true,
classes: 'freeze-col freeze-3',
headerClasses: 'freeze-col freeze-3',
style: 'width:220px; min-width:220px; max-width:220px',
headerStyle: 'width:220px; min-width:220px; max-width:220px'
},
{
name: 'department_code',
label: 'Departman Kodu',
field: 'department_code',
align: 'left',
sortable: true,
style: 'width:140px; min-width:140px; max-width:140px',
headerStyle: 'width:140px; min-width:140px; max-width:140px'
}
]
const moduleLabelMap = computed(() => {
const map = {}
;(store.modules || []).forEach((m) => {
map[m.value] = m.label || m.value
})
return map
})
const actionsByModule = computed(() => {
const map = {}
;(store.moduleActions || []).forEach((x) => {
if (!map[x.module_code]) map[x.module_code] = []
if (!map[x.module_code].includes(x.action)) {
map[x.module_code].push(x.action)
}
})
Object.keys(map).forEach((k) => map[k].sort())
return map
})
const activeModuleLabel = computed(() => {
if (!activeModuleCode.value) return 'Seçim'
return moduleLabelMap.value[activeModuleCode.value] || activeModuleCode.value
})
const actionsForActiveModule = computed(() => {
if (!activeModuleCode.value) return []
return actionsByModule.value[activeModuleCode.value] || []
})
function actionLabel (action) {
const key = String(action || '').toLowerCase().trim()
return actionLabelMap[key] || key
}
function syncSelections () {
const availableModules = (store.modules || []).map((m) => m.value)
if (!availableModules.length) {
selectedModules.value = []
selectedActionsByModule.value = {}
activeModuleCode.value = ''
return
}
const selected = selectedModules.value.filter((m) => availableModules.includes(m))
selectedModules.value = selected.length ? selected : [...availableModules]
if (!selectedModules.value.includes(activeModuleCode.value)) {
activeModuleCode.value = selectedModules.value[0]
}
const next = {}
selectedModules.value.forEach((m) => {
const allActions = actionsByModule.value[m] || []
const prev = selectedActionsByModule.value[m] || []
const filtered = prev.filter((a) => allActions.includes(a))
next[m] = filtered.length ? filtered : [...allActions]
})
selectedActionsByModule.value = next
}
watch(
() => [store.modules, store.moduleActions],
() => {
syncSelections()
},
{ deep: true }
)
function isModuleSelected (moduleCode) {
return selectedModules.value.includes(moduleCode)
}
function toggleModule (moduleCode, checked) {
const set = new Set(selectedModules.value)
if (checked) {
set.add(moduleCode)
} else {
set.delete(moduleCode)
}
selectedModules.value = [...set]
if (!selectedModules.value.length) {
selectedModules.value = [moduleCode]
}
if (!selectedModules.value.includes(activeModuleCode.value)) {
activeModuleCode.value = selectedModules.value[0]
}
syncSelections()
}
function onModuleRowClick (moduleCode) {
activeModuleCode.value = moduleCode
}
function selectAllModules () {
selectedModules.value = (store.modules || []).map((m) => m.value)
syncSelections()
}
function isActionSelected (moduleCode, action) {
return (selectedActionsByModule.value[moduleCode] || []).includes(action)
}
function toggleAction (moduleCode, action, checked) {
const current = new Set(selectedActionsByModule.value[moduleCode] || [])
if (checked) {
current.add(action)
} else {
current.delete(action)
}
if (current.size === 0) {
current.add(action)
}
selectedActionsByModule.value = {
...selectedActionsByModule.value,
[moduleCode]: [...current]
}
}
function selectAllActionsForActive () {
if (!activeModuleCode.value) return
selectedActionsByModule.value = {
...selectedActionsByModule.value,
[activeModuleCode.value]: [...actionsForActiveModule.value]
}
}
const permissionColumns = computed(() => {
const cols = []
selectedModules.value.forEach((m) => {
const actions = selectedActionsByModule.value[m] || []
actions.forEach((a) => {
const key = `${m}|${a}`
cols.push({
name: `perm_${key}`,
label: `${moduleLabelMap.value[m] || m}\n${actionLabel(a)}`,
field: (row) => Boolean(row.module_flags?.[key]),
align: 'center',
sortable: true,
style: 'width:150px; min-width:150px; max-width:150px',
headerStyle: 'width:150px; min-width:150px; max-width:150px; white-space:pre-line; line-height:1.15'
})
})
})
return cols
})
const columns = computed(() => [
...fixedColumns,
...permissionColumns.value
])
const tableRows = computed(() =>
(store.rows || []).map((r) => ({
...r,
row_key: `${r.role_id}:${r.department_code}`
}))
)
function isPermissionColumn (name) {
return String(name || '').startsWith('perm_')
}
let searchTimer = null
watch(
() => store.filters.search,
() => {
clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
if (canUpdateUser.value) {
store.fetchRows()
}
}, 350)
}
)
function openEditor (row) {
if (!row?.role_id || !row?.department_code) {
$q.notify({ type: 'warning', message: 'Kayıt bilgisi eksik' })
return
}
router.push({
name: 'role-dept-permissions-editor',
query: {
mode: 'edit',
roleId: String(row.role_id),
deptCode: String(row.department_code)
}
})
}
function clearFilters () {
store.filters.search = ''
store.fetchRows()
}
onMounted(async () => {
if (canUpdateUser.value) {
await store.fetchRows()
syncSelections()
}
})
</script>
<style scoped>
.rdp-list-page {
--rdp-header-h: 56px;
--rdp-filter-h: 96px;
height: calc(100vh - var(--rdp-header-h));
overflow: auto;
background: #fff;
display: flex;
flex-direction: column;
padding: 10px;
}
.rdp-filter-bar {
position: sticky;
top: 0;
z-index: 600;
background: #fff;
border-bottom: 1px solid #ddd;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
min-height: var(--rdp-filter-h);
display: flex;
align-items: center;
padding: 10px 12px;
margin-bottom: 8px;
}
.rdp-filter-row {
display: flex;
align-items: flex-end;
gap: 12px;
flex-wrap: nowrap;
}
.rdp-filter-input {
min-width: 180px;
max-width: 300px;
}
.rdp-search {
min-width: 300px;
max-width: 520px;
flex: 1 1 360px;
}
.rdp-filter-actions {
display: flex;
gap: 8px;
align-items: center;
white-space: nowrap;
flex: 0 0 auto;
}
.rdp-config-menus {
display: flex;
gap: 8px;
align-items: center;
white-space: nowrap;
flex: 0 0 auto;
}
.rdp-menu-list {
min-width: 260px;
max-height: 420px;
}
.rdp-summary {
margin-left: auto;
background: #f9fafb;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 8px 12px;
white-space: nowrap;
font-size: 12px;
color: #4b5563;
align-self: center;
}
.rdp-table :deep(.q-table__middle) {
overflow: visible !important;
max-height: none !important;
}
.rdp-table :deep(.q-table thead th) {
position: sticky;
top: var(--rdp-filter-h);
z-index: 500;
background: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
font-size: 11px;
white-space: nowrap;
}
.rdp-table :deep(.q-table tbody td) {
font-size: 11px;
padding: 3px 6px;
}
.rdp-table :deep(.q-checkbox__inner) {
pointer-events: none;
}
.rdp-table :deep(.freeze-col) {
position: sticky;
background: #fff;
z-index: 510;
}
.rdp-table :deep(thead .freeze-col) {
z-index: 520;
background: #fff;
}
.rdp-table :deep(.freeze-1) {
left: 0;
}
.rdp-table :deep(.freeze-2) {
left: 56px;
}
.rdp-table :deep(.freeze-3) {
left: 276px;
}
@media (max-width: 1400px) {
.rdp-filter-row {
flex-wrap: wrap;
align-items: flex-start;
}
.rdp-filter-actions {
flex-wrap: wrap;
}
.rdp-config-menus {
flex-wrap: wrap;
}
.rdp-summary {
min-width: 100%;
margin-left: 0;
margin-top: 6px;
}
}
</style>