475 lines
8.6 KiB
Vue
475 lines
8.6 KiB
Vue
<template>
|
||
<div v-if="canUpdateUser && !lookupsLoaded" class="q-pa-xl flex flex-center">
|
||
|
||
<q-spinner
|
||
color="primary"
|
||
size="48px"
|
||
/>
|
||
|
||
</div>
|
||
|
||
<q-page v-if="canUpdateUser" class="permissions-page">
|
||
|
||
<!-- ================= STICKY STACK ================= -->
|
||
<div class="sticky-stack">
|
||
|
||
<!-- FILTER BAR -->
|
||
<div
|
||
v-if="lookupsLoaded"
|
||
class="filter-bar row q-col-gutter-md"
|
||
>
|
||
|
||
|
||
<div class="col-4">
|
||
<q-select
|
||
v-model="roleId"
|
||
:options="roles"
|
||
option-value="id"
|
||
option-label="title"
|
||
emit-value
|
||
map-options
|
||
label="Rol"
|
||
dense
|
||
outlined
|
||
@update:model-value="loadMatrix"
|
||
/>
|
||
|
||
|
||
|
||
|
||
|
||
</div>
|
||
|
||
<div class="col-4">
|
||
<q-select
|
||
v-model="deptCode"
|
||
:options="departments"
|
||
option-value="id"
|
||
option-label="title"
|
||
emit-value
|
||
map-options
|
||
label="Departman"
|
||
dense
|
||
outlined
|
||
@update:model-value="loadMatrix"
|
||
/>
|
||
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- SAVE TOOLBAR -->
|
||
<div class="save-toolbar">
|
||
|
||
<div class="label">
|
||
Rol + Departman Yetkilendirme
|
||
</div>
|
||
|
||
<q-btn
|
||
flat
|
||
icon="list"
|
||
label="Liste"
|
||
@click="goList"
|
||
/>
|
||
|
||
<q-btn
|
||
v-if="canUpdateUser"
|
||
color="primary"
|
||
icon="save"
|
||
label="Kaydet"
|
||
:disable="!dirty"
|
||
@click="save"
|
||
/>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
<!-- ================= TABLE SCROLL ================= -->
|
||
|
||
<div
|
||
v-if="lookupsLoaded"
|
||
class="permissions-table-scroll"
|
||
>
|
||
|
||
|
||
<q-table
|
||
class="permissions-table"
|
||
:rows="rows"
|
||
:columns="columns"
|
||
row-key="module"
|
||
flat
|
||
bordered
|
||
dense
|
||
:loading="loading"
|
||
|
||
:rows-per-page-options="[0]"
|
||
:pagination="{ rowsPerPage: 0 }"
|
||
>
|
||
|
||
<!-- ========== HEADER ========== -->
|
||
<template v-slot:header-cell="props">
|
||
|
||
<q-th :props="props">
|
||
|
||
<!-- Module başlığı -->
|
||
<span v-if="props.col.name === 'module'">
|
||
{{ props.col.label }}
|
||
</span>
|
||
|
||
<!-- Checkbox kolon başlığı -->
|
||
<div v-else class="column items-center">
|
||
|
||
<span class="text-caption">
|
||
{{ props.col.label }}
|
||
</span>
|
||
|
||
<q-checkbox
|
||
dense
|
||
:model-value="isColumnChecked(props.col.name)"
|
||
@update:model-value="toggleColumn(props.col.name, $event)"
|
||
/>
|
||
|
||
</div>
|
||
|
||
</q-th>
|
||
|
||
</template>
|
||
|
||
|
||
<!-- ========== BODY ========== -->
|
||
<template v-slot:body-cell="props">
|
||
|
||
<q-td
|
||
:props="props"
|
||
:class="props.col.name === 'module'
|
||
? 'permissions-sticky-col'
|
||
: ''"
|
||
>
|
||
|
||
<!-- Module adı -->
|
||
<span v-if="props.col.name === 'module'">
|
||
{{ props.row.label }}
|
||
</span>
|
||
|
||
<!-- Checkbox -->
|
||
<q-checkbox
|
||
v-else
|
||
v-model="props.row[props.col.name]"
|
||
dense
|
||
@update:model-value="dirty = true"
|
||
/>
|
||
|
||
</q-td>
|
||
|
||
</template>
|
||
|
||
</q-table>
|
||
|
||
</div>
|
||
|
||
</q-page>
|
||
|
||
<q-page v-else class="q-pa-md flex flex-center">
|
||
<div class="text-negative text-subtitle1">
|
||
Bu module erisim yetkiniz yok.
|
||
</div>
|
||
</q-page>
|
||
</template>
|
||
|
||
|
||
<script setup>
|
||
|
||
import { ref, onMounted, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { Notify } from 'quasar'
|
||
import api from 'src/services/api'
|
||
import { usePermission } from 'src/composables/usePermission'
|
||
|
||
const { canUpdate } = usePermission()
|
||
const canUpdateUser = canUpdate('user')
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
|
||
|
||
/* ================= STATE ================= */
|
||
|
||
const roles = ref([])
|
||
const departments = ref([])
|
||
|
||
const roleId = ref(null)
|
||
const deptCode = ref(null)
|
||
|
||
const rows = ref([])
|
||
|
||
const loading = ref(false)
|
||
const dirty = ref(false)
|
||
const lookupsLoaded = ref(false)
|
||
|
||
|
||
/* ================= ACTIONS ================= */
|
||
|
||
const actions = [
|
||
{ key: 'write', label: 'Ekleme' },
|
||
{ key: 'read', label: 'Görüntüleme' },
|
||
{ key: 'delete', label: 'Silme' },
|
||
{ key: 'update', label: 'Güncelleme' },
|
||
{ key: 'export', label: 'Çıktı' }
|
||
]
|
||
|
||
|
||
/* ================= MODULES ================= */
|
||
|
||
const modules = ref([])
|
||
|
||
|
||
/* ================= TABLE ================= */
|
||
|
||
const columns = [
|
||
|
||
{
|
||
name: 'module',
|
||
label: 'Modül',
|
||
field: 'label',
|
||
align: 'left'
|
||
},
|
||
|
||
...actions.map(a => ({
|
||
name: a.key,
|
||
label: a.label,
|
||
align: 'center'
|
||
}))
|
||
]
|
||
|
||
let matrixLoading = false
|
||
|
||
function goList () {
|
||
router.push({ name: 'role-dept-permissions-list' })
|
||
}
|
||
|
||
function applyRouteSelection () {
|
||
const qRole = String(route.query.roleId || '').trim()
|
||
const qDept = String(route.query.deptCode || '').trim()
|
||
|
||
if (/^\d+$/.test(qRole) && Number(qRole) > 0) {
|
||
roleId.value = qRole
|
||
}
|
||
|
||
if (qDept) {
|
||
deptCode.value = qDept
|
||
}
|
||
|
||
if (roleId.value && deptCode.value) {
|
||
loadMatrix()
|
||
}
|
||
}
|
||
|
||
|
||
/* ================= LOOKUPS ================= */
|
||
|
||
async function loadLookups () {
|
||
|
||
const [r, d, m] = await Promise.all([
|
||
api.get('/lookups/roles-perm'),
|
||
api.get('/lookups/departments-perm'),
|
||
api.get('/lookups/modules')
|
||
])
|
||
|
||
roles.value = r.data || []
|
||
departments.value = d.data || []
|
||
modules.value = m.data || []
|
||
|
||
lookupsLoaded.value = true
|
||
}
|
||
|
||
|
||
/* ================= INIT MATRIX ================= */
|
||
|
||
function initMatrix () {
|
||
|
||
rows.value = modules.value.map(m => {
|
||
|
||
const row = {
|
||
module: String(m.value).toLowerCase().trim(),
|
||
label: m.label
|
||
}
|
||
|
||
actions.forEach(a => {
|
||
row[a.key] = false
|
||
})
|
||
|
||
return row
|
||
})
|
||
}
|
||
|
||
|
||
/* ================= LOAD ================= */
|
||
|
||
async function loadMatrix () {
|
||
|
||
if (!roleId.value || !deptCode.value) return
|
||
if (matrixLoading) return
|
||
|
||
matrixLoading = true
|
||
loading.value = true
|
||
|
||
try {
|
||
|
||
if (!modules.value.length) {
|
||
await loadLookups()
|
||
}
|
||
|
||
initMatrix()
|
||
|
||
const res = await api.get(
|
||
`/roles/${roleId.value}/departments/${deptCode.value}/permissions`
|
||
)
|
||
|
||
const list = Array.isArray(res.data) ? res.data : []
|
||
|
||
console.log('PERM LIST:', list.slice(0, 10))
|
||
|
||
|
||
// ✅ BACKEND → UI ACTION MAP
|
||
const actionMap = {
|
||
insert: 'write',
|
||
view: 'read',
|
||
delete: 'delete',
|
||
update: 'update',
|
||
export: 'export'
|
||
}
|
||
|
||
|
||
list.forEach(p => {
|
||
|
||
const code = String(p.module_code || p.module)
|
||
.toLowerCase()
|
||
.trim()
|
||
|
||
const rawAction = String(p.action)
|
||
.toLowerCase()
|
||
.trim()
|
||
|
||
const mappedAction = actionMap[rawAction] || rawAction
|
||
|
||
|
||
const row = rows.value.find(r => r.module === code)
|
||
|
||
if (row && row.hasOwnProperty(mappedAction)) {
|
||
row[mappedAction] = Boolean(p.allowed)
|
||
}
|
||
})
|
||
|
||
|
||
dirty.value = false
|
||
|
||
} catch (err) {
|
||
|
||
console.error('PERM LOAD ERROR:', err)
|
||
|
||
Notify.create({
|
||
type: 'negative',
|
||
message: 'Yetkiler yüklenemedi'
|
||
})
|
||
|
||
} finally {
|
||
|
||
loading.value = false
|
||
matrixLoading = false
|
||
}
|
||
}
|
||
|
||
|
||
/* ================= SAVE ================= */
|
||
|
||
async function save () {
|
||
|
||
try {
|
||
|
||
loading.value = true
|
||
|
||
const payload = []
|
||
|
||
rows.value.forEach(r => {
|
||
|
||
actions.forEach(a => {
|
||
|
||
payload.push({
|
||
module: r.module,
|
||
action: a.key,
|
||
allowed: r[a.key]
|
||
})
|
||
})
|
||
})
|
||
|
||
|
||
await api.post(
|
||
`/roles/${roleId.value}/departments/${deptCode.value}/permissions`,
|
||
payload
|
||
)
|
||
|
||
|
||
Notify.create({
|
||
type: 'positive',
|
||
message: 'Kaydedildi'
|
||
})
|
||
|
||
dirty.value = false
|
||
|
||
} catch {
|
||
|
||
Notify.create({
|
||
type: 'negative',
|
||
message: 'Kayıt hatası'
|
||
})
|
||
|
||
} finally {
|
||
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
|
||
/* ================= COLUMN ================= */
|
||
|
||
function isColumnChecked (key) {
|
||
|
||
if (!rows.value.length) return false
|
||
|
||
return rows.value.every(r => r[key] === true)
|
||
}
|
||
|
||
|
||
function toggleColumn (key, val) {
|
||
|
||
rows.value.forEach(r => {
|
||
r[key] = val
|
||
})
|
||
|
||
dirty.value = true
|
||
}
|
||
|
||
|
||
/* ================= INIT ================= */
|
||
|
||
onMounted(async () => {
|
||
await loadLookups()
|
||
applyRouteSelection()
|
||
})
|
||
|
||
watch(roleId, v => console.log('ROLE_ID >>>', v))
|
||
watch(deptCode, v => console.log('DEPT >>>', v))
|
||
watch(
|
||
() => [route.query.roleId, route.query.deptCode],
|
||
() => {
|
||
if (!lookupsLoaded.value) return
|
||
applyRouteSelection()
|
||
}
|
||
)
|
||
|
||
</script>
|