Hakakses dan verifikasi akun
This commit is contained in:
@@ -0,0 +1,700 @@
|
||||
# API Endpoint Documentation - Hak Akses
|
||||
|
||||
## Deskripsi
|
||||
Dokumentasi ini menjelaskan semua API endpoint yang diperlukan untuk fitur **Hak Akses** di halaman `/pages/Setting/HakAkses.vue`. Endpoint-endpoint ini akan dihubungkan ke backend API yang disediakan oleh tim backend.
|
||||
|
||||
---
|
||||
|
||||
## Base URL
|
||||
```
|
||||
{API_BASE_URL}/api/v1
|
||||
```
|
||||
**Catatan:** Ganti `{API_BASE_URL}` dengan URL backend yang sebenarnya (contoh: `http://10.10.150.131:8089`)
|
||||
|
||||
---
|
||||
|
||||
## 1. Get List Users
|
||||
|
||||
Mengambil daftar semua users beserta roles, groups, dan informasi lainnya yang diperlukan untuk form hak akses.
|
||||
|
||||
### Endpoint
|
||||
```
|
||||
GET /api/users/list
|
||||
```
|
||||
|
||||
### Request
|
||||
**Headers:**
|
||||
```
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**Query Parameters:** Tidak ad
|
||||
|
||||
### Response
|
||||
**Success (200 OK):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "user-123",
|
||||
"namaLengkap": "John Doe",
|
||||
"namaUser": "johndoe",
|
||||
"email": "[email protected]",
|
||||
"tipeUser": "Super Admin",
|
||||
"lastLogin": 1704067200,
|
||||
"roles": ["superadmin", "admin"],
|
||||
"realmRoles": ["default-roles-sandbox"],
|
||||
"accountRoles": [],
|
||||
"resourceRoles": [],
|
||||
"groups": [
|
||||
"/Instalasi STIM/Devops/Superadmin",
|
||||
"/Instalasi STIM/Admin"
|
||||
],
|
||||
"given_name": "John",
|
||||
"family_name": "Doe",
|
||||
"createdAt": 1704067200,
|
||||
"updatedAt": 1704067200
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Error (500 Internal Server Error):**
|
||||
```json
|
||||
{
|
||||
"statusCode": 500,
|
||||
"statusMessage": "Failed to fetch users"
|
||||
}
|
||||
```
|
||||
|
||||
### Field Description
|
||||
- `id`: Unique identifier user
|
||||
- `namaLengkap`: Nama lengkap user
|
||||
- `namaUser`: Username untuk login
|
||||
- `email`: Email user
|
||||
- `tipeUser`: Tipe/jenis user (contoh: "Super Admin", "Admin Loket", dll)
|
||||
- `lastLogin`: Timestamp terakhir login (Unix timestamp dalam detik)
|
||||
- `roles`: Array of role names
|
||||
- `realmRoles`: Array of realm roles dari Keycloak
|
||||
- `accountRoles`: Array of account roles
|
||||
- `resourceRoles`: Array of resource roles
|
||||
- `groups`: Array of group paths (contoh: "/Instalasi STIM/Devops/Superadmin")
|
||||
- `given_name`: Nama depan
|
||||
- `family_name`: Nama belakang
|
||||
- `createdAt`: Timestamp pembuatan (Unix timestamp dalam detik)
|
||||
- `updatedAt`: Timestamp update terakhir (Unix timestamp dalam detik)
|
||||
|
||||
---
|
||||
|
||||
## 2. Get Permissions by Role and Group
|
||||
|
||||
Mengambil daftar permissions dari backend berdasarkan role dan group yang dipilih.
|
||||
|
||||
### Endpoint
|
||||
```
|
||||
GET /api/v1/permission
|
||||
```
|
||||
|
||||
### Request
|
||||
**Headers:**
|
||||
```
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `roles` | string | Yes | Role name (contoh: "superadmin", "admin") |
|
||||
| `groups` | string | Yes | Group name atau group path (contoh: "STIM", "/Instalasi STIM/Devops/Superadmin") |
|
||||
|
||||
**Example:**
|
||||
```
|
||||
GET /api/v1/permission?roles=superadmin&groups=STIM
|
||||
```
|
||||
|
||||
### Response
|
||||
**Success (200 OK):**
|
||||
```json
|
||||
{
|
||||
"message": "Data permission berhasil diambil",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"create": false,
|
||||
"read": true,
|
||||
"update": false,
|
||||
"disable": false,
|
||||
"delete": false,
|
||||
"active": true,
|
||||
"pagename": "Halaman Utama",
|
||||
"pagesID": 1,
|
||||
"level": 1,
|
||||
"sort": 1,
|
||||
"parent": null
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"create": false,
|
||||
"read": true,
|
||||
"update": false,
|
||||
"disable": false,
|
||||
"delete": false,
|
||||
"active": true,
|
||||
"pagename": "Pengaturan",
|
||||
"pagesID": 2,
|
||||
"level": 1,
|
||||
"sort": 2,
|
||||
"parent": null
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"create": true,
|
||||
"read": true,
|
||||
"update": true,
|
||||
"disable": true,
|
||||
"delete": false,
|
||||
"active": true,
|
||||
"pagename": "Hak Akses",
|
||||
"pagesID": 3,
|
||||
"level": 2,
|
||||
"sort": 3,
|
||||
"parent": 2
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"count": 3,
|
||||
"total": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error (400 Bad Request):**
|
||||
```json
|
||||
{
|
||||
"statusCode": 400,
|
||||
"statusMessage": "roles or groups parameter is required"
|
||||
}
|
||||
```
|
||||
|
||||
**Error (500 Internal Server Error):**
|
||||
```json
|
||||
{
|
||||
"message": "Failed to fetch permissions",
|
||||
"data": [],
|
||||
"meta": {
|
||||
"count": 0,
|
||||
"total": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Field Description
|
||||
- `id`: Unique identifier permission
|
||||
- `create`: Boolean, permission untuk create/tambah data
|
||||
- `read`: Boolean, permission untuk read/lihat data
|
||||
- `update`: Boolean, permission untuk update/edit data
|
||||
- `disable`: Boolean, permission untuk disable/nonaktifkan
|
||||
- `delete`: Boolean, permission untuk delete/hapus data
|
||||
- `active`: Boolean, status aktif permission
|
||||
- `pagename`: Nama halaman/menu (akan di-mapping ke menu sidebar)
|
||||
- `pagesID`: ID halaman di sistem backend
|
||||
- `level`: Level hierarki menu (1 = parent, 2 = child, dll)
|
||||
- `sort`: Urutan tampil
|
||||
- `parent`: ID parent menu (null jika parent menu)
|
||||
|
||||
---
|
||||
|
||||
## 3. Get List Hak Akses
|
||||
|
||||
Mengambil daftar semua hak akses yang sudah tersimpan.
|
||||
|
||||
### Endpoint
|
||||
```
|
||||
GET /api/v1/hak-akses
|
||||
```
|
||||
|
||||
### Request
|
||||
**Headers:**
|
||||
```
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**Query Parameters:** Tidak ada
|
||||
|
||||
### Response
|
||||
**Success (200 OK):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"userId": "user-123",
|
||||
"namaLengkap": "John Doe",
|
||||
"namaUser": "johndoe",
|
||||
"tipeUser": "Super Admin",
|
||||
"role": "superadmin",
|
||||
"group": "STIM",
|
||||
"groupPath": "/Instalasi STIM/Devops/Superadmin",
|
||||
"namaTipeUser": "Super Admin",
|
||||
"hakAksesMenu": [
|
||||
{
|
||||
"name": "Dashboard",
|
||||
"canAccess": true,
|
||||
"canView": true,
|
||||
"canAdd": false,
|
||||
"canEdit": false,
|
||||
"canDelete": false
|
||||
},
|
||||
{
|
||||
"name": "Master Data",
|
||||
"canAccess": true,
|
||||
"canView": true,
|
||||
"canAdd": true,
|
||||
"canEdit": true,
|
||||
"canDelete": false
|
||||
}
|
||||
],
|
||||
"isGroupBased": true,
|
||||
"createdAt": 1704067200,
|
||||
"updatedAt": 1704067200
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Error (500 Internal Server Error):**
|
||||
```json
|
||||
{
|
||||
"statusCode": 500,
|
||||
"statusMessage": "Failed to fetch hak akses"
|
||||
}
|
||||
```
|
||||
|
||||
### Field Description
|
||||
- `id`: Unique identifier hak akses
|
||||
- `userId`: ID user (optional, untuk backward compatibility)
|
||||
- `namaLengkap`: Nama lengkap user (optional)
|
||||
- `namaUser`: Username (optional)
|
||||
- `tipeUser`: Tipe user (optional)
|
||||
- `role`: Role name (required)
|
||||
- `group`: Group name (required)
|
||||
- `groupPath`: Full group path (optional, untuk group-based access)
|
||||
- `namaTipeUser`: Nama tipe user (optional)
|
||||
- `hakAksesMenu`: Array of menu permissions
|
||||
- `name`: Nama menu
|
||||
- `canAccess`: Boolean, apakah bisa akses menu
|
||||
- `canView`: Boolean, apakah bisa lihat data
|
||||
- `canAdd`: Boolean, apakah bisa tambah data
|
||||
- `canEdit`: Boolean, apakah bisa edit data
|
||||
- `canDelete`: Boolean, apakah bisa hapus data
|
||||
- `isGroupBased`: Boolean, apakah hak akses ini group-based (true) atau individual (false)
|
||||
- `createdAt`: Timestamp pembuatan (Unix timestamp dalam detik)
|
||||
- `updatedAt`: Timestamp update terakhir (Unix timestamp dalam detik)
|
||||
|
||||
---
|
||||
|
||||
## 4. Create Hak Akses
|
||||
|
||||
Membuat hak akses baru (group-based atau individual).
|
||||
|
||||
### Endpoint
|
||||
```
|
||||
POST /api/v1/hak-akses
|
||||
```
|
||||
|
||||
### Request
|
||||
**Headers:**
|
||||
```
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"role": "superadmin",
|
||||
"group": "STIM",
|
||||
"groupPath": "/Instalasi STIM/Devops/Superadmin",
|
||||
"namaTipeUser": "Super Admin",
|
||||
"hakAksesMenu": [
|
||||
{
|
||||
"name": "Dashboard",
|
||||
"canAccess": true,
|
||||
"canView": true,
|
||||
"canAdd": false,
|
||||
"canEdit": false,
|
||||
"canDelete": false
|
||||
},
|
||||
{
|
||||
"name": "Master Data",
|
||||
"canAccess": true,
|
||||
"canView": true,
|
||||
"canAdd": true,
|
||||
"canEdit": true,
|
||||
"canDelete": false
|
||||
}
|
||||
],
|
||||
"isGroupBased": true
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
**Success (201 Created):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Hak akses berhasil dibuat",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"role": "superadmin",
|
||||
"group": "STIM",
|
||||
"groupPath": "/Instalasi STIM/Devops/Superadmin",
|
||||
"namaTipeUser": "Super Admin",
|
||||
"hakAksesMenu": [
|
||||
{
|
||||
"name": "Dashboard",
|
||||
"canAccess": true,
|
||||
"canView": true,
|
||||
"canAdd": false,
|
||||
"canEdit": false,
|
||||
"canDelete": false
|
||||
}
|
||||
],
|
||||
"isGroupBased": true,
|
||||
"createdAt": 1704067200,
|
||||
"updatedAt": 1704067200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error (400 Bad Request):**
|
||||
```json
|
||||
{
|
||||
"statusCode": 400,
|
||||
"statusMessage": "role and group are required"
|
||||
}
|
||||
```
|
||||
|
||||
**Error (409 Conflict):**
|
||||
```json
|
||||
{
|
||||
"statusCode": 409,
|
||||
"statusMessage": "Hak akses untuk group dan role ini sudah ada"
|
||||
}
|
||||
```
|
||||
|
||||
### Field Description
|
||||
**Required Fields:**
|
||||
- `role`: Role name (string, required)
|
||||
- `group`: Group name (string, required)
|
||||
- `hakAksesMenu`: Array of menu permissions (array, required)
|
||||
|
||||
**Optional Fields:**
|
||||
- `userId`: ID user (string, optional)
|
||||
- `namaLengkap`: Nama lengkap (string, optional)
|
||||
- `namaUser`: Username (string, optional)
|
||||
- `tipeUser`: Tipe user (string, optional)
|
||||
- `groupPath`: Full group path (string, optional)
|
||||
- `namaTipeUser`: Nama tipe user (string, optional)
|
||||
- `isGroupBased`: Boolean, default true (boolean, optional)
|
||||
|
||||
---
|
||||
|
||||
## 5. Update Hak Akses
|
||||
|
||||
Mengupdate hak akses yang sudah ada.
|
||||
|
||||
### Endpoint
|
||||
```
|
||||
PATCH /api/v1/hak-akses/{id}
|
||||
```
|
||||
|
||||
### Request
|
||||
**Headers:**
|
||||
```
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**Path Parameters:**
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | integer | Yes | ID hak akses yang akan diupdate |
|
||||
|
||||
**Body (semua field optional, hanya kirim field yang ingin diupdate):**
|
||||
```json
|
||||
{
|
||||
"role": "admin",
|
||||
"group": "LOKET",
|
||||
"namaTipeUser": "Admin Loket",
|
||||
"hakAksesMenu": [
|
||||
{
|
||||
"name": "Dashboard",
|
||||
"canAccess": true,
|
||||
"canView": true,
|
||||
"canAdd": false,
|
||||
"canEdit": false,
|
||||
"canDelete": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
**Success (200 OK):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Hak akses berhasil diperbarui",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"role": "admin",
|
||||
"group": "LOKET",
|
||||
"groupPath": "/Instalasi STIM/Loket",
|
||||
"namaTipeUser": "Admin Loket",
|
||||
"hakAksesMenu": [
|
||||
{
|
||||
"name": "Dashboard",
|
||||
"canAccess": true,
|
||||
"canView": true,
|
||||
"canAdd": false,
|
||||
"canEdit": false,
|
||||
"canDelete": false
|
||||
}
|
||||
],
|
||||
"isGroupBased": true,
|
||||
"createdAt": 1704067200,
|
||||
"updatedAt": 1704067300
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error (400 Bad Request):**
|
||||
```json
|
||||
{
|
||||
"statusCode": 400,
|
||||
"statusMessage": "ID is required"
|
||||
}
|
||||
```
|
||||
|
||||
**Error (404 Not Found):**
|
||||
```json
|
||||
{
|
||||
"statusCode": 404,
|
||||
"statusMessage": "Hak akses not found"
|
||||
}
|
||||
```
|
||||
|
||||
**Error (500 Internal Server Error):**
|
||||
```json
|
||||
{
|
||||
"statusCode": 500,
|
||||
"statusMessage": "Failed to update hak akses"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Delete Hak Akses
|
||||
|
||||
Menghapus hak akses.
|
||||
|
||||
### Endpoint
|
||||
```
|
||||
DELETE /api/v1/hak-akses/{id}
|
||||
```
|
||||
|
||||
### Request
|
||||
**Headers:**
|
||||
```
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**Path Parameters:**
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | integer | Yes | ID hak akses yang akan dihapus |
|
||||
|
||||
### Response
|
||||
**Success (200 OK):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Hak akses berhasil dihapus"
|
||||
}
|
||||
```
|
||||
|
||||
**Error (400 Bad Request):**
|
||||
```json
|
||||
{
|
||||
"statusCode": 400,
|
||||
"statusMessage": "ID is required"
|
||||
}
|
||||
```
|
||||
|
||||
**Error (404 Not Found):**
|
||||
```json
|
||||
{
|
||||
"statusCode": 404,
|
||||
"statusMessage": "Hak akses not found"
|
||||
}
|
||||
```
|
||||
|
||||
**Error (500 Internal Server Error):**
|
||||
```json
|
||||
{
|
||||
"statusCode": 500,
|
||||
"statusMessage": "Failed to delete hak akses"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mapping Permissions ke Menu Sidebar
|
||||
|
||||
Sistem akan otomatis memetakan `pagename` dari API permission ke nama menu di sidebar. Mapping dilakukan dengan:
|
||||
|
||||
1. **Exact match**: `pagename.toLowerCase() === menu.name.toLowerCase()`
|
||||
2. **Partial match**: `pagename.toLowerCase().includes(menu.name.toLowerCase())` atau sebaliknya
|
||||
|
||||
### Contoh Mapping:
|
||||
- `pagename: "Halaman Utama"` → Menu: "Dashboard"
|
||||
- `pagename: "Pengaturan"` → Menu: "Master Data"
|
||||
- `pagename: "Hak Akses"` → Menu: "Hak Akses"
|
||||
|
||||
### Permission Mapping:
|
||||
- `read: true` → `canView: true`
|
||||
- `create: true` → `canAdd: true`
|
||||
- `update: true` → `canEdit: true`
|
||||
- `delete: true` → `canDelete: true`
|
||||
- `active: true || read: true` → `canAccess: true`
|
||||
|
||||
---
|
||||
|
||||
## Group-Based Access
|
||||
|
||||
Sistem mendukung **Group-Based Access** dimana:
|
||||
- Satu hak akses dapat diberikan ke semua user yang memiliki group dan role yang sama
|
||||
- Setiap user dapat memiliki multiple hak akses dari berbagai group
|
||||
- Hak akses disimpan dengan flag `isGroupBased: true` dan `groupPath` untuk identifikasi
|
||||
|
||||
### Contoh:
|
||||
Jika hak akses dibuat dengan:
|
||||
- `role: "superadmin"`
|
||||
- `groupPath: "/Instalasi STIM/Devops/Superadmin"`
|
||||
- `isGroupBased: true`
|
||||
|
||||
Maka semua user yang memiliki:
|
||||
- Role: "superadmin" **DAN**
|
||||
- Group: "/Instalasi STIM/Devops/Superadmin"
|
||||
|
||||
Akan otomatis mendapatkan hak akses tersebut.
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
Semua endpoint harus mengembalikan error dengan format konsisten:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusCode": 400|404|409|500,
|
||||
"statusMessage": "Error message description"
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP Status Codes:**
|
||||
- `200 OK`: Request berhasil
|
||||
- `201 Created`: Resource berhasil dibuat
|
||||
- `400 Bad Request`: Request tidak valid (missing required fields, invalid format)
|
||||
- `404 Not Found`: Resource tidak ditemukan
|
||||
- `409 Conflict`: Resource sudah ada (untuk create duplicate)
|
||||
- `500 Internal Server Error`: Server error
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
**Catatan:** Tim backend perlu menentukan apakah endpoint-endpoint ini memerlukan authentication token atau tidak. Jika diperlukan, tambahkan:
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Authorization: Bearer {access_token}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Contoh Request dengan cURL:
|
||||
|
||||
**1. Get Users List:**
|
||||
```bash
|
||||
curl -X GET "http://{API_BASE_URL}/api/users/list" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
**2. Get Permissions:**
|
||||
```bash
|
||||
curl -X GET "http://{API_BASE_URL}/api/v1/permission?roles=superadmin&groups=STIM" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
**3. Create Hak Akses:**
|
||||
```bash
|
||||
curl -X POST "http://{API_BASE_URL}/api/v1/hak-akses" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"role": "superadmin",
|
||||
"group": "STIM",
|
||||
"groupPath": "/Instalasi STIM/Devops/Superadmin",
|
||||
"namaTipeUser": "Super Admin",
|
||||
"hakAksesMenu": [
|
||||
{
|
||||
"name": "Dashboard",
|
||||
"canAccess": true,
|
||||
"canView": true,
|
||||
"canAdd": false,
|
||||
"canEdit": false,
|
||||
"canDelete": false
|
||||
}
|
||||
],
|
||||
"isGroupBased": true
|
||||
}'
|
||||
```
|
||||
|
||||
**4. Update Hak Akses:**
|
||||
```bash
|
||||
curl -X PATCH "http://{API_BASE_URL}/api/v1/hak-akses/1" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"namaTipeUser": "Updated Admin",
|
||||
"hakAksesMenu": [...]
|
||||
}'
|
||||
```
|
||||
|
||||
**5. Delete Hak Akses:**
|
||||
```bash
|
||||
curl -X DELETE "http://{API_BASE_URL}/api/v1/hak-akses/1" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Catatan Penting
|
||||
|
||||
1. **Base URL**: Ganti `{API_BASE_URL}` dengan URL backend yang sebenarnya
|
||||
2. **Timestamps**: Semua timestamp menggunakan format Unix timestamp (detik sejak epoch)
|
||||
3. **Group Path**: Group path harus full path (contoh: "/Instalasi STIM/Devops/Superadmin") untuk group-based access
|
||||
4. **Menu Permissions**: Array `hakAksesMenu` harus sesuai dengan struktur menu sidebar
|
||||
5. **Validation**: Backend harus melakukan validasi untuk:
|
||||
- Required fields (role, group, hakAksesMenu)
|
||||
- Duplicate check untuk group+role combination (jika isGroupBased = true)
|
||||
- Format data yang valid
|
||||
|
||||
---
|
||||
|
||||
## Kontak
|
||||
|
||||
Jika ada pertanyaan atau perubahan requirement, silakan hubungi tim frontend.
|
||||
|
||||
@@ -1,83 +1,93 @@
|
||||
<template>
|
||||
<v-card-text class="pa-0 bg-white">
|
||||
<v-list lines="two" class="pa-0">
|
||||
<v-list-item
|
||||
v-for="patient in patients"
|
||||
:key="patient.rm"
|
||||
class="patient-item"
|
||||
:class="{ 'verified-item': patient.status === 'Terverifikasi' }"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar
|
||||
:color="patient.status === 'Terverifikasi' ? 'secondary-600' : 'primary-600'"
|
||||
size="64"
|
||||
class="patient-avatar"
|
||||
>
|
||||
<v-icon size="36" color="white">
|
||||
{{ patient.status === 'Terverifikasi' ? 'mdi-check-decagram' : 'mdi-clock-alert' }}
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-table class="patient-table">
|
||||
<!-- Table Body -->
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="patient in patients"
|
||||
:key="patient.rm"
|
||||
class="table-row"
|
||||
:class="{ 'verified-row': patient.status === 'Terverifikasi' }"
|
||||
>
|
||||
<!-- Status Avatar -->
|
||||
<td class="table-cell status-cell">
|
||||
<div class="status-wrapper">
|
||||
<v-avatar
|
||||
:color="patient.status === 'Terverifikasi' ? 'secondary-600' : 'primary-600'"
|
||||
size="48"
|
||||
class="patient-avatar"
|
||||
>
|
||||
<v-icon size="24" color="white">
|
||||
{{ patient.status === 'Terverifikasi' ? 'mdi-check-decagram' : 'mdi-clock-alert' }}
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<v-list-item-title class="patient-name">
|
||||
{{ patient.nama }}
|
||||
</v-list-item-title>
|
||||
<!-- Nama -->
|
||||
<td class="table-cell">
|
||||
<span class="patient-name">{{ patient.nama }}</span>
|
||||
</td>
|
||||
|
||||
<v-list-item-subtitle class="mt-2">
|
||||
<v-row dense class="patient-info">
|
||||
<v-col cols="12" sm="3" md="2" class="py-1">
|
||||
<v-chip size="default" color="primary-200" class="chip-rm" variant="flat">
|
||||
<v-icon start size="18" color="secondary-600">mdi-file-document</v-icon>
|
||||
<span class="chip-rm-text">{{ patient.rm }}</span>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="5" md="6" class="py-1 info-item">
|
||||
<v-icon size="18" class="mr-2" color="neutral-600">mdi-map-marker</v-icon>
|
||||
<!-- RM -->
|
||||
<td class="table-cell">
|
||||
<v-chip size="small" color="primary-200" class="chip-rm" variant="flat">
|
||||
<v-icon start size="14" color="secondary-600">mdi-file-document</v-icon>
|
||||
<span class="chip-rm-text">{{ patient.rm }}</span>
|
||||
</v-chip>
|
||||
</td>
|
||||
|
||||
<!-- Alamat -->
|
||||
<td class="table-cell">
|
||||
<div class="info-item">
|
||||
<v-icon size="14" class="mr-2" color="neutral-600">mdi-map-marker</v-icon>
|
||||
<span>{{ patient.alamat }}</span>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="4" md="4" class="py-1 info-item">
|
||||
<v-icon size="18" class="mr-2" color="neutral-600">mdi-phone</v-icon>
|
||||
<span>{{ patient.telepon || 'Belum diisi' }}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-list-item-subtitle>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<template #append>
|
||||
<v-btn
|
||||
v-if="patient.status === 'Belum Terverifikasi'"
|
||||
color="primary-600"
|
||||
size="x-large"
|
||||
@click="$emit('verify', patient)"
|
||||
prepend-icon="mdi-qrcode-scan"
|
||||
variant="flat"
|
||||
class="action-btn"
|
||||
rounded="xl"
|
||||
>
|
||||
VERIFIKASI
|
||||
</v-btn>
|
||||
|
||||
<v-chip
|
||||
v-else
|
||||
color="secondary-600"
|
||||
size="x-large"
|
||||
variant="flat"
|
||||
class="verified-chip"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-icon start size="24">mdi-shield-check</v-icon>
|
||||
VERIFIED
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="patients.length === 0">
|
||||
<v-list-item-title class="empty-state">
|
||||
{{ emptyMessage }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<!-- Nomor Telepon -->
|
||||
<td class="table-cell">
|
||||
<div class="info-item">
|
||||
<v-icon size="14" class="mr-2" color="neutral-600">mdi-phone</v-icon>
|
||||
<span>{{ patient.telepon || 'Belum diisi' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Aksi -->
|
||||
<td class="table-cell action-col">
|
||||
<div class="action-wrapper">
|
||||
<v-btn
|
||||
v-if="patient.status === 'Belum Terverifikasi'"
|
||||
variant="flat"
|
||||
class="action-btn"
|
||||
@click="$emit('verify', patient)"
|
||||
>
|
||||
<v-icon start size="16">mdi-qrcode-scan</v-icon>
|
||||
VERIFIKASI
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
v-else
|
||||
color="secondary-600"
|
||||
variant="flat"
|
||||
class="verified-btn"
|
||||
disabled
|
||||
>
|
||||
<v-icon start size="16">mdi-shield-check</v-icon>
|
||||
VERIFIED
|
||||
</v-btn>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Empty State -->
|
||||
<tr v-if="patients.length === 0">
|
||||
<td colspan="6" class="empty-state-cell">
|
||||
<div class="empty-state">{{ emptyMessage }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
</template>
|
||||
|
||||
@@ -99,12 +109,14 @@ defineEmits(['verify']);
|
||||
<style scoped lang="scss">
|
||||
$neutral-100: #FFFFFF;
|
||||
$neutral-400: #E5F7FA;
|
||||
$neutral-500: #ABBED1;
|
||||
$neutral-600: #89939E;
|
||||
$neutral-700: #717171;
|
||||
$neutral-900: #212121;
|
||||
$primary-100: #FFE8CC;
|
||||
$primary-200: #FFDCAF;
|
||||
$primary-600: #FFA532;
|
||||
$primary-700: #FF9B1B;
|
||||
$secondary-200: #EDF5FF;
|
||||
$secondary-300: #DBEDFF;
|
||||
$secondary-400: #B3D9FF;
|
||||
@@ -116,63 +128,129 @@ $font-weight-semibold: 600;
|
||||
$font-weight-bold: 700;
|
||||
$font-weight-extra-bold: 800;
|
||||
|
||||
.patient-item {
|
||||
border-bottom: 1px solid $neutral-400;
|
||||
padding: 24px;
|
||||
.patient-table {
|
||||
font-family: $font-family-base;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: auto;
|
||||
|
||||
:deep(thead),
|
||||
:deep(tbody),
|
||||
:deep(tr) {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TABLE HEADER
|
||||
// ============================================
|
||||
.table-header {
|
||||
background-color: $neutral-400;
|
||||
}
|
||||
|
||||
.header-cell {
|
||||
padding: 16px 24px !important;
|
||||
text-align: left;
|
||||
font-weight: $font-weight-bold;
|
||||
font-size: 14px;
|
||||
color: $neutral-900;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-family: $font-family-base;
|
||||
border-bottom: 2px solid $neutral-500;
|
||||
vertical-align: middle;
|
||||
|
||||
&.status-header-hidden {
|
||||
padding: 16px 24px !important;
|
||||
width: 96px;
|
||||
min-width: 96px;
|
||||
max-width: 96px;
|
||||
}
|
||||
|
||||
&.action-col {
|
||||
text-align: right;
|
||||
padding-right: 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TABLE BODY
|
||||
// ============================================
|
||||
.table-row {
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
background-color: $primary-100 !important;
|
||||
|
||||
&::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 4px;
|
||||
background: $primary-600;
|
||||
transform: scaleY(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.verified-row {
|
||||
background-color: $secondary-200 !important;
|
||||
|
||||
&::before {
|
||||
background: $secondary-600 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $secondary-300 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
padding: 16px 24px !important;
|
||||
vertical-align: middle;
|
||||
font-family: $font-family-base;
|
||||
border-bottom: 1px solid $neutral-400;
|
||||
|
||||
&.status-cell {
|
||||
text-align: center;
|
||||
padding: 16px 24px !important;
|
||||
width: 96px;
|
||||
min-width: 96px;
|
||||
max-width: 96px;
|
||||
}
|
||||
|
||||
&.action-col {
|
||||
text-align: right;
|
||||
padding-right: 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.patient-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 4px;
|
||||
background: $primary-600;
|
||||
transform: scaleY(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.patient-item:hover::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.patient-item:hover {
|
||||
background: $primary-100 !important;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.verified-item {
|
||||
background: $secondary-200 !important;
|
||||
}
|
||||
|
||||
.verified-item::before {
|
||||
background: $secondary-600 !important;
|
||||
}
|
||||
|
||||
.verified-item:hover {
|
||||
background: $secondary-300 !important;
|
||||
.status-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.patient-avatar {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border: 3px solid $neutral-100;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.patient-name {
|
||||
font-size: 24px;
|
||||
font-size: 18px;
|
||||
font-weight: $font-weight-extra-bold;
|
||||
color: $neutral-900;
|
||||
margin-bottom: 8px;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.patient-info {
|
||||
font-size: 14px;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
@@ -195,51 +273,93 @@ $font-weight-extra-bold: 800;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.action-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background-color: $primary-700 !important;
|
||||
text-transform: none;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: $font-weight-extra-bold;
|
||||
font-size: 16px;
|
||||
color: $neutral-100;
|
||||
padding: 12px 32px;
|
||||
box-shadow: 0 2px 8px rgba(255, 155, 27, 0.25);
|
||||
font-size: 14px;
|
||||
color: $neutral-100 !important;
|
||||
padding: 8px 24px !important;
|
||||
height: 40px !important;
|
||||
min-width: 150px;
|
||||
box-shadow: 0 4px 12px rgba(255, 155, 27, 0.4), 0 2px 6px rgba(255, 155, 27, 0.3);
|
||||
font-family: $font-family-base;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.verified-chip {
|
||||
.action-btn:hover {
|
||||
background-color: #E68A00 !important;
|
||||
box-shadow: 0 6px 16px rgba(255, 155, 27, 0.5), 0 3px 8px rgba(255, 155, 27, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.verified-btn {
|
||||
text-transform: none;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: $font-weight-extra-bold;
|
||||
font-size: 16px;
|
||||
color: $neutral-100;
|
||||
padding: 12px 24px;
|
||||
font-size: 14px;
|
||||
color: $neutral-100 !important;
|
||||
padding: 8px 24px !important;
|
||||
height: 40px !important;
|
||||
min-width: 150px;
|
||||
font-family: $font-family-base;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.empty-state-cell {
|
||||
padding: 40px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 32px 0;
|
||||
font-size: 18px;
|
||||
color: $neutral-600;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// RESPONSIVE
|
||||
// ============================================
|
||||
@media (max-width: 960px) {
|
||||
.patient-item {
|
||||
padding: 20px;
|
||||
.patient-table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
font-size: 14px;
|
||||
padding: 10px 20px;
|
||||
.table-cell {
|
||||
padding: 20px 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-cell {
|
||||
padding: 12px 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.action-btn,
|
||||
.verified-btn {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.patient-avatar {
|
||||
width: 56px !important;
|
||||
height: 56px !important;
|
||||
width: 44px !important;
|
||||
height: 44px !important;
|
||||
}
|
||||
|
||||
.patient-name {
|
||||
font-size: 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,43 @@
|
||||
<template>
|
||||
<v-container fluid class="pa-0">
|
||||
|
||||
<div class="pa-4 pb-0">
|
||||
<v-toolbar flat color="#B3E5FC" class="floating-toolbar custom-padding"> <v-toolbar-title class="text-h6 font-weight-bold text-blue-darken-3">
|
||||
<v-icon left class="mr-2" color="blue-darken-3">mdi-heart-pulse</v-icon>
|
||||
Monitoring Pasien
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-chip color="blue-darken-1" text-color="white" class="font-weight-medium admin-chip"> <v-icon start>mdi-account-tie</v-icon>
|
||||
Admin
|
||||
</v-chip>
|
||||
</v-toolbar>
|
||||
</div>
|
||||
<v-container>
|
||||
<v-card>
|
||||
<!-- Header -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<div class="header-icon">
|
||||
<v-icon size="32" color="white">mdi-heart-pulse</v-icon>
|
||||
</div>
|
||||
<div class="header-text">
|
||||
<h2 class="page-title">Monitoring Pasien</h2>
|
||||
<p class="page-subtitle">{{ currentDate }} - Manajemen Data Pasien</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>
|
||||
{{ totalCount }} Total
|
||||
</v-chip>
|
||||
<v-chip color="white" variant="flat" class="stat-chip mr-2">
|
||||
<v-icon start size="16">mdi-clock-outline</v-icon>
|
||||
{{ waitingCount }} Menunggu
|
||||
</v-chip>
|
||||
<v-chip color="white" variant="flat" class="stat-chip">
|
||||
<v-icon start size="16">mdi-check-circle</v-icon>
|
||||
{{ doneCount }} Selesai
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-container class="mt-6">
|
||||
|
||||
<v-card flat class="mb-4">
|
||||
<!-- Tabs -->
|
||||
<v-card-text class="tabs-section">
|
||||
<v-tabs
|
||||
v-model="activeTab"
|
||||
color="primary"
|
||||
align-tabs="start"
|
||||
show-arrows
|
||||
class="mb-4"
|
||||
class="status-tabs"
|
||||
>
|
||||
<v-tab value="all" @click="filterByStatus('all')">
|
||||
Semua Pasien <v-badge color="grey-lighten-1" :content="totalCount" inline class="ml-2"></v-badge>
|
||||
@@ -32,56 +48,99 @@
|
||||
<v-tab value="Diperiksa Dokter" @click="filterByStatus('Diperiksa Dokter')">
|
||||
Diperiksa Dokter <v-badge color="green-lighten-1" :content="examiningCount" inline class="ml-2"></v-badge>
|
||||
</v-tab>
|
||||
<v-tab value="Selesai Pelayanan" @click="filterByStatus('Selesai Pelayanan')">
|
||||
<v-tab value="Selesai Pelayanan" @click="filterByStatus('Selesai Pelayanan')">
|
||||
Selesai Pelayanan <v-badge color="blue-lighten-1" :content="doneCount" inline class="ml-2"></v-badge>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</v-card>
|
||||
|
||||
<v-card class="mb-6 pa-4 elevation-1">
|
||||
<v-row class="align-center">
|
||||
<v-col cols="12" sm="3">
|
||||
<v-text-field label="No. RM" v-model="filters.rm_number" variant="outlined" density="compact" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<!-- <v-col cols="12" sm="3">
|
||||
<v-text-field label="No. Kode QR" v-model="filters.qr_code" variant="outlined" density="compact" hide-details></v-text-field>
|
||||
</v-col> -->
|
||||
<v-col cols="12" sm="2">
|
||||
<v-text-field label="No. Antrean" v-model="filters.queue_number" variant="outlined" density="compact" hide-details type="number"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="2">
|
||||
<v-select label="Layanan" v-model="filters.service" :items="['Klinik Jiwa', 'Radiologi', 'Fisioterapi', 'Klinik Umum']" variant="outlined" density="compact" hide-details clearable></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="2">
|
||||
<v-btn color="blue-lighten-1" block height="40" @click="applyFilters">Cari</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="paginatedPatients"
|
||||
:items-per-page="10"
|
||||
class="elevation-1"
|
||||
:search="currentSearchTerm"
|
||||
>
|
||||
<template v-slot:item.status="{ item }">
|
||||
<v-chip :color="getStatusColor(item.status)" size="small" class="font-weight-medium" label>
|
||||
{{ item.status }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.aksi="{ item }">
|
||||
<v-btn size="small" color="orange-darken-1" dark @click="viewPatient(item.id)">
|
||||
Lihat
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:no-data>
|
||||
<v-alert :value="true" color="info" icon="mdi-information-outline">
|
||||
Tidak ada data pasien untuk ditampilkan.
|
||||
</v-alert>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-container>
|
||||
<!-- Filter Section -->
|
||||
<v-card-text class="filter-section">
|
||||
<div class="filter-card">
|
||||
<v-row class="align-center">
|
||||
<v-col cols="12" sm="3">
|
||||
<v-text-field
|
||||
label="No. RM"
|
||||
v-model="filters.rm_number"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="input-field"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="2">
|
||||
<v-text-field
|
||||
label="No. Antrean"
|
||||
v-model="filters.queue_number"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
type="number"
|
||||
class="input-field"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="2">
|
||||
<v-select
|
||||
label="Layanan"
|
||||
v-model="filters.service"
|
||||
:items="['Klinik Jiwa', 'Radiologi', 'Fisioterapi', 'Klinik Umum']"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
clearable
|
||||
class="input-field"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="2">
|
||||
<v-btn
|
||||
color="success"
|
||||
block
|
||||
height="40"
|
||||
@click="applyFilters"
|
||||
class="btn-search"
|
||||
variant="flat"
|
||||
>
|
||||
<v-icon left size="18">mdi-magnify</v-icon>
|
||||
Cari
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Table -->
|
||||
<v-card-text>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="paginatedPatients"
|
||||
:items-per-page="10"
|
||||
class="elevation-0 data-table"
|
||||
:search="currentSearchTerm"
|
||||
>
|
||||
<template v-slot:item.status="{ item }">
|
||||
<v-chip :color="getStatusColor(item.status)" size="small" class="font-weight-medium status-chip" label>
|
||||
{{ item.status }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.aksi="{ item }">
|
||||
<v-btn
|
||||
size="small"
|
||||
@click="viewPatient(item.id)"
|
||||
class="btn-view"
|
||||
variant="flat"
|
||||
>
|
||||
<v-icon size="16" left>mdi-eye</v-icon>
|
||||
Lihat
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:no-data>
|
||||
<v-alert :value="true" color="info" icon="mdi-information-outline">
|
||||
Tidak ada data pasien untuk ditampilkan.
|
||||
</v-alert>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
@@ -95,6 +154,14 @@ definePageMeta({
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Get current date formatted
|
||||
const currentDate = computed(() => {
|
||||
const date = new Date();
|
||||
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
|
||||
const months = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
|
||||
return `${days[date.getDay()]}, ${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`;
|
||||
});
|
||||
|
||||
// === STATE MANAGEMENT ===
|
||||
const activeTab = ref('all');
|
||||
|
||||
@@ -224,29 +291,180 @@ const viewPatient = (patientId) => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.floating-toolbar {
|
||||
border-radius: 9999px;
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15);
|
||||
height: 64px !important;
|
||||
<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;
|
||||
}
|
||||
|
||||
/* Jarak PENTING: Tambahkan padding horizontal ke toolbar */
|
||||
.floating-toolbar.custom-padding {
|
||||
padding: 0 16px; /* Memberi jarak 16px di kiri dan kanan */
|
||||
// ============================================
|
||||
// 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);
|
||||
}
|
||||
|
||||
/* Penyesuaian agar teks dan ikon kontras dengan soft blue background */
|
||||
.v-toolbar-title {
|
||||
margin-left: 0 !important; /* Reset default margin */
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32px;
|
||||
color: $neutral-100;
|
||||
}
|
||||
|
||||
/* Pastikan chip Admin tidak mepet */
|
||||
.admin-chip {
|
||||
margin-right: 0 !important; /* Reset default margin */
|
||||
.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;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TABS SECTION
|
||||
// ============================================
|
||||
.tabs-section {
|
||||
padding: 24px 24px 0 24px !important;
|
||||
background: $neutral-100;
|
||||
}
|
||||
|
||||
.status-tabs {
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.v-tab.v-tab--selected {
|
||||
font-weight: 600;
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// FILTER SECTION
|
||||
// ============================================
|
||||
.filter-section {
|
||||
padding: 24px !important;
|
||||
background: $neutral-100;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
background: $neutral-300;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid $neutral-400;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background-color: $success-600 !important;
|
||||
color: $neutral-100 !important;
|
||||
text-transform: none;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DATA TABLE
|
||||
// ============================================
|
||||
.data-table {
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
font-weight: $font-weight-medium;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.btn-view {
|
||||
background-color: $success-600 !important;
|
||||
color: $neutral-100 !important;
|
||||
text-transform: none;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// RESPONSIVE
|
||||
// ============================================
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-stats {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +1,44 @@
|
||||
<template>
|
||||
<v-container fluid class="pa-0">
|
||||
<v-container>
|
||||
<v-card>
|
||||
<!-- Header -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<div class="header-icon">
|
||||
<v-icon size="32" color="white">mdi-account-details</v-icon>
|
||||
</div>
|
||||
<div class="header-text">
|
||||
<h2 class="page-title">Informasi Pasien</h2>
|
||||
<p class="page-subtitle">{{ currentDate }} - Detail Data Pasien</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-stats">
|
||||
<v-chip
|
||||
v-if="patient.status"
|
||||
color="white"
|
||||
variant="flat"
|
||||
class="stat-chip mr-2"
|
||||
>
|
||||
<v-icon start size="16">mdi-progress-check</v-icon>
|
||||
{{ patient.status }}
|
||||
</v-chip>
|
||||
<v-btn
|
||||
color="white"
|
||||
elevation="0"
|
||||
class="btn-back"
|
||||
@click="$router.go(-1)"
|
||||
>
|
||||
<v-icon left size="20">mdi-arrow-left</v-icon>
|
||||
Kembali
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pa-4 pb-0">
|
||||
<v-toolbar flat color="#B3E5FC" class="floating-toolbar custom-padding">
|
||||
<v-toolbar-title class="text-h6 font-weight-bold text-blue-darken-3">
|
||||
<v-icon left class="mr-2" color="blue-darken-3">mdi-account-details</v-icon>
|
||||
Informasi Pasien
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="orange-darken-1"
|
||||
dark
|
||||
variant="flat"
|
||||
size="small"
|
||||
@click="$router.go(-1)"
|
||||
prepend-icon="mdi-arrow-left"
|
||||
class="admin-chip"
|
||||
>
|
||||
Kembali
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
</div>
|
||||
|
||||
<v-container class="mt-6">
|
||||
|
||||
<v-card class="mb-6 pa-6 elevation-4 rounded-lg" color="white">
|
||||
<!-- Content -->
|
||||
<v-card-text class="content-section">
|
||||
<v-card class="mb-6 patient-info-card" elevation="0">
|
||||
<v-row class="align-center mb-4">
|
||||
<v-col cols="12" md="8">
|
||||
<div class="d-flex align-center mb-2">
|
||||
@@ -42,15 +56,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" class="d-flex justify-md-end">
|
||||
<v-chip :color="patient.status === 'Aktif' ? 'green-darken-1' : 'grey-darken-1'" size="x-large" class="font-weight-bold px-6 py-2">
|
||||
<v-icon start>mdi-progress-check</v-icon>
|
||||
Status: {{ patient.status }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="my-4"></v-divider>
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" class="py-1">
|
||||
@@ -84,17 +92,17 @@
|
||||
</v-card>
|
||||
|
||||
|
||||
<h2 class="text-h5 mb-4 font-weight-bold text-blue-darken-3">Tiket Pelayanan Aktif</h2>
|
||||
<h2 class="section-title">Tiket Pelayanan Aktif</h2>
|
||||
|
||||
<v-row class="ticket-row">
|
||||
|
||||
<v-col
|
||||
v-for="ticket in patient.activeTickets"
|
||||
:key="ticket.title"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="3"
|
||||
v-for="ticket in patient.activeTickets"
|
||||
:key="ticket.title"
|
||||
lg="3"
|
||||
>
|
||||
<MonitorPasienTicketCard
|
||||
:title="ticket.title"
|
||||
@@ -105,24 +113,33 @@
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="!patient.activeTickets || patient.activeTickets.length === 0" cols="12">
|
||||
<v-card class="pa-6 text-center" variant="tonal" color="grey-lighten-2">
|
||||
<v-icon size="48" color="grey">mdi-ticket-off-outline</v-icon>
|
||||
<div class="text-subtitle-1 text-grey-darken-1 mt-2">Pasien ini tidak memiliki tiket pelayanan aktif saat ini.</div>
|
||||
</v-card>
|
||||
<v-card class="empty-ticket-card" elevation="0">
|
||||
<v-icon size="48" color="grey">mdi-ticket-off-outline</v-icon>
|
||||
<div class="empty-text">Pasien ini tidak memiliki tiket pelayanan aktif saat ini.</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
definePageMeta({
|
||||
middleware:['auth']
|
||||
})
|
||||
|
||||
// Get current date formatted
|
||||
const currentDate = computed(() => {
|
||||
const date = new Date();
|
||||
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
|
||||
const months = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
|
||||
return `${days[date.getDay()]}, ${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`;
|
||||
});
|
||||
// ===============================================
|
||||
// JSON MOCK DATA STRUCTURE (Tetap sama)
|
||||
// ===============================================
|
||||
@@ -255,44 +272,175 @@ const fetchPatientData = () => {
|
||||
onMounted(fetchPatientData);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* =============================================== */
|
||||
/* FLOATING TOOLBAR STYLES */
|
||||
/* =============================================== */
|
||||
.floating-toolbar {
|
||||
border-radius: 9999px;
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15);
|
||||
height: 64px !important;
|
||||
<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;
|
||||
}
|
||||
|
||||
.floating-toolbar.custom-padding {
|
||||
padding: 0 16px;
|
||||
// ============================================
|
||||
// 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);
|
||||
}
|
||||
|
||||
.admin-chip {
|
||||
margin-right: 0 !important;
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32px;
|
||||
color: $neutral-100;
|
||||
}
|
||||
|
||||
/* =============================================== */
|
||||
/* TICKET CARD ALIGNMENT STYLES (Fix Tinggi Card) */
|
||||
/* =============================================== */
|
||||
.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;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
font-weight: $font-weight-semibold;
|
||||
text-transform: none;
|
||||
letter-spacing: 0.5px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: $success-600 !important;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CONTENT SECTION
|
||||
// ============================================
|
||||
.content-section {
|
||||
padding: 24px !important;
|
||||
background: $neutral-100;
|
||||
}
|
||||
|
||||
.patient-info-card {
|
||||
background: $neutral-300;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid $neutral-400;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $success-600;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TICKET ROW
|
||||
// ============================================
|
||||
.ticket-row {
|
||||
/* Mengaktifkan flex container pada row */
|
||||
display: flex;
|
||||
/* Agar item (v-col) menyesuaikan tinggi item tertinggi */
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* Pastikan v-col children juga menggunakan full height */
|
||||
.ticket-row > .v-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* =============================================== */
|
||||
/* TIMELINE STYLES (Jika ada) */
|
||||
/* =============================================== */
|
||||
.v-timeline-item {
|
||||
padding-bottom: 8px !important;
|
||||
.empty-ticket-card {
|
||||
background: $neutral-300;
|
||||
padding: 48px 24px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
border: 1px solid $neutral-400;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: $font-weight-regular;
|
||||
color: $neutral-700;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// RESPONSIVE
|
||||
// ============================================
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-stats {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.patient-info-card {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+400
-335
@@ -45,74 +45,22 @@
|
||||
</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"
|
||||
<v-col cols="12">
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
density="comfortable"
|
||||
class="mt-1"
|
||||
@update:model-value="fillUserData"
|
||||
clearable
|
||||
class="mb-4"
|
||||
>
|
||||
<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-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="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-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"
|
||||
@@ -120,20 +68,54 @@
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="mt-1"
|
||||
required
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-label class="font-weight-bold text-medium-emphasis">Group</v-label>
|
||||
<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="editedItem.group"
|
||||
:items="availableGroups"
|
||||
v-model="selectedGroup"
|
||||
:items="availableGroupPaths"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
placeholder="Pilih Group"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="mt-1"
|
||||
></v-select>
|
||||
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-col cols="12" md="4">
|
||||
</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"
|
||||
@@ -423,35 +405,23 @@
|
||||
</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 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>
|
||||
@@ -589,20 +559,7 @@
|
||||
|
||||
<!-- 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-col cols="12" sm="4">
|
||||
<v-select
|
||||
v-model="filterTipeUser"
|
||||
:items="availableTipeUsers"
|
||||
@@ -613,7 +570,7 @@
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="3">
|
||||
<v-col cols="12" sm="4">
|
||||
<v-select
|
||||
v-model="filterRole"
|
||||
:items="availableRoles"
|
||||
@@ -624,7 +581,7 @@
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="3">
|
||||
<v-col cols="12" sm="4">
|
||||
<v-select
|
||||
v-model="filterGroup"
|
||||
:items="availableGroups"
|
||||
@@ -670,10 +627,10 @@
|
||||
<div class="text-center">{{ item.id }}</div>
|
||||
</template>
|
||||
|
||||
<!-- Tampilkan User ID virtual yang dikelompokkan per tipe user -->
|
||||
<!-- Tampilkan User ID dengan format generated -->
|
||||
<template v-slot:item.userId="{ item }">
|
||||
<div class="text-center">
|
||||
{{ formatDisplayUserId(item) }}
|
||||
{{ generateDisplayUserId(item) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -689,40 +646,30 @@
|
||||
<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>
|
||||
<!-- 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>
|
||||
@@ -830,7 +777,7 @@ interface NavItem {
|
||||
|
||||
interface HakAksesData {
|
||||
id: number;
|
||||
userId?: string;
|
||||
userId?: string; // Optional: untuk backward compatibility dan tracking
|
||||
namaLengkap?: string;
|
||||
namaUser?: string;
|
||||
tipeUser?: string;
|
||||
@@ -838,6 +785,9 @@ interface HakAksesData {
|
||||
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 {
|
||||
@@ -870,16 +820,14 @@ 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
|
||||
// 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: '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: 'Hak Akses', key: 'hakAksesList' as const, sortable: false },
|
||||
{ title: 'Aksi', align: 'center' as const, key: 'actions' as const, sortable: false },
|
||||
]);
|
||||
|
||||
@@ -923,9 +871,34 @@ 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[] = [];
|
||||
@@ -970,82 +943,98 @@ const showingEntriesText = computed(() => {
|
||||
});
|
||||
|
||||
// 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
|
||||
// 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(() =>
|
||||
allHakAksesData.value.filter((item) => {
|
||||
const matchUserId = !filterUserId.value || item.id === filterUserId.value;
|
||||
groupedHakAksesByUser.value.filter((item) => {
|
||||
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;
|
||||
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 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
|
||||
// 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
|
||||
);
|
||||
|
||||
const number = index >= 0 ? index + 1 : sameTypeItems.length + 1;
|
||||
|
||||
return `${prefix}-${String(number).padStart(4, '0')}`;
|
||||
|
||||
// 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(() => {
|
||||
@@ -1120,33 +1109,136 @@ const showAddForm = () => {
|
||||
viewMode.value = 'add';
|
||||
};
|
||||
|
||||
const viewItem = (item: HakAksesData) => {
|
||||
editedItem.value = { ...item };
|
||||
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: HakAksesData) => {
|
||||
editedIndex.value = allHakAksesData.value.findIndex(d => d.id === item.id);
|
||||
editedItem.value = { ...item };
|
||||
selectedTipeUser.value = item.tipeUser || null;
|
||||
viewMode.value = 'editName';
|
||||
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: HakAksesData) => {
|
||||
editedIndex.value = allHakAksesData.value.findIndex(d => d.id === item.id);
|
||||
const baseMenuItems = buildMenuTemplate(navItemsStore.navItems);
|
||||
const mergedMenuItems = mergePermissions(baseMenuItems, item.hakAksesMenu);
|
||||
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 = {
|
||||
...item,
|
||||
hakAksesMenu: mergedMenuItems
|
||||
};
|
||||
viewMode.value = 'editAccess';
|
||||
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: HakAksesData) => {
|
||||
editedIndex.value = allHakAksesData.value.findIndex(d => d.id === item.id);
|
||||
showDeleteDialog.value = true;
|
||||
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 = () => {
|
||||
@@ -1198,18 +1290,19 @@ const updateItemAccess = (updatedItem: HakAksesData & { backendPermissions?: Bac
|
||||
const resetForm = () => {
|
||||
editedItem.value = {
|
||||
id: 0,
|
||||
userId: '',
|
||||
namaLengkap: '',
|
||||
namaUser: '',
|
||||
tipeUser: '',
|
||||
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
|
||||
@@ -1225,72 +1318,28 @@ const getMappingStatus = (pagename: string): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
// 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;
|
||||
// Handle group selection
|
||||
const onGroupSelected = (groupPath: string | null) => {
|
||||
if (!groupPath) {
|
||||
selectedGroupUsers.value = [];
|
||||
editedItem.value.group = '';
|
||||
editedItem.value.groupPath = '';
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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`);
|
||||
};
|
||||
|
||||
// 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({
|
||||
@@ -1436,7 +1485,7 @@ const loadAvailableRolesAndGroups = async () => {
|
||||
// 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
|
||||
groupsSet.add(parts[1]); // Get second part as group name for filter
|
||||
} else if (parts.length === 1) {
|
||||
groupsSet.add(parts[0]);
|
||||
}
|
||||
@@ -1455,6 +1504,8 @@ const loadAvailableRolesAndGroups = async () => {
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
@@ -1462,7 +1513,7 @@ const loadAvailableRolesAndGroups = async () => {
|
||||
|
||||
const saveItem = async () => {
|
||||
// Validate required fields
|
||||
if (!editedItem.value.role || !editedItem.value.group) {
|
||||
if (!editedItem.value.role || !selectedGroup.value) {
|
||||
snackbar.value = {
|
||||
show: true,
|
||||
message: 'Role dan Group wajib diisi!',
|
||||
@@ -1497,51 +1548,65 @@ const saveItem = async () => {
|
||||
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 || [],
|
||||
};
|
||||
// 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
|
||||
);
|
||||
|
||||
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);
|
||||
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: 'Data hak akses berhasil diperbarui!',
|
||||
message: `Hak akses untuk group "${editedItem.value.group}" berhasil diperbarui! ${selectedGroupUsers.value.length} user terpengaruh.`,
|
||||
color: 'success',
|
||||
timeout: 3000,
|
||||
timeout: 4000,
|
||||
};
|
||||
} else {
|
||||
// Add item with a new ID
|
||||
// Create new group-based access entry
|
||||
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);
|
||||
|
||||
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: 'Data hak akses berhasil ditambahkan!',
|
||||
message: `Hak akses untuk group "${editedItem.value.group}" berhasil ditambahkan! ${selectedGroupUsers.value.length} user mendapatkan hak akses ini.`,
|
||||
color: 'success',
|
||||
timeout: 3000,
|
||||
timeout: 4000,
|
||||
};
|
||||
}
|
||||
|
||||
// Force update localStorage by triggering reactivity
|
||||
// useLocalStorage should auto-save, but we ensure it by reassigning
|
||||
await nextTick();
|
||||
allHakAksesData.value = [...allHakAksesData.value];
|
||||
|
||||
|
||||
@@ -222,7 +222,8 @@
|
||||
<v-btn
|
||||
color="grey-darken-1"
|
||||
variant="flat"
|
||||
class="text-capitalize rounded-lg mr-3 px-6"
|
||||
class="text-capitalize mr-3 px-6"
|
||||
rounded="0"
|
||||
prepend-icon="mdi-close"
|
||||
@click="cancelForm"
|
||||
:disabled="isSaving"
|
||||
@@ -234,7 +235,8 @@
|
||||
v-if="!readOnly"
|
||||
:color="isEditMode ? 'orange-darken-2' : 'green-darken-2'"
|
||||
variant="flat"
|
||||
class="text-capitalize rounded-lg px-6"
|
||||
class="text-capitalize px-6"
|
||||
rounded="0"
|
||||
:prepend-icon="isEditMode ? 'mdi-content-save' : 'mdi-check'"
|
||||
@click="saveItem"
|
||||
:loading="isSaving"
|
||||
@@ -280,6 +282,7 @@
|
||||
@click="refreshAllUsers"
|
||||
elevation="0"
|
||||
class="add-btn"
|
||||
rounded="0"
|
||||
:disabled="isPending || isSaving"
|
||||
:loading="isSaving"
|
||||
>
|
||||
@@ -451,13 +454,13 @@
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<v-btn icon color="blue" size="small" class="mr-2" @click="viewItem(item)">
|
||||
<v-btn icon color="blue" size="small" class="mr-2" rounded="0" @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-btn icon color="orange" size="small" class="mr-2" rounded="0" @click="editItem(item)">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn> -->
|
||||
<v-btn icon color="red" size="small" @click="deleteItem(item)">
|
||||
<v-btn icon color="red" size="small" rounded="0" @click="deleteItem(item)">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
@@ -494,8 +497,8 @@
|
||||
<v-card-title class="text-h6 font-weight-bold">Hapus Data</v-card-title>
|
||||
<v-card-text>Apakah Anda yakin ingin menghapus data **{{ itemToDelete?.namaLengkap }}**?</v-card-text>
|
||||
<v-card-actions class="d-flex justify-end">
|
||||
<v-btn color="grey-darken-1" variant="text" @click="closeDeleteDialog" :disabled="isSaving">Batal</v-btn>
|
||||
<v-btn color="red" variant="text" @click="confirmDelete" :loading="isSaving">Hapus</v-btn>
|
||||
<v-btn color="grey-darken-1" variant="text" rounded="0" @click="closeDeleteDialog" :disabled="isSaving">Batal</v-btn>
|
||||
<v-btn color="red" variant="text" rounded="0" @click="confirmDelete" :loading="isSaving">Hapus</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -1,68 +1,78 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-app>
|
||||
<!-- Page Header -->
|
||||
<PageHeader
|
||||
icon="mdi-shield-check"
|
||||
title="Verifikasi Akun"
|
||||
subtitle="Manajemen Pasien Digital"
|
||||
:show-add-button="false"
|
||||
theme="primary"
|
||||
<v-card>
|
||||
<!-- 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-check</v-icon>
|
||||
</div>
|
||||
<div class="header-text">
|
||||
<h2 class="page-title">Verifikasi Akun</h2>
|
||||
<p class="page-subtitle">{{ currentDate }} - Manajemen Pasien Digital</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>
|
||||
{{ patients.length }} User
|
||||
</v-chip>
|
||||
<v-chip color="white" variant="flat" class="stat-chip mr-2">
|
||||
<v-icon start size="16">mdi-clock-alert</v-icon>
|
||||
{{ unverifiedPatients.length }} Pending
|
||||
</v-chip>
|
||||
<v-chip color="white" variant="flat" class="stat-chip">
|
||||
<v-icon start size="16">mdi-check-decagram</v-icon>
|
||||
{{ verifiedPatients.length }} Terverifikasi
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search & Content -->
|
||||
<v-card-text class="pa-0">
|
||||
<!-- Search & Tabs -->
|
||||
<VerificationSearchTabs
|
||||
v-model:search-query="search"
|
||||
v-model:selected-tab="filterStatus"
|
||||
:all-count="patients.length"
|
||||
:pending-count="unverifiedPatients.length"
|
||||
:verified-count="verifiedPatients.length"
|
||||
/>
|
||||
|
||||
<!-- Patient List -->
|
||||
<PatientVerificationList
|
||||
:patients="filteredPatients"
|
||||
:empty-message="noDataText"
|
||||
@verify="openDaftarModal"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- QR Verification Dialog -->
|
||||
<QRVerificationDialog
|
||||
v-model="isModalOpen"
|
||||
v-model:phone-number="tempTelepon"
|
||||
:patient="selectedPatient"
|
||||
:qr-generated="qrCodeGenerated"
|
||||
:is-mobile="display.xs.value"
|
||||
@generate="generateQrCode"
|
||||
@reload="reloadQr"
|
||||
@complete="completeVerification"
|
||||
@close="closeModal"
|
||||
>
|
||||
<template #actions>
|
||||
<v-chip color="secondary-600" class="admin-chip" variant="flat" rounded="xl" size="large">
|
||||
<v-icon start color="white">mdi-account-star</v-icon>
|
||||
<span class="chip-text">Admin</span>
|
||||
</v-chip>
|
||||
<template #qr-code>
|
||||
<qrcode-vue
|
||||
:value="qrCodeData"
|
||||
:size="qrSize"
|
||||
level="H"
|
||||
:foreground="'#FFA532'"
|
||||
:background="'#FFFFFF'"
|
||||
class="qr-code"
|
||||
/>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<v-main class="main-bg">
|
||||
<v-container fluid class="pa-6 pt-8 pb-12">
|
||||
<v-card class="content-card">
|
||||
<!-- Search & Tabs -->
|
||||
<VerificationSearchTabs
|
||||
v-model:search-query="search"
|
||||
v-model:selected-tab="filterStatus"
|
||||
:all-count="patients.length"
|
||||
:pending-count="unverifiedPatients.length"
|
||||
:verified-count="verifiedPatients.length"
|
||||
/>
|
||||
|
||||
<!-- Patient List -->
|
||||
<PatientVerificationList
|
||||
:patients="filteredPatients"
|
||||
:empty-message="noDataText"
|
||||
@verify="openDaftarModal"
|
||||
/>
|
||||
</v-card>
|
||||
|
||||
<!-- QR Verification Dialog -->
|
||||
<QrVerificationDialog
|
||||
v-model="isModalOpen"
|
||||
:patient="selectedPatient"
|
||||
v-model:phone-number="tempTelepon"
|
||||
:qr-generated="qrCodeGenerated"
|
||||
:is-mobile="display.xs.value"
|
||||
@generate="generateQrCode"
|
||||
@reload="reloadQr"
|
||||
@complete="completeVerification"
|
||||
@close="closeModal"
|
||||
>
|
||||
<template #qr-code>
|
||||
<qrcode-vue
|
||||
:value="qrCodeData"
|
||||
:size="qrSize"
|
||||
level="H"
|
||||
:foreground="'#0663C7'"
|
||||
:background="'#FFFFFF'"
|
||||
class="qr-code"
|
||||
/>
|
||||
</template>
|
||||
</QrVerificationDialog>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</QRVerificationDialog>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
@@ -70,10 +80,9 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import { useDisplay } from 'vuetify';
|
||||
import QrcodeVue from 'qrcode.vue';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import VerificationSearchTabs from '@/components/verification/VerificationSearchTabs.vue';
|
||||
import PatientVerificationList from '@/components/verification/PatientVerificationList.vue';
|
||||
import QrVerificationDialog from '@/components/verification/QrVerificationDialog.vue';
|
||||
import QRVerificationDialog from '@/components/verification/QRVerificationDialog.vue';
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['auth']
|
||||
@@ -98,6 +107,13 @@ const tempTelepon = ref('');
|
||||
const qrCodeGenerated = ref(false);
|
||||
|
||||
// Computed
|
||||
const currentDate = computed(() => {
|
||||
const now = new Date();
|
||||
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
|
||||
const months = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
|
||||
return `${days[now.getDay()]}, ${now.getDate()} ${months[now.getMonth()]} ${now.getFullYear()}`;
|
||||
});
|
||||
|
||||
const noDataText = computed(() =>
|
||||
search.value
|
||||
? 'Tidak ada data pasien yang cocok dengan pencarian.'
|
||||
@@ -196,42 +212,121 @@ const completeVerification = () => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$neutral-100: #FFFFFF;
|
||||
$neutral-300: #F5F7FA;
|
||||
// Colors from Design System
|
||||
$neutral-900: #212121;
|
||||
$neutral-800: #4D4D4D;
|
||||
$neutral-700: #717171;
|
||||
$neutral-600: #89939E;
|
||||
$neutral-500: #ABBED1;
|
||||
$secondary-600: #0671E0;
|
||||
$neutral-400: #E5F7FA;
|
||||
$neutral-300: #F5F7FA;
|
||||
$neutral-100: #FFFFFF;
|
||||
|
||||
// Orange/Primary Colors
|
||||
$primary-700: #FF9B1B;
|
||||
$primary-600: #FFA532;
|
||||
$primary-500: #FFB95F;
|
||||
$primary-400: #FFCD8D;
|
||||
$primary-300: #FFDCAF;
|
||||
$primary-200: #FFE6C6;
|
||||
|
||||
$danger-600: #E02B1D;
|
||||
|
||||
// Font Family & Weights
|
||||
$font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
$font-weight-bold: 700;
|
||||
$font-weight-regular: 400;
|
||||
$font-weight-medium: 500;
|
||||
$font-weight-semibold: 600;
|
||||
|
||||
.main-bg {
|
||||
background: $neutral-300;
|
||||
min-height: 100vh;
|
||||
// Apply font family
|
||||
* {
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.admin-chip {
|
||||
font-weight: $font-weight-bold;
|
||||
padding: 12px 20px;
|
||||
// ============================================
|
||||
// PAGE HEADER
|
||||
// ============================================
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
|
||||
border-radius: 16px 16px 0 0;
|
||||
box-shadow: 0 4px 16px rgba(255, 165, 50, 0.2);
|
||||
}
|
||||
|
||||
.chip-text {
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32px;
|
||||
color: $neutral-100;
|
||||
font-weight: $font-weight-bold;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
background: $neutral-100;
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
border: 1px solid $neutral-500;
|
||||
.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: $primary-600 !important;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// RESPONSIVE
|
||||
// ============================================
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-stats {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,16 @@
|
||||
// Quick database access script
|
||||
// Usage: node scripts/db-access.js
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
const readline = require('readline');
|
||||
import Database from 'better-sqlite3';
|
||||
import path from 'path';
|
||||
import readline from 'readline';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const dbPath = path.join(process.cwd(), 'data', 'users.db');
|
||||
|
||||
// Check if database exists
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync(dbPath)) {
|
||||
console.error('❌ Database not found at:', dbPath);
|
||||
process.exit(1);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// Quick database viewer - shows all content and exits
|
||||
// Usage: node scripts/view-db.js
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const dbPath = path.join(process.cwd(), 'data', 'users.db');
|
||||
|
||||
if (!fs.existsSync(dbPath)) {
|
||||
console.error('❌ Database not found at:', dbPath);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const db = new Database(dbPath);
|
||||
|
||||
console.log('📊 Database Content Viewer\n');
|
||||
console.log('Database path:', dbPath);
|
||||
console.log('='.repeat(80));
|
||||
|
||||
// Get all tables
|
||||
const tables = db.prepare(`
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
||||
`).all();
|
||||
|
||||
console.log('\n📋 Available Tables:');
|
||||
tables.forEach((table, index) => {
|
||||
console.log(` ${index + 1}. ${table.name}`);
|
||||
});
|
||||
|
||||
// Show content for each table
|
||||
tables.forEach((table) => {
|
||||
const tableName = table.name;
|
||||
|
||||
// Get row count
|
||||
const count = db.prepare(`SELECT COUNT(*) as total FROM ${tableName}`).get();
|
||||
|
||||
console.log('\n' + '='.repeat(80));
|
||||
console.log(`\n📊 Table: ${tableName} (${count.total} rows)`);
|
||||
console.log('-'.repeat(80));
|
||||
|
||||
// Get table schema
|
||||
const schema = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
||||
console.log('\n📐 Schema:');
|
||||
console.table(schema.map(col => ({
|
||||
column: col.name,
|
||||
type: col.type,
|
||||
nullable: col.notnull === 0 ? 'YES' : 'NO',
|
||||
default: col.dflt_value || '-'
|
||||
})));
|
||||
|
||||
// Get all data
|
||||
const data = db.prepare(`SELECT * FROM ${tableName}`).all();
|
||||
|
||||
if (data.length === 0) {
|
||||
console.log('\n📭 No data in this table.');
|
||||
} else {
|
||||
console.log(`\n📄 Data (showing all ${data.length} rows):`);
|
||||
console.table(data);
|
||||
|
||||
// If there are many rows, also show a summary
|
||||
if (data.length > 10) {
|
||||
console.log(`\n💡 Tip: Showing all ${data.length} rows. Use the interactive script for filtering.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n' + '='.repeat(80));
|
||||
console.log('\n✅ Done! Use `node scripts/db-access.js` for interactive queries.\n');
|
||||
|
||||
db.close();
|
||||
|
||||
Reference in New Issue
Block a user