diff --git a/components/layout/full/vertical-header/ProfileDD.vue b/components/layout/full/vertical-header/ProfileDD.vue
index dcc4369..f19e6ac 100644
--- a/components/layout/full/vertical-header/ProfileDD.vue
+++ b/components/layout/full/vertical-header/ProfileDD.vue
@@ -10,7 +10,6 @@ const auth = useAuth();
// Load user data on mount
onMounted(async () => {
if (!auth.user.value) {
- console.log('ProfileDD: Loading user data...');
await auth.checkAuth();
}
});
@@ -113,7 +112,7 @@ const sessionInfo = computed(() => ({
:loading="auth.isLoading.value"
:disabled="auth.isLoading.value"
>
- Logout
+ Keluar
diff --git a/components/layout/full/vertical-sidebar/VerticalSidebar.vue b/components/layout/full/vertical-sidebar/VerticalSidebar.vue
index 4a11733..d98c378 100644
--- a/components/layout/full/vertical-sidebar/VerticalSidebar.vue
+++ b/components/layout/full/vertical-sidebar/VerticalSidebar.vue
@@ -1,5 +1,5 @@
+```
+
+#### Option B: Global di nuxt.config.ts
+```typescript
+export default defineNuxtConfig({
+ router: {
+ middleware: ['auth', 'check-page-access']
+ }
+})
+```
+
+### 2. Manual Check di Component
+
+Untuk show/hide content berdasarkan akses:
+
+```vue
+
+
+
+
+ Settings
+
+
+```
+
+### 3. Conditional Rendering dengan Allowed Pages
+
+```vue
+
+
+
+
+
+
+
+```
+
+## Mengelola Hak Akses
+
+### 1. Menambah Pages ke Role
+
+Di halaman **Hak Akses** (`/setting/hak-akses`):
+
+1. Pilih role yang ingin diedit
+2. Set status menjadi **Aktif**
+3. Pilih pages yang boleh diakses
+4. Simpan
+
+### 2. Format Path di Pages Array
+
+Path harus **exact match** dengan route path:
+
+â
**Benar:**
+- `/dashboard`
+- `/antrean/all`
+- `/setting/hak-akses`
+
+â **Salah:**
+- `dashboard` (tanpa slash)
+- `/dashboard/` (dengan trailing slash)
+- `/antrean/*` (wildcard tidak support)
+
+### 3. Testing Hak Akses
+
+1. Login dengan user yang punya role tertentu
+2. Cek console log: `Allowed pages for user: [...]`
+3. Sidebar akan otomatis filter
+4. Coba akses URL langsung yang tidak ada di allowed pages
+5. Harus redirect ke dashboard atau first allowed page
+
+## Troubleshooting
+
+### Menu Tidak Muncul di Sidebar
+
+**Penyebab:**
+1. Status hakAkses masih **tidak aktif**
+2. Path di pages array tidak match dengan `to` di sidebarItem.ts
+3. User tidak punya role yang sesuai
+
+**Solusi:**
+```typescript
+// Check di console
+const { getAllowedPages } = useHakAkses();
+const pages = await getAllowedPages();
+console.log('Allowed pages:', pages);
+
+// Check role user
+const { getRoles } = useRoles();
+console.log('User roles:', getRoles());
+```
+
+### User Bisa Akses Page Tanpa Permission
+
+**Penyebab:**
+- Middleware belum ditambahkan di page
+
+**Solusi:**
+```vue
+
+```
+
+### Error "You do not have access to any pages"
+
+**Penyebab:**
+- Semua hakAkses user statusnya **tidak aktif**
+- Pages array kosong di semua hakAkses user
+
+**Solusi:**
+1. Buka `/setting/hak-akses`
+2. Edit hakAkses yang sesuai dengan role user
+3. Set status **Aktif**
+4. Tambahkan minimal `/dashboard` di pages
+5. Simpan
+
+## Best Practices
+
+### 1. Default Pages untuk Semua Role
+
+Pastikan semua role aktif punya akses ke:
+- `/dashboard` - Landing page setelah login
+
+### 2. Hierarchical Access
+
+Jika user punya akses ke child path, berikan juga akses ke parent:
+```json
+{
+ "pages": [
+ "/setting", // Parent
+ "/setting/hak-akses", // Child
+ "/setting/user" // Child
+ ]
+}
+```
+
+### 3. Testing Multiple Roles
+
+Test dengan user yang punya multiple roles untuk pastikan combine pages works:
+
+```typescript
+// User dengan role: ['admin', 'dokter']
+// hakAkses admin: ['/dashboard', '/setting/user']
+// hakAkses dokter: ['/dashboard', '/antrean/all']
+// Result: ['/dashboard', '/setting/user', '/antrean/all'] â
+```
+
+### 4. Sync Status
+
+Selalu pastikan:
+1. â
Role di Keycloak
+2. â
Auto-sync ke hakAkses.json saat login
+3. â
Status diaktifkan di UI hak akses
+4. â
Pages ditambahkan
+
+## API Endpoints
+
+### Get All Hak Akses
+```typescript
+const response = await $fetch('/api/hak-akses');
+// Returns all hakAkses with their pages
+```
+
+### Get User Allowed Pages (Client Side)
+```typescript
+const { getAllowedPages } = useHakAkses();
+const pages = await getAllowedPages();
+// Returns only pages from active hakAkses that match user's roles
+```
+
+## Security Notes
+
+1. **Server-side validation** diperlukan untuk API calls yang sensitive
+2. Middleware hanya **client-side protection** - jangan andalkan untuk security critical operations
+3. Gunakan `requireRole()` atau `requireAnyRole()` di server API handlers untuk real protection
+4. Pages array di hakAkses.json bisa diedit manual, tapi lebih baik via UI
+
+## Example: Complete Setup
+
+```typescript
+// 1. User login dengan role: 'manage-account'
+
+// 2. Auto-sync creates/updates hakAkses.json:
+{
+ "namaHakAkses": "manage-account",
+ "status": "tidak aktif", // Default
+ "pages": []
+}
+
+// 3. Admin edit via UI:
+{
+ "namaHakAkses": "manage-account",
+ "status": "aktif", // â Changed
+ "pages": [
+ "/dashboard",
+ "/antrean/all",
+ "/setting/hak-akses"
+ ] // â Added
+}
+
+// 4. User refresh/re-login:
+// - Sidebar shows: Dashboard, Antrean > Semua, Settings > Hak Akses
+// - Can access: /dashboard, /antrean/all, /setting/hak-akses
+// - Cannot access: /antrean/list-kategori, /setting/user (will redirect)
+```
diff --git a/data/mock/hakAkses.json b/data/mock/hakAkses.json
index c4f77c3..68d97c4 100644
--- a/data/mock/hakAkses.json
+++ b/data/mock/hakAkses.json
@@ -1,32 +1,20 @@
[
{
- "id": 1,
- "namaHakAkses": "Super Admin",
- "jumlahHalaman": 15,
- "status": "aktif"
+ "id": "a37f9466-ea80-47a8-8540-2043f6c1ba06",
+ "namaHakAkses": "manage-account",
+ "status": "tidak aktif",
+ "pages": []
},
{
- "id": 2,
- "namaHakAkses": "Admin Operasi",
- "jumlahHalaman": 8,
- "status": "aktif"
+ "id": "aec03180-cae0-4164-a861-12e2b60861c1",
+ "namaHakAkses": "manage-account-links",
+ "status": "tidak aktif",
+ "pages": []
},
{
- "id": 3,
- "namaHakAkses": "Dokter",
- "jumlahHalaman": 5,
- "status": "aktif"
- },
- {
- "id": 4,
- "namaHakAkses": "Perawat",
- "jumlahHalaman": 4,
- "status": "aktif"
- },
- {
- "id": 5,
- "namaHakAkses": "Admin Rekam Medis",
- "jumlahHalaman": 6,
- "status": "tidak aktif"
+ "id": "4917180d-1b8c-4f92-a8a6-ae40852415ae",
+ "namaHakAkses": "view-profile",
+ "status": "tidak aktif",
+ "pages": []
}
-]
+]
\ No newline at end of file
diff --git a/data/mock/users.json b/data/mock/users.json
index 303f0a4..cf878ba 100644
--- a/data/mock/users.json
+++ b/data/mock/users.json
@@ -1,42 +1,24 @@
[
{
- "id": 1,
- "namaUser": "Dr. John Doe",
- "username": "john.doe",
- "email": "john.doe@hospital.com",
- "hakAkses": "Dokter",
+ "id": "d6621539-9e8e-4937-ba9a-fca68f625e39",
+ "namaUser": "yusron",
+ "email": "yusron.sandbox@gmai.com",
+ "hakAkses": [
+ "manage-account",
+ "manage-account-links",
+ "view-profile"
+ ],
"status": "aktif"
},
{
- "id": 2,
- "namaUser": "Admin Sistem",
- "username": "admin",
- "email": "admin@hospital.com",
- "hakAkses": "Super Admin",
+ "id": "a2507dbf-5dfc-4f38-bdd5-bb227eb20015",
+ "namaUser": "Akbar Attallah",
+ "email": "akbarantrean@gmail.com",
+ "hakAkses": [
+ "manage-account",
+ "manage-account-links",
+ "view-profile"
+ ],
"status": "aktif"
- },
- {
- "id": 3,
- "namaUser": "Perawat Siti",
- "username": "siti.perawat",
- "email": "siti@hospital.com",
- "hakAkses": "Perawat",
- "status": "aktif"
- },
- {
- "id": 4,
- "namaUser": "Admin Operasi 1",
- "username": "admin.op1",
- "email": "adminop1@hospital.com",
- "hakAkses": "Admin Operasi",
- "status": "aktif"
- },
- {
- "id": 5,
- "namaUser": "RM Staff",
- "username": "rm.staff",
- "email": "rm@hospital.com",
- "hakAkses": "Admin Rekam Medis",
- "status": "tidak aktif"
}
-]
+]
\ No newline at end of file
diff --git a/data/users.db b/data/users.db
index 5bc6f90..5db7387 100644
Binary files a/data/users.db and b/data/users.db differ
diff --git a/middleware/checkPageAccess.ts b/middleware/checkPageAccess.ts
new file mode 100644
index 0000000..f1ff24b
--- /dev/null
+++ b/middleware/checkPageAccess.ts
@@ -0,0 +1,41 @@
+// middleware/checkPageAccess.ts
+// Middleware to check if user has access to the page based on hakAkses
+
+export default defineNuxtRouteMiddleware(async (to, from) => {
+ // Skip check for auth pages, error pages
+ const publicPaths = ['/auth/login', '/auth/register', '/'];
+ if (publicPaths.includes(to.path)) {
+ return;
+ }
+
+ const { user } = useUserInfo();
+
+ // If not authenticated, let auth middleware handle it
+ if (!user.value) {
+ return;
+ }
+
+ const { getAllowedPages } = useHakAkses();
+
+ try {
+ const allowedPages = await getAllowedPages();
+
+ // Check if user has access to this page
+ if (!allowedPages.includes(to.path)) {
+ console.warn(`Access denied to ${to.path}. User allowed pages:`, allowedPages);
+
+ // Redirect to dashboard or first allowed page
+ if (allowedPages.length > 0) {
+ const firstPage = allowedPages.includes('/dashboard') ? '/dashboard' : allowedPages[0];
+ return navigateTo(firstPage);
+ } else {
+ // No access to any page - redirect to login with error
+ return navigateTo('/auth/login?error=' + encodeURIComponent('You do not have access to any pages. Please contact administrator.'));
+ }
+ }
+ } catch (error) {
+ console.error('Error checking page access:', error);
+ // Allow access on error to prevent blocking user
+ return;
+ }
+});
diff --git a/pages/antrean/all.vue b/pages/antrean/all.vue
index 215b11c..ca2d160 100644
--- a/pages/antrean/all.vue
+++ b/pages/antrean/all.vue
@@ -4,7 +4,7 @@ import TableAntrian from '@/components/pendaftaran/TableAntrian.vue';
import ModalPendaftaran from '@/components/pendaftaran/ModalPendaftaran.vue';
import ModalDetailPendaftaran from '@/components/pendaftaran/ModalDetailPendaftaran.vue';
import ModalUpdateStatus from '@/components/pendaftaran/ModalUpdateStatus.vue';
-import { getAntrianOperasi } from '~/services/antrean';
+import { getAntrianOperasi, deleteAntrianOperasi } from '~/services/antrean';
import { Icon } from '@iconify/vue';
import type { AntreanOperasi } from '~/types/antrean';
import { STATUS } from '~/types/antrean';
@@ -110,7 +110,7 @@ const showSnackbar = (message: string, color: string = 'success') => {
snackbar.value = true;
};
-const headers = [
+const allHeaders = [
{ title: 'Nomor', key: 'NoUrutKategori', width: '50px', align: 'center' as const, sortable: false },
{ title: 'Nomor Spesialis', key: 'NoUrutSpesialis', width: '100px', align: 'center' as const, sortable: false },
{ title: 'Nomor Sub Spesialis', key: 'NoUrutSubSpesialis', width: '140px', align: 'center' as const, sortable: false },
@@ -122,12 +122,34 @@ const headers = [
{ title: 'Actions', key: 'actions', sortable: false, width: '120px' }
];
-const actions = [
- { icon: 'mdi-eye', color: 'info', tooltip: 'View', event: 'view' },
- { icon: 'mdi-pencil', color: 'primary', tooltip: 'Edit', event: 'edit' },
- { icon: 'mdi-clipboard-check', color: 'success', tooltip: 'Update Status', event: 'updateStatus' },
- { icon: 'mdi-delete', color: 'error', tooltip: 'Delete', event: 'delete' }
-];
+// Hide queue number columns when filtering
+const headers = computed(() => {
+ const isFiltering = search.value || statusFilter.value;
+ if (isFiltering) {
+ return allHeaders.filter(header =>
+ !['NoUrutKategori', 'NoUrutSpesialis', 'NoUrutSubSpesialis'].includes(header.key)
+ );
+ }
+ return allHeaders;
+});
+
+// Function to get actions based on item status
+const getActionsForItem = (item: AntreanOperasi) => {
+ const allActions = [
+ { icon: 'mdi-eye', color: 'info', tooltip: 'View', event: 'view' },
+ { icon: 'mdi-pencil', color: 'primary', tooltip: 'Edit', event: 'edit' },
+ { icon: 'mdi-clipboard-check', color: 'success', tooltip: 'Update Status', event: 'updateStatus' },
+ { icon: 'mdi-delete', color: 'error', tooltip: 'Delete', event: 'delete' }
+ ];
+
+ // If status is Selesai or Batal, only show View action
+ if (item.StatusOperasi === STATUS.SELESAI || item.StatusOperasi === STATUS.BATAL) {
+ return allActions.filter(action => action.event === 'view');
+ }
+
+ // For other statuses (Belum, Tunda), show all actions
+ return allActions;
+};
const handleView = async (item: unknown) => {
const data = item as AntreanOperasi;
@@ -153,9 +175,21 @@ const handleUpdateStatus = async (item: unknown) => {
showUpdateStatusModal.value = true;
};
-const handleDelete = (item: unknown) => {
+const handleDelete = async (item: unknown) => {
+ const data = item as AntreanOperasi;
if(confirm('Apakah Anda yakin ingin menghapus data ini?')) {
- showSnackbar('Data berhasil dihapus.', 'success');
+ try {
+ const response = await deleteAntrianOperasi(data.id);
+ if (response.success) {
+ showSnackbar('Data berhasil dihapus.', 'success');
+ fetchData(); // Refresh the table after deletion
+ } else {
+ showSnackbar(response.message || 'Gagal menghapus data', 'error');
+ }
+ } catch (error: any) {
+ console.error('Error deleting antrian operasi:', error);
+ showSnackbar(error.response?.data?.message || 'Gagal menghapus data', 'error');
+ }
}
};
@@ -229,7 +263,7 @@ const handleUpdateStatusSuccess = () => {
>([]);
const kategoriAntrianData = ref>([]);
+const antrianPerHariData = ref>([]);
// Fetch data perbandingan status antrian
const fetchStatusAntrian = async () => {
@@ -84,6 +88,89 @@ const fetchKategoriAntrian = async () => {
}
};
+// Fetch data antrian per hari
+const fetchAntrianPerHari = async () => {
+ loadingAntrianPerHari.value = true;
+ try {
+ const response = await api.get('/dashboard/antrian-per-hari', {
+ params: {
+ year: selectedYear.value,
+ month: selectedMonth.value + 1 // API expects 1-12, not 0-11
+ }
+ });
+
+ if (response.data?.success && response.data?.data) {
+ antrianPerHariData.value = response.data.data;
+ }
+ } catch (error) {
+ console.error('Error fetching antrian per hari:', error);
+ antrianPerHariData.value = [];
+ } finally {
+ loadingAntrianPerHari.value = false;
+ }
+};
+
+// Fetch data antrian per spesialis
+const fetchAntrianPerSpesialis = async () => {
+ loadingAntrianPerSpesialis.value = true;
+ try {
+ const response = await api.get('/dashboard/table-antrian-per-spesialis', {
+ params: {
+ year: selectedYear.value,
+ month: selectedMonth.value + 1 // API expects 1-12, not 0-11
+ }
+ });
+
+ if (response.data?.success && response.data?.data) {
+ // Transform API data to match table headers (lowercase keys)
+ antrianPerSpesialis.value = response.data.data.map((item: any) => ({
+ spesialis: item.Spesialis,
+ total: item.Total,
+ belum: item.Belum,
+ selesai: item.Selesai,
+ tunda: item.Tunda,
+ batal: item.Batal
+ }));
+ }
+ } catch (error) {
+ console.error('Error fetching antrian per spesialis:', error);
+ antrianPerSpesialis.value = [];
+ } finally {
+ loadingAntrianPerSpesialis.value = false;
+ }
+};
+
+// Fetch data antrian per subspesialis
+const fetchAntrianPerSubspesialis = async () => {
+ loadingAntrianPerSubspesialis.value = true;
+ try {
+ const response = await api.get('/dashboard/table-antrian-per-subspesialis', {
+ params: {
+ year: selectedYear.value,
+ month: selectedMonth.value + 1 // API expects 1-12, not 0-11
+ }
+ });
+
+ if (response.data?.success && response.data?.data) {
+ // Transform API data to match table headers (lowercase keys)
+ antrianPerSubspesialis.value = response.data.data.map((item: any) => ({
+ spesialis: item.Spesialis,
+ subspesialis: item.SubSpesialis,
+ total: item.Total,
+ belum: item.Belum,
+ selesai: item.Selesai,
+ tunda: item.Tunda,
+ batal: item.Batal
+ }));
+ }
+ } catch (error) {
+ console.error('Error fetching antrian per subspesialis:', error);
+ antrianPerSubspesialis.value = [];
+ } finally {
+ loadingAntrianPerSubspesialis.value = false;
+ }
+};
+
// Computed properties untuk total masing-masing status
const totalBelum = computed(() => {
const item = statusAntrianData.value.find(d => d.statust === 'Belum');
@@ -118,6 +205,9 @@ const handleModalSuccess = () => {
// Refresh data dashboard setelah pendaftaran berhasil
fetchStatusAntrian();
fetchKategoriAntrian();
+ fetchAntrianPerHari();
+ fetchAntrianPerSpesialis();
+ fetchAntrianPerSubspesialis();
console.log('Pendaftaran berhasil');
};
@@ -199,9 +289,16 @@ const pieKategoriChartSeries = computed(() => {
// Data untuk Line Chart - Antrian Per Hari dalam 1 Bulan
const lineChartOptions = computed(() => {
- const daysInMonth = new Date(selectedYear.value, selectedMonth.value + 1, 0).getDate();
const monthName = months[selectedMonth.value].text.substring(0, 3);
+ // Get categories from API data or generate default
+ const categories = antrianPerHariData.value.length > 0
+ ? antrianPerHariData.value.map(item => {
+ const date = new Date(item.TanggalDaftar);
+ return `${date.getDate()} ${monthName}`;
+ })
+ : [];
+
return {
chart: {
type: 'line',
@@ -222,7 +319,7 @@ const lineChartOptions = computed(() => {
width: 3
},
xaxis: {
- categories: Array.from({ length: daysInMonth }, (_, i) => `${i + 1} ${monthName}`),
+ categories: categories,
title: {
text: 'Tanggal'
}
@@ -251,56 +348,42 @@ const lineChartOptions = computed(() => {
});
const lineChartSeries = computed(() => {
- const daysInMonth = new Date(selectedYear.value, selectedMonth.value + 1, 0).getDate();
-
- // Generate random data for demo purposes
- const generateData = (base: number, variance: number) => {
- return Array.from({ length: daysInMonth }, () =>
- Math.floor(Math.random() * variance) + base
- );
- };
+ // If no data from API, return empty series
+ if (antrianPerHariData.value.length === 0) {
+ return [
+ { name: 'Belum', data: [] },
+ { name: 'Selesai', data: [] },
+ { name: 'Tunda', data: [] },
+ { name: 'Batal', data: [] }
+ ];
+ }
+ // Map data from API
return [
{
name: 'Belum',
- data: generateData(15, 20)
+ data: antrianPerHariData.value.map(item => item.Belum)
},
{
name: 'Selesai',
- data: generateData(25, 25)
+ data: antrianPerHariData.value.map(item => item.Selesai)
},
{
name: 'Tunda',
- data: generateData(5, 10)
+ data: antrianPerHariData.value.map(item => item.Tunda)
},
{
name: 'Batal',
- data: generateData(2, 6)
+ data: antrianPerHariData.value.map(item => item.Batal)
}
];
});
-// Data antrian per spesialis
-const antrianPerSpesialis = ref([
- { spesialis: 'Bedah Umum', total: 15, belum: 12, selesai: 3, tunda: 1, batal: 1 },
- { spesialis: 'Bedah Ortopedi', total: 10, belum: 8, selesai: 2, tunda: 1, batal: 0 },
- { spesialis: 'Bedah Saraf', total: 8, belum: 7, selesai: 1, tunda: 0, batal: 0 },
- { spesialis: 'Bedah Jantung', total: 12, belum: 10, selesai: 2, tunda: 0, batal: 0 },
- { spesialis: 'Bedah Plastik', total: 7, belum: 5, selesai: 2, tunda: 0, batal: 0 },
- { spesialis: 'Bedah Urologi', total: 5, belum: 3, selesai: 2, tunda: 0, batal: 0 }
-]);
+// Data antrian per spesialis - populated from API
+const antrianPerSpesialis = ref>([]);
-// Data antrian per subspesialis
-const antrianPerSubspesialis = ref([
- { spesialis: 'Bedah Umum', subspesialis: 'Digestif', total: 8, belum: 6, selesai: 2, tunda: 1, batal: 0 },
- { spesialis: 'Bedah Umum', subspesialis: 'Onkologi', total: 7, belum: 5, selesai: 2, tunda: 0, batal: 0 },
- { spesialis: 'Bedah Ortopedi', subspesialis: 'Traumatologi', total: 6, belum: 5, selesai: 1, tunda: 0, batal: 0 },
- { spesialis: 'Bedah Ortopedi', subspesialis: 'Spine', total: 4, belum: 3, selesai: 1, tunda: 0, batal: 0 },
- { spesialis: 'Bedah Saraf', subspesialis: 'Tumor', total: 5, belum: 4, selesai: 1, tunda: 0, batal: 0 },
- { spesialis: 'Bedah Saraf', subspesialis: 'Vaskular', total: 3, belum: 3, selesai: 0, tunda: 0, batal: 0 },
- { spesialis: 'Bedah Jantung', subspesialis: 'Dewasa', total: 7, belum: 6, selesai: 1, tunda: 0, batal: 0 },
- { spesialis: 'Bedah Jantung', subspesialis: 'Anak', total: 5, belum: 4, selesai: 1, tunda: 0, batal: 0 }
-]);
+// Data antrian per subspesialis - populated from API
+const antrianPerSubspesialis = ref>([]);
const searchSpesialis = ref('');
const searchSubspesialis = ref('');
@@ -328,12 +411,18 @@ const headersSubspesialis = [
watch([selectedMonth, selectedYear], () => {
fetchStatusAntrian();
fetchKategoriAntrian();
+ fetchAntrianPerHari();
+ fetchAntrianPerSpesialis();
+ fetchAntrianPerSubspesialis();
});
// Fetch data saat component mounted
onMounted(() => {
fetchStatusAntrian();
fetchKategoriAntrian();
+ fetchAntrianPerHari();
+ fetchAntrianPerSpesialis();
+ fetchAntrianPerSubspesialis();
});
definePageMeta({
@@ -552,7 +641,10 @@ definePageMeta({
-
+
+
+
+
-
+
+
+
@@ -623,7 +718,10 @@ definePageMeta({
-
+
+
+
diff --git a/pages/setting/hak-akses/form.vue b/pages/setting/hak-akses/form.vue
index 99871be..a1e466a 100644
--- a/pages/setting/hak-akses/form.vue
+++ b/pages/setting/hak-akses/form.vue
@@ -69,6 +69,19 @@ const valid = ref(true);
const namaHakAkses = ref('');
const status = ref('aktif');
const selectedPages = ref([]);
+const loading = ref(false);
+const currentId = ref(null);
+
+// Snackbar state
+const snackbar = ref(false);
+const snackbarMessage = ref('');
+const snackbarColor = ref('success');
+
+const showSnackbar = (message: string, color: string = 'success') => {
+ snackbarMessage.value = message;
+ snackbarColor.value = color;
+ snackbar.value = true;
+};
// Rules
const rules = {
@@ -132,17 +145,57 @@ const handleSimpan = async () => {
if (valid) {
if (selectedPages.value.length === 0) {
- alert('Pilih minimal satu halaman untuk hak akses');
+ showSnackbar('Pilih minimal satu halaman untuk hak akses', 'error');
return;
}
- console.log({
- namaHakAkses: namaHakAkses.value,
- status: status.value,
- pages: selectedPages.value
- });
+ loading.value = true;
- alert('Data berhasil disimpan!');
+ try {
+ const payload = {
+ namaHakAkses: namaHakAkses.value,
+ status: status.value,
+ pages: selectedPages.value
+ };
+
+ let response;
+
+ if (mode.value === 'edit' && currentId.value) {
+ // Update existing
+ response = await $fetch(`/api/hak-akses/${currentId.value}`, {
+ method: 'PUT',
+ body: payload
+ });
+ } else {
+ // Create new
+ response = await $fetch('/api/hak-akses', {
+ method: 'POST',
+ body: payload
+ });
+ }
+
+ if (response && typeof response === 'object' && 'success' in response && response.success) {
+ const message = 'message' in response && typeof response.message === 'string'
+ ? response.message
+ : 'Data berhasil disimpan';
+ showSnackbar(message, 'success');
+
+ // Redirect to list after 1 second
+ setTimeout(() => {
+ router.push('/setting/hak-akses');
+ }, 1000);
+ } else {
+ const message = response && typeof response === 'object' && 'message' in response && typeof response.message === 'string'
+ ? response.message
+ : 'Gagal menyimpan data';
+ showSnackbar(message, 'error');
+ }
+ } catch (error) {
+ console.error('Error saving hak akses:', error);
+ showSnackbar('Terjadi kesalahan saat menyimpan data', 'error');
+ } finally {
+ loading.value = false;
+ }
}
};
@@ -160,23 +213,47 @@ const handleKembali = () => {
};
// Load data if edit or view mode
-onMounted(() => {
+onMounted(async () => {
if (mode.value !== 'create') {
const id = route.query.id;
- // TODO: Load data based on id
- // For now, just mock data
- if (id === '1') {
- namaHakAkses.value = 'Super Admin';
- status.value = 'aktif';
- selectedPages.value = ['/antrean/all', '/antrean/pendaftaran', '/master/guarantor'];
- } else if (id === '2') {
- namaHakAkses.value = 'Admin Operasi';
- status.value = 'aktif';
- selectedPages.value = ['/antrean/all', '/antrean/pendaftaran'];
- } else if (id === '5') {
- namaHakAkses.value = 'Admin Rekam Medis';
- status.value = 'tidak aktif';
- selectedPages.value = ['/antrean/all'];
+
+ if (id) {
+ loading.value = true;
+
+ try {
+ const response = await $fetch(`/api/hak-akses/${id}`);
+
+ if (response && typeof response === 'object' && 'success' in response && response.success && 'data' in response) {
+ const data = response.data;
+
+ if (data && typeof data === 'object') {
+ currentId.value = 'id' in data && typeof data.id === 'number' ? data.id : null;
+ namaHakAkses.value = 'namaHakAkses' in data && typeof data.namaHakAkses === 'string' ? data.namaHakAkses : '';
+ status.value = 'status' in data && typeof data.status === 'string' ? data.status : 'aktif';
+ selectedPages.value = 'pages' in data && Array.isArray(data.pages) ? data.pages : [];
+ }
+ } else {
+ const message = response && typeof response === 'object' && 'message' in response && typeof response.message === 'string'
+ ? response.message
+ : 'Data tidak ditemukan';
+ showSnackbar(message, 'error');
+
+ // Redirect to list after 2 seconds
+ setTimeout(() => {
+ router.push('/setting/hak-akses');
+ }, 2000);
+ }
+ } catch (error) {
+ console.error('Error loading hak akses:', error);
+ showSnackbar('Gagal memuat data', 'error');
+
+ // Redirect to list after 2 seconds
+ setTimeout(() => {
+ router.push('/setting/hak-akses');
+ }, 2000);
+ } finally {
+ loading.value = false;
+ }
}
}
});
@@ -331,6 +408,8 @@ onMounted(() => {
size="large"
prepend-icon="mdi-content-save"
@click="handleSimpan"
+ :loading="loading"
+ :disabled="loading"
>
Simpan
@@ -340,4 +419,22 @@ onMounted(() => {
+
+
+
+ {{ snackbarMessage }}
+
+
+ Close
+
+
+
\ No newline at end of file
diff --git a/pages/setting/hak-akses/index.vue b/pages/setting/hak-akses/index.vue
index e4a77e7..9f2e0dd 100644
--- a/pages/setting/hak-akses/index.vue
+++ b/pages/setting/hak-akses/index.vue
@@ -1,6 +1,5 @@