Files
bssapp/ui/src/pages/RoleDepartmentPermissionList.vue

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