Files
bssapp/ui/src/pages/RoleDepartmentPermissionPage.vue
2026-06-02 16:15:07 +03:00

669 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>
<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
v-if="lookupsLoaded && roleId && deptCode"
class="group-members-toolbar"
>
<div class="group-members-toolbar__members">
<div class="text-caption text-weight-bold">
Grup Kullanicilari ({{ members.length }})
</div>
<div v-if="membersLoading" class="q-ml-sm">
<q-spinner color="primary" size="18px" />
</div>
<div v-else-if="members.length" class="group-members-toolbar__chips">
<q-chip
v-for="member in members"
:key="member.id"
dense
square
color="blue-1"
text-color="primary"
>
{{ member.id }} - {{ member.full_name || member.username }}
<q-tooltip>{{ member.username }}</q-tooltip>
</q-chip>
</div>
<div v-else class="text-caption text-grey-7 q-ml-sm">
Bu grupta aktif kullanici bulunmuyor.
</div>
</div>
<div class="group-members-toolbar__add">
<q-select
v-model="memberUserId"
:options="filteredUserOptions"
option-value="id"
option-label="title"
emit-value
map-options
dense
outlined
clearable
use-input
input-debounce="150"
label="Kullanici ekle"
class="group-members-toolbar__select"
@filter="filterUsers"
/>
<q-btn
color="primary"
icon="person_add"
label="Ekle"
:disable="!memberUserId || addingMember"
:loading="addingMember"
@click="addMember"
/>
</div>
</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 { computed, 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()
// This screen manages system-wide permission sets; gate by system:update.
const canUpdateUser = canUpdate('system')
const route = useRoute()
const router = useRouter()
/* ================= STATE ================= */
const roles = ref([])
const departments = ref([])
const users = ref([])
const filteredUserOptions = ref([])
const members = ref([])
const roleId = ref(null)
const deptCode = ref(null)
const memberUserId = ref(null)
const rows = ref([])
const loading = ref(false)
const membersLoading = ref(false)
const addingMember = 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, u] = await Promise.all([
api.get('/lookups/roles-perm'),
api.get('/lookups/departments-perm'),
api.get('/lookups/modules'),
api.get('/lookups/users-perm')
])
roles.value = r.data || []
departments.value = d.data || []
modules.value = m.data || []
users.value = u.data || []
filteredUserOptions.value = [...users.value]
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) {
members.value = []
return
}
if (matrixLoading) return
matrixLoading = true
loading.value = true
try {
if (!modules.value.length) {
await loadLookups()
}
initMatrix()
const [res] = await Promise.all([
api.get(`/roles/${roleId.value}/departments/${deptCode.value}/permissions`),
loadMembers()
])
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
}
}
const availableUserOptions = computed(() => {
const memberIDs = new Set(members.value.map(member => Number(member.id)))
return users.value.filter(user => !memberIDs.has(Number(user.id)))
})
function filterUsers (value, update) {
update(() => {
const needle = String(value || '').trim().toLocaleLowerCase('tr')
filteredUserOptions.value = needle
? availableUserOptions.value.filter(user => String(user.title || '').toLocaleLowerCase('tr').includes(needle))
: [...availableUserOptions.value]
})
}
async function loadMembers () {
if (!roleId.value || !deptCode.value) {
members.value = []
return
}
membersLoading.value = true
try {
const res = await api.get(
`/roles/${roleId.value}/departments/${encodeURIComponent(deptCode.value)}/members`
)
members.value = Array.isArray(res.data) ? res.data : []
filteredUserOptions.value = [...availableUserOptions.value]
} catch (err) {
console.error('GROUP MEMBERS LOAD ERROR:', err)
members.value = []
Notify.create({
type: 'negative',
message: 'Grup kullanicilari yuklenemedi'
})
} finally {
membersLoading.value = false
}
}
async function addMember () {
if (!roleId.value || !deptCode.value || !memberUserId.value) return
addingMember.value = true
try {
const res = await api.post(
`/roles/${roleId.value}/departments/${encodeURIComponent(deptCode.value)}/members`,
{ user_id: Number(memberUserId.value) }
)
members.value = Array.isArray(res.data) ? res.data : []
memberUserId.value = null
filteredUserOptions.value = [...availableUserOptions.value]
Notify.create({
type: 'positive',
message: 'Kullanici gruba eklendi'
})
} catch (err) {
console.error('GROUP MEMBER ADD ERROR:', err)
Notify.create({
type: 'negative',
message: 'Kullanici gruba eklenemedi'
})
} finally {
addingMember.value = false
}
}
/* ================= SAVE ================= */
async function save () {
try {
loading.value = true
const payload = []
// UI action keys -> backend action codes
const toBackendAction = {
write: 'insert',
read: 'view',
delete: 'delete',
update: 'update',
export: 'export'
}
rows.value.forEach(r => {
actions.forEach(a => {
payload.push({
module: r.module,
action: toBackendAction[a.key] || 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>
<style scoped>
.group-members-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 8px 12px;
background: #fff;
border-top: 1px solid #e0e0e0;
border-bottom: 1px solid #e0e0e0;
}
.group-members-toolbar__members {
min-width: 0;
display: flex;
align-items: center;
flex: 1 1 auto;
}
.group-members-toolbar__chips {
min-width: 0;
display: flex;
gap: 4px;
overflow-x: auto;
padding-left: 8px;
}
.group-members-toolbar__add {
display: flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
}
.group-members-toolbar__select {
width: 300px;
}
@media (max-width: 1100px) {
.group-members-toolbar {
align-items: stretch;
flex-direction: column;
}
.group-members-toolbar__select {
width: min(100%, 420px);
}
}
</style>