Files
web-antrean/pages/Setting/HakAkses.vue
T
2026-01-05 08:32:59 +07:00

2009 lines
62 KiB
Vue

<template>
<div class="hak-akses-page pa-4">
<v-breadcrumbs :items="breadcrumbs" class="pl-0 mb-4">
<template v-slot:divider>
<v-icon icon="mdi-chevron-right"></v-icon>
</template>
</v-breadcrumbs>
<!-- Dialog for Add/Edit Form -->
<v-dialog
v-model="showAddEditDialog"
max-width="1200px"
persistent
scrollable
transition="dialog-bottom-transition"
>
<v-card class="modern-dialog-card">
<div class="dialog-header" :class="viewMode === 'add' ? 'header-add' : 'header-edit'">
<div class="dialog-header-content">
<div class="dialog-header-left">
<v-avatar
:color="viewMode === 'add' ? 'green-darken-2' : 'orange-darken-2'"
size="56"
class="dialog-avatar elevation-4"
>
<v-icon :icon="viewMode === 'add' ? 'mdi-plus' : 'mdi-pencil'" color="white" size="28"></v-icon>
</v-avatar>
<div class="dialog-header-text">
<h2 class="dialog-title">{{ formTitle }}</h2>
<p class="dialog-subtitle">
{{ viewMode === 'add' ? 'Tambahkan hak akses baru ke sistem' : 'Ubah informasi hak akses yang ada' }}
</p>
</div>
</div>
<v-btn
icon
variant="text"
size="small"
class="dialog-close-btn"
@click="cancelForm"
>
<v-icon color="white" size="24">mdi-close</v-icon>
</v-btn>
</div>
</div>
<v-card-text class="dialog-content">
<v-row>
<v-col cols="12" md="6">
<v-label class="font-weight-bold text-medium-emphasis">Pilih User (Optional)</v-label>
<v-select
v-model="selectedUserId"
:items="availableUsers"
item-title="namaLengkap"
item-value="id"
placeholder="Pilih User untuk auto-fill data"
variant="outlined"
density="comfortable"
class="mt-1"
@update:model-value="fillUserData"
clearable
>
<template v-slot:item="{ props, item }">
<v-list-item v-bind="props" :title="`${item.raw.namaLengkap} (${item.raw.namaUser})`" :subtitle="item.raw.tipeUser"></v-list-item>
</template>
</v-select>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="3">
<v-label class="font-weight-bold text-medium-emphasis">User ID</v-label>
<v-text-field
v-model="editedItem.userId"
placeholder="User ID"
variant="outlined"
density="comfortable"
class="mt-1"
></v-text-field>
</v-col>
<v-col cols="12" md="3">
<v-label class="font-weight-bold text-medium-emphasis">Nama Lengkap</v-label>
<v-text-field
v-model="editedItem.namaLengkap"
placeholder="Nama Lengkap"
variant="outlined"
density="comfortable"
class="mt-1"
></v-text-field>
</v-col>
<v-col cols="12" md="3">
<v-label class="font-weight-bold text-medium-emphasis">Nama User</v-label>
<v-text-field
v-model="editedItem.namaUser"
placeholder="Nama User"
variant="outlined"
density="comfortable"
class="mt-1"
></v-text-field>
</v-col>
<v-col cols="12" md="3">
<v-label class="font-weight-bold text-medium-emphasis">Tipe User</v-label>
<v-select
v-model="selectedTipeUser"
:items="availableTipeUsers"
placeholder="Pilih Tipe User"
variant="outlined"
density="comfortable"
class="mt-1"
@update:model-value="fillUserDataByTipeUser"
clearable
></v-select>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="4">
<v-label class="font-weight-bold text-medium-emphasis">Role</v-label>
<v-select
v-model="editedItem.role"
:items="availableRoles"
placeholder="Pilih Role"
variant="outlined"
density="comfortable"
class="mt-1"
></v-select>
</v-col>
<v-col cols="12" md="4">
<v-label class="font-weight-bold text-medium-emphasis">Group</v-label>
<v-select
v-model="editedItem.group"
:items="availableGroups"
placeholder="Pilih Group"
variant="outlined"
density="comfortable"
class="mt-1"
></v-select>
</v-col>
<v-col cols="12" md="4">
<v-label class="font-weight-bold text-medium-emphasis">Nama Tipe User (Optional)</v-label>
<v-text-field
v-model="editedItem.namaTipeUser"
placeholder="Masukkan Nama Tipe User"
variant="outlined"
density="comfortable"
class="mt-1"
></v-text-field>
</v-col>
</v-row>
<v-row v-if="viewMode === 'add' || viewMode === 'editName'">
<v-col cols="12">
<v-btn
color="info"
prepend-icon="mdi-download"
variant="outlined"
rounded="lg"
class="text-capitalize"
@click="fetchPermissionsFromBackend"
:loading="isFetchingPermissions"
:disabled="!editedItem.role || !editedItem.group"
>
Ambil Data dari Backend API
</v-btn>
<span class="ml-3 text-caption text-medium-emphasis">
* Klik tombol ini untuk mengambil permissions dari backend berdasarkan Role dan Group yang sudah diisi
</span>
</v-col>
</v-row>
<!-- Display fetched permissions info -->
<v-row v-if="fetchedBackendData && fetchedBackendData.length > 0" class="mt-4">
<v-col cols="12">
<v-card variant="outlined" class="pa-4 bg-blue-lighten-5">
<v-card-title class="text-subtitle-1 font-weight-bold pa-0 mb-3">
<v-icon icon="mdi-information" class="mr-2" color="info"></v-icon>
Data dari Backend API
</v-card-title>
<v-card-text class="pa-0">
<div class="mb-3">
<strong>Total Permissions:</strong> {{ fetchedBackendData.length }} items
</div>
<v-table density="compact" class="elevation-1">
<thead>
<tr>
<th class="text-left">No</th>
<th class="text-left">Page Name</th>
<th class="text-center">Active</th>
<th class="text-center">Read</th>
<th class="text-center">Create</th>
<th class="text-center">Update</th>
<th class="text-center">Delete</th>
<th class="text-center">Level</th>
<th class="text-center">Status</th>
</tr>
</thead>
<tbody>
<tr v-for="(perm, index) in fetchedBackendData" :key="perm.id">
<td>{{ index + 1 }}</td>
<td>
<strong>{{ perm.pagename }}</strong>
<div class="text-caption text-grey">{{ perm.pagesID }}</div>
</td>
<td class="text-center">
<v-icon :color="perm.active ? 'green' : 'grey'">
{{ perm.active ? 'mdi-check-circle' : 'mdi-close-circle' }}
</v-icon>
</td>
<td class="text-center">
<v-icon :color="perm.read ? 'green' : 'grey'">
{{ perm.read ? 'mdi-check-circle' : 'mdi-close-circle' }}
</v-icon>
</td>
<td class="text-center">
<v-icon :color="perm.create ? 'green' : 'grey'">
{{ perm.create ? 'mdi-check-circle' : 'mdi-close-circle' }}
</v-icon>
</td>
<td class="text-center">
<v-icon :color="perm.update ? 'green' : 'grey'">
{{ perm.update ? 'mdi-check-circle' : 'mdi-close-circle' }}
</v-icon>
</td>
<td class="text-center">
<v-icon :color="perm.delete ? 'green' : 'grey'">
{{ perm.delete ? 'mdi-check-circle' : 'mdi-close-circle' }}
</v-icon>
</td>
<td class="text-center">{{ perm.level || '-' }}</td>
<td class="text-center">
<v-chip
:color="getMappingStatus(perm.pagename) ? 'success' : 'warning'"
size="small"
>
{{ getMappingStatus(perm.pagename) ? 'Mapped' : 'Not Mapped' }}
</v-chip>
</td>
</tr>
</tbody>
</v-table>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Display mapped permissions summary -->
<v-row v-if="editedItem.hakAksesMenu && editedItem.hakAksesMenu.length > 0" class="mt-4">
<v-col cols="12">
<v-card variant="outlined" class="pa-4 bg-green-lighten-5">
<v-card-title class="text-subtitle-1 font-weight-bold pa-0 mb-3">
<v-icon icon="mdi-check-circle" class="mr-2" color="success"></v-icon>
Permissions yang Sudah di-Mapping ke Menu
</v-card-title>
<v-card-text class="pa-0">
<div class="mb-3">
<strong>Total Menu:</strong> {{ editedItem.hakAksesMenu.length }} items
<span class="ml-4">
<strong>Dengan Akses:</strong>
{{ editedItem.hakAksesMenu.filter(m => m.canAccess).length }} items
</span>
</div>
<v-alert
type="info"
variant="tonal"
density="compact"
class="mb-3"
>
Data permissions dari backend telah di-mapping ke struktur menu sidebar.
Anda dapat mengedit permissions di tab "Edit Hak Akses" setelah menyimpan data ini.
</v-alert>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="dialog-actions">
<v-spacer></v-spacer>
<v-btn
color="grey-darken-1"
variant="outlined"
rounded="lg"
class="text-capitalize mr-2"
@click="cancelForm"
size="large"
>
<v-icon left>mdi-close</v-icon>
Batal
</v-btn>
<v-btn
:color="viewMode === 'add' ? 'green-darken-2' : 'orange-darken-2'"
variant="flat"
rounded="lg"
class="text-capitalize"
@click="saveItem"
size="large"
>
<v-icon left>{{ viewMode === 'add' ? 'mdi-check' : 'mdi-content-save' }}</v-icon>
{{ viewMode === 'add' ? 'Simpan' : 'Update' }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Dialog for Edit Access -->
<v-dialog
v-model="showEditAccessDialog"
max-width="1400px"
persistent
scrollable
transition="dialog-bottom-transition"
>
<v-card class="modern-dialog-card">
<div class="dialog-header header-edit-access">
<div class="dialog-header-content">
<div class="dialog-header-left">
<v-avatar
color="green-darken-2"
size="56"
class="dialog-avatar elevation-4"
>
<v-icon icon="mdi-lock-check" color="white" size="28"></v-icon>
</v-avatar>
<div class="dialog-header-text">
<h2 class="dialog-title">Edit Hak Akses</h2>
<p class="dialog-subtitle">Kelola permissions dan akses menu untuk pengguna</p>
</div>
</div>
<v-btn
icon
variant="text"
size="small"
class="dialog-close-btn"
@click="cancelForm"
>
<v-icon color="white" size="24">mdi-close</v-icon>
</v-btn>
</div>
</div>
<v-card-text class="dialog-content">
<EditHakAkses
:item="editedItem"
@save="updateItemAccess"
@cancel="cancelForm"
/>
</v-card-text>
</v-card>
</v-dialog>
<!-- Dialog for View Detail -->
<v-dialog
v-model="showViewDialog"
max-width="1200px"
scrollable
:persistent="false"
transition="dialog-bottom-transition"
>
<v-card class="modern-dialog-card">
<div class="dialog-header header-view">
<div class="dialog-header-content">
<div class="dialog-header-left">
<v-avatar
color="blue-darken-2"
size="56"
class="dialog-avatar elevation-4"
>
<v-icon icon="mdi-eye" color="white" size="28"></v-icon>
</v-avatar>
<div class="dialog-header-text">
<h2 class="dialog-title">Detail Hak Akses</h2>
<p class="dialog-subtitle">Lihat informasi detail hak akses pengguna</p>
</div>
</div>
<v-btn
icon
variant="text"
size="small"
class="dialog-close-btn"
@click="cancelForm"
>
<v-icon color="white" size="24">mdi-close</v-icon>
</v-btn>
</div>
</div>
<v-card-text class="dialog-content">
<div v-if="viewMode === 'view'">
<v-row>
<v-col cols="12" md="3">
<v-label class="font-weight-bold text-medium-emphasis">User ID</v-label>
<v-text-field
v-model="editedItem.userId"
variant="outlined"
density="comfortable"
class="mt-1"
readonly
></v-text-field>
</v-col>
<v-col cols="12" md="3">
<v-label class="font-weight-bold text-medium-emphasis">Nama Lengkap</v-label>
<v-text-field
v-model="editedItem.namaLengkap"
variant="outlined"
density="comfortable"
class="mt-1"
readonly
></v-text-field>
</v-col>
<v-col cols="12" md="3">
<v-label class="font-weight-bold text-medium-emphasis">Nama User</v-label>
<v-text-field
v-model="editedItem.namaUser"
variant="outlined"
density="comfortable"
class="mt-1"
readonly
></v-text-field>
</v-col>
<v-col cols="12" md="3">
<v-label class="font-weight-bold text-medium-emphasis">Tipe User</v-label>
<v-text-field
v-model="editedItem.tipeUser"
variant="outlined"
density="comfortable"
class="mt-1"
readonly
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="4">
<v-label class="font-weight-bold text-medium-emphasis">Role</v-label>
<v-text-field
v-model="editedItem.role"
variant="outlined"
density="comfortable"
class="mt-1"
readonly
></v-text-field>
</v-col>
<v-col cols="12" md="4">
<v-label class="font-weight-bold text-medium-emphasis">Group</v-label>
<v-text-field
v-model="editedItem.group"
variant="outlined"
density="comfortable"
class="mt-1"
readonly
></v-text-field>
</v-col>
<v-col cols="12" md="4">
<v-label class="font-weight-bold text-medium-emphasis">Nama Tipe User</v-label>
<v-text-field
v-model="editedItem.namaTipeUser"
variant="outlined"
density="comfortable"
class="mt-1"
readonly
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-card-title class="text-subtitle-1 font-weight-bold pa-0 mb-4">Hak Akses Menu</v-card-title>
<v-table density="comfortable" class="elevation-1 rounded-xl">
<thead>
<tr>
<th class="text-left text-uppercase font-weight-bold text-grey-darken-1">No</th>
<th class="text-left text-uppercase font-weight-bold text-grey-darken-1">Menu</th>
<th class="text-center text-uppercase font-weight-bold text-grey-darken-1">Akses</th>
<th class="text-center text-uppercase font-weight-bold text-grey-darken-1">Lihat</th>
<th class="text-center text-uppercase font-weight-bold text-grey-darken-1">Tambah</th>
<th class="text-center text-uppercase font-weight-bold text-grey-darken-1">Edit</th>
<th class="text-center text-uppercase font-weight-bold text-grey-darken-1">Hapus</th>
</tr>
</thead>
<tbody>
<tr v-for="(menu, index) in editedItem.hakAksesMenu" :key="index">
<td>{{ index + 1 }}</td>
<td>{{ menu.name }}</td>
<td class="text-center">
<v-icon :color="menu.canAccess ? 'green' : 'grey-lighten-2'">{{ menu.canAccess ? 'mdi-check-circle' : 'mdi-close-circle' }}</v-icon>
</td>
<td class="text-center">
<v-icon :color="menu.canView ? 'green' : 'grey-lighten-2'">{{ menu.canView ? 'mdi-check-circle' : 'mdi-close-circle' }}</v-icon>
</td>
<td class="text-center">
<v-icon :color="menu.canAdd ? 'green' : 'grey-lighten-2'">{{ menu.canAdd ? 'mdi-check-circle' : 'mdi-close-circle' }}</v-icon>
</td>
<td class="text-center">
<v-icon :color="menu.canEdit ? 'green' : 'grey-lighten-2'">{{ menu.canEdit ? 'mdi-check-circle' : 'mdi-close-circle' }}</v-icon>
</td>
<td class="text-center">
<v-icon :color="menu.canDelete ? 'green' : 'grey-lighten-2'">{{ menu.canDelete ? 'mdi-check-circle' : 'mdi-close-circle' }}</v-icon>
</td>
</tr>
</tbody>
</v-table>
</v-col>
</v-row>
</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="dialog-actions">
<v-spacer></v-spacer>
<v-btn
color="grey-darken-1"
variant="outlined"
rounded="lg"
class="text-capitalize"
@click="cancelForm"
size="large"
>
<v-icon left>mdi-close</v-icon>
Tutup
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Main Table View - Always visible, dialog overlays it -->
<div>
<v-card class="rounded-xl elevation-2" style="background-color: white;">
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-shield-lock-outline</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Hak Akses</h2>
<p class="page-subtitle">{{ new Date().toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) }} - Manajemen Hak Akses</p>
</div>
</div>
<div class="header-stats">
<v-chip color="white" variant="flat" class="stat-chip mr-2">
<v-icon start size="16">mdi-account-group</v-icon>
{{ filteredHakAksesData.length }} User
</v-chip>
<v-btn
color="white"
@click="showAddForm"
elevation="0"
class="add-btn mr-2"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Baru
</v-btn>
<v-btn
color="white"
@click="toggleReorderMode"
elevation="0"
class="add-btn"
>
<v-icon left size="20">mdi-format-list-numbered</v-icon>
{{ reorderMode ? 'Selesai' : 'Atur Urutan' }}
</v-btn>
</div>
</div>
</div>
<!-- Table -->
<v-card-text>
<div class="d-flex flex-column flex-sm-row align-center justify-space-between mb-4">
<div class="d-flex align-center mb-4 mb-sm-0">
<span class="mr-2 text-subtitle-1 text-medium-emphasis">Show</span>
<v-select
v-model="itemsPerPage"
:items="[10, 25, 50]"
variant="outlined"
density="compact"
hide-details
style="max-width: 80px;"
rounded="lg"
class="mr-2"
></v-select>
<span class="text-subtitle-1 text-medium-emphasis">entries</span>
</div>
<div class="d-flex align-center">
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
label="Search"
variant="outlined"
density="compact"
hide-details
rounded="lg"
clearable
style="min-width: 300px; width: 100%; max-width: 400px;"
></v-text-field>
</div>
</div>
<!-- Filter bar -->
<v-row class="mb-4" dense>
<v-col cols="12" sm="3">
<v-select
v-model="filterUserId"
:items="filterUserIdOptions"
item-title="label"
item-value="value"
label="Filter User ID"
variant="outlined"
density="compact"
clearable
hide-details
/>
</v-col>
<v-col cols="12" sm="3">
<v-select
v-model="filterTipeUser"
:items="availableTipeUsers"
label="Filter Tipe User"
variant="outlined"
density="compact"
clearable
hide-details
/>
</v-col>
<v-col cols="12" sm="3">
<v-select
v-model="filterRole"
:items="availableRoles"
label="Filter Role"
variant="outlined"
density="compact"
clearable
hide-details
/>
</v-col>
<v-col cols="12" sm="3">
<v-select
v-model="filterGroup"
:items="availableGroups"
label="Filter Group"
variant="outlined"
density="compact"
clearable
hide-details
/>
</v-col>
</v-row>
<v-data-table
:headers="headers"
:items="filteredHakAksesData"
:search="search"
:items-per-page="itemsPerPage"
v-model:page="page"
class="rounded-lg elevation-0 custom-table"
>
<template v-slot:[`item.actions`]="{ item }">
<v-btn icon color="blue" size="small" class="mr-2" @click="viewItem(item)">
<v-icon>mdi-eye</v-icon>
</v-btn>
<v-btn icon color="orange" size="small" class="mr-2" @click="editItem(item)">
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn icon color="red" size="small" class="mr-2" @click="deleteItem(item)">
<v-icon>mdi-delete</v-icon>
</v-btn>
<v-btn icon color="green" size="small" @click="editAccess(item)">
<v-icon>mdi-lock-check</v-icon>
</v-btn>
</template>
<template v-slot:no-data>
<v-alert :value="true" color="grey-lighten-3" icon="mdi-information">
Tidak ada data yang tersedia.
</v-alert>
</template>
<template v-slot:item.id="{ item }">
<div class="text-center">{{ item.id }}</div>
</template>
<!-- Tampilkan User ID virtual yang dikelompokkan per tipe user -->
<template v-slot:item.userId="{ item }">
<div class="text-center">
{{ formatDisplayUserId(item) }}
</div>
</template>
<!-- Tipe User dengan chip -->
<template v-slot:item.tipeUser="{ item }">
<v-chip
v-if="item.tipeUser"
color="purple-lighten-1"
size="small"
>
{{ item.tipeUser }}
</v-chip>
<span v-else class="text-grey">-</span>
</template>
<!-- Role dengan chip -->
<template v-slot:item.role="{ item }">
<v-chip
v-if="item.role"
color="blue-lighten-1"
size="small"
>
{{ item.role }}
</v-chip>
<span v-else class="text-grey">-</span>
</template>
<!-- Group dengan chip -->
<template v-slot:item.group="{ item }">
<v-chip
v-if="item.group"
color="green-lighten-1"
size="small"
>
{{ item.group }}
</v-chip>
<span v-else class="text-grey">-</span>
</template>
<!-- Nama Tipe User dengan chip -->
<template v-slot:item.namaTipeUser="{ item }">
<v-chip
v-if="item.namaTipeUser"
color="orange-lighten-1"
size="small"
>
{{ item.namaTipeUser }}
</v-chip>
<span v-else class="text-grey">-</span>
</template>
<template v-slot:bottom>
<v-row class="ma-2">
<v-col cols="12" sm="6" class="d-flex align-center justify-start text-caption text-grey">
{{ showingEntriesText }}
</v-col>
<v-col cols="12" sm="6" class="d-flex align-center justify-end">
<v-pagination
v-model="page"
:length="pageCount"
rounded="circle"
:total-visible="5"
></v-pagination>
</v-col>
</v-row>
</template>
</v-data-table>
</v-card-text>
</v-card>
</div>
<v-dialog v-model="showDeleteDialog" max-width="400px">
<v-card class="pa-6 rounded-xl elevation-4">
<v-card-title class="text-h6 font-weight-bold">Hapus Data</v-card-title>
<v-card-text>Apakah Anda yakin ingin menghapus data ini?</v-card-text>
<v-card-actions class="d-flex justify-end">
<v-btn color="grey-darken-1" variant="text" rounded="lg" @click="closeDeleteDialog">Batal</v-btn>
<v-btn color="red-darken-1" variant="text" rounded="lg" @click="confirmDelete">Hapus</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="reorderMode" max-width="600px">
<v-card class="pa-6 rounded-xl elevation-4">
<v-card-title class="text-h6 font-weight-bold">Atur Urutan Menu</v-card-title>
<v-card-text>
<VueDraggableNext
v-model="draggableMenus"
item-key="name"
tag="v-list"
class="pa-0"
handle=".handle"
:animation="200"
>
<template #item="{ element }">
<v-list-item class="rounded-lg elevation-1 my-2" :title="element.name">
<template #prepend>
<v-icon icon="mdi-drag-vertical" class="handle"></v-icon>
</template>
</v-list-item>
</template>
</VueDraggableNext>
</v-card-text>
<v-card-actions class="d-flex justify-end">
<v-btn color="grey-darken-1" variant="text" rounded="lg" @click="cancelReorder">Batal</v-btn>
<v-btn color="primary" variant="text" rounded="lg" @click="saveReorder">Simpan Urutan</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="snackbar.timeout"
location="top"
>
{{ snackbar.message }}
<template v-slot:actions>
<v-btn variant="text" @click="snackbar.show = false">
Tutup
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue';
import { useLocalStorage } from '@vueuse/core';
import { VueDraggableNext } from 'vue-draggable-next';
import EditHakAkses from '@/components/HakAkses/editHakAkses.vue';
import { useNavItemsStore } from '~/stores/navItems1';
definePageMeta({
middleware: ['auth']
})
interface HakAksesMenu {
name: string;
canAccess: boolean;
canView: boolean;
canAdd: boolean;
canEdit: boolean;
canDelete: boolean;
}
interface NavItem {
id: number;
name: string;
path: string;
icon: string;
children?: NavItem[];
}
interface HakAksesData {
id: number;
userId?: string;
namaLengkap?: string;
namaUser?: string;
tipeUser?: string;
role: string;
group: string;
namaTipeUser: string;
hakAksesMenu: HakAksesMenu[];
}
interface BackendPermissionItem {
id: number;
create: boolean;
read: boolean;
update: boolean;
disable: boolean;
delete: boolean;
active: boolean;
pagename: string;
pagesID: number;
level?: number;
sort?: number;
parent?: number;
}
interface SessionData {
roles: string[];
groups: string[];
}
// State for views
const viewMode = ref<'table' | 'add' | 'editName' | 'editAccess' | 'view'>('table');
const reorderMode = ref(false);
const allHakAksesData = useLocalStorage<HakAksesData[]>('allHakAksesData', []);
const navItemsStore = useNavItemsStore();
const draggableMenus = ref<NavItem[]>([]);
// Data table headers
// Kolom "No" dan "User ID" di-center agar sejajar dengan isi sel yang juga center
const headers = ref([
{ title: 'No', key: 'id' as const, align: 'center' as const },
{ title: 'User ID', key: 'userId' as const, sortable: true, align: 'center' as const },
{ title: 'Nama Lengkap', key: 'namaLengkap' as const, sortable: true },
{ title: 'Nama User', key: 'namaUser' as const, sortable: true },
{ title: 'Tipe User', key: 'tipeUser' as const, sortable: true },
{ title: 'Role', key: 'role' as const, sortable: true },
{ title: 'Group', key: 'group' as const, sortable: true },
{ title: 'Nama Tipe User', key: 'namaTipeUser' as const, sortable: true },
{ title: 'Aksi', align: 'center' as const, key: 'actions' as const, sortable: false },
]);
// Breadcrumbs
const breadcrumbs = computed(() => {
const baseCrumbs = [
{ title: 'Dashboard', disabled: false, href: '/dashboard' },
{ title: 'Setting', disabled: false, href: '/setting' },
{ title: 'Hak Akses', disabled: false, href: '/setting/hakakses' },
];
if (viewMode.value === 'add') {
return [...baseCrumbs, { title: 'Tambah Hak Akses', disabled: true, href: '/setting/tambahhakakses' }];
} else if (viewMode.value === 'editName') {
return [...baseCrumbs, { title: 'Edit Nama Tipe User', disabled: true, href: '/setting/editnamahakakses' }];
} else if (viewMode.value === 'editAccess') {
return [...baseCrumbs, { title: 'Edit Hak Akses', disabled: true, href: '/setting/edithakakses' }];
} else if (viewMode.value === 'view') {
return [...baseCrumbs, { title: 'Detail Hak Akses', disabled: true, href: '/setting/viewhakakses' }];
}
return baseCrumbs;
});
// Table and pagination state
const itemsPerPage = ref(10);
const search = ref('');
const page = ref(1);
const editedIndex = ref(-1);
const editedItem = ref<HakAksesData>({
id: 0,
role: '',
group: '',
namaTipeUser: '',
hakAksesMenu: [],
});
// Available roles and groups from UserLogin data
const availableRoles = ref<string[]>([]);
const availableGroups = ref<string[]>([]);
const availableUsers = ref<any[]>([]);
const availableTipeUsers = ref<string[]>([]);
const selectedUserId = ref<string | null>(null);
const selectedTipeUser = ref<string | null>(null);
const isFetchingPermissions = ref(false);
const fetchedBackendData = ref<BackendPermissionItem[]>([]);
// --- Menu management logic ---
const buildMenuTemplate = (items: NavItem[]): HakAksesMenu[] => {
const result: HakAksesMenu[] = [];
const walk = (list: NavItem[]) => {
list.forEach((item) => {
result.push({
name: item.name,
canAccess: false,
canView: false,
canAdd: false,
canEdit: false,
canDelete: false,
});
if (item.children?.length) {
walk(item.children);
}
});
};
walk(navItemsStore.navItems);
return result;
};
const mergePermissions = (base: HakAksesMenu[], existing: HakAksesMenu[]) => {
return base.map((menu) => {
const matched = existing.find((item) => item.name === menu.name);
return matched ? { ...menu, ...matched } : menu;
});
};
// Computed properties
const pageCount = computed(() => {
return Math.ceil(allHakAksesData.value.length / itemsPerPage.value);
});
const showingEntriesText = computed(() => {
const start = (page.value - 1) * itemsPerPage.value + 1;
const end = Math.min(page.value * itemsPerPage.value, allHakAksesData.value.length);
const total = allHakAksesData.value.length;
return `Showing ${start} to ${end} of ${total} entries`;
});
// Filter state
const filterUserId = ref<number | null>(null);
const filterTipeUser = ref<string | null>(null);
const filterRole = ref<string | null>(null);
const filterGroup = ref<string | null>(null);
// Opsi dropdown User ID (pakai ID baris + label User ID virtual)
const filterUserIdOptions = computed(() =>
Array.from(
new Map(
allHakAksesData.value.map((item) => [
item.id,
{
value: item.id,
label: formatDisplayUserId(item),
},
]),
).values(),
),
);
// Data tabel setelah difilter oleh dropdown
const filteredHakAksesData = computed(() =>
allHakAksesData.value.filter((item) => {
const matchUserId = !filterUserId.value || item.id === filterUserId.value;
const matchTipeUser = !filterTipeUser.value || (item.tipeUser || '') === filterTipeUser.value;
const matchRole = !filterRole.value || item.role === filterRole.value;
const matchGroup = !filterGroup.value || item.group === filterGroup.value;
return matchUserId && matchTipeUser && matchRole && matchGroup;
}),
);
// Generate display User ID grouped by tipeUser
const getTipeUserCode = (tipeUser?: string): string => {
if (!tipeUser) return 'USR';
const normalized = tipeUser.toLowerCase().trim();
if (normalized.includes('super') && normalized.includes('admin')) return 'SA';
if (normalized === 'admin') return 'ADM';
if (normalized.includes('loket')) return 'LOK';
if (normalized.includes('klinik')) return 'KLN';
if (normalized.includes('barcode')) return 'BAR';
if (normalized.includes('inova')) return 'INV';
if (normalized.includes('ranap')) return 'RNP';
if (normalized.includes('report')) return 'RPT';
if (normalized.includes('farmasi')) return 'FRM';
if (normalized.includes('manager')) return 'MGR';
// Default: ambil inisial dari tiap kata, maksimal 3 huruf
const initials = normalized
.split(/\s+/)
.filter(Boolean)
.map((w) => w[0]?.toUpperCase() || '')
.join('')
.slice(0, 3);
return initials || 'USR';
};
const formatDisplayUserId = (item: HakAksesData): string => {
const tipe = item.tipeUser || 'Unknown';
const prefix = getTipeUserCode(tipe);
// Kelompokkan berdasarkan tipeUser, urut berdasarkan userId untuk konsistensi
const sameTypeItems = allHakAksesData.value
.filter((i) => (i.tipeUser || 'Unknown') === tipe)
.slice()
.sort((a, b) => (a.userId || '').localeCompare(b.userId || ''));
const index = sameTypeItems.findIndex(
(i) => i.userId === item.userId && (i.tipeUser || 'Unknown') === tipe
);
const number = index >= 0 ? index + 1 : sameTypeItems.length + 1;
return `${prefix}-${String(number).padStart(4, '0')}`;
};
const formTitle = computed(() => {
if (viewMode.value === 'editName') return 'Edit Nama Tipe User';
if (viewMode.value === 'editAccess') return 'Edit Hak Akses';
if (viewMode.value === 'view') return 'Detail Hak Akses';
return 'Tambah Hak Akses';
});
// Delete dialog state
const showDeleteDialog = ref(false);
// Functions to reorder data and sync
const toggleReorderMode = () => {
reorderMode.value = !reorderMode.value;
if (reorderMode.value) {
draggableMenus.value = navItemsStore.navItems.map(item => ({ ...item }));
}
};
const saveReorder = () => {
navItemsStore.updateNavItems(draggableMenus.value);
reorderMode.value = false;
reindexData();
};
const cancelReorder = () => {
reorderMode.value = false;
};
// Re-indexes IDs to be sequential
const reindexData = () => {
allHakAksesData.value.forEach((item, index) => {
item.id = index + 1;
});
};
// Dialog state
const showAddEditDialog = computed({
get: () => viewMode.value === 'add' || viewMode.value === 'editName',
set: (val: boolean) => {
if (!val) {
viewMode.value = 'table';
resetForm();
}
}
});
const showEditAccessDialog = computed({
get: () => viewMode.value === 'editAccess',
set: (val: boolean) => {
if (!val) {
viewMode.value = 'table';
resetForm();
}
}
});
const showViewDialog = computed({
get: () => viewMode.value === 'view',
set: (val: boolean) => {
if (!val) {
viewMode.value = 'table';
resetForm();
}
}
});
// Functions for actions
const showAddForm = () => {
resetForm();
viewMode.value = 'add';
};
const viewItem = (item: HakAksesData) => {
editedItem.value = { ...item };
viewMode.value = 'view';
};
const editItem = (item: HakAksesData) => {
editedIndex.value = allHakAksesData.value.findIndex(d => d.id === item.id);
editedItem.value = { ...item };
selectedTipeUser.value = item.tipeUser || null;
viewMode.value = 'editName';
};
const editAccess = (item: HakAksesData) => {
editedIndex.value = allHakAksesData.value.findIndex(d => d.id === item.id);
const baseMenuItems = buildMenuTemplate(navItemsStore.navItems);
const mergedMenuItems = mergePermissions(baseMenuItems, item.hakAksesMenu);
editedItem.value = {
...item,
hakAksesMenu: mergedMenuItems
};
viewMode.value = 'editAccess';
};
const deleteItem = (item: HakAksesData) => {
editedIndex.value = allHakAksesData.value.findIndex(d => d.id === item.id);
showDeleteDialog.value = true;
};
const closeDeleteDialog = () => {
showDeleteDialog.value = false;
editedIndex.value = -1;
};
const confirmDelete = () => {
if (editedIndex.value > -1) {
allHakAksesData.value.splice(editedIndex.value, 1);
reindexData();
}
closeDeleteDialog();
};
const cancelForm = () => {
viewMode.value = 'table';
resetForm();
};
const updateItemAccess = (updatedItem: HakAksesData & { backendPermissions?: BackendPermissionItem[] }) => {
if (editedIndex.value > -1) {
const itemToUpdate = allHakAksesData.value[editedIndex.value];
// If backend permissions are provided, we can store them or map them to menu structure
if (updatedItem.backendPermissions && updatedItem.backendPermissions.length > 0) {
// Store backend permissions for reference
(itemToUpdate as any).backendPermissions = updatedItem.backendPermissions;
// Also update the menu structure if needed
if (updatedItem.hakAksesMenu) {
itemToUpdate.hakAksesMenu = updatedItem.hakAksesMenu;
}
} else {
// Use existing menu structure update
Object.assign(itemToUpdate, updatedItem);
}
snackbar.value = {
show: true,
message: 'Hak akses berhasil diperbarui!',
color: 'success',
timeout: 3000,
};
}
cancelForm();
};
const resetForm = () => {
editedItem.value = {
id: 0,
userId: '',
namaLengkap: '',
namaUser: '',
tipeUser: '',
role: '',
group: '',
namaTipeUser: '',
hakAksesMenu: buildMenuTemplate(navItemsStore.navItems),
};
selectedUserId.value = null;
selectedTipeUser.value = null;
fetchedBackendData.value = [];
};
// Check if a permission is mapped to a menu
const getMappingStatus = (pagename: string): boolean => {
if (!editedItem.value.hakAksesMenu || editedItem.value.hakAksesMenu.length === 0) {
return false;
}
return editedItem.value.hakAksesMenu.some(menu =>
menu.name.toLowerCase() === pagename.toLowerCase() ||
menu.name.toLowerCase().includes(pagename.toLowerCase()) ||
pagename.toLowerCase().includes(menu.name.toLowerCase())
);
};
// Fill user data when user is selected by ID
const fillUserData = (userId: string | null) => {
if (!userId) {
editedItem.value.userId = '';
editedItem.value.namaLengkap = '';
editedItem.value.namaUser = '';
editedItem.value.tipeUser = '';
selectedTipeUser.value = null;
return;
}
const user = availableUsers.value.find(u => u.id === userId);
if (user) {
editedItem.value.userId = user.id;
editedItem.value.namaLengkap = user.namaLengkap || '';
editedItem.value.namaUser = user.namaUser || '';
editedItem.value.tipeUser = user.tipeUser || '';
selectedTipeUser.value = user.tipeUser || null;
// Auto-fill role and group if available
if (user.realmRoles && user.realmRoles.length > 0) {
editedItem.value.role = user.realmRoles[0];
}
if (user.groups && user.groups.length > 0) {
const groupPath = user.groups[0];
const parts = groupPath.split('/').filter(Boolean);
if (parts.length > 1) {
editedItem.value.group = parts[1];
} else if (parts.length === 1) {
editedItem.value.group = parts[0];
}
}
}
};
// Fill user data when tipe user is selected
const fillUserDataByTipeUser = (tipeUser: string | null) => {
if (!tipeUser) {
editedItem.value.tipeUser = '';
return;
}
editedItem.value.tipeUser = tipeUser;
// Find first user with this tipe user and fill their data
const user = availableUsers.value.find(u => u.tipeUser === tipeUser);
if (user) {
editedItem.value.userId = user.id;
editedItem.value.namaLengkap = user.namaLengkap || '';
editedItem.value.namaUser = user.namaUser || '';
// Auto-fill role and group if available
if (user.realmRoles && user.realmRoles.length > 0) {
editedItem.value.role = user.realmRoles[0];
}
if (user.groups && user.groups.length > 0) {
const groupPath = user.groups[0];
const parts = groupPath.split('/').filter(Boolean);
if (parts.length > 1) {
editedItem.value.group = parts[1];
} else if (parts.length === 1) {
editedItem.value.group = parts[0];
}
}
}
};
// Snackbar for notifications
const snackbar = ref({
show: false,
message: '',
color: 'success',
timeout: 3000,
});
// Fetch permissions from backend API
const fetchPermissionsFromBackend = async () => {
if (!editedItem.value.role || !editedItem.value.group) {
snackbar.value = {
show: true,
message: 'Role dan Group harus diisi terlebih dahulu!',
color: 'warning',
timeout: 3000,
};
return;
}
isFetchingPermissions.value = true;
try {
console.log('🔄 Fetching permissions from backend...');
console.log('📋 Request params:', {
roles: editedItem.value.role,
groups: editedItem.value.group,
});
const response = await $fetch<any>('/api/permission', {
query: {
roles: editedItem.value.role,
groups: editedItem.value.group,
},
});
console.log('📦 Full response:', response);
// Map backend permissions to our menu structure
// Backend response structure: { message, data: [...], meta: {...} }
if (response && (response as any).data && Array.isArray((response as any).data)) {
const backendPermissions = (response as any).data as BackendPermissionItem[];
console.log(`✅ Received ${backendPermissions.length} permissions from backend`);
console.log('📄 Backend permissions:', backendPermissions);
// Store fetched data for display
fetchedBackendData.value = backendPermissions;
const menuTemplate = buildMenuTemplate(navItemsStore.navItems);
console.log(`📋 Menu template has ${menuTemplate.length} items`);
// Map backend permissions to menu items
const mappedPermissions = menuTemplate.map((menu) => {
// Find matching permission from backend (by pagename or menu name)
const backendPerm = backendPermissions.find((perm: BackendPermissionItem) =>
perm.pagename?.toLowerCase() === menu.name.toLowerCase() ||
perm.pagename?.toLowerCase().includes(menu.name.toLowerCase()) ||
menu.name.toLowerCase().includes(perm.pagename?.toLowerCase() || '')
);
if (backendPerm) {
console.log(`✅ Matched menu "${menu.name}" with permission "${backendPerm.pagename}"`);
return {
name: menu.name,
canAccess: backendPerm.active || backendPerm.read || false,
canView: backendPerm.read || false,
canAdd: backendPerm.create || false,
canEdit: backendPerm.update || false,
canDelete: backendPerm.delete || false,
};
} else {
console.log(`⚠️ No match found for menu "${menu.name}"`);
}
return menu;
});
editedItem.value.hakAksesMenu = mappedPermissions;
const matchedCount = mappedPermissions.filter(m =>
backendPermissions.some(p =>
p.pagename?.toLowerCase() === m.name.toLowerCase() ||
p.pagename?.toLowerCase().includes(m.name.toLowerCase()) ||
m.name.toLowerCase().includes(p.pagename?.toLowerCase() || '')
)
).length;
snackbar.value = {
show: true,
message: `Berhasil mengambil ${backendPermissions.length} permissions dari backend. ${matchedCount} menu berhasil di-mapping.`,
color: 'success',
timeout: 5000,
};
} else {
console.warn('⚠️ Response does not have expected structure:', response);
fetchedBackendData.value = [];
snackbar.value = {
show: true,
message: 'Response dari backend tidak memiliki struktur yang diharapkan',
color: 'warning',
timeout: 3000,
};
}
} catch (error: any) {
console.error('❌ Error fetching permissions from backend:', error);
snackbar.value = {
show: true,
message: `Gagal mengambil data dari backend: ${error.message || 'Unknown error'}`,
color: 'error',
timeout: 5000,
};
} finally {
isFetchingPermissions.value = false;
}
};
// Load available roles and groups from UserLogin data
const loadAvailableRolesAndGroups = async () => {
try {
const users = await $fetch('/api/users/list').catch(() => []);
const rolesSet = new Set<string>();
const groupsSet = new Set<string>();
// Store users for selection
availableUsers.value = (users as any[]).map((u: any) => ({
id: u.id,
namaLengkap: u.namaLengkap || '',
namaUser: u.namaUser || '',
tipeUser: u.tipeUser || '',
roles: u.roles || [],
realmRoles: u.realmRoles || [],
groups: u.groups || [],
}));
(users as any[]).forEach((u: any) => {
if (Array.isArray(u.roles)) {
u.roles.forEach((r: string) => rolesSet.add(r));
}
if (Array.isArray(u.realmRoles)) {
u.realmRoles.forEach((r: string) => rolesSet.add(r));
}
if (Array.isArray(u.groups)) {
u.groups.forEach((g: string) => {
// Extract group name from path (e.g., "/Instalasi STIM/Devops/Superadmin" -> "STIM")
const parts = g.split('/').filter(Boolean);
if (parts.length > 1) {
groupsSet.add(parts[1]); // Get second part as group name
} else if (parts.length === 1) {
groupsSet.add(parts[0]);
}
});
}
});
availableRoles.value = Array.from(rolesSet).sort();
availableGroups.value = Array.from(groupsSet).sort();
// Extract unique tipe users
const tipeUsersSet = new Set<string>();
(users as any[]).forEach((u: any) => {
if (u.tipeUser) {
tipeUsersSet.add(u.tipeUser);
}
});
availableTipeUsers.value = Array.from(tipeUsersSet).sort();
} catch (error) {
console.error('Error loading roles and groups:', error);
}
};
const saveItem = async () => {
// Validate required fields
if (!editedItem.value.role || !editedItem.value.group) {
snackbar.value = {
show: true,
message: 'Role dan Group wajib diisi!',
color: 'warning',
timeout: 3000,
};
return;
}
// If hakAksesMenu is empty or not fetched yet, fetch from backend
const needsFetch = !editedItem.value.hakAksesMenu ||
editedItem.value.hakAksesMenu.length === 0 ||
!editedItem.value.hakAksesMenu.some(m => m.canAccess || m.canView || m.canAdd || m.canEdit || m.canDelete);
if (needsFetch) {
snackbar.value = {
show: true,
message: 'Mengambil permissions dari backend...',
color: 'info',
timeout: 2000,
};
// Fetch permissions from backend before saving
await fetchPermissionsFromBackend();
// Wait a bit for the fetch to complete
await new Promise(resolve => setTimeout(resolve, 500));
}
// Ensure hakAksesMenu exists
if (!editedItem.value.hakAksesMenu || editedItem.value.hakAksesMenu.length === 0) {
editedItem.value.hakAksesMenu = buildMenuTemplate(navItemsStore.navItems);
}
// Create a clean copy of the item to save
const itemToSave: HakAksesData = {
id: editedItem.value.id || 0,
userId: editedItem.value.userId || '',
namaLengkap: editedItem.value.namaLengkap || '',
namaUser: editedItem.value.namaUser || '',
tipeUser: editedItem.value.tipeUser || '',
role: editedItem.value.role,
group: editedItem.value.group,
namaTipeUser: editedItem.value.namaTipeUser || editedItem.value.tipeUser || '',
hakAksesMenu: editedItem.value.hakAksesMenu || [],
};
console.log('💾 Saving item:', itemToSave);
console.log('📊 Current allHakAksesData length:', allHakAksesData.value.length);
if (editedIndex.value > -1) {
// Edit item
allHakAksesData.value[editedIndex.value] = { ...itemToSave };
console.log('✅ Item updated at index:', editedIndex.value);
snackbar.value = {
show: true,
message: 'Data hak akses berhasil diperbarui!',
color: 'success',
timeout: 3000,
};
} else {
// Add item with a new ID
const newId = allHakAksesData.value.length > 0
? Math.max(...allHakAksesData.value.map(i => i.id)) + 1
: 1;
itemToSave.id = newId;
allHakAksesData.value.push({ ...itemToSave });
console.log('✅ New item added with ID:', newId);
console.log('📊 New allHakAksesData length:', allHakAksesData.value.length);
snackbar.value = {
show: true,
message: 'Data hak akses berhasil ditambahkan!',
color: 'success',
timeout: 3000,
};
}
// Force update localStorage by triggering reactivity
// useLocalStorage should auto-save, but we ensure it by reassigning
await nextTick();
allHakAksesData.value = [...allHakAksesData.value];
// Double-check localStorage is updated
await nextTick();
if (typeof window !== 'undefined') {
const stored = localStorage.getItem('allHakAksesData');
console.log('💾 localStorage after save:', stored ? JSON.parse(stored).length + ' items' : 'empty');
}
console.log('💾 Final allHakAksesData:', allHakAksesData.value);
console.log('💾 Final allHakAksesData length:', allHakAksesData.value.length);
// Reset editedIndex after save
editedIndex.value = -1;
cancelForm();
};
// Handle initial data load and ID re-indexing
onMounted(async () => {
console.log('🚀 HakAkses page mounted');
console.log('📊 Initial allHakAksesData:', allHakAksesData.value);
console.log('📊 Initial allHakAksesData length:', allHakAksesData.value.length);
// Only reindex if data exists and IDs are not sequential
if (allHakAksesData.value.length > 0) {
const needsReindex = allHakAksesData.value.some((item, index) => item.id !== index + 1);
if (needsReindex) {
console.log('🔄 Re-indexing data...');
reindexData();
// Force save after reindex
allHakAksesData.value = [...allHakAksesData.value];
}
}
await loadAvailableRolesAndGroups();
console.log('✅ After load - allHakAksesData length:', allHakAksesData.value.length);
console.log('✅ After load - allHakAksesData:', allHakAksesData.value);
// Debug: Check localStorage directly
if (typeof window !== 'undefined') {
const stored = localStorage.getItem('allHakAksesData');
console.log('💾 localStorage value:', stored);
if (stored) {
try {
const parsed = JSON.parse(stored);
console.log('📦 Parsed localStorage data:', parsed);
console.log('📦 Parsed data length:', parsed.length);
} catch (e) {
console.error('❌ Error parsing localStorage:', e);
}
} else {
console.log('⚠️ No data in localStorage');
}
}
});
</script>
<style scoped lang="scss">
// Colors from Design System
$neutral-900: #212121;
$neutral-800: #4D4D4D;
$neutral-700: #717171;
$neutral-600: #89939E;
$neutral-500: #ABBED1;
$neutral-400: #E5F7FA;
$neutral-300: #F5F7FA;
$neutral-100: #FFFFFF;
$success-700: #1B6E53;
$success-600: #009262;
$success-400: #32C997;
$success-300: #84DFC1;
$success-200: #F1FBF8;
$danger-600: #E02B1D;
// Font Family & Weights
$font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
$font-weight-regular: 400;
$font-weight-medium: 500;
$font-weight-semibold: 600;
// Apply font family
* {
font-family: $font-family-base;
}
// Ensure page container doesn't shrink
.hak-akses-page,
.hak-akses-page * {
box-sizing: border-box;
}
.hak-akses-page {
font-family: $font-family-base;
background-color: $neutral-300;
min-height: 100vh;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
padding: 16px !important;
box-sizing: border-box;
margin: 0 !important;
padding-left: 16px !important;
padding-right: 16px !important;
display: block !important;
position: relative !important;
}
// Ensure no max-width constraints and white background
.hak-akses-page :deep(.v-card) {
max-width: 100% !important;
width: 100% !important;
min-width: 100% !important;
background-color: $neutral-100 !important;
border-radius: 12px !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
margin: 0 !important;
}
// Override v-main constraints from layout
:deep(.v-main) {
padding: 0 !important;
width: 100% !important;
max-width: 100% !important;
}
:deep(.v-main__wrap) {
padding: 0 !important;
max-width: 100% !important;
width: 100% !important;
}
// Override any container constraints
:deep(.v-container),
:deep(.container) {
max-width: 100% !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
// Ensure v-card-text has white background and proper padding
.hak-akses-page :deep(.v-card-text) {
padding: 24px !important;
background-color: $neutral-100 !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
// Ensure wrapper div doesn't shrink
.hak-akses-page > div {
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
display: block !important;
}
// Ensure v-card wrapper doesn't shrink
.hak-akses-page > div > .v-card {
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
display: block !important;
}
// Ensure all child elements don't constrain width
.hak-akses-page > div > * {
max-width: 100% !important;
}
// ============================================
// PAGE HEADER
// ============================================
.page-header {
background: linear-gradient(135deg, $success-600 0%, $success-700 100%);
border-radius: 16px 16px 0 0;
box-shadow: 0 4px 16px rgba(0, 146, 98, 0.2);
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32px;
color: $neutral-100;
}
.header-left {
display: flex;
align-items: center;
}
.header-icon {
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
padding: 16px;
margin-right: 20px;
backdrop-filter: blur(10px);
}
.page-title {
font-size: 36px;
line-height: 44px;
font-weight: $font-weight-semibold;
margin: 0;
color: $neutral-100;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.page-subtitle {
margin: 4px 0 0 0;
opacity: 0.9;
font-size: 16px;
line-height: 24px;
font-weight: $font-weight-regular;
color: $neutral-100;
}
.header-stats {
display: flex;
align-items: center;
gap: 8px;
}
.stat-chip {
color: $success-600 !important;
font-weight: $font-weight-semibold;
font-size: 14px;
line-height: 20px;
}
.add-btn {
font-weight: $font-weight-semibold;
text-transform: none;
letter-spacing: 0.5px;
font-size: 16px;
line-height: 24px;
color: $success-600 !important;
}
// ============================================
// DATA TABLE
// ============================================
.custom-table :deep(table) {
border-collapse: collapse;
}
.custom-table :deep(th) {
background-color: #f5f5f5 !important;
font-weight: bold;
font-size: 0.875rem; /* text-sm */
}
.custom-table :deep(td) {
padding-top: 12px !important;
padding-bottom: 12px !important;
}
.v-btn--icon {
border-radius: 8px;
}
.v-select :deep(.v-field__input) {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
// Ensure search field stays large
.hak-akses-page :deep(.v-text-field[label="Search"]) {
min-width: 300px !important;
width: 100% !important;
max-width: 400px !important;
}
.hak-akses-page :deep(.v-text-field[label="Search"] .v-field) {
min-width: 300px !important;
}
.handle {
cursor: grab;
}
// ============================================
// MODERN DIALOG STYLING
// ============================================
// Ensure overlay doesn't hide content completely
:deep(.v-overlay__scrim) {
background-color: rgba(0, 0, 0, 0.4) !important;
}
.modern-dialog-card {
border-radius: 16px !important;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12) !important;
}
.dialog-header {
padding: 32px;
color: white;
position: relative;
overflow: hidden;
}
.dialog-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
pointer-events: none;
}
.dialog-header-content {
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
z-index: 1;
}
.dialog-header-left {
display: flex;
align-items: center;
gap: 20px;
}
.dialog-avatar {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
}
.dialog-header-text {
flex: 1;
}
.dialog-title {
font-size: 28px;
font-weight: $font-weight-semibold;
margin: 0 0 8px 0;
color: white;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.dialog-subtitle {
font-size: 14px;
margin: 0;
opacity: 0.9;
color: white;
}
.dialog-close-btn {
color: white !important;
background: rgba(255, 255, 255, 0.2) !important;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.dialog-close-btn:hover {
background: rgba(255, 255, 255, 0.3) !important;
transform: rotate(90deg);
}
.header-add {
background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);
}
.header-edit {
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
}
.header-edit-access {
background: linear-gradient(135deg, #009262 0%, #1B6E53 100%);
}
.header-view {
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
}
.dialog-content {
padding: 32px !important;
background-color: #f5f7fa;
max-height: 70vh;
overflow-y: auto;
}
.dialog-actions {
padding: 20px 32px !important;
background-color: white;
border-top: 1px solid rgba(0, 0, 0, 0.08);
}
// Dialog animation
.dialog-bottom-transition-enter-active,
.dialog-bottom-transition-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.dialog-bottom-transition-enter-from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
.dialog-bottom-transition-leave-to {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
// ============================================
// RESPONSIVE
// ============================================
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 16px;
}
.header-stats {
width: 100%;
flex-wrap: wrap;
}
.dialog-header {
padding: 24px;
}
.dialog-title {
font-size: 24px;
}
.dialog-content {
padding: 24px !important;
}
.dialog-actions {
padding: 16px 24px !important;
}
.dialog-header-left {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
}
// Ensure container never shrinks on any screen size
@media (min-width: 0px) {
.hak-akses-page {
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
}
.hak-akses-page :deep(.v-card) {
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
}
}
</style>