2012 lines
64 KiB
Vue
2012 lines
64 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">
|
|
<v-alert
|
|
type="info"
|
|
variant="tonal"
|
|
density="comfortable"
|
|
class="mb-4"
|
|
>
|
|
<v-alert-title>Group-Based Access</v-alert-title>
|
|
Hak akses akan diberikan ke semua user yang memiliki group dan role yang dipilih.
|
|
Setiap user dapat memiliki multiple hak akses dari berbagai group.
|
|
</v-alert>
|
|
</v-col>
|
|
</v-row>
|
|
<v-row>
|
|
<v-col cols="12" md="6">
|
|
<v-label class="font-weight-bold text-medium-emphasis">Role <span class="text-error">*</span></v-label>
|
|
<v-select
|
|
v-model="editedItem.role"
|
|
:items="availableRoles"
|
|
placeholder="Pilih Role"
|
|
variant="outlined"
|
|
density="comfortable"
|
|
class="mt-1"
|
|
required
|
|
></v-select>
|
|
</v-col>
|
|
<v-col cols="12" md="6">
|
|
<v-label class="font-weight-bold text-medium-emphasis">Group <span class="text-error">*</span></v-label>
|
|
<v-select
|
|
v-model="selectedGroup"
|
|
:items="availableGroupPaths"
|
|
item-title="label"
|
|
item-value="value"
|
|
placeholder="Pilih Group"
|
|
variant="outlined"
|
|
density="comfortable"
|
|
class="mt-1"
|
|
required
|
|
@update:model-value="onGroupSelected"
|
|
>
|
|
<template v-slot:item="{ props, item }">
|
|
<v-list-item v-bind="props" :title="item.raw.label" :subtitle="item.raw.path"></v-list-item>
|
|
</template>
|
|
</v-select>
|
|
<div v-if="selectedGroupUsers.length > 0" class="mt-2">
|
|
<v-chip size="small" color="info" class="mr-1 mb-1">
|
|
{{ selectedGroupUsers.length }} user akan mendapatkan hak akses ini
|
|
</v-chip>
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
<v-row v-if="selectedGroupUsers.length > 0">
|
|
<v-col cols="12">
|
|
<v-label class="font-weight-bold text-medium-emphasis mb-2">User yang akan mendapatkan hak akses:</v-label>
|
|
<v-card variant="outlined" class="pa-3">
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<v-chip
|
|
v-for="user in selectedGroupUsers"
|
|
:key="user.id"
|
|
size="small"
|
|
color="primary"
|
|
variant="tonal"
|
|
>
|
|
{{ user.namaLengkap || user.namaUser }} ({{ user.namaUser }})
|
|
</v-chip>
|
|
</div>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
<v-row>
|
|
<v-col cols="12" md="6">
|
|
<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">
|
|
<v-label class="font-weight-bold text-medium-emphasis mb-2">Hak Akses (Multiple Groups)</v-label>
|
|
<v-card variant="outlined" class="pa-3">
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<v-chip
|
|
v-for="(hakAkses, idx) in viewModeHakAksesList"
|
|
:key="idx"
|
|
size="small"
|
|
color="primary"
|
|
variant="tonal"
|
|
>
|
|
<v-icon start size="14">mdi-account-group</v-icon>
|
|
{{ hakAkses.group }} / {{ hakAkses.role }}
|
|
</v-chip>
|
|
<span v-if="viewModeHakAksesList.length === 0" class="text-grey">Tidak ada hak akses</span>
|
|
</div>
|
|
</v-card>
|
|
</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"
|
|
>
|
|
<v-icon left size="20">mdi-plus-circle</v-icon>
|
|
Tambah Baru
|
|
</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="4">
|
|
<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="4">
|
|
<v-select
|
|
v-model="filterRole"
|
|
:items="availableRoles"
|
|
label="Filter Role"
|
|
variant="outlined"
|
|
density="compact"
|
|
clearable
|
|
hide-details
|
|
/>
|
|
</v-col>
|
|
<v-col cols="12" sm="4">
|
|
<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 dengan format generated -->
|
|
<template v-slot:item.userId="{ item }">
|
|
<div class="text-center">
|
|
{{ generateDisplayUserId(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>
|
|
|
|
<!-- Hak Akses List - menampilkan multiple hak akses per user -->
|
|
<template v-slot:item.hakAksesList="{ item }">
|
|
<div class="d-flex flex-wrap gap-1">
|
|
<v-tooltip
|
|
v-for="(hakAkses, idx) in item.hakAksesList"
|
|
:key="idx"
|
|
location="top"
|
|
>
|
|
<template v-slot:activator="{ props }">
|
|
<v-chip
|
|
v-bind="props"
|
|
:color="hakAkses.isGroupBased ? 'primary' : 'grey'"
|
|
size="small"
|
|
variant="tonal"
|
|
class="mb-1"
|
|
>
|
|
<v-icon start size="14">{{ hakAkses.isGroupBased ? 'mdi-account-group' : 'mdi-account' }}</v-icon>
|
|
{{ hakAkses.group }} / {{ hakAkses.role }}
|
|
</v-chip>
|
|
</template>
|
|
<span>{{ hakAkses.groupPath || hakAkses.group }} - {{ hakAkses.role }}</span>
|
|
</v-tooltip>
|
|
<span v-if="item.hakAksesList.length === 0" class="text-grey">-</span>
|
|
</div>
|
|
</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-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 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; // Optional: untuk backward compatibility dan tracking
|
|
namaLengkap?: string;
|
|
namaUser?: string;
|
|
tipeUser?: string;
|
|
role: string;
|
|
group: string;
|
|
namaTipeUser: string;
|
|
hakAksesMenu: HakAksesMenu[];
|
|
// New: untuk group-based access
|
|
isGroupBased?: boolean; // Flag untuk membedakan group-based vs individual
|
|
groupPath?: string; // Full group path untuk matching
|
|
}
|
|
|
|
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 allHakAksesData = useLocalStorage<HakAksesData[]>('allHakAksesData', []);
|
|
const navItemsStore = useNavItemsStore();
|
|
|
|
// Data table headers
|
|
// Changed to show user with their multiple access rights
|
|
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: 'Hak Akses', key: 'hakAksesList' as const, sortable: false },
|
|
{ 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 selectedGroup = ref<string | null>(null);
|
|
const selectedGroupUsers = ref<any[]>([]);
|
|
const isFetchingPermissions = ref(false);
|
|
const fetchedBackendData = ref<BackendPermissionItem[]>([]);
|
|
|
|
// Group paths dengan label yang lebih user-friendly
|
|
const availableGroupPaths = computed(() => {
|
|
const groupMap = new Map<string, { value: string; label: string; path: string }>();
|
|
|
|
availableUsers.value.forEach((user) => {
|
|
if (Array.isArray(user.groups)) {
|
|
user.groups.forEach((groupPath: string) => {
|
|
if (groupPath && !groupMap.has(groupPath)) {
|
|
const parts = groupPath.split('/').filter(Boolean);
|
|
const label = parts.length > 1 ? parts[1] : parts[0] || groupPath;
|
|
groupMap.set(groupPath, {
|
|
value: groupPath,
|
|
label: label,
|
|
path: groupPath,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
return Array.from(groupMap.values()).sort((a, b) => a.label.localeCompare(b.label));
|
|
});
|
|
|
|
// --- 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 filterTipeUser = ref<string | null>(null);
|
|
const filterRole = ref<string | null>(null);
|
|
const filterGroup = ref<string | null>(null);
|
|
|
|
|
|
// Group hak akses data by user untuk menampilkan multiple hak akses per user
|
|
const groupedHakAksesByUser = computed(() => {
|
|
const userMap = new Map<string, {
|
|
userId: string;
|
|
namaLengkap: string;
|
|
namaUser: string;
|
|
tipeUser: string;
|
|
hakAksesList: HakAksesData[];
|
|
}>();
|
|
|
|
allHakAksesData.value.forEach((item) => {
|
|
// Untuk group-based, kita perlu mencari user yang memiliki group tersebut
|
|
if (item.isGroupBased && item.groupPath) {
|
|
// Find users with this group
|
|
const usersWithGroup = availableUsers.value.filter((u: any) =>
|
|
Array.isArray(u.groups) && u.groups.includes(item.groupPath)
|
|
);
|
|
|
|
usersWithGroup.forEach((user: any) => {
|
|
const key = user.id;
|
|
if (!userMap.has(key)) {
|
|
userMap.set(key, {
|
|
userId: user.id,
|
|
namaLengkap: user.namaLengkap || '',
|
|
namaUser: user.namaUser || '',
|
|
tipeUser: user.tipeUser || '',
|
|
hakAksesList: [],
|
|
});
|
|
}
|
|
const userData = userMap.get(key)!;
|
|
userData.hakAksesList.push(item);
|
|
});
|
|
} else if (item.userId) {
|
|
// Legacy: individual user access
|
|
const key = item.userId;
|
|
if (!userMap.has(key)) {
|
|
userMap.set(key, {
|
|
userId: item.userId,
|
|
namaLengkap: item.namaLengkap || '',
|
|
namaUser: item.namaUser || '',
|
|
tipeUser: item.tipeUser || '',
|
|
hakAksesList: [],
|
|
});
|
|
}
|
|
const userData = userMap.get(key)!;
|
|
userData.hakAksesList.push(item);
|
|
}
|
|
});
|
|
|
|
return Array.from(userMap.values()).map((userData, index) => ({
|
|
id: index + 1,
|
|
...userData,
|
|
}));
|
|
});
|
|
|
|
// Data tabel setelah difilter oleh dropdown (menggunakan grouped data)
|
|
const filteredHakAksesData = computed(() =>
|
|
groupedHakAksesByUser.value.filter((item) => {
|
|
const matchTipeUser = !filterTipeUser.value || (item.tipeUser || '') === filterTipeUser.value;
|
|
const matchRole = !filterRole.value ||
|
|
item.hakAksesList.some(ha => ha.role === filterRole.value);
|
|
const matchGroup = !filterGroup.value ||
|
|
item.hakAksesList.some(ha => ha.group === filterGroup.value || ha.groupPath === filterGroup.value);
|
|
return matchTipeUser && matchRole && matchGroup;
|
|
}),
|
|
);
|
|
|
|
// Generate display User ID dengan format US-001, US-002, dll
|
|
// Berdasarkan urutan di filteredHakAksesData (bukan user ID asli)
|
|
const generateDisplayUserId = (item: any): string => {
|
|
if (!item || !item.userId) {
|
|
return 'US-000';
|
|
}
|
|
|
|
// Cari index di filteredHakAksesData
|
|
const index = filteredHakAksesData.value.findIndex(
|
|
(i) => i.userId === item.userId
|
|
);
|
|
|
|
// Jika tidak ditemukan, cari di groupedHakAksesByUser
|
|
const finalIndex = index >= 0
|
|
? index
|
|
: groupedHakAksesByUser.value.findIndex((i) => i.userId === item.userId);
|
|
|
|
const number = finalIndex >= 0 ? finalIndex + 1 : 1;
|
|
|
|
return `US-${String(number).padStart(3, '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);
|
|
|
|
// 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 viewModeHakAksesList = ref<HakAksesData[]>([]);
|
|
|
|
const viewItem = (item: any) => {
|
|
// Item is from groupedHakAksesByUser
|
|
// For view, we'll show all hak akses for this user
|
|
// Store the list of hak akses for display
|
|
viewModeHakAksesList.value = item.hakAksesList || [];
|
|
|
|
if (item.hakAksesList && item.hakAksesList.length > 0) {
|
|
// Merge all hak akses menus (OR logic - if any access allows, then allow)
|
|
const baseMenuItems = buildMenuTemplate(navItemsStore.navItems);
|
|
const mergedMenu: HakAksesMenu[] = baseMenuItems.map(menu => {
|
|
let canAccess = false;
|
|
let canView = false;
|
|
let canAdd = false;
|
|
let canEdit = false;
|
|
let canDelete = false;
|
|
|
|
item.hakAksesList.forEach((ha: HakAksesData) => {
|
|
const haMenu = ha.hakAksesMenu.find(m => m.name === menu.name);
|
|
if (haMenu) {
|
|
canAccess = canAccess || haMenu.canAccess;
|
|
canView = canView || haMenu.canView;
|
|
canAdd = canAdd || haMenu.canAdd;
|
|
canEdit = canEdit || haMenu.canEdit;
|
|
canDelete = canDelete || haMenu.canDelete;
|
|
}
|
|
});
|
|
|
|
return {
|
|
name: menu.name,
|
|
canAccess,
|
|
canView,
|
|
canAdd,
|
|
canEdit,
|
|
canDelete,
|
|
};
|
|
});
|
|
|
|
editedItem.value = {
|
|
id: item.id,
|
|
userId: item.userId,
|
|
namaLengkap: item.namaLengkap,
|
|
namaUser: item.namaUser,
|
|
tipeUser: item.tipeUser,
|
|
role: '', // Not used in view mode
|
|
group: '', // Not used in view mode
|
|
namaTipeUser: item.hakAksesList[0]?.namaTipeUser || '',
|
|
hakAksesMenu: mergedMenu,
|
|
};
|
|
} else {
|
|
editedItem.value = {
|
|
id: item.id,
|
|
userId: item.userId,
|
|
namaLengkap: item.namaLengkap,
|
|
namaUser: item.namaUser,
|
|
tipeUser: item.tipeUser,
|
|
role: '',
|
|
group: '',
|
|
namaTipeUser: '',
|
|
hakAksesMenu: [],
|
|
};
|
|
}
|
|
viewMode.value = 'view';
|
|
};
|
|
|
|
const editItem = (item: any) => {
|
|
// Item is now from groupedHakAksesByUser, so we need to edit the first hak akses or create new
|
|
// For now, we'll edit the group-based access if it exists
|
|
if (item.hakAksesList && item.hakAksesList.length > 0) {
|
|
// Find the group-based access for this user
|
|
const groupBasedAccess = item.hakAksesList.find((ha: HakAksesData) => ha.isGroupBased);
|
|
if (groupBasedAccess) {
|
|
editedIndex.value = allHakAksesData.value.findIndex(d => d.id === groupBasedAccess.id);
|
|
editedItem.value = { ...groupBasedAccess };
|
|
selectedGroup.value = groupBasedAccess.groupPath || null;
|
|
onGroupSelected(selectedGroup.value);
|
|
viewMode.value = 'editName';
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Fallback: create new access
|
|
resetForm();
|
|
viewMode.value = 'add';
|
|
};
|
|
|
|
const editAccess = (item: any) => {
|
|
// Item is from groupedHakAksesByUser
|
|
// For edit access, we'll edit all group-based accesses for this user
|
|
// For simplicity, we'll edit the first one or create new
|
|
if (item.hakAksesList && item.hakAksesList.length > 0) {
|
|
const groupBasedAccess = item.hakAksesList.find((ha: HakAksesData) => ha.isGroupBased);
|
|
if (groupBasedAccess) {
|
|
editedIndex.value = allHakAksesData.value.findIndex(d => d.id === groupBasedAccess.id);
|
|
const baseMenuItems = buildMenuTemplate(navItemsStore.navItems);
|
|
const mergedMenuItems = mergePermissions(baseMenuItems, groupBasedAccess.hakAksesMenu);
|
|
|
|
editedItem.value = {
|
|
...groupBasedAccess,
|
|
hakAksesMenu: mergedMenuItems
|
|
};
|
|
selectedGroup.value = groupBasedAccess.groupPath || null;
|
|
onGroupSelected(selectedGroup.value);
|
|
viewMode.value = 'editAccess';
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Fallback
|
|
resetForm();
|
|
viewMode.value = 'add';
|
|
};
|
|
|
|
const deleteItem = (item: any) => {
|
|
// Item is from groupedHakAksesByUser
|
|
// We need to delete all group-based accesses for this user
|
|
// For now, we'll delete the first group-based access
|
|
if (item.hakAksesList && item.hakAksesList.length > 0) {
|
|
const groupBasedAccess = item.hakAksesList.find((ha: HakAksesData) => ha.isGroupBased);
|
|
if (groupBasedAccess) {
|
|
editedIndex.value = allHakAksesData.value.findIndex(d => d.id === groupBasedAccess.id);
|
|
showDeleteDialog.value = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Fallback
|
|
editedIndex.value = -1;
|
|
showDeleteDialog.value = false;
|
|
};
|
|
|
|
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,
|
|
role: '',
|
|
group: '',
|
|
groupPath: '',
|
|
namaTipeUser: '',
|
|
hakAksesMenu: buildMenuTemplate(navItemsStore.navItems),
|
|
isGroupBased: true,
|
|
};
|
|
selectedUserId.value = null;
|
|
selectedTipeUser.value = null;
|
|
selectedGroup.value = null;
|
|
selectedGroupUsers.value = [];
|
|
fetchedBackendData.value = [];
|
|
viewModeHakAksesList.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())
|
|
);
|
|
};
|
|
|
|
// Handle group selection
|
|
const onGroupSelected = (groupPath: string | null) => {
|
|
if (!groupPath) {
|
|
selectedGroupUsers.value = [];
|
|
editedItem.value.group = '';
|
|
editedItem.value.groupPath = '';
|
|
return;
|
|
}
|
|
|
|
// Extract group name from path
|
|
const parts = groupPath.split('/').filter(Boolean);
|
|
editedItem.value.group = parts.length > 1 ? parts[1] : parts[0] || groupPath;
|
|
editedItem.value.groupPath = groupPath;
|
|
|
|
// Find all users with this group
|
|
selectedGroupUsers.value = availableUsers.value.filter((u: any) =>
|
|
Array.isArray(u.groups) && u.groups.includes(groupPath)
|
|
);
|
|
|
|
console.log(`📋 Selected group: ${groupPath}, found ${selectedGroupUsers.value.length} users`);
|
|
};
|
|
|
|
|
|
// 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 for filter
|
|
} 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();
|
|
|
|
console.log(`✅ Loaded ${availableUsers.value.length} users, ${availableRoles.value.length} roles, ${availableGroups.value.length} groups`);
|
|
} catch (error) {
|
|
console.error('Error loading roles and groups:', error);
|
|
}
|
|
};
|
|
|
|
const saveItem = async () => {
|
|
// Validate required fields
|
|
if (!editedItem.value.role || !selectedGroup.value) {
|
|
snackbar.value = {
|
|
show: true,
|
|
message: 'Role dan Group wajib diisi!',
|
|
color: 'warning',
|
|
timeout: 3000,
|
|
};
|
|
return;
|
|
}
|
|
|
|
// Check if permissions have been fetched from backend
|
|
// If not, show warning and ask user to fetch first
|
|
const hasFetchedPermissions = fetchedBackendData.value.length > 0 ||
|
|
(editedItem.value.hakAksesMenu &&
|
|
editedItem.value.hakAksesMenu.length > 0 &&
|
|
editedItem.value.hakAksesMenu.some(m => m.canAccess || m.canView || m.canAdd || m.canEdit || m.canDelete));
|
|
|
|
if (!hasFetchedPermissions) {
|
|
snackbar.value = {
|
|
show: true,
|
|
message: 'Silakan klik tombol "Ambil Data dari Backend API" terlebih dahulu untuk mengambil permissions!',
|
|
color: 'warning',
|
|
timeout: 5000,
|
|
};
|
|
return;
|
|
}
|
|
|
|
// Ensure hakAksesMenu exists (fallback to empty template if somehow missing)
|
|
if (!editedItem.value.hakAksesMenu || editedItem.value.hakAksesMenu.length === 0) {
|
|
editedItem.value.hakAksesMenu = buildMenuTemplate(navItemsStore.navItems);
|
|
}
|
|
|
|
// Check if this group+role combination already exists
|
|
const existingIndex = allHakAksesData.value.findIndex(
|
|
item => item.isGroupBased &&
|
|
item.groupPath === editedItem.value.groupPath &&
|
|
item.role === editedItem.value.role
|
|
);
|
|
|
|
if (editedIndex.value > -1 || existingIndex > -1) {
|
|
// Edit existing group-based access
|
|
const indexToUpdate = editedIndex.value > -1 ? editedIndex.value : existingIndex;
|
|
const existingItem = allHakAksesData.value[indexToUpdate];
|
|
|
|
// Update the group-based access entry
|
|
allHakAksesData.value[indexToUpdate] = {
|
|
...existingItem,
|
|
role: editedItem.value.role,
|
|
group: editedItem.value.group,
|
|
groupPath: editedItem.value.groupPath,
|
|
namaTipeUser: editedItem.value.namaTipeUser || '',
|
|
hakAksesMenu: editedItem.value.hakAksesMenu || [],
|
|
isGroupBased: true,
|
|
};
|
|
|
|
console.log('✅ Group-based access updated at index:', indexToUpdate);
|
|
snackbar.value = {
|
|
show: true,
|
|
message: `Hak akses untuk group "${editedItem.value.group}" berhasil diperbarui! ${selectedGroupUsers.value.length} user terpengaruh.`,
|
|
color: 'success',
|
|
timeout: 4000,
|
|
};
|
|
} else {
|
|
// Create new group-based access entry
|
|
const newId = allHakAksesData.value.length > 0
|
|
? Math.max(...allHakAksesData.value.map(i => i.id)) + 1
|
|
: 1;
|
|
|
|
const groupBasedAccess: HakAksesData = {
|
|
id: newId,
|
|
role: editedItem.value.role,
|
|
group: editedItem.value.group,
|
|
groupPath: editedItem.value.groupPath || selectedGroup.value || '',
|
|
namaTipeUser: editedItem.value.namaTipeUser || '',
|
|
hakAksesMenu: editedItem.value.hakAksesMenu || [],
|
|
isGroupBased: true,
|
|
};
|
|
|
|
allHakAksesData.value.push(groupBasedAccess);
|
|
|
|
console.log('✅ New group-based access added with ID:', newId);
|
|
console.log(`📋 Affected users: ${selectedGroupUsers.value.length}`);
|
|
snackbar.value = {
|
|
show: true,
|
|
message: `Hak akses untuk group "${editedItem.value.group}" berhasil ditambahkan! ${selectedGroupUsers.value.length} user mendapatkan hak akses ini.`,
|
|
color: 'success',
|
|
timeout: 4000,
|
|
};
|
|
}
|
|
|
|
// Force update localStorage by triggering reactivity
|
|
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> |