592 lines
14 KiB
Vue
592 lines
14 KiB
Vue
<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>
|