Files
bssapp/ui/src/pages/RoleDepartmentPermissionList.vue
2026-02-19 12:28:13 +03:00

622 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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-item clickable @click="clearAllModules">
<q-item-section>Tümünü Temizle</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-item clickable @click="clearAllActionsForActive">
<q-item-section>Tümünü Temizle</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 allowEmptySelection = ref(false)
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))
if (selected.length) {
selectedModules.value = selected
} else {
selectedModules.value = allowEmptySelection.value ? [] : [...availableModules]
}
if (!selectedModules.value.length) {
activeModuleCode.value = ''
} else 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 : (allowEmptySelection.value ? [] : [...allActions])
})
selectedActionsByModule.value = next
}
watch(
() => [store.modules, store.moduleActions],
() => {
syncSelections()
},
{ deep: true }
)
function isModuleSelected (moduleCode) {
return selectedModules.value.includes(moduleCode)
}
function toggleModule (moduleCode, checked) {
allowEmptySelection.value = false
const set = new Set(selectedModules.value)
if (checked) {
set.add(moduleCode)
} else {
set.delete(moduleCode)
}
selectedModules.value = [...set]
if (!selectedModules.value.length) {
activeModuleCode.value = ''
} else if (!selectedModules.value.includes(activeModuleCode.value)) {
activeModuleCode.value = selectedModules.value[0]
}
syncSelections()
}
function onModuleRowClick (moduleCode) {
activeModuleCode.value = moduleCode
}
function selectAllModules () {
allowEmptySelection.value = false
selectedModules.value = (store.modules || []).map((m) => m.value)
syncSelections()
}
function clearAllModules () {
allowEmptySelection.value = true
selectedModules.value = []
selectedActionsByModule.value = {}
activeModuleCode.value = ''
syncSelections()
}
function isActionSelected (moduleCode, action) {
return (selectedActionsByModule.value[moduleCode] || []).includes(action)
}
function toggleAction (moduleCode, action, checked) {
allowEmptySelection.value = false
const current = new Set(selectedActionsByModule.value[moduleCode] || [])
if (checked) {
current.add(action)
} else {
current.delete(action)
}
selectedActionsByModule.value = {
...selectedActionsByModule.value,
[moduleCode]: [...current]
}
}
function selectAllActionsForActive () {
allowEmptySelection.value = false
if (!activeModuleCode.value) return
selectedActionsByModule.value = {
...selectedActionsByModule.value,
[activeModuleCode.value]: [...actionsForActiveModule.value]
}
}
function clearAllActionsForActive () {
allowEmptySelection.value = true
if (!activeModuleCode.value) return
selectedActionsByModule.value = {
...selectedActionsByModule.value,
[activeModuleCode.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>