update akbar tampilan login dashboard dan verifikasi
This commit is contained in:
No files matched your search
@@ -1,60 +1,47 @@
|
||||
/* assets/styles/loginpage/login.css */
|
||||
|
||||
/* Main Background */
|
||||
/* --- COLOR DEFINITIONS --- */
|
||||
/* Base Orange: #FF9B1B (New) */
|
||||
/* Hover/Darker Orange: #E68A00 (Calculated darker shade for hover effect) */
|
||||
/* --- END COLOR DEFINITIONS --- */
|
||||
|
||||
|
||||
/* Main Background - Now Orange */
|
||||
.login-background {
|
||||
background: linear-gradient(135deg, #FFC857 0%, #FF9B1B 25%, #FF8F00 50%, #FF6F00 75%, #E65100 100%);
|
||||
background: #FF9B1B; /* Solid Orange Background: #FF9B1B */
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding-top: 64px;
|
||||
/* padding-top: 64px; - REMOVED */
|
||||
}
|
||||
|
||||
.login-background::before {
|
||||
/* No radial gradients */
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
radial-gradient(circle at 10% 20%, rgba(255, 200, 87, 0.4) 0%, transparent 50%),
|
||||
radial-gradient(circle at 90% 80%, rgba(255, 155, 27, 0.4) 0%, transparent 50%),
|
||||
radial-gradient(circle at 50% 50%, rgba(230, 81, 0, 0.3) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* Navigation Bar */
|
||||
.navbar {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
backdrop-filter: blur(50px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
position: fixed !important;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
/* Navigation Bar Styles - REMOVED */
|
||||
|
||||
.brand-logo {
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: white !important;
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Floating Medical Icons */
|
||||
/* Floating Medical Icons - Subtle on Orange Background */
|
||||
.floating-medical-icon {
|
||||
position: absolute;
|
||||
animation: dvdBounce 20s linear infinite;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 1;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
top: auto;
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.floating-medical-icon:hover {
|
||||
@@ -62,141 +49,18 @@
|
||||
}
|
||||
|
||||
.floating-medical-icon .v-icon {
|
||||
color: rgba(255, 255, 255, 0.15) !important;
|
||||
color: rgba(255, 255, 255, 0.15) !important; /* Light white for icons on orange background */
|
||||
}
|
||||
|
||||
.icon-1 {
|
||||
top: 0%;
|
||||
left: 0%;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 25s;
|
||||
animation-name: dvdBounce1;
|
||||
}
|
||||
/* Specific icon positioning - now static */
|
||||
.icon-1 { top: 15%; left: 5%; }
|
||||
.icon-2 { top: 20%; right: 10%; }
|
||||
.icon-3 { bottom: 10%; left: 15%; }
|
||||
.icon-4 { top: 60%; left: 10%; }
|
||||
.icon-5 { bottom: 20%; right: 5%; }
|
||||
.icon-6 { top: 35%; left: 20%; }
|
||||
|
||||
.icon-1 .v-icon {
|
||||
color: rgba(6, 37, 83, 0.15) !important;
|
||||
}
|
||||
|
||||
.icon-2 {
|
||||
top: 0%;
|
||||
right: 0%;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 30s;
|
||||
animation-name: dvdBounce2;
|
||||
}
|
||||
|
||||
.icon-2 .v-icon {
|
||||
color: rgba(15, 180, 70, 0.12) !important;
|
||||
}
|
||||
|
||||
.icon-3 {
|
||||
bottom: 0%;
|
||||
left: 0%;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 22s;
|
||||
animation-name: dvdBounce3;
|
||||
}
|
||||
|
||||
.icon-3 .v-icon {
|
||||
color: rgba(23, 178, 206, 0.18) !important;
|
||||
}
|
||||
|
||||
.icon-4 {
|
||||
top: 50%;
|
||||
left: 0%;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 28s;
|
||||
animation-name: dvdBounce4;
|
||||
}
|
||||
|
||||
.icon-4 .v-icon {
|
||||
color: rgba(223, 8, 8, 0.14) !important;
|
||||
}
|
||||
|
||||
.icon-5 {
|
||||
bottom: 0%;
|
||||
right: 0%;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 26s;
|
||||
animation-name: dvdBounce5;
|
||||
}
|
||||
|
||||
.icon-5 .v-icon {
|
||||
color: rgba(148, 15, 236, 0.16) !important;
|
||||
}
|
||||
|
||||
.icon-6 {
|
||||
top: 25%;
|
||||
left: 0%;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 24s;
|
||||
animation-name: dvdBounce6;
|
||||
}
|
||||
|
||||
.icon-6 .v-icon {
|
||||
color: rgba(87, 48, 5, 0.13) !important;
|
||||
}
|
||||
|
||||
/* DVD Bounce Animation 1 - Top Left to Bottom Right */
|
||||
@keyframes dvdBounce1 {
|
||||
0% { transform: translate(0, 0); }
|
||||
25% { transform: translate(80vw, 70vh); }
|
||||
50% { transform: translate(20vw, 10vh); }
|
||||
75% { transform: translate(70vw, 80vh); }
|
||||
100% { transform: translate(0, 0); }
|
||||
}
|
||||
|
||||
/* DVD Bounce Animation 2 - Top Right to Bottom Left */
|
||||
@keyframes dvdBounce2 {
|
||||
0% { transform: translate(0, 0); }
|
||||
25% { transform: translate(-75vw, 60vh); }
|
||||
50% { transform: translate(-30vw, 20vh); }
|
||||
75% { transform: translate(-85vw, 75vh); }
|
||||
100% { transform: translate(0, 0); }
|
||||
}
|
||||
|
||||
/* DVD Bounce Animation 3 - Bottom Left to Top Right */
|
||||
@keyframes dvdBounce3 {
|
||||
0% { transform: translate(0, 0); }
|
||||
25% { transform: translate(70vw, -60vh); }
|
||||
50% { transform: translate(40vw, -80vh); }
|
||||
75% { transform: translate(90vw, -30vh); }
|
||||
100% { transform: translate(0, 0); }
|
||||
}
|
||||
|
||||
/* DVD Bounce Animation 4 - Middle Left across screen */
|
||||
@keyframes dvdBounce4 {
|
||||
0% { transform: translate(0, 0); }
|
||||
16.6% { transform: translate(60vw, -30vh); }
|
||||
33.3% { transform: translate(90vw, 20vh); }
|
||||
50% { transform: translate(50vw, 40vh); }
|
||||
66.6% { transform: translate(10vw, -20vh); }
|
||||
83.3% { transform: translate(80vw, -40vh); }
|
||||
100% { transform: translate(0, 0); }
|
||||
}
|
||||
|
||||
/* DVD Bounce Animation 5 - Bottom Right to Top Left */
|
||||
@keyframes dvdBounce5 {
|
||||
0% { transform: translate(0, 0); }
|
||||
25% { transform: translate(-60vw, -70vh); }
|
||||
50% { transform: translate(-90vw, -20vh); }
|
||||
75% { transform: translate(-40vw, -80vh); }
|
||||
100% { transform: translate(0, 0); }
|
||||
}
|
||||
|
||||
/* DVD Bounce Animation 6 - Complex zigzag pattern */
|
||||
@keyframes dvdBounce6 {
|
||||
0% { transform: translate(0, 0); }
|
||||
14.3% { transform: translate(50vw, 30vh); }
|
||||
28.6% { transform: translate(85vw, -20vh); }
|
||||
42.9% { transform: translate(30vw, 60vh); }
|
||||
57.1% { transform: translate(70vw, 10vh); }
|
||||
71.4% { transform: translate(15vw, 70vh); }
|
||||
85.7% { transform: translate(80vw, 40vh); }
|
||||
100% { transform: translate(0, 0); }
|
||||
}
|
||||
|
||||
/* Main Card - Contains Both Sections */
|
||||
/* Main Card - White */
|
||||
.main-card {
|
||||
z-index: 3;
|
||||
max-width: 1100px;
|
||||
@@ -205,16 +69,14 @@
|
||||
}
|
||||
|
||||
.white-card {
|
||||
background: white !important;
|
||||
background: white !important; /* Main card background is white */
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow:
|
||||
0 20px 50px rgba(0, 0, 0, 0.15),
|
||||
0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: none; /* No shadow for the main white card */
|
||||
}
|
||||
|
||||
/* Left Section Styling */
|
||||
/* Left Section Styling (White) */
|
||||
.text-section-box {
|
||||
background: linear-gradient(135deg, #61cbdd 0%, #4194c4 50%, #2089b9 100%);
|
||||
background: white; /* Kept as white */
|
||||
min-height: 600px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -227,9 +89,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.15) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 70%, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
|
||||
background: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -240,31 +100,31 @@
|
||||
}
|
||||
|
||||
.hero-title-box {
|
||||
color: white;
|
||||
color: #FF9B1B; /* Orange title on white background: #FF9B1B */
|
||||
font-size: 2.5rem;
|
||||
font-weight: 900;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.2);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.hero-description-box {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
color: #555; /* Darker text for readability on white */
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
margin-top: 2rem;
|
||||
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.2);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.hospital-logo-large {
|
||||
height: 80px;
|
||||
width: auto;
|
||||
filter: drop-shadow(2px 2px 6px rgba(0, 0, 0, 0.3));
|
||||
filter: drop-shadow(2px 2px 6px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
/* Right Section Styling */
|
||||
/* Right Login Section (White) */
|
||||
.login-section-box {
|
||||
background: white;
|
||||
background: white; /* Changed to white */
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
@@ -279,9 +139,9 @@
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Card Header - Dark Text */
|
||||
/* Card Header - Dark Text for white background */
|
||||
.welcome-title-dark {
|
||||
color: #37474F;
|
||||
color: #333; /* Dark text on white background */
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
@@ -289,14 +149,14 @@
|
||||
}
|
||||
|
||||
.login-instruction-dark {
|
||||
color: #546E7A;
|
||||
color: #777; /* Grey text for instruction on white background */
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* App Title - Dark */
|
||||
/* App Title - Dark for white background */
|
||||
.app-title-dark {
|
||||
color: #0053AD;
|
||||
color: #FF9B1B; /* Orange app title: #FF9B1B */
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
@@ -307,19 +167,19 @@
|
||||
}
|
||||
|
||||
.sso-text-dark {
|
||||
color: #546E7A;
|
||||
color: #555; /* Dark grey for SSO text */
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
/* Buttons (Orange theme) */
|
||||
.login-btn {
|
||||
background: linear-gradient(135deg, #0053AD 0%, #0663C7 50%, #0671E0 100%) !important;
|
||||
color: white !important;
|
||||
background: #FF9B1B !important; /* Solid Orange button: #FF9B1B */
|
||||
color: white !important; /* White text on orange button */
|
||||
border: none;
|
||||
box-shadow:
|
||||
0 8px 25px rgba(0, 83, 173, 0.3),
|
||||
0 4px 12px rgba(0, 83, 173, 0.2);
|
||||
box-shadow:
|
||||
0 8px 25px rgba(255, 155, 27, 0.3), /* Orange shadow: #FF9B1B */
|
||||
0 4px 12px rgba(255, 155, 27, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
text-transform: none;
|
||||
font-size: 1rem;
|
||||
@@ -328,15 +188,15 @@
|
||||
|
||||
.login-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
background: linear-gradient(135deg, #004A9B 0%, #0558B0 50%, #0661CA 100%) !important;
|
||||
box-shadow:
|
||||
0 12px 30px rgba(0, 83, 173, 0.4),
|
||||
0 6px 15px rgba(0, 83, 173, 0.3);
|
||||
background: #E68A00 !important; /* Slightly darker orange on hover */
|
||||
box-shadow:
|
||||
0 12px 30px rgba(255, 155, 27, 0.4),
|
||||
0 6px 15px rgba(255, 155, 27, 0.3);
|
||||
}
|
||||
|
||||
.register-btn-dark {
|
||||
color: #0053AD !important;
|
||||
border: 2px solid #0053AD !important;
|
||||
color: #FF9B1B !important; /* Orange text for register button: #FF9B1B */
|
||||
border: 2px solid #FF9B1B !important; /* Orange border: #FF9B1B */
|
||||
background: transparent !important;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: none;
|
||||
@@ -344,39 +204,39 @@
|
||||
}
|
||||
|
||||
.register-btn-dark:hover {
|
||||
background: rgba(0, 83, 173, 0.05) !important;
|
||||
border-color: #0663C7 !important;
|
||||
background: rgba(255, 155, 27, 0.05) !important; /* Light orange hover background */
|
||||
border-color: #E68A00 !important; /* Darker orange border on hover */
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Custom Divider - Dark */
|
||||
/* Custom Divider - Dark for white background */
|
||||
.custom-divider-dark {
|
||||
border-color: rgba(0, 0, 0, 0.12) !important;
|
||||
border-color: rgba(0, 0, 0, 0.1) !important; /* Light grey divider */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Help Text and Links - Dark */
|
||||
/* Help Text and Links - Dark for white background */
|
||||
.help-text-dark {
|
||||
color: #546E7A;
|
||||
color: #555; /* Dark grey help text */
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.contact-link-dark {
|
||||
color: #0053AD !important;
|
||||
color: #FF9B1B !important; /* Orange link: #FF9B1B */
|
||||
text-decoration: underline;
|
||||
text-transform: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.help-link-dark {
|
||||
color: #78909C;
|
||||
color: #777; /* Grey link */
|
||||
font-size: 0.85rem;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.help-link-dark:hover {
|
||||
color: #0053AD;
|
||||
color: #FF9B1B; /* Orange on hover: #FF9B1B */
|
||||
}
|
||||
|
||||
/* Transparent Background */
|
||||
@@ -384,53 +244,27 @@
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Navigation Dropdown Styles */
|
||||
.nav-dropdown {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
min-width: 220px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
/* Navigation Dropdown Styles - REMOVED */
|
||||
|
||||
/* Dialog Accents */
|
||||
.v-card-title .v-icon,
|
||||
.v-list-item .v-icon {
|
||||
color: #FF9B1B !important; /* Orange accents: #FF9B1B */
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
color: #FF9B1B;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 8px;
|
||||
margin: 4px 8px;
|
||||
padding: 8px 12px;
|
||||
.v-btn[color="#FF9B1B"] {
|
||||
background-color: #FF9B1B !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: linear-gradient(135deg, #FF9B1B 0%, #FF8F00 100%);
|
||||
color: white;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.dropdown-item .v-icon {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dropdown-item:hover .v-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
.v-alert[color="orange"] {
|
||||
border-left: 8px solid #FF9B1B !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 960px) {
|
||||
.login-background {
|
||||
padding-top: 56px;
|
||||
}
|
||||
/* .login-background padding adjustment REMOVED */
|
||||
|
||||
.main-card {
|
||||
margin: 1rem;
|
||||
@@ -455,9 +289,7 @@
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.login-background {
|
||||
padding-top: 56px;
|
||||
}
|
||||
/* .login-background padding adjustment REMOVED */
|
||||
|
||||
.main-card {
|
||||
margin: 0.5rem;
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<v-card
|
||||
class="pa-4 rounded-lg elevation-2 d-flex flex-column"
|
||||
color="white"
|
||||
style="border-top: 8px solid #FFB95F;"
|
||||
height="100%"
|
||||
>
|
||||
<div class="d-flex align-center justify-center mb-4">
|
||||
<v-icon size="36" color="orange-lighten-2" class="mr-2">mdi-hospital-box-outline</v-icon>
|
||||
<div class="text-h6 font-weight-bold text-orange-lighten-2">{{ title }}</div>
|
||||
</div>
|
||||
|
||||
<v-divider class="mb-4"></v-divider>
|
||||
|
||||
<div class="timeline-scroll-container">
|
||||
<v-timeline density="compact" align="start" line-inset="12" line-color="#FFB95F">
|
||||
<v-timeline-item
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
:dot-color="getStepColor(index)"
|
||||
:icon="getStepIcon(index)"
|
||||
size="small"
|
||||
icon-color="white"
|
||||
>
|
||||
<div class="d-flex flex-column align-start">
|
||||
<span
|
||||
class="font-weight-bold"
|
||||
:class="index === currentStepIndex ? 'text-orange-lighten-1' : index < currentStepIndex ? 'text-success' : 'text-grey-darken-1'"
|
||||
>
|
||||
{{ step.label }}
|
||||
</span>
|
||||
<span class="text-caption text-grey-darken-1">
|
||||
{{ step.date !== '-' ? `${step.date}, ${step.time}` : 'Menunggu' }}
|
||||
</span>
|
||||
</div>
|
||||
</v-timeline-item>
|
||||
</v-timeline>
|
||||
</div>
|
||||
|
||||
<v-divider class="mt-4 mb-4"></v-divider>
|
||||
|
||||
<v-btn
|
||||
size="large"
|
||||
variant="tonal"
|
||||
color="#FFA532"
|
||||
class="mt-auto font-weight-bold"
|
||||
prepend-icon="mdi-printer"
|
||||
block
|
||||
>
|
||||
Cetak Ulang Tiket
|
||||
</v-btn>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
steps: Array,
|
||||
color: String,
|
||||
currentStepLabel: String,
|
||||
});
|
||||
|
||||
const currentStepIndex = computed(() => {
|
||||
return props.steps.findIndex(step => step.label === props.currentStepLabel);
|
||||
});
|
||||
|
||||
const getStepIcon = (index) => {
|
||||
if (index < currentStepIndex.value) {
|
||||
return 'mdi-check';
|
||||
} else if (index === currentStepIndex.value) {
|
||||
return 'mdi-progress-check';
|
||||
} else {
|
||||
return 'mdi-circle-outline';
|
||||
}
|
||||
};
|
||||
|
||||
const getStepColor = (index) => {
|
||||
if (index < currentStepIndex.value) {
|
||||
return 'success';
|
||||
} else if (index === currentStepIndex.value) {
|
||||
return 'orange-lighten-1';
|
||||
}
|
||||
return 'grey-lighten-1';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* =============================================== */
|
||||
/* SCROLLABLE TIMELINE STYLES (Menggunakan Tinggi Tetap) */
|
||||
/* =============================================== */
|
||||
.timeline-scroll-container {
|
||||
/* Tinggi Tetap: Memastikan tinggi card konsisten di semua skenario responsive */
|
||||
height: 320px;
|
||||
|
||||
/* Membuat konten scrollable di sumbu Y jika melebihi height */
|
||||
overflow-y: auto;
|
||||
|
||||
/* Memberi jarak internal di bagian scrollable */
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
/* PENTING: Menghilangkan padding bawah dari V-Timeline bawaan */
|
||||
.v-timeline {
|
||||
padding-bottom: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* Kustomisasi scrollbar untuk tampilan yang lebih bersih (Opsional) */
|
||||
.timeline-scroll-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.timeline-scroll-container::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* =============================================== */
|
||||
/* V-TIMELINE KUSTOMISASI */
|
||||
/* =============================================== */
|
||||
.v-timeline-item :deep(.v-timeline-item__body) {
|
||||
padding-inline-start: 16px !important;
|
||||
}
|
||||
|
||||
.v-timeline-item :deep(.v-timeline-item__opposite) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
+322
-82
@@ -4,7 +4,7 @@
|
||||
:close-on-content-click="false"
|
||||
location="top end"
|
||||
origin="bottom right"
|
||||
transition="slide-y-transition"
|
||||
transition="scale-transition"
|
||||
open-on-hover
|
||||
>
|
||||
<template v-slot:activator="{ props: menuProps }">
|
||||
@@ -13,20 +13,22 @@
|
||||
v-bind="menuProps"
|
||||
:title="user?.name || user?.preferred_username || 'User'"
|
||||
:subtitle="user?.email || 'No email'"
|
||||
class="pa-2 ma-1 rounded-lg bg-blue-grey-lighten-5 text-blue-grey-darken-4"
|
||||
color="blue-grey-darken-4"
|
||||
class="pa-3 ma-1 rounded-xl profile-activator"
|
||||
link
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-avatar size="40">
|
||||
<v-img
|
||||
:src="user?.picture || 'https://i.pravatar.cc/300?img=68'"
|
||||
:alt="`${user?.name || 'User'} Profile`"
|
||||
></v-img>
|
||||
</v-avatar>
|
||||
<div class="avatar-wrapper">
|
||||
<v-avatar size="44" class="avatar-glow">
|
||||
<v-img
|
||||
:src="user?.picture || 'https://i.pravatar.cc/300?img=68'"
|
||||
:alt="`${user?.name || 'User'} Profile`"
|
||||
></v-img>
|
||||
</v-avatar>
|
||||
<div class="status-dot"></div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<v-icon size="20">mdi-chevron-up</v-icon>
|
||||
<v-icon size="20" class="chevron-icon">mdi-menu-down</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
@@ -35,87 +37,142 @@
|
||||
icon
|
||||
v-bind="menuProps"
|
||||
size="large"
|
||||
variant="text"
|
||||
color="blue-grey-darken-4"
|
||||
variant="flat"
|
||||
class="avatar-btn"
|
||||
:title="user?.name || 'Profile'"
|
||||
>
|
||||
<v-avatar size="40">
|
||||
<v-img
|
||||
:src="user?.picture || 'https://i.pravatar.cc/300?img=68'"
|
||||
:alt="`${user?.name || 'User'} Profile`"
|
||||
></v-img>
|
||||
</v-avatar>
|
||||
<div class="avatar-wrapper">
|
||||
<v-avatar size="44" class="avatar-glow">
|
||||
<v-img
|
||||
:src="user?.picture || 'https://i.pravatar.cc/300?img=68'"
|
||||
:alt="`${user?.name || 'User'} Profile`"
|
||||
></v-img>
|
||||
</v-avatar>
|
||||
<div class="status-dot"></div>
|
||||
</div>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-card class="rounded-lg elevation-4 pa-4" width="300" color="blue-grey-lighten-5">
|
||||
<div class="d-flex align-center pb-2">
|
||||
<v-avatar size="48">
|
||||
<v-img
|
||||
:src="user?.picture || 'https://i.pravatar.cc/300?img=68'"
|
||||
:alt="`${user?.name || 'User'} Profile`"
|
||||
></v-img>
|
||||
</v-avatar>
|
||||
<div class="ml-4">
|
||||
<div class="text-subtitle-1 font-weight-bold text-blue-grey-darken-4">
|
||||
<v-card class="profile-card rounded-xl elevation-12" width="340">
|
||||
<!-- Decorative Header -->
|
||||
<div class="card-header">
|
||||
<div class="header-pattern"></div>
|
||||
<div class="header-content pa-6 text-center">
|
||||
<div class="avatar-container mb-4">
|
||||
<v-avatar size="90" class="profile-avatar">
|
||||
<v-img
|
||||
:src="user?.picture || 'https://i.pravatar.cc/300?img=68'"
|
||||
:alt="`${user?.name || 'User'} Profile`"
|
||||
></v-img>
|
||||
</v-avatar>
|
||||
<div class="status-badge">
|
||||
<v-icon size="12" color="white">mdi-check</v-icon>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-h6 font-weight-bold text-white mb-1">
|
||||
{{ user?.name || user?.preferred_username || 'User' }}
|
||||
</div>
|
||||
<div class="text-caption text-grey-darken-1">
|
||||
</h3>
|
||||
<p class="text-body-2 text-white opacity-90 mb-2">
|
||||
{{ user?.email || 'No email' }}
|
||||
</div>
|
||||
<div class="text-caption text-grey-darken-2">
|
||||
ID: {{ user?.id?.substring(0, 8) }}...
|
||||
</div>
|
||||
</p>
|
||||
<v-chip
|
||||
size="small"
|
||||
class="glass-chip"
|
||||
prepend-icon="mdi-identifier"
|
||||
>
|
||||
{{ user?.id?.substring(0, 10) }}...
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
<v-divider class="my-2"></v-divider>
|
||||
|
||||
<v-list dense>
|
||||
<v-list-item link class="rounded-lg text-blue-grey-darken-4" @click="handleAction('account')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="blue-grey-darken-4">mdi-cog</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Pengaturan Akun</v-list-item-title>
|
||||
</v-list-item>
|
||||
<!-- Menu Items -->
|
||||
<v-card-text class="pa-4">
|
||||
<div class="menu-grid">
|
||||
<div
|
||||
class="menu-tile rounded-lg pa-4 text-center cursor-pointer"
|
||||
@click="handleAction('profile')"
|
||||
>
|
||||
<div class="tile-icon-wrapper mb-2 mx-auto">
|
||||
<v-icon size="24" color="blue-darken-2">mdi-account-circle</v-icon>
|
||||
</div>
|
||||
<div class="text-caption font-weight-medium">Profil</div>
|
||||
</div>
|
||||
|
||||
<v-list-item link class="rounded-lg text-blue-grey-darken-4" @click="handleAction('darkMode')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="blue-grey-darken-4">mdi-weather-night</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Mode Gelap</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<v-switch
|
||||
v-model="darkMode"
|
||||
hide-details
|
||||
density="compact"
|
||||
color="orange-darken-2"
|
||||
></v-switch>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<div
|
||||
class="menu-tile rounded-lg pa-4 text-center cursor-pointer"
|
||||
@click="handleAction('account')"
|
||||
>
|
||||
<div class="tile-icon-wrapper mb-2 mx-auto">
|
||||
<v-icon size="24" color="orange-darken-2">mdi-cog-outline</v-icon>
|
||||
</div>
|
||||
<div class="text-caption font-weight-medium">Pengaturan</div>
|
||||
</div>
|
||||
|
||||
<v-list-item link class="rounded-lg text-blue-grey-darken-4" @click="handleAction('profile')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="blue-grey-darken-4">mdi-account</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Profil Saya</v-list-item-title>
|
||||
</v-list-item>
|
||||
<div
|
||||
class="menu-tile rounded-lg pa-4 text-center cursor-pointer"
|
||||
@click="handleAction('darkMode')"
|
||||
>
|
||||
<div class="tile-icon-wrapper mb-2 mx-auto">
|
||||
<v-icon size="24" :color="darkMode ? 'deep-purple-darken-2' : 'amber-darken-2'">
|
||||
{{ darkMode ? 'mdi-moon-waning-crescent' : 'mdi-white-balance-sunny' }}
|
||||
</v-icon>
|
||||
</div>
|
||||
<div class="text-caption font-weight-medium">
|
||||
{{ darkMode ? 'Gelap' : 'Terang' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-divider class="my-2"></v-divider>
|
||||
<v-divider class="my-4"></v-divider>
|
||||
|
||||
<v-list-item
|
||||
link
|
||||
class="rounded-lg text-red"
|
||||
<!-- Quick Actions -->
|
||||
<div class="quick-actions">
|
||||
<v-list-item
|
||||
class="rounded-lg px-3 py-2 action-item"
|
||||
link
|
||||
@click="handleAction('notifications')"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="20" color="blue-darken-1">mdi-bell-outline</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-body-2 font-weight-medium">
|
||||
Notifikasi
|
||||
</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<v-badge color="orange-darken-2" content="3" inline></v-badge>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
class="rounded-lg px-3 py-2 action-item"
|
||||
link
|
||||
@click="handleAction('help')"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="20" color="blue-darken-1">mdi-help-circle-outline</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-body-2 font-weight-medium">
|
||||
Bantuan & Dukungan
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</div>
|
||||
|
||||
<v-divider class="my-4"></v-divider>
|
||||
|
||||
<!-- Logout Button -->
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg logout-btn"
|
||||
variant="flat"
|
||||
color="error"
|
||||
prepend-icon="mdi-logout"
|
||||
@click="signOut"
|
||||
:disabled="isLoggingOut"
|
||||
:loading="isLoggingOut"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="red">mdi-logout</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>
|
||||
{{ isLoggingOut ? 'Logging out...' : 'Keluar' }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
{{ isLoggingOut ? 'Keluar...' : 'Keluar' }}
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</template>
|
||||
@@ -146,7 +203,6 @@ const signOut = async () => {
|
||||
menu.value = false;
|
||||
|
||||
try {
|
||||
// 🚨 Emits event up to SideBar to trigger useAuth().logout()
|
||||
emit('logout');
|
||||
} finally {
|
||||
isLoggingOut.value = false;
|
||||
@@ -156,21 +212,205 @@ const signOut = async () => {
|
||||
const handleAction = (action) => {
|
||||
switch(action) {
|
||||
case 'account':
|
||||
navigateTo('/settings/account')
|
||||
navigateTo('/Profile/Pengaturan')
|
||||
break;
|
||||
case 'profile':
|
||||
navigateTo('/profile')
|
||||
navigateTo('/Profile/Profil')
|
||||
break;
|
||||
case 'notifications':
|
||||
navigateTo('/notifications')
|
||||
break;
|
||||
case 'help':
|
||||
navigateTo('/help')
|
||||
break;
|
||||
case 'darkMode':
|
||||
darkMode.value = !darkMode.value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (action !== 'darkMode') {
|
||||
menu.value = false;
|
||||
}
|
||||
menu.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-red {
|
||||
color: rgb(244, 67, 54) !important;
|
||||
.profile-activator {
|
||||
background: linear-gradient(135deg, rgba(25, 118, 210, 0.1) 0%, rgba(245, 124, 0, 0.1) 100%);
|
||||
border: 1px solid rgba(25, 118, 210, 0.2);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.profile-activator:hover {
|
||||
background: linear-gradient(135deg, rgba(25, 118, 210, 0.15) 0%, rgba(245, 124, 0, 0.15) 100%);
|
||||
border-color: rgba(25, 118, 210, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatar-glow {
|
||||
border: 3px solid white;
|
||||
box-shadow: 0 0 20px rgba(25, 118, 210, 0.4);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: linear-gradient(135deg, #4caf50 0%, #8bc34a 100%);
|
||||
border: 2px solid white;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.profile-activator:hover .chevron-icon {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
.avatar-btn {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(25, 118, 210, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #1976d2 0%, #fb8c00 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 50%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 80%, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: linear-gradient(135deg, #4caf50 0%, #8bc34a 100%);
|
||||
border: 3px solid white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.glass-chip {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
color: white !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.menu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.menu-tile {
|
||||
background: linear-gradient(135deg, rgba(25, 118, 210, 0.05) 0%, rgba(245, 124, 0, 0.05) 100%);
|
||||
border: 1px solid rgba(25, 118, 210, 0.1);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-tile:hover {
|
||||
background: linear-gradient(135deg, rgba(25, 118, 210, 0.1) 0%, rgba(245, 124, 0, 0.1) 100%);
|
||||
border-color: rgba(25, 118, 210, 0.3);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tile-icon-wrapper {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
transition: all 0.2s ease;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.action-item:hover {
|
||||
background: linear-gradient(135deg, rgba(25, 118, 210, 0.05) 0%, rgba(245, 124, 0, 0.05) 100%);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
text-transform: none;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 4px 12px rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
box-shadow: 0 6px 20px rgba(244, 67, 54, 0.4);
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.opacity-90 {
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
@@ -1,75 +0,0 @@
|
||||
<!-- <template>
|
||||
<v-dialog v-model="dialog" max-width="500px">
|
||||
<v-card>
|
||||
<v-card-title class="text-h6 font-weight-bold">
|
||||
Atur Urutan Menu
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list dense>
|
||||
<draggable v-model="localMenus" item-key="title" @end="onDragEnd">
|
||||
<template #item="{ element }">
|
||||
<v-list-item class="reorder-item">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ element.title }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-drag</v-icon>
|
||||
</v-list-item-icon>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</draggable>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey-darken-1" text @click="dialog = false">Batal</v-btn>
|
||||
<v-btn color="blue" text @click="saveOrder">Simpan Urutan</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import draggable from 'vuedraggable';
|
||||
import navigationItems from '~/data/menu.js';
|
||||
|
||||
const dialog = ref(false);
|
||||
const localMenus = ref([]);
|
||||
|
||||
// Watch for changes in the dialog's visibility
|
||||
watch(dialog, (val) => {
|
||||
if (val) {
|
||||
// Make a deep copy to avoid mutating the original data
|
||||
localMenus.value = JSON.parse(JSON.stringify(navigationItems));
|
||||
}
|
||||
});
|
||||
|
||||
const onDragEnd = (event) => {
|
||||
// Logic to handle the end of a drag event
|
||||
};
|
||||
|
||||
const saveOrder = () => {
|
||||
// Emit an event with the new menu order
|
||||
// You would then handle this in the parent component
|
||||
// to update the ~/data/menu.js file (or your state management)
|
||||
// and trigger a UI refresh.
|
||||
dialog.value = false;
|
||||
};
|
||||
|
||||
const openDialog = () => {
|
||||
dialog.value = true;
|
||||
};
|
||||
|
||||
defineExpose({ openDialog });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.reorder-item {
|
||||
cursor: grab;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.reorder-item:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
</style> -->
|
||||
+32
-12
@@ -23,7 +23,8 @@
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-list density="compact" nav class="mt-2" v-model:opened="openedGroups"> <template v-for="item in items" :key="item.name">
|
||||
<v-list density="compact" nav class="mt-2" v-model:opened="openedGroups">
|
||||
<template v-for="item in items" :key="item.name">
|
||||
|
||||
<v-list-group
|
||||
v-if="item.children"
|
||||
@@ -99,12 +100,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, defineEmits, onMounted, ref, watch } from 'vue'; // IMPORT WATCH
|
||||
import { navigateTo } from '#app';
|
||||
import { defineProps, defineEmits, onMounted, ref, watch } from 'vue';
|
||||
import { navigateTo, useRoute } from '#app';
|
||||
import ProfilePopup from './ProfilePopup.vue';
|
||||
import { useAuth } from '~/composables/useAuth';
|
||||
import { useAuth } from '~/composables/useAuth'; // Ensure this path is correct
|
||||
|
||||
const { user, logout, checkAuth } = useAuth();
|
||||
const route = useRoute(); // Access the current route
|
||||
|
||||
interface NavItem {
|
||||
id: number;
|
||||
@@ -131,22 +133,38 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update:drawer', 'toggle-rail']);
|
||||
|
||||
// --- NEW STATE FOR DROPDOWN GROUPS ---
|
||||
// This holds the names of the currently open list groups.
|
||||
const openedGroups = ref<string[]>([]);
|
||||
const isHovering = ref(false);
|
||||
|
||||
// --- NEW WATCHER TO CLOSE DROPDOWNS ---
|
||||
// --- WATCHERS FOR DROPDOWN AND HIGHLIGHTING ---
|
||||
|
||||
// 1. WATCHER: Close dropdowns when sidebar collapses (rail = true)
|
||||
watch(() => props.rail, (newRailState) => {
|
||||
if (newRailState === true) {
|
||||
// If the sidebar is collapsing (rail is true), close all groups
|
||||
openedGroups.value = [];
|
||||
}
|
||||
});
|
||||
// ------------------------------------
|
||||
|
||||
// 2. WATCHER: Open the parent group when a child route is active
|
||||
watch(() => route.path, (newPath) => {
|
||||
// Find the parent item whose children contain the current path
|
||||
const activeParent = props.items.find(item =>
|
||||
item.children && item.children.some(child =>
|
||||
child.path === newPath
|
||||
)
|
||||
);
|
||||
|
||||
if (activeParent) {
|
||||
// If a parent is active, ensure it's in the openedGroups array
|
||||
if (!openedGroups.value.includes(activeParent.name)) {
|
||||
openedGroups.value.push(activeParent.name);
|
||||
}
|
||||
|
||||
// Optional: Keep only the active parent open, closing others
|
||||
// openedGroups.value = [activeParent.name];
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// Local state for hover-to-expand rail feature
|
||||
const isHovering = ref(false);
|
||||
|
||||
// --- HOVER LOGIC for Rail Expansion ---
|
||||
|
||||
@@ -164,11 +182,12 @@ const handleMouseLeave = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// --- AUTH LOGIC (Kept the same) ---
|
||||
// --- AUTH LOGIC ---
|
||||
|
||||
const handleLogout = async () => {
|
||||
console.log('🚪 SideBar logout initiated...');
|
||||
try {
|
||||
// Calls the external logout function (from useAuth.ts)
|
||||
await logout();
|
||||
} catch (error) {
|
||||
console.error('❌ SideBar logout error:', error);
|
||||
@@ -180,6 +199,7 @@ const redirectToLogin = () => {
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// Fetches real user data on mount
|
||||
await checkAuth();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// /composables/usePermissions.ts
|
||||
export const usePermission = () => {
|
||||
/**
|
||||
* Extract group and role from path
|
||||
|
||||
+7
-1
@@ -3,7 +3,13 @@ import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-05-15',
|
||||
devtools: { enabled: true },
|
||||
devtools: {
|
||||
enabled: true,
|
||||
|
||||
timeline: {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
'@nuxt/content',
|
||||
|
||||
Generated
+18
@@ -31,6 +31,7 @@
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"sass-embedded": "^1.89.2",
|
||||
"vite-plugin-vuetify": "^2.1.2",
|
||||
"vuetify": "^3.9.3"
|
||||
@@ -5123,6 +5124,16 @@
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz",
|
||||
"integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/parse-path": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz",
|
||||
@@ -16590,6 +16601,13 @@
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
|
||||
"integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unenv": {
|
||||
"version": "2.0.0-rc.21",
|
||||
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.21.tgz",
|
||||
|
||||
+4
-1
@@ -4,7 +4,9 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"_command_dev": "nuxt dev -o --host --port 3001",
|
||||
"_command_dev2": "nuxt dev -o --port 3001",
|
||||
"dev": "nuxt dev -o --port 3001",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
@@ -34,6 +36,7 @@
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"sass-embedded": "^1.89.2",
|
||||
"vite-plugin-vuetify": "^2.1.2",
|
||||
"vuetify": "^3.9.3"
|
||||
|
||||
File diff suppressed because it is too large
Load diff
+359
-161
@@ -1,114 +1,155 @@
|
||||
<template>
|
||||
<v-container fluid class="pa-6 bg-white-lighten-4">
|
||||
<div class="d-flex justify-space-between align-center mb-6">
|
||||
<v-container fluid class="pa-8 dashboard-bg">
|
||||
|
||||
<div class="d-flex justify-space-between align-center mb-10">
|
||||
<div>
|
||||
<h1 class="text-h4 font-weight-bold">Dashboard</h1>
|
||||
<p v-if="user" class="text-subtitle-1 text-grey-darken-1 mt-1">
|
||||
Selamat Datang, {{ user.name || user.preferred_username }}!
|
||||
<h1 class="text-h3 font-weight-black primary-text">
|
||||
Antrean Dashboard
|
||||
</h1>
|
||||
<p v-if="user" class="text-subtitle-1 secondary-text mt-2">
|
||||
Selamat Datang,
|
||||
<span class="font-weight-bold primary-accent">
|
||||
{{ user.name || user.preferred_username }}
|
||||
</span>! 🍊
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<v-chip color="green-lighten-1" class="mr-2 pa-3 font-weight-bold">
|
||||
<v-icon start icon="mdi-calendar"></v-icon>
|
||||
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
color="#FF9B1B"
|
||||
variant="flat"
|
||||
class="text-caption font-weight-bold rounded-lg elevation-2 mr-4 export-btn"
|
||||
>
|
||||
<v-icon start icon="mdi-download-box-outline"></v-icon>
|
||||
Export Data ({{ exportType }})
|
||||
<v-icon end>mdi-menu-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="(item, index) in exportOptions"
|
||||
:key="index"
|
||||
@click="handleExport(item.type)"
|
||||
>
|
||||
<v-list-item-title>
|
||||
<v-icon start :icon="item.icon"></v-icon>
|
||||
{{ item.title }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-chip color="white" variant="flat" class="pa-4 font-weight-bold text-caption secondary-text rounded-lg date-chip">
|
||||
<v-icon start icon="mdi-calendar-check-outline" class="primary-accent"></v-icon>
|
||||
{{ currentDate }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-row class="mb-6">
|
||||
<v-row class="mb-10">
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-card class="pa-4 rounded-xl elevation-6" color="blue-lighten-1" theme="dark">
|
||||
<v-card class="pa-6 rounded-xl elevation-3 hover-effect card-style" style="border-left: 4px solid #D17A4A;">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="64" class="mr-4">mdi-account-group</v-icon>
|
||||
<v-icon size="48" class="mr-4 primary-accent">mdi-tablet-dashboard</v-icon>
|
||||
<div>
|
||||
<div class="text-h4 font-weight-black">2635</div>
|
||||
<div class="text-subtitle-1">Total Visitors</div>
|
||||
<div class="text-h4 font-weight-black primary-accent">150</div>
|
||||
<div class="text-caption text-uppercase font-weight-medium secondary-light mt-1">Total Antrean Online</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-card class="pa-4 rounded-xl elevation-6" color="cyan-lighten-1" theme="dark">
|
||||
<v-card class="pa-6 rounded-xl elevation-3 hover-effect card-style" style="border-left: 4px solid #4A5F7A;">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="64" class="mr-4">mdi-account-multiple-plus</v-icon>
|
||||
<v-icon size="48" class="mr-4 secondary-accent">mdi-account-hard-hat</v-icon>
|
||||
<div>
|
||||
<div class="text-h4 font-weight-black">759</div>
|
||||
<div class="text-subtitle-1">Offline Registrants</div>
|
||||
<div class="text-h4 font-weight-black secondary-accent">100</div>
|
||||
<div class="text-caption text-uppercase font-weight-medium secondary-light mt-1">Total Antrean MJKN</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-card class="pa-4 rounded-xl elevation-6" color="green-lighten-1" theme="dark">
|
||||
<v-card class="pa-6 rounded-xl elevation-3 hover-effect card-style" style="border-left: 4px solid #D17A4A;">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="64" class="mr-4">mdi-account-multiple-plus-outline</v-icon>
|
||||
<v-icon size="48" class="mr-4 primary-accent">mdi-account-group</v-icon>
|
||||
<div>
|
||||
<div class="text-h4 font-weight-black">1876</div>
|
||||
<div class="text-subtitle-1">Online Registrants</div>
|
||||
<div class="text-h4 font-weight-black primary-accent">500</div>
|
||||
<div class="text-caption text-uppercase font-weight-medium secondary-light mt-1">Total Kunjungan</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-card class="pa-4 rounded-xl elevation-6" color="orange-lighten-1" theme="dark">
|
||||
<v-card class="pa-6 rounded-xl elevation-3 hover-effect card-style" style="border-left: 4px solid #4A5F7A;">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="64" class="mr-4">mdi-ticket</v-icon>
|
||||
<v-icon size="48" class="mr-4 secondary-accent">mdi-timer-sand</v-icon>
|
||||
<div>
|
||||
<div class="text-h4 font-weight-black">248</div>
|
||||
<div class="text-subtitle-1">Total Tickets Printed</div>
|
||||
<div class="text-h4 font-weight-black secondary-accent">7.5s</div>
|
||||
<div class="text-caption text-uppercase font-weight-medium secondary-light mt-1">Avg. Check-in Time</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12" md="6">
|
||||
<v-card class="pa-4 rounded-xl elevation-6">
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span class="text-h6 font-weight-bold">Grafik Jumlah Antrean</span>
|
||||
<v-btn-toggle v-model="queueTimePeriod" mandatory divided variant="outlined" color="primary" class="text-caption">
|
||||
<v-btn size="small" value="day">Hari</v-btn>
|
||||
<v-btn size="small" value="week">Minggu</v-btn>
|
||||
<v-btn size="small" value="month">Bulan</v-btn>
|
||||
<v-btn size="small" value="year">Tahun</v-btn>
|
||||
<v-card class="pa-6 rounded-xl elevation-3 card-style chart-card">
|
||||
<v-card-title class="d-flex justify-space-between align-center px-0 pt-0 mb-4">
|
||||
<span class="text-h6 font-weight-bold primary-text">Registration Trend</span>
|
||||
<v-btn-toggle v-model="queueTimePeriod" mandatory variant="outlined" color="#D17A4A" class="text-caption rounded-lg">
|
||||
<v-btn size="small" value="day">Day</v-btn>
|
||||
<!-- <v-btn size="small" value="week">Week</v-btn>
|
||||
<v-btn size="small" value="month">Month</v-btn> -->
|
||||
</v-btn-toggle>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-card-text class="pa-0">
|
||||
<Bar
|
||||
:data="queueChartData"
|
||||
:options="queueChartOptions"
|
||||
style="height: 300px"
|
||||
style="height: 350px"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-card class="pa-4 rounded-xl elevation-6">
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span class="text-h6 font-weight-bold">Monthly Visitor Data</span>
|
||||
<v-card class="pa-6 rounded-xl elevation-3 card-style chart-card">
|
||||
<v-card-title class="d-flex justify-space-between align-center px-0 pt-0 mb-4">
|
||||
<span class="text-h6 font-weight-bold primary-text">Total Registrasi Perbulan</span>
|
||||
<div>
|
||||
<v-chip
|
||||
:color="activeYear === '2024' ? 'green-lighten-1' : 'grey-lighten-2'"
|
||||
class="mr-2 text-caption font-weight-bold cursor-pointer"
|
||||
:color="activeYear === '2024' ? '#D17A4A' : '#F5F5F5'"
|
||||
:text-color="activeYear === '2024' ? 'white' : '#FF9B1B'"
|
||||
variant="flat"
|
||||
class="mr-2 text-caption font-weight-bold cursor-pointer transition-chip rounded-pill"
|
||||
@click="changeYear('2024')"
|
||||
>
|
||||
2024
|
||||
</v-chip>
|
||||
<v-chip
|
||||
:color="activeYear === '2025' ? 'green-lighten-1' : 'grey-lighten-2'"
|
||||
class="text-caption font-weight-bold cursor-pointer"
|
||||
:color="activeYear === '2025' ? '#D17A4A' : '#F5F5F5'"
|
||||
:text-color="activeYear === '2025' ? 'white' : '#FF9B1B'"
|
||||
variant="flat"
|
||||
class="text-caption font-weight-bold cursor-pointer transition-chip rounded-pill"
|
||||
@click="changeYear('2025')"
|
||||
>
|
||||
2025
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-card-text class="pa-0">
|
||||
<Bar
|
||||
:data="barData"
|
||||
:options="barOptions"
|
||||
style="height: 300px"
|
||||
style="height: 350px"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -117,35 +158,35 @@
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-card class="pa-4 rounded-xl elevation-6">
|
||||
<v-card-title class="text-h6 font-weight-bold">Realtime Ticket Queue</v-card-title>
|
||||
<v-card-text>
|
||||
<v-card class="pa-6 rounded-xl elevation-3 card-style chart-card">
|
||||
<v-card-title class="text-h6 font-weight-bold primary-text px-0 pt-0 mb-4">Realtime Check-in Velocity</v-card-title>
|
||||
<v-card-text class="pa-0">
|
||||
<Line
|
||||
ref="realtimeChart"
|
||||
:data="initialLineData"
|
||||
:options="lineOptions"
|
||||
style="height: 300px"
|
||||
style="height: 350px"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-card class="pa-4 rounded-xl elevation-6">
|
||||
<v-card-title class="text-h6 font-weight-bold">Registrant Breakdown</v-card-title>
|
||||
<v-card-text>
|
||||
<v-card class="pa-6 rounded-xl elevation-3 card-style chart-card">
|
||||
<v-card-title class="text-h6 font-weight-bold primary-text px-0 pt-0 mb-4">Registrant Type Breakdown</v-card-title>
|
||||
<v-card-text class="d-flex justify-center align-center pa-0" style="height: 350px;">
|
||||
<Pie
|
||||
:data="pieData"
|
||||
:options="pieOptions"
|
||||
style="height: 300px"
|
||||
style="max-height: 100%; max-width: 100%;"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-overlay v-model="isLoading" contained class="align-center justify-center">
|
||||
<v-progress-circular indeterminate size="64" color="primary"></v-progress-circular>
|
||||
<p class="mt-4">Loading dashboard...</p>
|
||||
<v-overlay v-model="isLoading" contained class="align-center justify-center dashboard-bg">
|
||||
<v-progress-circular indeterminate size="64" color="#D17A4A"></v-progress-circular>
|
||||
<p class="mt-4 primary-text">Loading dashboard...</p>
|
||||
</v-overlay>
|
||||
</v-container>
|
||||
</template>
|
||||
@@ -174,6 +215,7 @@ import {
|
||||
LineElement,
|
||||
} from 'chart.js';
|
||||
|
||||
// Ensure this middleware setup is correct for your framework (e.g., Nuxt)
|
||||
definePageMeta({
|
||||
middleware:['auth']
|
||||
})
|
||||
@@ -181,54 +223,59 @@ definePageMeta({
|
||||
// Register necessary Chart.js elements
|
||||
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, ArcElement, PointElement, LineElement);
|
||||
|
||||
// Use the auth composable
|
||||
// --- KEYCLOAK/AUTH INTEGRATION ---
|
||||
const { user, isLoading, checkAuth, logout } = useAuth()
|
||||
const navigateTo = (path) => { console.log('Navigating to', path); };
|
||||
// ---------------------------------
|
||||
|
||||
// --- EXPORT STATE ---
|
||||
const exportType = ref('JSON');
|
||||
const exportOptions = ref([
|
||||
{ title: 'JSON (.json)', type: 'json', icon: 'mdi-code-json' },
|
||||
{ title: 'CSV (.csv)', type: 'csv', icon: 'mdi-file-delimited' },
|
||||
{ title: 'Excel (.xlsx) - Server Only', type: 'excel', icon: 'mdi-file-excel' },
|
||||
{ title: 'PDF (.pdf) - Placeholder', type: 'pdf', icon: 'mdi-file-pdf' },
|
||||
]);
|
||||
// --------------------
|
||||
|
||||
const isLoggingOut = ref(false)
|
||||
|
||||
// Dashboard data
|
||||
// Dashboard state
|
||||
const currentDate = ref('');
|
||||
const activeYear = ref('2025');
|
||||
const queueTimePeriod = ref('month');
|
||||
const queueTimePeriod = ref('day');
|
||||
|
||||
// --- MOCK DATA DINAMIS ANTREEAN (Tetap) ---
|
||||
// --- MOCK DATA FOR CHARTS ---
|
||||
const mockQueueData = ref([
|
||||
// ... (data tetap sama)
|
||||
{ date: '2025-09-22', count: 120 },
|
||||
{ date: '2025-09-23', count: 150 },
|
||||
{ date: '2025-09-24', count: 135 },
|
||||
{ date: '2025-09-25', count: 160 },
|
||||
{ date: '2025-09-26', count: 145 },
|
||||
{ date: '2025-09-27', count: 170 },
|
||||
{ date: '2025-09-28', count: 180 },
|
||||
{ date: '2025-08-10', count: 300 },
|
||||
{ date: '2025-08-20', count: 350 },
|
||||
{ date: '2025-09-01', count: 400 },
|
||||
{ date: '2025-09-15', count: 450 },
|
||||
{ date: '2024-01-01', count: 1200 },
|
||||
{ date: '2024-06-01', count: 1800 },
|
||||
{ date: '2025-01-01', count: 2000 },
|
||||
{ date: '2025-06-01', count: 2500 },
|
||||
{ date: '2025-10-13', registrants: 300, attendees: 250 },
|
||||
{ date: '2025-10-14', registrants: 450, attendees: 400 },
|
||||
{ date: '2025-10-15', registrants: 500, attendees: 420 },
|
||||
{ date: '2025-10-16', registrants: 400, attendees: 350 },
|
||||
{ date: '2025-10-17', registrants: 550, attendees: 500 },
|
||||
{ date: '2025-10-18', registrants: 435, attendees: 380 },
|
||||
]);
|
||||
// --- AKHIR MOCK DATA ANTREEAN ---
|
||||
|
||||
// --- REFACTORED REALTIME LOGIC ---
|
||||
const realtimeChart = ref(null); // Ref untuk mengakses komponen Line
|
||||
// --- REALTIME CHART LOGIC ---
|
||||
const realtimeChart = ref(null);
|
||||
const maxDataPoints = 10;
|
||||
let realtimeInterval = null;
|
||||
|
||||
// Initial data structure (non-reactive for update function)
|
||||
const initialLineData = {
|
||||
labels: Array.from({ length: maxDataPoints }, (_, i) =>
|
||||
dayjs().subtract((maxDataPoints - 1 - i) * 5, 'second').format('HH:mm:ss')
|
||||
dayjs().subtract((maxDataPoints - 1 - i) * 10, 'second').format('HH:mm:ss')
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Tickets Processed',
|
||||
backgroundColor: '#FF5722',
|
||||
borderColor: '#FF5722',
|
||||
data: Array.from({ length: maxDataPoints }, () => Math.floor(Math.random() * 50) + 100),
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
label: 'Check-ins/min',
|
||||
backgroundColor: 'rgba(74, 95, 122, 0.15)',
|
||||
borderColor: '#5FB7FF',
|
||||
data: Array.from({ length: maxDataPoints }, () => Math.floor(Math.random() * 20) + 40),
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
borderWidth: 3,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
pointBackgroundColor: '#4A5F7A',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -236,9 +283,7 @@ const initialLineData = {
|
||||
const lineOptions = ref({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
duration: 0 // Crucial: Disable animation for smooth realtime scrolling
|
||||
},
|
||||
animation: { duration: 0 },
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
title: { display: false }
|
||||
@@ -246,9 +291,9 @@ const lineOptions = ref({
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
suggestedMax: 200,
|
||||
title: { display: true, text: 'Count' },
|
||||
grid: { display: true }
|
||||
suggestedMax: 80,
|
||||
title: { display: true, text: 'Check-ins per Minute' },
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' }
|
||||
},
|
||||
x: {
|
||||
title: { display: true, text: 'Time (HH:MM:SS)' },
|
||||
@@ -257,49 +302,28 @@ const lineOptions = ref({
|
||||
}
|
||||
});
|
||||
|
||||
// Function to simulate realtime data update using chart.update()
|
||||
const updateRealtimeData = () => {
|
||||
const chart = realtimeChart.value?.chart;
|
||||
if (!chart) return;
|
||||
|
||||
// 1. Get the current data arrays
|
||||
const dataArray = chart.data.datasets[0].data;
|
||||
const labelArray = chart.data.labels;
|
||||
|
||||
// 2. Shift (remove) the oldest data point and label
|
||||
dataArray.shift();
|
||||
labelArray.shift();
|
||||
|
||||
// 3. Generate new data point and time label
|
||||
const newDataPoint = Math.floor(Math.random() * 50) + 100;
|
||||
const newDataPoint = Math.floor(Math.random() * 20) + 40;
|
||||
const newTimeLabel = dayjs().format('HH:mm:ss');
|
||||
|
||||
// 4. Push the new data and label
|
||||
dataArray.push(newDataPoint);
|
||||
labelArray.push(newTimeLabel);
|
||||
|
||||
// 5. CRUCIAL: Tell Chart.js to redraw itself without destroying the instance
|
||||
chart.update();
|
||||
};
|
||||
// --- END REFACTORED REALTIME LOGIC ---
|
||||
// --- END REALTIME CHART LOGIC ---
|
||||
|
||||
|
||||
// Example data for both years (Tetap)
|
||||
const visitorData2024 = [150, 200, 350, 400, 380, 500, 550, 600, 520, 480, 650, 700];
|
||||
const visitorData2025 = [200, 250, 400, 450, 420, 550, 600, 650, 570, 520, 700, 750];
|
||||
|
||||
// Check authentication and setup on page load
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const sessionUser = await checkAuth()
|
||||
if (sessionUser) {
|
||||
// Set current date
|
||||
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
||||
currentDate.value = new Date().toLocaleDateString('id-ID', options);
|
||||
|
||||
// Start realtime data update interval
|
||||
// Pastikan chart instance sudah siap sebelum memulai interval
|
||||
realtimeInterval = setInterval(updateRealtimeData, 5000);
|
||||
realtimeInterval = setInterval(updateRealtimeData, 10000);
|
||||
} else {
|
||||
await navigateTo('/LoginPage');
|
||||
}
|
||||
@@ -309,34 +333,86 @@ onMounted(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Clear the interval when the component is unmounted
|
||||
onUnmounted(() => {
|
||||
if (realtimeInterval) {
|
||||
clearInterval(realtimeInterval);
|
||||
}
|
||||
});
|
||||
|
||||
// Updated logout handler (Tetap)
|
||||
const handleLogout = async () => {
|
||||
if (isLoggingOut.value) return
|
||||
|
||||
try {
|
||||
isLoggingOut.value = true
|
||||
console.log('🚪 Dashboard logout initiated...')
|
||||
await logout()
|
||||
} catch (error) {
|
||||
console.error('❌ Dashboard logout error:', error)
|
||||
} finally {
|
||||
isLoggingOut.value = false
|
||||
}
|
||||
};
|
||||
|
||||
// Function to change the active year (Tetap)
|
||||
const changeYear = (year) => {
|
||||
activeYear.value = year;
|
||||
};
|
||||
|
||||
// --- LOGIKA UTAMA UNTUK GRAFIK ANTREEAN (Tetap) ---
|
||||
// --- DATA UTILITY FUNCTIONS ---
|
||||
const getKpiData = () => {
|
||||
return [
|
||||
{ name: 'Antrean Online', value: 150 },
|
||||
{ name: 'Antrean MJKN', value: 100 },
|
||||
{ name: 'Total Kunjungan', value: 500 },
|
||||
{ name: 'Avg. Check-in Time', value: '7.5s' },
|
||||
];
|
||||
};
|
||||
|
||||
const convertJsonToCsv = (jsonData, fields) => {
|
||||
if (!jsonData.length) return '';
|
||||
|
||||
const header = fields.join(',');
|
||||
|
||||
const csv = jsonData.map(row =>
|
||||
fields.map(fieldName => {
|
||||
let value = row[fieldName];
|
||||
if (typeof value === 'string') {
|
||||
value = `"${value.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return value;
|
||||
}).join(',')
|
||||
).join('\n');
|
||||
|
||||
return header + '\n' + csv;
|
||||
};
|
||||
// --- END DATA UTILITY FUNCTIONS ---
|
||||
|
||||
// --- EXPORT FUNCTION HANDLER ---
|
||||
const downloadFile = (data, filename, mimeType) => {
|
||||
const dataStr = `data:${mimeType};charset=utf-8,` + encodeURIComponent(data);
|
||||
const downloadAnchorNode = document.createElement('a');
|
||||
downloadAnchorNode.setAttribute("href", dataStr);
|
||||
downloadAnchorNode.setAttribute("download", filename);
|
||||
document.body.appendChild(downloadAnchorNode);
|
||||
downloadAnchorNode.click();
|
||||
downloadAnchorNode.remove();
|
||||
}
|
||||
|
||||
const handleExport = (type) => {
|
||||
exportType.value = type.toUpperCase();
|
||||
const filenamePrefix = "antrean_data_" + dayjs().format('YYYYMMDD');
|
||||
|
||||
const dataToExport = mockQueueData.value;
|
||||
|
||||
if (type === 'json') {
|
||||
downloadFile(JSON.stringify(dataToExport, null, 2), `${filenamePrefix}.json`, 'application/json');
|
||||
alert("Data Antrean berhasil diexport sebagai JSON! ✅");
|
||||
|
||||
} else if (type === 'csv') {
|
||||
const fields = ['date', 'registrants', 'attendees'];
|
||||
const csvContent = convertJsonToCsv(dataToExport, fields);
|
||||
|
||||
const kpiData = getKpiData().map(kpi => `# ${kpi.name}: ${kpi.value}`).join('\n');
|
||||
const finalCsvContent = kpiData + '\n' + csvContent;
|
||||
|
||||
downloadFile(finalCsvContent, `${filenamePrefix}.csv`, 'text/csv');
|
||||
alert("Data Antrean berhasil diexport sebagai CSV! 📊");
|
||||
|
||||
} else if (type === 'excel') {
|
||||
alert("Excel (.xlsx) export requires a dedicated server endpoint. Simulating download... ⚠️");
|
||||
|
||||
} else if (type === 'pdf') {
|
||||
alert("PDF (.pdf) export requires client-side libraries (like jsPDF) or a server service. Simulating download... 📝");
|
||||
}
|
||||
};
|
||||
// --- END EXPORT FUNCTION HANDLER ---
|
||||
|
||||
// --- CHART DATA LOGIC ---
|
||||
const processQueueData = (data, period) => {
|
||||
const grouped = {};
|
||||
const sortedDates = data.map(item => ({
|
||||
@@ -347,37 +423,49 @@ const processQueueData = (data, period) => {
|
||||
sortedDates.forEach(item => {
|
||||
let key;
|
||||
let label;
|
||||
|
||||
if (period === 'day') {
|
||||
key = item.date.format('YYYY-MM-DD');
|
||||
label = item.date.format('DD/MM');
|
||||
label = item.date.format('ddd, DD/MM');
|
||||
} else if (period === 'week') {
|
||||
key = item.date.format('YYYY-WW');
|
||||
label = `Wk ${item.date.week()} ${item.date.year()}`;
|
||||
label = `Wk ${item.date.week()}`;
|
||||
} else if (period === 'month') {
|
||||
key = item.date.format('YYYY-MM');
|
||||
label = item.date.format('MMM YYYY');
|
||||
} else if (period === 'year') {
|
||||
key = item.date.format('YYYY');
|
||||
label = item.date.format('YYYY');
|
||||
}
|
||||
|
||||
if (!grouped[key]) {
|
||||
grouped[key] = { label: label, count: 0 };
|
||||
grouped[key] = { label: label, registrants: 0, attendees: 0 };
|
||||
}
|
||||
grouped[key].count += item.count;
|
||||
grouped[key].registrants += item.registrants;
|
||||
grouped[key].attendees += item.attendees;
|
||||
});
|
||||
|
||||
const finalLabels = Object.values(grouped).map(g => g.label);
|
||||
const finalCounts = Object.values(grouped).map(g => g.count);
|
||||
const finalRegistrants = Object.values(grouped).map(g => g.registrants);
|
||||
const finalAttendees = Object.values(grouped).map(g => g.attendees);
|
||||
|
||||
return {
|
||||
labels: finalLabels,
|
||||
datasets: [
|
||||
{
|
||||
label: `Total Antrean per ${period}`,
|
||||
backgroundColor: '#FFB300',
|
||||
data: finalCounts,
|
||||
label: `Registrants`,
|
||||
backgroundColor: '#FFB95F',
|
||||
data: finalRegistrants,
|
||||
yAxisID: 'y1',
|
||||
type: 'bar',
|
||||
borderRadius: 6,
|
||||
},
|
||||
{
|
||||
label: `Attendees`,
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: '#5FB7FF',
|
||||
data: finalAttendees,
|
||||
yAxisID: 'y1',
|
||||
type: 'line',
|
||||
pointRadius: 5,
|
||||
pointBackgroundColor: '#4A5F7A',
|
||||
tension: 0.4,
|
||||
borderWidth: 3,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -392,66 +480,94 @@ const queueChartOptions = ref({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
legend: { display: true, position: 'top' },
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
y1: {
|
||||
beginAtZero: true,
|
||||
grid: { display: true }
|
||||
title: { display: true, text: 'Count' },
|
||||
grid: { display: true, color: 'rgba(0, 0, 0, 0.05)' }
|
||||
},
|
||||
x: {
|
||||
grid: { display: false }
|
||||
}
|
||||
}
|
||||
});
|
||||
// --- AKHIR LOGIKA GRAFIK ANTREEAN ---
|
||||
|
||||
const visitorData2024 = [150, 200, 350, 400, 380, 500, 550, 600, 520, 480, 650, 700];
|
||||
const visitorData2025 = [200, 250, 400, 450, 420, 550, 600, 650, 570, 520, 700, 750];
|
||||
const conversionRate2025 = [2.5, 3.1, 4.0, 4.5, 4.2, 5.5, 6.0, 5.8, 5.7, 5.2, 6.5, 7.5];
|
||||
|
||||
// Computed property Monthly Visitor (Tetap)
|
||||
const barData = computed(() => {
|
||||
const dataForYear = activeYear.value === '2024' ? visitorData2024 : visitorData2025;
|
||||
const conversionData = activeYear.value === '2024' ? [2.0, 2.5, 3.5, 3.8, 3.6, 4.5, 5.0, 5.2, 4.8, 4.5, 5.5, 6.0] : conversionRate2025;
|
||||
|
||||
return {
|
||||
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
|
||||
datasets: [
|
||||
{
|
||||
label: `Total Visitors ${activeYear.value}`,
|
||||
backgroundColor: '#2196F3',
|
||||
label: `Total Registrants`,
|
||||
backgroundColor: '#FFB95F',
|
||||
data: dataForYear,
|
||||
yAxisID: 'y1',
|
||||
type: 'bar',
|
||||
borderRadius: 6,
|
||||
},
|
||||
{
|
||||
label: `Conv. Rate (%)`,
|
||||
borderColor: '#5FB7FF',
|
||||
backgroundColor: 'transparent',
|
||||
data: conversionData,
|
||||
yAxisID: 'y2',
|
||||
type: 'line',
|
||||
pointRadius: 5,
|
||||
pointBackgroundColor: '#4A5F7A',
|
||||
tension: 0.4,
|
||||
borderWidth: 3,
|
||||
}
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// Bar chart options (Tetap)
|
||||
const barOptions = ref({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
legend: { display: true, position: 'top' },
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
y1: {
|
||||
position: 'left',
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: 'Registrants' },
|
||||
grid: { display: false }
|
||||
},
|
||||
y2: {
|
||||
position: 'right',
|
||||
beginAtZero: true,
|
||||
suggestedMax: 10,
|
||||
title: { display: true, text: 'Conv. Rate (%)' },
|
||||
grid: { drawOnChartArea: false }
|
||||
},
|
||||
x: {
|
||||
grid: { display: false }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Pie chart data (Tetap)
|
||||
const pieData = ref({
|
||||
labels: ['Offline Registrants', 'Online Registrants'],
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: ['#2196F3', '#4CAF50'],
|
||||
backgroundColor: ['#5FB7FF', '#FFB95F'],
|
||||
hoverBackgroundColor: ['#1B98FF ', '#FF9B1B'],
|
||||
data: [759, 1876],
|
||||
borderWidth: 3,
|
||||
borderColor: '#ffffff',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Pie chart options (Tetap)
|
||||
const pieOptions = ref({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
@@ -461,7 +577,9 @@ const pieOptions = ref({
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed || 0;
|
||||
return `${label}: ${value}`;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((value / total) * 100).toFixed(1);
|
||||
return `${label}: ${value} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -470,9 +588,89 @@ const pieOptions = ref({
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Color Palette */
|
||||
.dashboard-bg {
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
.card-style {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* Text Colors */
|
||||
.primary-text {
|
||||
color: #2C3E50 !important;
|
||||
}
|
||||
|
||||
.secondary-text {
|
||||
color: #7B8794 !important;
|
||||
}
|
||||
|
||||
.secondary-light {
|
||||
color: #7B8794 !important;
|
||||
}
|
||||
|
||||
.primary-accent {
|
||||
color: #FF9B1B !important;
|
||||
}
|
||||
|
||||
.secondary-accent {
|
||||
color: #1B98FF !important;
|
||||
}
|
||||
|
||||
/* Buttons & Interactive Elements */
|
||||
.export-btn {
|
||||
transition: all 0.3s ease;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.export-btn:hover {
|
||||
background-color: #E0916E !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Card Hover Effect - Subtle and Clean */
|
||||
.hover-effect {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.hover-effect:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
/* Chip Transition */
|
||||
.transition-chip {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.transition-chip:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Date Chip Styling */
|
||||
.date-chip {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* Chart Card Fixed Height */
|
||||
.chart-card {
|
||||
min-height: 480px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-card .v-card-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
+66
-141
@@ -1,78 +1,6 @@
|
||||
<!-- // Pages/LoginPage.vue -->
|
||||
|
||||
<template>
|
||||
<v-container fluid fill-height class="login-background">
|
||||
|
||||
<!-- Navigation Bar -->
|
||||
<v-app-bar class="navbar" flat>
|
||||
<v-toolbar-title class="brand-logo">
|
||||
<v-icon class="mr-2" color="white">mdi-hospital-building</v-icon>
|
||||
<span class="font-weight-bold text-blue-darken-3">ANTREAN</span> <span class="font-weight-bold">RSSA</span>
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<!-- Navigation with Dropdown Menus -->
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="white"
|
||||
class="nav-link"
|
||||
v-bind="props"
|
||||
>
|
||||
Tentang
|
||||
<v-icon right small>mdi-chevron-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list class="nav-dropdown">
|
||||
<v-list-item class="dropdown-item">
|
||||
<v-icon class="mr-3">mdi-hospital-building</v-icon>
|
||||
<v-list-item-title>Profil Rumah Sakit</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="white"
|
||||
class="nav-link"
|
||||
v-bind="props"
|
||||
>
|
||||
Kontak
|
||||
<v-icon right small>mdi-chevron-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list class="nav-dropdown">
|
||||
<v-list-item class="dropdown-item">
|
||||
<v-icon class="mr-3">mdi-phone</v-icon>
|
||||
<v-list-item-title>Hubungi Kami</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item class="dropdown-item">
|
||||
<v-icon class="mr-3">mdi-map-marker</v-icon>
|
||||
<v-list-item-title>Alamat & Lokasi</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item class="dropdown-item">
|
||||
<v-icon class="mr-3">mdi-email</v-icon>
|
||||
<v-list-item-title>Email</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item class="dropdown-item">
|
||||
<v-icon class="mr-3">mdi-help-circle</v-icon>
|
||||
<v-list-item-title>Bantuan</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-btn icon color="white" class="ml-4">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
</v-app-bar>
|
||||
|
||||
<!-- Floating Medical Icons Background -->
|
||||
<div class="floating-medical-icon icon-1">
|
||||
<v-icon size="144">mdi-heart-pulse</v-icon>
|
||||
</div>
|
||||
@@ -92,18 +20,16 @@
|
||||
<v-icon size="114">mdi-bandage</v-icon>
|
||||
</div>
|
||||
|
||||
<!-- Centered White Box with Both Sections -->
|
||||
<v-row class="fill-height align-center justify-center">
|
||||
<v-col cols="12" class="d-flex justify-center align-center">
|
||||
<v-card class="main-card white-card rounded-xl pa-0" max-width="1100" width="100%">
|
||||
<v-row class="ma-0">
|
||||
<!-- Left Content Section -->
|
||||
<v-col cols="12" md="6" class="text-section-box d-flex align-center pa-8">
|
||||
<div class="hero-content">
|
||||
<div class="logo-section mb-6">
|
||||
<img
|
||||
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
|
||||
alt="Logo Rumah Sakit"
|
||||
src="/AntreanLogobg.png"
|
||||
alt="Antrean Logo"
|
||||
class="hospital-logo-large mb-6"
|
||||
/>
|
||||
<h1 class="hero-title-box">Sistem Terbaik</h1>
|
||||
@@ -119,26 +45,22 @@
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<!-- Right Login Section -->
|
||||
<v-col cols="12" md="6" class="login-section-box d-flex align-center pa-8">
|
||||
<div class="login-content w-100">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-6">
|
||||
<h2 class="welcome-title-dark">SELAMAT DATANG KEMBALI</h2>
|
||||
<p class="login-instruction-dark">MASUK UNTUK MELANJUTKAN</p>
|
||||
</div>
|
||||
|
||||
<!-- Logo Section -->
|
||||
<div class="d-flex flex-column align-center text-center mb-6">
|
||||
<span class="text-h5 font-weight-bold app-title-dark text-blue-darken-3">Antrean RSSA</span>
|
||||
<span class="text-h5 font-weight-bold app-title-dark">Antrean RSSA</span>
|
||||
<img
|
||||
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
|
||||
alt="Logo Rumah Sakit"
|
||||
src="/AntreanLogobg.png"
|
||||
alt="Antrean Logo"
|
||||
class="mt-3 hospital-logo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Alert Messages -->
|
||||
<v-alert v-if="errorMessage" type="error" class="mb-4" dismissible @click:close="errorMessage = ''">
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
@@ -147,12 +69,10 @@
|
||||
{{ successMessage }}
|
||||
</v-alert>
|
||||
|
||||
<!-- SSO Info -->
|
||||
<div class="text-center mb-6">
|
||||
<p class="sso-text-dark">Login menggunakan Single Sign-On</p>
|
||||
</div>
|
||||
|
||||
<!-- Keycloak Login Button -->
|
||||
<v-btn
|
||||
@click="handleLogin"
|
||||
class="login-btn"
|
||||
@@ -169,7 +89,6 @@
|
||||
<v-icon right>mdi-arrow-right</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<!-- Registration Section -->
|
||||
<v-divider class="my-6 custom-divider-dark"></v-divider>
|
||||
|
||||
<div class="text-center">
|
||||
@@ -193,8 +112,7 @@
|
||||
<v-btn
|
||||
@click="showAdminContact = true"
|
||||
variant="text"
|
||||
color="#0053AD"
|
||||
size="small"
|
||||
color="#FF9B1B" size="small"
|
||||
class="contact-link-dark mt-1"
|
||||
>
|
||||
Hubungi Administrator
|
||||
@@ -202,7 +120,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Help -->
|
||||
<div class="text-center mt-4">
|
||||
<span class="help-link-dark">Masalah dengan kata sandi Anda?</span>
|
||||
</div>
|
||||
@@ -213,53 +130,47 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Registration Information Dialog -->
|
||||
<v-dialog v-model="showRegistrationDialog" max-width="500">
|
||||
<v-card class="white-dialog rounded-xl">
|
||||
<v-card-title class="text-h5 text-grey-darken-3 text-center pa-6 bg-grey-lighten-4">
|
||||
<v-icon left color="#0053AD">mdi-account-plus</v-icon>
|
||||
Pendaftaran Akun Baru
|
||||
<v-card-title class="text-h5 text-center pa-6 bg-grey-lighten-4">
|
||||
<v-icon left color="#FF9B1B">mdi-account-plus</v-icon> Pendaftaran Akun Baru
|
||||
</v-card-title>
|
||||
|
||||
|
||||
<v-card-text class="text-grey-darken-2 pa-6">
|
||||
<div class="text-center mb-4">
|
||||
<v-icon size="64" color="#0053AD" class="mb-4">mdi-information</v-icon>
|
||||
</div>
|
||||
|
||||
<v-icon size="64" color="#FF9B1B" class="mb-4">mdi-information</v-icon> </div>
|
||||
|
||||
<p class="text-body-1 mb-4">
|
||||
Untuk mendaftar akun baru pada sistem Antrean RSSA, silakan ikuti langkah berikut:
|
||||
</p>
|
||||
|
||||
|
||||
<v-list class="transparent">
|
||||
<v-list-item class="text-grey-darken-2 px-0">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="#0053AD">mdi-numeric-1-circle</v-icon>
|
||||
</template>
|
||||
<v-icon color="#FF9B1B">mdi-numeric-1-circle</v-icon> </template>
|
||||
<v-list-item-title class="text-grey-darken-2">
|
||||
Hubungi Administrator IT Rumah Sakit
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
|
||||
<v-list-item class="text-grey-darken-2 px-0">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="#0053AD">mdi-numeric-2-circle</v-icon>
|
||||
</template>
|
||||
<v-icon color="#FF9B1B">mdi-numeric-2-circle</v-icon> </template>
|
||||
<v-list-item-title class="text-grey-darken-2">
|
||||
Siapkan dokumen identitas dan surat penugasan
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
|
||||
<v-list-item class="text-grey-darken-2 px-0">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="#0053AD">mdi-numeric-3-circle</v-icon>
|
||||
</template>
|
||||
<v-icon color="#FF9B1B">mdi-numeric-3-circle</v-icon> </template>
|
||||
<v-list-item-title class="text-grey-darken-2">
|
||||
Tunggu proses verifikasi dan aktivasi akun
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
||||
|
||||
<v-card-actions class="pa-6 bg-grey-lighten-5">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
@@ -272,8 +183,7 @@
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="showRegistrationDialog = false; showAdminContact = true"
|
||||
color="#0053AD"
|
||||
rounded
|
||||
color="#FF9B1B" rounded
|
||||
>
|
||||
<v-icon left>mdi-phone</v-icon>
|
||||
Hubungi Admin
|
||||
@@ -282,24 +192,20 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Admin Contact Dialog -->
|
||||
<v-dialog v-model="showAdminContact" max-width="500">
|
||||
<v-card class="white-dialog rounded-xl">
|
||||
<v-card-title class="text-h5 text-grey-darken-3 text-center pa-6 bg-grey-lighten-4">
|
||||
<v-icon left color="#0053AD">mdi-account-tie</v-icon>
|
||||
Kontak Administrator
|
||||
<v-card-title class="text-h5 text-center pa-6 bg-grey-lighten-4">
|
||||
<v-icon left color="#FF9B1B">mdi-account-tie</v-icon> Kontak Administrator
|
||||
</v-card-title>
|
||||
|
||||
|
||||
<v-card-text class="text-grey-darken-2 pa-6">
|
||||
<div class="text-center mb-4">
|
||||
<v-icon size="64" color="#0053AD" class="mb-4">mdi-phone-settings</v-icon>
|
||||
</div>
|
||||
|
||||
<v-icon size="64" color="#FF9B1B" class="mb-4">mdi-phone-settings</v-icon> </div>
|
||||
|
||||
<v-list class="transparent">
|
||||
<v-list-item class="text-grey-darken-2 px-0 mb-2">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="#0053AD">mdi-office-building</v-icon>
|
||||
</template>
|
||||
<v-icon color="#FF9B1B">mdi-office-building</v-icon> </template>
|
||||
<div>
|
||||
<v-list-item-title class="text-grey-darken-2 font-weight-bold">
|
||||
IT Support RSSA
|
||||
@@ -309,11 +215,10 @@
|
||||
</v-list-item-subtitle>
|
||||
</div>
|
||||
</v-list-item>
|
||||
|
||||
|
||||
<v-list-item class="text-grey-darken-2 px-0 mb-2">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="#0053AD">mdi-phone</v-icon>
|
||||
</template>
|
||||
<v-icon color="#FF9B1B">mdi-phone</v-icon> </template>
|
||||
<div>
|
||||
<v-list-item-title class="text-grey-darken-2">
|
||||
(0341) 343343 ext. 1234
|
||||
@@ -323,11 +228,10 @@
|
||||
</v-list-item-subtitle>
|
||||
</div>
|
||||
</v-list-item>
|
||||
|
||||
|
||||
<v-list-item class="text-grey-darken-2 px-0 mb-2">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="#0053AD">mdi-email</v-icon>
|
||||
</template>
|
||||
<v-icon color="#FF9B1B">mdi-email</v-icon> </template>
|
||||
<div>
|
||||
<v-list-item-title class="text-grey-darken-2">
|
||||
it-support@rssa.malang.go.id
|
||||
@@ -337,11 +241,10 @@
|
||||
</v-list-item-subtitle>
|
||||
</div>
|
||||
</v-list-item>
|
||||
|
||||
|
||||
<v-list-item class="text-grey-darken-2 px-0">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="#0053AD">mdi-clock</v-icon>
|
||||
</template>
|
||||
<v-icon color="#FF9B1B">mdi-clock</v-icon> </template>
|
||||
<div>
|
||||
<v-list-item-title class="text-grey-darken-2">
|
||||
Senin - Jumat: 07:00 - 15:00
|
||||
@@ -352,19 +255,18 @@
|
||||
</div>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mt-4"
|
||||
color="blue"
|
||||
>
|
||||
color="orange" >
|
||||
<div class="text-grey-darken-2">
|
||||
<strong>Catatan:</strong> Pendaftaran akun memerlukan verifikasi dokumen dan dapat memakan waktu 1-2 hari kerja.
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
|
||||
|
||||
<v-card-actions class="pa-6 bg-grey-lighten-5">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
@@ -377,8 +279,7 @@
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="copyContactInfo"
|
||||
color="#0053AD"
|
||||
rounded
|
||||
color="#FF9B1B" rounded
|
||||
>
|
||||
<v-icon left>mdi-content-copy</v-icon>
|
||||
Salin Info
|
||||
@@ -406,6 +307,15 @@ const successMessage = ref<string>('')
|
||||
const showRegistrationDialog = ref<boolean>(false)
|
||||
const showAdminContact = ref<boolean>(false)
|
||||
|
||||
// Hospital Profile URL
|
||||
const HOSPITAL_PROFILE_URL = 'https://rsusaifulanwar.jatimprov.go.id/v2/'
|
||||
|
||||
// Function to navigate to external hospital profile site
|
||||
// NOTE: This function is no longer called in the template since the menu item was removed.
|
||||
const goToHospitalProfile = (): void => {
|
||||
window.open(HOSPITAL_PROFILE_URL, '_blank')
|
||||
}
|
||||
|
||||
// Check URL parameters for errors
|
||||
const route = useRoute()
|
||||
onMounted(() => {
|
||||
@@ -414,7 +324,7 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Custom login handler
|
||||
// Custom login handler (pointing to Keycloak SSO logic)
|
||||
const handleLogin = async (): Promise<void> => {
|
||||
isLoading.value = true
|
||||
errorMessage.value = ''
|
||||
@@ -422,7 +332,8 @@ const handleLogin = async (): Promise<void> => {
|
||||
|
||||
try {
|
||||
console.log('Starting login process...')
|
||||
|
||||
|
||||
// Call API route to initiate Keycloak login process
|
||||
const response = await $fetch<LoginResponse>('/api/auth/keycloak-login', {
|
||||
method: 'POST'
|
||||
})
|
||||
@@ -430,7 +341,8 @@ const handleLogin = async (): Promise<void> => {
|
||||
if (response?.success && response?.data?.authUrl) {
|
||||
console.log('Redirecting to Keycloak...')
|
||||
successMessage.value = 'Redirecting to Keycloak...'
|
||||
|
||||
|
||||
// Redirect the user to the Keycloak authorization URL
|
||||
setTimeout(() => {
|
||||
window.location.href = response.data!.authUrl
|
||||
}, 500)
|
||||
@@ -439,31 +351,44 @@ const handleLogin = async (): Promise<void> => {
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Login error:', error)
|
||||
// Display the error message
|
||||
errorMessage.value = `Login failed: ${error.message || 'Please try again.'}`
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
// Only set isLoading back to false if no redirect is happening
|
||||
if (!successMessage.value.includes('Redirecting')) {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy contact information to clipboard
|
||||
const copyContactInfo = async (): Promise<void> => {
|
||||
// Updated contact info block from user's provided code
|
||||
const contactInfo = `
|
||||
IT Support RSSA
|
||||
Telepon: (0341) 343343 ext. 1234
|
||||
Email: [email protected]
|
||||
Jam Operasional: Senin - Jumat, 08:00 - 16:00
|
||||
Jam Operasional: Senin - Jumat, 07:00 - 15:00
|
||||
`.trim()
|
||||
|
||||
|
||||
try {
|
||||
// Use the Clipboard API
|
||||
await navigator.clipboard.writeText(contactInfo)
|
||||
successMessage.value = 'Informasi kontak berhasil disalin!'
|
||||
showAdminContact.value = false
|
||||
|
||||
|
||||
// Clear the success message after a short delay
|
||||
setTimeout(() => {
|
||||
successMessage.value = ''
|
||||
}, 3000)
|
||||
} catch (error) {
|
||||
console.error('Failed to copy contact info:', error)
|
||||
// Fallback or simple alert for error
|
||||
errorMessage.value = 'Gagal menyalin informasi kontak. Silakan coba salin manual.'
|
||||
|
||||
setTimeout(() => {
|
||||
errorMessage.value = ''
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
<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 class="mt-6">
|
||||
|
||||
<v-card flat class="mb-4">
|
||||
<v-tabs
|
||||
v-model="activeTab"
|
||||
color="primary"
|
||||
align-tabs="start"
|
||||
show-arrows
|
||||
class="mb-4"
|
||||
>
|
||||
<v-tab value="all" @click="filterByStatus('all')">
|
||||
Semua Pasien <v-badge color="grey-lighten-1" :content="totalCount" inline class="ml-2"></v-badge>
|
||||
</v-tab>
|
||||
<v-tab value="Menunggu Poli" @click="filterByStatus('Menunggu Poli')">
|
||||
Menunggu Poli <v-badge color="orange-lighten-1" :content="waitingCount" inline class="ml-2"></v-badge>
|
||||
</v-tab>
|
||||
<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')">
|
||||
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-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>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
definePageMeta({
|
||||
middleware:['auth']
|
||||
})
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// === STATE MANAGEMENT ===
|
||||
const activeTab = ref('all');
|
||||
|
||||
// Filter Model: Menampung input dari form pencarian
|
||||
const filters = ref({
|
||||
rm_number: '',
|
||||
qr_code: '',
|
||||
queue_number: '', // Filter baru
|
||||
service: null, // Filter Layanan
|
||||
});
|
||||
|
||||
// State untuk menyimpan hasil filtering
|
||||
const filteredPatientList = ref([]);
|
||||
const currentSearchTerm = ref('');
|
||||
|
||||
// Table Headers
|
||||
const headers = [
|
||||
{ title: 'No. RM', key: 'rm_number' },
|
||||
{ title: 'Nama', key: 'name' },
|
||||
{ title: 'No. Antrean', key: 'queue_number' },
|
||||
{ title: 'Layanan', key: 'service' },
|
||||
{ title: 'Status', key: 'status' },
|
||||
{ title: 'Aksi', key: 'aksi', sortable: false },
|
||||
];
|
||||
|
||||
// Mock Patient Data
|
||||
const ALL_PATIENTS_DATA = [
|
||||
{ id: '11111111', rm_number: '11111111', name: 'Ragil Bayu N.', queue_number: 1, service: 'Klinik Jiwa', status: 'Diperiksa Dokter', qr_code: 'QR123' },
|
||||
{ id: '22222222', rm_number: '22222222', name: 'Siti Aisyah', queue_number: 5, service: 'Radiologi', status: 'Menunggu Poli', qr_code: 'QR456' },
|
||||
{ id: '33333333', rm_number: '33333333', name: 'Ahmad Dani', queue_number: 2, service: 'Klinik Jiwa', status: 'Selesai Pelayanan', qr_code: 'QR789' },
|
||||
{ id: '44444444', rm_number: '44444444', name: 'Dewi Sartika', queue_number: 6, service: 'Fisioterapi', status: 'Menunggu Poli', qr_code: 'QR101' },
|
||||
{ id: '55555555', rm_number: '55555555', name: 'Joko Susilo', queue_number: 3, service: 'Klinik Umum', status: 'Diperiksa Dokter', qr_code: 'QR112' },
|
||||
{ id: '66666666', rm_number: '66666666', name: 'Budi Santoso', queue_number: 4, service: 'Radiologi', status: 'Menunggu Poli', qr_code: 'QR131' },
|
||||
];
|
||||
|
||||
// Initialize the filtered list with all data
|
||||
filteredPatientList.value = [...ALL_PATIENTS_DATA];
|
||||
|
||||
// === FILTERING LOGIC ===
|
||||
|
||||
// 1. Logic Pencarian Gabungan saat tombol "Cari" ditekan
|
||||
const applyFilters = () => {
|
||||
// 1. Filter berdasarkan teks di semua kolom yang relevan (RM, QR, Antrean, Nama)
|
||||
let textSearch = '';
|
||||
|
||||
// Gabungkan semua nilai filter menjadi satu string pencarian
|
||||
if (filters.value.rm_number) textSearch += filters.value.rm_number.toLowerCase() + ' ';
|
||||
if (filters.value.qr_code) textSearch += filters.value.qr_code.toLowerCase() + ' ';
|
||||
if (filters.value.queue_number) textSearch += filters.value.queue_number.toString().toLowerCase() + ' ';
|
||||
|
||||
// Vuetify v-data-table memiliki fitur pencarian bawaan (di properti `search`).
|
||||
// Kita gunakan fitur ini untuk pencarian berbasis teks (No. RM, QR, Antrean).
|
||||
currentSearchTerm.value = textSearch.trim();
|
||||
|
||||
// 2. Filter berdasarkan Layanan (Service), yang tidak didukung oleh `v-data-table` search
|
||||
let results = ALL_PATIENTS_DATA;
|
||||
|
||||
if (filters.value.service) {
|
||||
results = results.filter(p => p.service === filters.value.service);
|
||||
}
|
||||
|
||||
// Terapkan hasil filter Layanan
|
||||
filteredPatientList.value = results;
|
||||
|
||||
// Reset status tab (penting agar hasil pencarian tampil terlepas dari tab yang aktif)
|
||||
activeTab.value = 'all';
|
||||
};
|
||||
|
||||
// 2. Logic Filter Status (saat tab ditekan)
|
||||
const filterByStatus = (status) => {
|
||||
// Pastikan status tab diperbarui
|
||||
activeTab.value = status;
|
||||
|
||||
// Clear the text search term when switching tabs
|
||||
currentSearchTerm.value = '';
|
||||
|
||||
if (status === 'all') {
|
||||
filteredPatientList.value = ALL_PATIENTS_DATA;
|
||||
} else {
|
||||
filteredPatientList.value = ALL_PATIENTS_DATA.filter(p => p.status === status);
|
||||
}
|
||||
|
||||
// Clear form filters (opsional, tetapi membuat UX lebih jelas)
|
||||
filters.value.rm_number = '';
|
||||
filters.value.qr_code = '';
|
||||
filters.value.queue_number = '';
|
||||
filters.value.service = null;
|
||||
};
|
||||
|
||||
// === COMPUTED PROPERTIES ===
|
||||
|
||||
// Hitungan untuk Badge Tab
|
||||
const listForCounts = computed(() => {
|
||||
// Gunakan ALL_PATIENTS_DATA untuk hitungan badge agar hitungan selalu stabil
|
||||
return ALL_PATIENTS_DATA;
|
||||
});
|
||||
|
||||
const totalCount = computed(() => listForCounts.value.length);
|
||||
const waitingCount = computed(() => listForCounts.value.filter(p => p.status === 'Menunggu Poli').length);
|
||||
const examiningCount = computed(() => listForCounts.value.filter(p => p.status === 'Diperiksa Dokter').length);
|
||||
const doneCount = computed(() => listForCounts.value.filter(p => p.status === 'Selesai Pelayanan').length);
|
||||
|
||||
// Data yang ditampilkan di tabel adalah data yang sudah difilter
|
||||
const paginatedPatients = computed(() => {
|
||||
// Jika ada filter status yang aktif (setelah menekan tab)
|
||||
if (activeTab.value !== 'all') {
|
||||
return filteredPatientList.value.filter(p => p.status === activeTab.value);
|
||||
}
|
||||
|
||||
// Jika tidak ada filter status yang aktif, tampilkan hasil dari applyFilters()
|
||||
return filteredPatientList.value;
|
||||
});
|
||||
|
||||
// === UTILITIES & NAVIGATION ===
|
||||
|
||||
// Utility function to set color based on status
|
||||
const getStatusColor = (status) => {
|
||||
if (status === 'Diperiksa Dokter') return 'green-darken-1';
|
||||
if (status === 'Menunggu Poli') return 'orange-darken-1';
|
||||
if (status === 'Selesai Pelayanan') return 'blue-darken-2';
|
||||
return 'grey';
|
||||
};
|
||||
|
||||
// Function to navigate to the detailed patient information page
|
||||
const viewPatient = (patientId) => {
|
||||
router.push(`/MonitoringPasien/Pasien/${patientId}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.floating-toolbar {
|
||||
border-radius: 9999px;
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15);
|
||||
height: 64px !important;
|
||||
}
|
||||
|
||||
/* Jarak PENTING: Tambahkan padding horizontal ke toolbar */
|
||||
.floating-toolbar.custom-padding {
|
||||
padding: 0 16px; /* Memberi jarak 16px di kiri dan kanan */
|
||||
}
|
||||
|
||||
/* Penyesuaian agar teks dan ikon kontras dengan soft blue background */
|
||||
.v-toolbar-title {
|
||||
margin-left: 0 !important; /* Reset default margin */
|
||||
}
|
||||
|
||||
/* Pastikan chip Admin tidak mepet */
|
||||
.admin-chip {
|
||||
margin-right: 0 !important; /* Reset default margin */
|
||||
}
|
||||
|
||||
.v-tab.v-tab--selected {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,298 @@
|
||||
<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-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">
|
||||
<v-row class="align-center mb-4">
|
||||
<v-col cols="12" md="8">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon size="36" color="blue-darken-2" class="mr-3">mdi-account-circle</v-icon>
|
||||
<div>
|
||||
<div class="text-caption text-uppercase text-grey-darken-1">Nama Pasien</div>
|
||||
<div class="text-h5 font-weight-black text-blue-darken-4">{{ patient.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="24" color="grey-darken-1" class="mr-3">mdi-numeric</v-icon>
|
||||
<div>
|
||||
<div class="text-caption text-uppercase text-grey-darken-1">Nomor Rekam Medis</div>
|
||||
<v-chip color="orange-darken-1" size="large" class="font-weight-bold">{{ patient.rm_number }}</v-chip>
|
||||
</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-row>
|
||||
<v-col cols="12" sm="6" md="4" class="py-1">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon class="mr-3" color="light-blue-darken-2">mdi-calendar</v-icon>
|
||||
<div>
|
||||
<div class="text-caption text-uppercase text-grey-darken-1">Tanggal Lahir</div>
|
||||
<div class="font-weight-medium text-subtitle-1">{{ patient.dob }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" class="py-1">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon class="mr-3" color="light-blue-darken-2">mdi-phone</v-icon>
|
||||
<div>
|
||||
<div class="text-caption text-uppercase text-grey-darken-1">No Telepon</div>
|
||||
<div class="font-weight-medium text-subtitle-1">{{ patient.phone }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="4" class="py-1">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon class="mr-3" color="light-blue-darken-2">mdi-map-marker</v-icon>
|
||||
<div>
|
||||
<div class="text-caption text-uppercase text-grey-darken-1">Alamat</div>
|
||||
<div class="font-weight-medium text-subtitle-1">{{ patient.address }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
|
||||
|
||||
<h2 class="text-h5 mb-4 font-weight-bold text-blue-darken-3">Tiket Pelayanan Aktif</h2>
|
||||
|
||||
<v-row class="ticket-row">
|
||||
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="3"
|
||||
v-for="ticket in patient.activeTickets"
|
||||
:key="ticket.title"
|
||||
>
|
||||
<MonitorPasienTicketCard
|
||||
:title="ticket.title"
|
||||
:color="ticket.color"
|
||||
:steps="ticket.steps"
|
||||
:current-step-label="ticket.currentStepLabel"
|
||||
/>
|
||||
</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-col>
|
||||
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
definePageMeta({
|
||||
middleware:['auth']
|
||||
})
|
||||
// ===============================================
|
||||
// JSON MOCK DATA STRUCTURE (Tetap sama)
|
||||
// ===============================================
|
||||
const mockPatientData = [
|
||||
{
|
||||
id: '11111111',
|
||||
name: 'Ragil Bayu N. Wibowo',
|
||||
rm_number: '11111111',
|
||||
dob: '1 Juni 1967',
|
||||
phone: '0851-4563-2145',
|
||||
address: 'Jalan Sumpah Pemuda No. 12, Jakarta',
|
||||
status: 'Aktif',
|
||||
activeTickets: [
|
||||
// 1. TIKET JIWA
|
||||
{
|
||||
title: 'Klinik JIWA (Antrean: 1)',
|
||||
color: 'orange-darken-1',
|
||||
currentStepLabel: 'Klinik Jiwa',
|
||||
steps: [
|
||||
{ label: 'Daftar', date: '15-8-2023', time: '15.01' },
|
||||
{ label: 'Check in', date: '15-8-2023', time: '07.58' },
|
||||
{ label: 'Loket 1/3', date: '15-8-2023', time: '08.51' },
|
||||
{ label: 'Klinik Jiwa', date: '15-8-2023', time: '09.45' },
|
||||
{ label: 'Apotek', date: '-', time: '-' },
|
||||
{ label: 'Selesai', date: '-', time: '-' },
|
||||
]
|
||||
},
|
||||
// 2. TIKET RADIOLOGI
|
||||
{
|
||||
title: 'Penunjang RADIOLOGI (Antrean: 5)',
|
||||
color: 'blue-darken-2',
|
||||
currentStepLabel: 'Loket 5/6/7',
|
||||
steps: [
|
||||
{ label: 'Daftar', date: '15-8-2023', time: '15.01' },
|
||||
{ label: 'Check in', date: '15-8-2023', time: '07.58' },
|
||||
{ label: 'Loket 5/6/7', date: '15-8-2023', time: '08.51' },
|
||||
{ label: 'Tunggu Radiologi', date: '-', time: '-' },
|
||||
{ label: 'Selesai', date: '-', time: '-' },
|
||||
]
|
||||
},
|
||||
// 3. TIKET GIZI
|
||||
{
|
||||
title: 'Klinik GIZI (Antrean: 12)',
|
||||
color: 'purple-darken-1',
|
||||
currentStepLabel: 'Konsultasi Gizi',
|
||||
steps: [
|
||||
{ label: 'Daftar', date: '16-8-2023', time: '07.00' },
|
||||
{ label: 'Tunggu Panggilan', date: '16-8-2023', time: '07.20' },
|
||||
{ label: 'Konsultasi Gizi', date: '16-8-2023', time: '07.45' },
|
||||
{ label: 'Selesai', date: '-', time: '-' },
|
||||
]
|
||||
},
|
||||
// 4. TIKET FISIOTERAPI
|
||||
{
|
||||
title: 'Fisioterapi (Antrean: 2)',
|
||||
color: 'teal-darken-1',
|
||||
currentStepLabel: 'Daftar',
|
||||
steps: [
|
||||
{ label: 'Daftar', date: '16-8-2023', time: '10.00' },
|
||||
{ label: 'Fisioterapi', date: '-', time: '-' },
|
||||
{ label: 'Selesai', date: '-', time: '-' },
|
||||
]
|
||||
},
|
||||
// 5. TIKET LABORATORIUM
|
||||
{
|
||||
title: 'Penunjang LABORATORIUM (Antrean: 7)',
|
||||
color: 'red-darken-2',
|
||||
currentStepLabel: 'Pemeriksaan Sampel',
|
||||
steps: [
|
||||
{ label: 'Daftar', date: '17-8-2023', time: '06.30' },
|
||||
{ label: 'Administrasi', date: '17-8-2023', time: '06.40' },
|
||||
{ label: 'Tunggu Panggilan', date: '17-8-2023', time: '06.50' },
|
||||
{ label: 'Pengambilan Sampel', date: '17-8-2023', time: '07.10' },
|
||||
{ label: 'Pemeriksaan Sampel', date: '17-8-2023', time: '07.30' },
|
||||
{ label: 'Input Hasil', date: '-', time: '-' },
|
||||
{ label: 'Verifikasi Dokter', date: '-', time: '-' },
|
||||
{ label: 'Ambil Hasil', date: '-', time: '-' },
|
||||
{ label: 'Selesai', date: '-', time: '-' },
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '33333333',
|
||||
name: 'Joko Susilo',
|
||||
rm_number: '33333333',
|
||||
dob: '20 Desember 1995',
|
||||
phone: '0878-1122-3344',
|
||||
address: 'Jl. Pahlawan No. 5, Surabaya',
|
||||
status: 'Tidak Aktif',
|
||||
activeTickets: []
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
// ===============================================
|
||||
// MAIN SCRIPT LOGIC
|
||||
// ===============================================
|
||||
const route = useRoute();
|
||||
const patientId = route.params.id;
|
||||
|
||||
const patient = ref({
|
||||
name: 'Memuat...',
|
||||
address: 'Memuat...',
|
||||
phone: 'Memuat...',
|
||||
rm_number: patientId,
|
||||
dob: 'Memuat...',
|
||||
status: 'Memuat',
|
||||
activeTickets: []
|
||||
});
|
||||
|
||||
const fetchPatientData = () => {
|
||||
const data = mockPatientData.find(p => p.id === patientId);
|
||||
|
||||
if (data) {
|
||||
patient.value = data;
|
||||
} else {
|
||||
patient.value = {
|
||||
name: 'Data Pasien Tidak Ditemukan',
|
||||
address: '-',
|
||||
phone: '-',
|
||||
rm_number: patientId,
|
||||
dob: '-',
|
||||
status: 'Error',
|
||||
activeTickets: []
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.floating-toolbar.custom-padding {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.admin-chip {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
/* =============================================== */
|
||||
/* TICKET CARD ALIGNMENT STYLES (Fix Tinggi Card) */
|
||||
/* =============================================== */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* =============================================== */
|
||||
/* TIMELINE STYLES (Jika ada) */
|
||||
/* =============================================== */
|
||||
.v-timeline-item {
|
||||
padding-bottom: 8px !important;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load diff
@@ -0,0 +1,800 @@
|
||||
<template>
|
||||
<v-container fluid class="pa-0">
|
||||
<!-- Hero Header Section -->
|
||||
<div class="hero-header">
|
||||
<v-container class="py-8">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon color="white" size="40" class="mr-3">mdi-account-circle</v-icon>
|
||||
<div>
|
||||
<h1 class="text-h4 font-weight-bold text-white mb-1">Profil Saya</h1>
|
||||
<p class="text-body-1 text-white opacity-90">Kelola informasi profil dan pengaturan akun Anda</p>
|
||||
</div>
|
||||
</div>
|
||||
</v-container>
|
||||
</div>
|
||||
|
||||
<v-container class="content-container pb-12">
|
||||
<v-row>
|
||||
<!-- Left Sidebar - Profile Card -->
|
||||
<v-col cols="12" lg="4">
|
||||
<v-card class="rounded-xl elevation-8 sticky-card profile-card">
|
||||
<div class="text-center profile-content">
|
||||
<div class="profile-avatar-container">
|
||||
<v-avatar size="140" class="profile-avatar elevation-8">
|
||||
<v-img
|
||||
:src="profileData.picture || 'https://i.pravatar.cc/300?img=68'"
|
||||
alt="Profile Picture"
|
||||
></v-img>
|
||||
</v-avatar>
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
color="orange-darken-2"
|
||||
class="avatar-edit-btn elevation-4"
|
||||
@click="openPhotoDialog"
|
||||
>
|
||||
<v-icon size="18">mdi-camera</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<h2 class="text-h5 font-weight-bold mb-2">{{ profileData.name }}</h2>
|
||||
<p class="text-body-2 text-grey-darken-1 mb-1">@{{ profileData.username }}</p>
|
||||
<p class="text-body-2 text-grey mb-4">{{ profileData.email }}</p>
|
||||
|
||||
<v-chip
|
||||
color="success"
|
||||
variant="flat"
|
||||
size="small"
|
||||
prepend-icon="mdi-check-circle"
|
||||
class="mb-4"
|
||||
>
|
||||
Akun Terverifikasi
|
||||
</v-chip>
|
||||
|
||||
<v-divider class="my-4"></v-divider>
|
||||
|
||||
<!-- Quick Info -->
|
||||
<div class="text-left px-6">
|
||||
<div class="info-item mb-3">
|
||||
<v-icon size="20" color="blue-darken-2" class="mr-2">mdi-identifier</v-icon>
|
||||
<span class="text-body-2 text-grey-darken-1">ID: {{ profileData.id }}</span>
|
||||
</div>
|
||||
<div class="info-item mb-3">
|
||||
<v-icon size="20" color="orange-darken-2" class="mr-2">mdi-calendar-check</v-icon>
|
||||
<span class="text-body-2 text-grey-darken-1">Bergabung: {{ profileData.joinDate }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<v-icon size="20" color="green-darken-2" class="mr-2">mdi-clock-outline</v-icon>
|
||||
<span class="text-body-2 text-grey-darken-1">Login: {{ profileData.lastLogin }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-btn
|
||||
color="blue-darken-2"
|
||||
variant="outlined"
|
||||
block
|
||||
class="rounded-lg"
|
||||
prepend-icon="mdi-cog"
|
||||
@click="navigateTo('/Profile/Pengaturan')"
|
||||
>
|
||||
Pengaturan Akun
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- Right Content - Profile Details -->
|
||||
<v-col cols="12" lg="8">
|
||||
<!-- Personal Information -->
|
||||
<v-card class="rounded-xl elevation-4 mb-4">
|
||||
<v-card-title class="d-flex align-center pa-6 pb-4">
|
||||
<v-icon color="blue-darken-2" class="mr-2">mdi-account-details</v-icon>
|
||||
<span class="font-weight-bold">Informasi Pribadi</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
v-if="!isEditing"
|
||||
color="orange-darken-2"
|
||||
variant="flat"
|
||||
size="small"
|
||||
class="rounded-lg"
|
||||
prepend-icon="mdi-pencil"
|
||||
@click="startEdit"
|
||||
>
|
||||
Edit
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text class="pa-6">
|
||||
<v-form ref="profileForm">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<label class="text-caption text-grey-darken-1 font-weight-bold mb-1 d-block">NAMA LENGKAP</label>
|
||||
<v-text-field
|
||||
v-model="profileData.name"
|
||||
placeholder="Masukkan nama lengkap"
|
||||
prepend-inner-icon="mdi-account"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
color="blue-darken-2"
|
||||
:readonly="!isEditing"
|
||||
hide-details="auto"
|
||||
class="mb-4"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<label class="text-caption text-grey-darken-1 font-weight-bold mb-1 d-block">USERNAME</label>
|
||||
<v-text-field
|
||||
v-model="profileData.username"
|
||||
placeholder="Masukkan username"
|
||||
prepend-inner-icon="mdi-at"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
color="blue-darken-2"
|
||||
:readonly="!isEditing"
|
||||
hide-details="auto"
|
||||
class="mb-4"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<label class="text-caption text-grey-darken-1 font-weight-bold mb-1 d-block">EMAIL</label>
|
||||
<v-text-field
|
||||
v-model="profileData.email"
|
||||
placeholder="Masukkan email"
|
||||
prepend-inner-icon="mdi-email"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
color="blue-darken-2"
|
||||
:readonly="!isEditing"
|
||||
hide-details="auto"
|
||||
class="mb-4"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<label class="text-caption text-grey-darken-1 font-weight-bold mb-1 d-block">NOMOR TELEPON</label>
|
||||
<v-text-field
|
||||
v-model="profileData.phone"
|
||||
placeholder="Masukkan nomor telepon"
|
||||
prepend-inner-icon="mdi-phone"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
color="blue-darken-2"
|
||||
:readonly="!isEditing"
|
||||
hide-details="auto"
|
||||
class="mb-4"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<label class="text-caption text-grey-darken-1 font-weight-bold mb-1 d-block">BIO</label>
|
||||
<v-textarea
|
||||
v-model="profileData.bio"
|
||||
placeholder="Ceritakan tentang diri Anda"
|
||||
prepend-inner-icon="mdi-text"
|
||||
variant="outlined"
|
||||
color="blue-darken-2"
|
||||
rows="3"
|
||||
:readonly="!isEditing"
|
||||
hide-details="auto"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider v-if="isEditing"></v-divider>
|
||||
<v-card-actions v-if="isEditing" class="pa-6 pt-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
color="grey-darken-1"
|
||||
class="rounded-lg px-6"
|
||||
@click="cancelEdit"
|
||||
>
|
||||
Batal
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue-darken-2"
|
||||
variant="flat"
|
||||
class="rounded-lg px-8"
|
||||
prepend-icon="mdi-check"
|
||||
@click="saveProfile"
|
||||
:loading="isSaving"
|
||||
>
|
||||
Simpan Perubahan
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
||||
<!-- Security Section -->
|
||||
<v-card class="rounded-xl elevation-4 mb-4">
|
||||
<v-card-title class="d-flex align-center pa-6 pb-4">
|
||||
<v-icon color="orange-darken-2" class="mr-2">mdi-shield-check</v-icon>
|
||||
<span class="font-weight-bold">Keamanan</span>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text class="pa-6">
|
||||
<v-list class="transparent">
|
||||
<v-list-item
|
||||
class="rounded-lg mb-2 px-4"
|
||||
prepend-icon="mdi-lock-reset"
|
||||
@click="openPasswordDialog"
|
||||
>
|
||||
<v-list-item-title class="font-weight-medium">Ubah Password</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption">Perbarui password akun Anda</v-list-item-subtitle>
|
||||
<template v-slot:append>
|
||||
<v-icon color="grey">mdi-chevron-right</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
class="rounded-lg mb-2 px-4"
|
||||
prepend-icon="mdi-two-factor-authentication"
|
||||
>
|
||||
<v-list-item-title class="font-weight-medium">Autentikasi Dua Faktor</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption">Tingkatkan keamanan akun</v-list-item-subtitle>
|
||||
<template v-slot:append>
|
||||
<v-switch
|
||||
v-model="twoFactorEnabled"
|
||||
color="blue-darken-2"
|
||||
hide-details
|
||||
inset
|
||||
@change="toggleTwoFactor"
|
||||
></v-switch>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
class="rounded-lg px-4"
|
||||
prepend-icon="mdi-devices"
|
||||
@click="openDevicesDialog"
|
||||
>
|
||||
<v-list-item-title class="font-weight-medium">Perangkat Aktif</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption">Kelola perangkat yang terhubung</v-list-item-subtitle>
|
||||
<template v-slot:append>
|
||||
<v-chip size="small" color="success" variant="flat">{{ activeSessions.length }} Perangkat</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Preferences Section -->
|
||||
<v-card class="rounded-xl elevation-4">
|
||||
<v-card-title class="d-flex align-center pa-6 pb-4">
|
||||
<v-icon color="blue-darken-2" class="mr-2">mdi-tune</v-icon>
|
||||
<span class="font-weight-bold">Preferensi</span>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text class="pa-6">
|
||||
<v-list class="transparent">
|
||||
<v-list-item class="rounded-lg mb-2 px-4" prepend-icon="mdi-bell">
|
||||
<v-list-item-title class="font-weight-medium">Notifikasi Email</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption">Terima update via email</v-list-item-subtitle>
|
||||
<template v-slot:append>
|
||||
<v-switch
|
||||
v-model="emailNotifications"
|
||||
color="orange-darken-2"
|
||||
hide-details
|
||||
inset
|
||||
></v-switch>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item class="rounded-lg mb-2 px-4" prepend-icon="mdi-theme-light-dark">
|
||||
<v-list-item-title class="font-weight-medium">Tema Gelap</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption">Aktifkan mode gelap</v-list-item-subtitle>
|
||||
<template v-slot:append>
|
||||
<v-switch
|
||||
v-model="darkMode"
|
||||
color="blue-darken-2"
|
||||
hide-details
|
||||
inset
|
||||
></v-switch>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item class="rounded-lg px-4" prepend-icon="mdi-web">
|
||||
<v-list-item-title class="font-weight-medium">Bahasa</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption">Indonesia</v-list-item-subtitle>
|
||||
<template v-slot:append>
|
||||
<v-icon color="grey">mdi-chevron-right</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
Change Password Dialog
|
||||
<v-dialog v-model="passwordDialog" max-width="500" persistent>
|
||||
<v-card class="rounded-xl">
|
||||
<v-card-title class="pa-6 pb-4">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon color="orange-darken-2" class="mr-2">mdi-lock-reset</v-icon>
|
||||
<span class="font-weight-bold">Ubah Password</span>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text class="pa-6">
|
||||
<v-form ref="passwordForm">
|
||||
<label class="text-caption text-grey-darken-1 font-weight-bold mb-1 d-block">PASSWORD SAAT INI</label>
|
||||
<v-text-field
|
||||
v-model="passwordData.current"
|
||||
type="password"
|
||||
prepend-inner-icon="mdi-lock"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
color="blue-darken-2"
|
||||
hide-details="auto"
|
||||
class="mb-4"
|
||||
></v-text-field>
|
||||
|
||||
<label class="text-caption text-grey-darken-1 font-weight-bold mb-1 d-block">PASSWORD BARU</label>
|
||||
<v-text-field
|
||||
v-model="passwordData.new"
|
||||
type="password"
|
||||
prepend-inner-icon="mdi-lock-plus"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
color="blue-darken-2"
|
||||
hide-details="auto"
|
||||
class="mb-4"
|
||||
></v-text-field>
|
||||
|
||||
<label class="text-caption text-grey-darken-1 font-weight-bold mb-1 d-block">KONFIRMASI PASSWORD</label>
|
||||
<v-text-field
|
||||
v-model="passwordData.confirm"
|
||||
type="password"
|
||||
prepend-inner-icon="mdi-lock-check"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
color="blue-darken-2"
|
||||
hide-details="auto"
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions class="pa-6 pt-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
color="grey-darken-1"
|
||||
class="rounded-lg"
|
||||
@click="passwordDialog = false"
|
||||
>
|
||||
Batal
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="orange-darken-2"
|
||||
variant="flat"
|
||||
class="rounded-lg px-6"
|
||||
@click="changePassword"
|
||||
:loading="isChangingPassword"
|
||||
>
|
||||
Ubah Password
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Active Devices Dialog -->
|
||||
<v-dialog v-model="devicesDialog" max-width="700" scrollable>
|
||||
<v-card class="rounded-xl">
|
||||
<v-card-title class="pa-6 pb-4">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon color="blue-darken-2" class="mr-2">mdi-devices</v-icon>
|
||||
<span class="font-weight-bold">Perangkat Aktif</span>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text class="pa-0" style="max-height: 500px;">
|
||||
<v-list class="py-0">
|
||||
<v-list-item
|
||||
v-for="(session, index) in activeSessions"
|
||||
:key="index"
|
||||
class="py-4 px-6"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-avatar :color="session.current ? 'success' : 'blue-grey-lighten-4'" size="48">
|
||||
<v-icon :color="session.current ? 'white' : 'blue-grey-darken-2'">
|
||||
{{ session.icon }}
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<v-list-item-title class="font-weight-bold mb-1">
|
||||
{{ session.device }}
|
||||
<v-chip v-if="session.current" size="x-small" color="success" class="ml-2">
|
||||
Sesi Ini
|
||||
</v-chip>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption">
|
||||
<div>{{ session.location }}</div>
|
||||
<div class="text-grey-darken-1 mt-1">
|
||||
<v-icon size="12">mdi-clock-outline</v-icon>
|
||||
{{ session.lastActive }}
|
||||
</div>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append v-if="!session.current">
|
||||
<v-btn
|
||||
icon="mdi-close-circle"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="removeSession(index)"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions class="pa-6 pt-4">
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="outlined"
|
||||
class="rounded-lg"
|
||||
prepend-icon="mdi-logout-variant"
|
||||
@click="logoutAllDevices"
|
||||
>
|
||||
Keluar dari Semua Perangkat
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
variant="text"
|
||||
class="rounded-lg"
|
||||
@click="devicesDialog = false"
|
||||
>
|
||||
Tutup
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Photo Upload Dialog -->
|
||||
<v-dialog v-model="photoDialog" max-width="400">
|
||||
<v-card class="rounded-xl">
|
||||
<v-card-title class="pa-6 pb-4">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon color="blue-darken-2" class="mr-2">mdi-camera</v-icon>
|
||||
<span class="font-weight-bold">Ubah Foto Profil</span>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text class="pa-6 text-center">
|
||||
<v-avatar size="150" class="mb-4">
|
||||
<v-img :src="profileData.picture"></v-img>
|
||||
</v-avatar>
|
||||
<v-file-input
|
||||
label="Pilih foto baru"
|
||||
variant="outlined"
|
||||
prepend-icon=""
|
||||
prepend-inner-icon="mdi-image"
|
||||
accept="image/*"
|
||||
hide-details
|
||||
></v-file-input>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions class="pa-6 pt-4">
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="text"
|
||||
class="rounded-lg"
|
||||
>
|
||||
Hapus Foto
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
color="grey-darken-1"
|
||||
class="rounded-lg"
|
||||
@click="photoDialog = false"
|
||||
>
|
||||
Batal
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue-darken-2"
|
||||
variant="flat"
|
||||
class="rounded-lg px-6"
|
||||
>
|
||||
Simpan
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Success Snackbar -->
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
:color="snackbarColor"
|
||||
location="top"
|
||||
timeout="3000"
|
||||
class="snackbar-custom"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon class="mr-2">{{ snackbarIcon }}</v-icon>
|
||||
<span>{{ snackbarMessage }}</span>
|
||||
</div>
|
||||
<template v-slot:actions>
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
icon="mdi-close"
|
||||
@click="snackbar = false"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { navigateTo } from '#app';
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth'
|
||||
});
|
||||
|
||||
const isEditing = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const isChangingPassword = ref(false);
|
||||
const snackbar = ref(false);
|
||||
const snackbarMessage = ref('');
|
||||
const snackbarColor = ref('success');
|
||||
|
||||
const passwordDialog = ref(false);
|
||||
const devicesDialog = ref(false);
|
||||
const photoDialog = ref(false);
|
||||
|
||||
const twoFactorEnabled = ref(false);
|
||||
const emailNotifications = ref(true);
|
||||
const darkMode = ref(false);
|
||||
|
||||
const snackbarIcon = computed(() => {
|
||||
return snackbarColor.value === 'success' ? 'mdi-check-circle' : 'mdi-alert-circle';
|
||||
});
|
||||
|
||||
const profileData = reactive({
|
||||
id: 'USR-12345678',
|
||||
name: 'John Doe',
|
||||
username: 'johndoe',
|
||||
email: '[email protected]',
|
||||
phone: '+62 812 3456 7890',
|
||||
bio: 'Passionate developer and tech enthusiast. Love building beautiful and functional web applications.',
|
||||
picture: 'https://i.pravatar.cc/300?img=68',
|
||||
joinDate: '15 Januari 2024',
|
||||
lastLogin: '16 Oktober 2025, 10:30 AM'
|
||||
});
|
||||
|
||||
const passwordData = reactive({
|
||||
current: '',
|
||||
new: '',
|
||||
confirm: ''
|
||||
});
|
||||
|
||||
const activeSessions = ref([
|
||||
{
|
||||
device: 'Chrome on Windows 11',
|
||||
location: 'Sidoarjo, Indonesia',
|
||||
lastActive: 'Sekarang',
|
||||
icon: 'mdi-laptop',
|
||||
current: true
|
||||
},
|
||||
{
|
||||
device: 'Mobile App on Android',
|
||||
location: 'Surabaya, Indonesia',
|
||||
lastActive: '2 jam yang lalu',
|
||||
icon: 'mdi-cellphone',
|
||||
current: false
|
||||
},
|
||||
{
|
||||
device: 'Safari on macOS',
|
||||
location: 'Jakarta, Indonesia',
|
||||
lastActive: '1 hari yang lalu',
|
||||
icon: 'mdi-laptop',
|
||||
current: false
|
||||
}
|
||||
]);
|
||||
|
||||
const originalData = ref({});
|
||||
|
||||
const startEdit = () => {
|
||||
originalData.value = { ...profileData };
|
||||
isEditing.value = true;
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
Object.assign(profileData, originalData.value);
|
||||
isEditing.value = false;
|
||||
};
|
||||
|
||||
const saveProfile = async () => {
|
||||
isSaving.value = true;
|
||||
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// TODO: Replace with actual API call
|
||||
// await $fetch('/api/profile/update', {
|
||||
// method: 'PUT',
|
||||
// body: profileData
|
||||
// });
|
||||
|
||||
originalData.value = { ...profileData };
|
||||
isEditing.value = false;
|
||||
snackbarMessage.value = 'Profil berhasil diperbarui!';
|
||||
snackbarColor.value = 'success';
|
||||
snackbar.value = true;
|
||||
} catch (error) {
|
||||
snackbarMessage.value = 'Gagal memperbarui profil!';
|
||||
snackbarColor.value = 'error';
|
||||
snackbar.value = true;
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openPhotoDialog = () => {
|
||||
photoDialog.value = true;
|
||||
};
|
||||
|
||||
const openPasswordDialog = () => {
|
||||
passwordDialog.value = true;
|
||||
passwordData.current = '';
|
||||
passwordData.new = '';
|
||||
passwordData.confirm = '';
|
||||
};
|
||||
|
||||
const changePassword = async () => {
|
||||
if (passwordData.new !== passwordData.confirm) {
|
||||
snackbarMessage.value = 'Password baru tidak cocok!';
|
||||
snackbarColor.value = 'error';
|
||||
snackbar.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
isChangingPassword.value = true;
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// TODO: Replace with actual API call
|
||||
|
||||
passwordDialog.value = false;
|
||||
snackbarMessage.value = 'Password berhasil diubah!';
|
||||
snackbarColor.value = 'success';
|
||||
snackbar.value = true;
|
||||
} catch (error) {
|
||||
snackbarMessage.value = 'Gagal mengubah password!';
|
||||
snackbarColor.value = 'error';
|
||||
snackbar.value = true;
|
||||
} finally {
|
||||
isChangingPassword.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openDevicesDialog = () => {
|
||||
devicesDialog.value = true;
|
||||
};
|
||||
|
||||
const removeSession = (index) => {
|
||||
activeSessions.value.splice(index, 1);
|
||||
snackbarMessage.value = 'Perangkat berhasil dihapus dari sesi aktif';
|
||||
snackbarColor.value = 'success';
|
||||
snackbar.value = true;
|
||||
};
|
||||
|
||||
const logoutAllDevices = () => {
|
||||
// Keep only current session
|
||||
activeSessions.value = activeSessions.value.filter(s => s.current);
|
||||
devicesDialog.value = false;
|
||||
snackbarMessage.value = 'Berhasil keluar dari semua perangkat lain';
|
||||
snackbarColor.value = 'success';
|
||||
snackbar.value = true;
|
||||
};
|
||||
|
||||
const toggleTwoFactor = () => {
|
||||
const status = twoFactorEnabled.value ? 'diaktifkan' : 'dinonaktifkan';
|
||||
snackbarMessage.value = `Autentikasi dua faktor ${status}`;
|
||||
snackbarColor.value = 'info';
|
||||
snackbar.value = true;
|
||||
};
|
||||
|
||||
// Initialize original data
|
||||
originalData.value = { ...profileData };
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hero-header {
|
||||
background: linear-gradient(135deg, #1976d2 0%, #1565c0 50%, #f57c00 100%);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.sticky-card {
|
||||
position: sticky;
|
||||
top: 80px;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.profile-content {
|
||||
padding-top: 0;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.profile-avatar-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-top: -70px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
border: 6px solid white;
|
||||
background: white;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.avatar-edit-btn {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
label {
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.v-list-item {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.v-list-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.snackbar-custom {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.sticky-card {
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,115 +1,138 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-card
|
||||
class="elevation-8 rounded-xl header-banner mx-6 mt-6 mb-n6"
|
||||
class="elevation-3 rounded-xl header-banner mx-6 mt-6 mb-n6"
|
||||
style="position: relative; z-index: 10;"
|
||||
>
|
||||
<v-container fluid class="py-4 px-6 d-flex align-center justify-space-between">
|
||||
<v-container fluid class="py-5 px-6 d-flex align-center justify-space-between">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="36" color="white" class="mr-3">mdi-shield-account-outline</v-icon>
|
||||
<h1 class="text-h5 font-weight-bold text-white">Verifikasi Akun Pasien</h1>
|
||||
<div class="icon-container">
|
||||
<v-icon size="44" class="primary-icon mr-4">mdi-shield-check</v-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-h3 font-weight-black primary-text mb-1">Verifikasi Akun</h1>
|
||||
<p class="text-subtitle-1 secondary-text mb-0">Manajemen Pasien Digital</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-chip color="light-blue-lighten-4" class="text-blue-darken-4 font-weight-bold" variant="flat" rounded="lg">
|
||||
<v-icon start>mdi-account-circle</v-icon>
|
||||
Admin Aktif
|
||||
<v-chip color="#5FB7FF" class="text-white font-weight-black px-5" variant="flat" rounded="xl" size="large">
|
||||
<v-icon start color="white">mdi-account-star</v-icon>
|
||||
Admin
|
||||
</v-chip>
|
||||
</v-container>
|
||||
</v-card>
|
||||
|
||||
<v-main class="bg-white">
|
||||
<v-container fluid class="pa-6 pt-12 pb-12">
|
||||
<v-main class="bg-main">
|
||||
<v-container fluid class="pa-6 pt-8 pb-12">
|
||||
|
||||
<v-card class="pa-0 mb-6 elevation-4 rounded-lg">
|
||||
<v-card class="pa-0 mb-6 elevation-3 rounded-xl overflow-hidden card-style">
|
||||
|
||||
<v-row class="ma-0 pa-4 align-center bg-white border-b-sm">
|
||||
<v-col cols="12" sm="6" class="py-1">
|
||||
<v-row class="ma-0 pa-6 align-center search-section">
|
||||
<v-col cols="12" sm="6" class="py-2">
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
density="comfortable"
|
||||
label="Pencarian Pasien..."
|
||||
label="Cari pasien berdasarkan nama atau RM..."#FFB95F
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
single-line
|
||||
color="#D17A4A"
|
||||
bg-color="white"
|
||||
class="search-field text-body-1"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="py-1">
|
||||
<v-col cols="12" sm="6" class="py-2">
|
||||
<v-tabs
|
||||
v-model="filterStatus"
|
||||
color="orange-darken-1"
|
||||
color="#D17A4A"
|
||||
align-tabs="end"
|
||||
class="pt-0"
|
||||
class="pt-0 filter-tabs"
|
||||
slider-color="#D17A4A"
|
||||
>
|
||||
<v-tab :value="0" class="font-weight-bold text-caption text-sm-button">
|
||||
SEMUA PASIEN <v-chip size="small" color="blue-grey-darken-1" class="ml-2">{{ patients.length }}</v-chip>
|
||||
<v-tab :value="0" class="font-weight-bold text-button">
|
||||
SEMUA <v-chip size="small" color="#7B8794" class="ml-2 text-white">{{ patients.length }}</v-chip>
|
||||
</v-tab>
|
||||
<v-tab :value="1" class="font-weight-bold text-caption text-sm-button">
|
||||
BELUM VERIFIKASI <v-chip size="small" color="orange-darken-1" class="ml-2">{{ unverifiedPatients.length }}</v-chip>
|
||||
<v-tab :value="1" class="font-weight-bold text-button">
|
||||
PENDING <v-chip size="small" color="#D17A4A" class="ml-2 text-white">{{ unverifiedPatients.length }}</v-chip>
|
||||
</v-tab>
|
||||
<v-tab :value="2" class="font-weight-bold text-caption text-sm-button">
|
||||
TERVERIFIKASI <v-chip size="small" color="blue-darken-1" class="ml-2">{{ verifiedPatients.length }}</v-chip>
|
||||
<v-tab :value="2" class="font-weight-bold text-button">
|
||||
VERIFIED <v-chip size="small" color="#4A5F7A" class="ml-2 text-white">{{ verifiedPatients.length }}</v-chip>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<v-card-text class="pa-0 bg-white">
|
||||
<v-list lines="two" class="pa-0">
|
||||
<v-list-item
|
||||
v-for="(patient, index) in filteredPatients"
|
||||
:key="patient.rm"
|
||||
class="border-b"
|
||||
:class="{ 'bg-blue-grey-lighten-5': patient.status === 'Terverifikasi', 'hover:bg-orange-lighten-5': patient.status !== 'Terverifikasi' }"
|
||||
class="patient-item py-5 px-6"
|
||||
:class="{ 'verified-item': patient.status === 'Terverifikasi' }"
|
||||
>
|
||||
<v-list-item-title class="text-h6 font-weight-bold mb-1 d-flex align-center">
|
||||
<v-icon :color="patient.status === 'Terverifikasi' ? 'blue' : 'orange'" class="mr-3" size="28">
|
||||
{{ patient.status === 'Terverifikasi' ? 'mdi-check-circle' : 'mdi-alert-circle' }}
|
||||
<template v-slot:prepend>
|
||||
<v-avatar :color="patient.status === 'Terverifikasi' ? '#32A3FF' : '#FFA532'" 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-list-item-title class="text-h5 font-weight-black mb-2 primary-text">
|
||||
{{ patient.nama }}
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="mt-1 ml-9">
|
||||
<v-row dense class="text-caption text-md-body-2">
|
||||
<v-col cols="12" md="3" class="py-0 text-grey-darken-1">
|
||||
RM: <span class="font-weight-medium text-blue-darken-2">{{ patient.rm }}</span>
|
||||
<v-list-item-subtitle class="mt-2">
|
||||
<v-row dense class="text-body-1">
|
||||
<v-col cols="12" sm="3" md="2" class="py-1">
|
||||
<v-chip size="default" color="#E3F2FD" class="font-weight-bold chip-rm" variant="flat">
|
||||
<v-icon start size="18" color="#4A5F7A">mdi-file-document</v-icon>
|
||||
<span style="color: #4A5F7A;">{{ patient.rm }}</span>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" class="py-0 text-grey-darken-1">
|
||||
Alamat: {{ patient.alamat }}
|
||||
|
||||
<v-col cols="12" sm="5" md="6" class="py-1 secondary-text d-flex align-center">
|
||||
<v-icon size="18" class="mr-2" color="#7B8794">mdi-map-marker</v-icon>
|
||||
<span class="text-body-1">{{ patient.alamat }}</span>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3" class="py-0 text-grey-darken-1">
|
||||
Telp: <span class="text-blue-darken-2">{{ patient.telepon || '-' }}</span>
|
||||
|
||||
<v-col cols="12" sm="4" md="4" class="py-1 secondary-text d-flex align-center">
|
||||
<v-icon size="18" class="mr-2" color="#7B8794">mdi-phone</v-icon>
|
||||
<span class="text-body-1">{{ patient.telepon || 'Belum diisi' }}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-btn
|
||||
v-if="patient.status === 'Belum Terverifikasi'"
|
||||
color="orange-darken-1"
|
||||
size="large"
|
||||
color="#FFA532"
|
||||
size="x-large"
|
||||
@click="openDaftarModal(patient)"
|
||||
prepend-icon="mdi-account-plus"
|
||||
prepend-icon="mdi-qrcode-scan"
|
||||
variant="flat"
|
||||
class="font-weight-bold elevation-3"
|
||||
class="font-weight-black elevation-2 action-btn px-8 text-h6 text-white"
|
||||
rounded="xl"
|
||||
>
|
||||
DAFTARKAN
|
||||
VERIFIKASI
|
||||
</v-btn>
|
||||
<v-chip
|
||||
v-else
|
||||
color="blue"
|
||||
label
|
||||
size="large"
|
||||
variant="tonal"
|
||||
class="font-weight-bold"
|
||||
color="#32A3FF"
|
||||
size="x-large"
|
||||
variant="flat"
|
||||
class="font-weight-black px-6 text-h6 text-white"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-icon start size="24">mdi-shield-check</v-icon>
|
||||
VERIFIED
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="filteredPatients.length === 0">
|
||||
<v-list-item-title class="text-center py-6 text-h6 text-grey-darken-1">
|
||||
<v-list-item-title class="text-center py-8 text-h5 secondary-text">
|
||||
{{ noDataText }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
@@ -117,81 +140,97 @@
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-dialog v-model="isModalOpen" max-width="450" transition="slide-y-transition">
|
||||
<v-card class="pa-6 border-t-lg border-t-blue-darken-3">
|
||||
<v-card-title class="text-h5 font-weight-bold text-center text-blue-darken-2">
|
||||
Aktivasi Akun Pasien
|
||||
</v-card-title>
|
||||
<v-card-subtitle class="text-center mb-4">
|
||||
{{ selectedPatient.nama }} (RM: {{ selectedPatient.rm }})
|
||||
</v-card-subtitle>
|
||||
<v-dialog v-model="isModalOpen" max-width="600" transition="dialog-bottom-transition">
|
||||
<v-card class="pa-0 rounded-xl overflow-hidden card-style">
|
||||
<div class="modal-header pa-7">
|
||||
<v-icon size="72" color="white" class="mb-3">mdi-qrcode-scan</v-icon>
|
||||
<h2 class="text-h3 font-weight-black text-white mb-2">Aktivasi Akun</h2>
|
||||
<p class="text-h6 text-white opacity-90">{{ selectedPatient.nama }}</p>
|
||||
<v-chip color="white" class="font-weight-bold mt-2" variant="flat" size="default">
|
||||
<span style="color: #D17A4A;">RM: {{ selectedPatient.rm }}</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<v-card-text>
|
||||
<v-card-text class="pa-7">
|
||||
<v-text-field
|
||||
v-model="tempTelepon"
|
||||
:disabled="qrCodeGenerated"
|
||||
label="No. Telepon Pasien"
|
||||
label="Nomor Telepon Pasien"
|
||||
placeholder="08xxxxxxxxxx"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:color="qrCodeGenerated ? 'grey' : 'orange-darken-1'"
|
||||
:color="qrCodeGenerated ? 'grey' : '#D17A4A'"
|
||||
:rules="[v => v.length >= 8 || 'Min. 8 digit']"
|
||||
prepend-inner-icon="mdi-phone"
|
||||
class="mb-3 text-body-1"
|
||||
base-color="grey-darken-1"
|
||||
></v-text-field>
|
||||
|
||||
<v-btn
|
||||
v-if="!qrCodeGenerated"
|
||||
:disabled="!isTeleponValid"
|
||||
color="orange-darken-1"
|
||||
color="#5FB7FF"
|
||||
block
|
||||
size="large"
|
||||
class="mt-1 font-weight-bold elevation-3"
|
||||
size="x-large"
|
||||
class="mt-3 font-weight-black elevation-2 text-h5 text-white"
|
||||
@click="generateQrCode"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-icon left>mdi-qrcode-scan</v-icon>
|
||||
Lanjutkan & Generate QR
|
||||
<v-icon left size="32">mdi-qrcode-plus</v-icon>
|
||||
Generate QR Code
|
||||
</v-btn>
|
||||
|
||||
<div v-else class="text-center mt-4 pa-4 bg-blue-grey-lighten-5 rounded-lg border-sm border-blue-darken-1">
|
||||
<v-icon color="orange-darken-1" size="48">mdi-cellphone-check</v-icon>
|
||||
<div class="text-h6 font-weight-bold mb-3 text-blue-darken-2">PINDAI UNTUK VERIFIKASI</div>
|
||||
<div v-else class="text-center mt-6 pa-7 qr-container rounded-xl">
|
||||
<div class="pulse-icon mb-4">
|
||||
<v-icon color="#4A5F7A" size="64">mdi-cellphone-check</v-icon>
|
||||
</div>
|
||||
<h3 class="text-h4 font-weight-black mb-3" style="color: #4A5F7A;">Pindai QR Code</h3>
|
||||
<p class="text-body-1 secondary-text mb-6">Arahkan kamera smartphone ke QR code</p>
|
||||
|
||||
<div class="d-flex justify-center mb-4">
|
||||
<qrcode-vue
|
||||
:value="qrCodeData"
|
||||
:size="170"
|
||||
level="H"
|
||||
:foreground="'#F57C00'"
|
||||
:background="'#FFFFFF'"
|
||||
class="border-4 rounded-lg border-orange-darken-1"
|
||||
/>
|
||||
<div class="qr-frame">
|
||||
<qrcode-vue
|
||||
:value="qrCodeData"
|
||||
:size="280"
|
||||
level="H"
|
||||
:foreground="'#4A5F7A'"
|
||||
:background="'#FFFFFF'"
|
||||
class="qr-code"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions v-if="qrCodeGenerated" class="d-flex justify-space-between pt-0">
|
||||
<v-card-actions v-if="qrCodeGenerated" class="pa-7 pt-0">
|
||||
<v-btn
|
||||
color="orange-darken-1"
|
||||
variant="tonal"
|
||||
color="#4A5F7A"
|
||||
variant="outlined"
|
||||
@click="reloadQr"
|
||||
prepend-icon="mdi-reload"
|
||||
size="x-large"
|
||||
class="font-weight-bold flex-grow-1 text-body-1"
|
||||
rounded="xl"
|
||||
>
|
||||
Reload
|
||||
Reload QR
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="green-darken-1"
|
||||
variant="elevated"
|
||||
color="#D17A4A"
|
||||
variant="flat"
|
||||
@click="completeVerification"
|
||||
prepend-icon="mdi-check-circle"
|
||||
size="large"
|
||||
class="font-weight-bold elevation-3"
|
||||
size="x-large"
|
||||
class="font-weight-black flex-grow-1 elevation-2 text-body-1"
|
||||
rounded="xl"
|
||||
>
|
||||
Selesai
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
<v-card-actions v-else>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" variant="text" @click="closeModal">Batal</v-btn>
|
||||
<v-card-actions v-else class="pa-7 pt-0">
|
||||
<v-btn color="#7B8794" variant="text" @click="closeModal" size="x-large" block rounded="xl" class="font-weight-bold text-body-1">
|
||||
Batal
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -202,9 +241,12 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
// PERUBAHAN DI SINI: Import QrcodeVue
|
||||
import QrcodeVue from 'qrcode.vue';
|
||||
|
||||
definePageMeta({
|
||||
middleware:['auth']
|
||||
})
|
||||
|
||||
// --- MOCK DATA SIMRS ---
|
||||
const patients = ref([
|
||||
{ nama: 'Budi Santoso', rm: '00123456', alamat: 'Jl. Melati No. 10', telepon: '', status: 'Belum Terverifikasi' },
|
||||
@@ -229,9 +271,9 @@ const verifiedPatients = computed(() =>
|
||||
const filteredPatients = computed(() => {
|
||||
let statusFiltered = patients.value;
|
||||
|
||||
if (filterStatus.value === 1) { // Belum Terverifikasi
|
||||
if (filterStatus.value === 1) {
|
||||
statusFiltered = unverifiedPatients.value;
|
||||
} else if (filterStatus.value === 2) { // Terverifikasi
|
||||
} else if (filterStatus.value === 2) {
|
||||
statusFiltered = verifiedPatients.value;
|
||||
}
|
||||
|
||||
@@ -252,7 +294,6 @@ const qrCodeGenerated = ref(false);
|
||||
|
||||
const isTeleponValid = computed(() => tempTelepon.value.length >= 8);
|
||||
|
||||
// Data yang akan di-encode dalam QR Code
|
||||
const qrCodeData = computed(() => {
|
||||
return qrCodeGenerated.value
|
||||
? JSON.stringify({
|
||||
@@ -263,10 +304,8 @@ const qrCodeData = computed(() => {
|
||||
})
|
||||
: '';
|
||||
});
|
||||
|
||||
|
||||
|
||||
// --- LOGIC MODAL ---
|
||||
|
||||
const openDaftarModal = (patient) => {
|
||||
selectedPatient.value = patient;
|
||||
tempTelepon.value = patient.telepon || '';
|
||||
@@ -315,31 +354,199 @@ const completeVerification = () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Custom style untuk Vuetify */
|
||||
.border-t-lg {
|
||||
border-top-width: 8px !important;
|
||||
}
|
||||
.border-t-blue-darken-3 {
|
||||
border-top-color: var(--v-theme-blue-darken-3) !important;
|
||||
/* Background */
|
||||
.bg-main {
|
||||
background-color: #FAFAFA;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Style untuk Header Card (Floating & Rounded) */
|
||||
.card-style {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* Text Colors */
|
||||
.primary-text {
|
||||
color: #2C3E50 !important;
|
||||
}
|
||||
|
||||
.secondary-text {
|
||||
color: #7B8794 !important;
|
||||
}
|
||||
|
||||
.primary-icon {
|
||||
color: #FF9B1B !important;
|
||||
}
|
||||
|
||||
/* Header Banner */
|
||||
.header-banner {
|
||||
background: linear-gradient(90deg, #1565C0, #1976D2);
|
||||
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);
|
||||
background-color: #E8F5FF;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* Memastikan elemen list item memiliki efek hover yang halus */
|
||||
.hover\:bg-orange-lighten-5:hover {
|
||||
background-color: var(--v-theme-orange-lighten-5) !important;
|
||||
.header-banner:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Memastikan container di v-main memiliki padding atas yang cukup */
|
||||
.v-main .v-container {
|
||||
padding-top: 5rem !important;
|
||||
.icon-container {
|
||||
background-color: rgba(209, 122, 74, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Search Section */
|
||||
.search-section {
|
||||
background-color: #FAFAFA;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.search-field {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Patient List Items */
|
||||
.patient-item {
|
||||
border-bottom: 1px solid #F0F0F0;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.patient-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 4px;
|
||||
background: #D17A4A;
|
||||
transform: scaleY(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.patient-item:hover::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.patient-item:hover {
|
||||
background: linear-gradient(90deg, #FFF5EE 0%, #ffffff 100%) !important;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.verified-item {
|
||||
background: linear-gradient(90deg, #F0F4F8 0%, #ffffff 100%) !important;
|
||||
}
|
||||
|
||||
.verified-item::before {
|
||||
background: #4A5F7A !important;
|
||||
}
|
||||
|
||||
.verified-item:hover {
|
||||
background: linear-gradient(90deg, #E8EFF5 0%, #ffffff 100%) !important;
|
||||
}
|
||||
|
||||
.patient-avatar {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border: 3px solid white;
|
||||
}
|
||||
|
||||
.chip-rm {
|
||||
border: 1px solid rgba(74, 95, 122, 0.2);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
text-transform: none !important;
|
||||
letter-spacing: 0.5px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(209, 122, 74, 0.3) !important;
|
||||
background-color: #E0916E !important;
|
||||
}
|
||||
|
||||
/* Modal Styling */
|
||||
.modal-header {
|
||||
background: linear-gradient(135deg, #4A5F7A 0%, #5D7290 100%);
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 12px rgba(74, 95, 122, 0.2);
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
background: linear-gradient(135deg, #F0F4F8 0%, #E8EFF5 100%);
|
||||
border: 3px solid #4A5F7A;
|
||||
box-shadow: 0 6px 20px rgba(74, 95, 122, 0.15);
|
||||
}
|
||||
|
||||
.qr-frame {
|
||||
padding: 24px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
display: inline-block;
|
||||
border: 4px solid #4A5F7A;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pulse-icon {
|
||||
animation: pulse-scale 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-scale {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.08);
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 960px) {
|
||||
.patient-item {
|
||||
padding: 20px !important;
|
||||
}
|
||||
|
||||
.header-banner h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.header-banner p {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
font-size: 1rem;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.patient-avatar {
|
||||
width: 56px !important;
|
||||
height: 56px !important;
|
||||
}
|
||||
|
||||
.text-h5 {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Menghapus padding default dari v-card header agar v-container di dalamnya yang mengatur padding */
|
||||
.header-banner .v-container {
|
||||
padding-left: 1.5rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
+44
-37
@@ -1,59 +1,55 @@
|
||||
// stores/navItems.ts
|
||||
import { defineStore } from 'pinia';
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
import { computed } from 'vue'; // Import computed dari Vue
|
||||
|
||||
interface NavItem {
|
||||
id: number;
|
||||
name: string;
|
||||
path: string;
|
||||
name: string; // Menggantikan 'title'
|
||||
path: string; // Menggantikan 'to'
|
||||
icon: string;
|
||||
children?: NavItem[];
|
||||
badge?: string; // Tambahkan properti badge
|
||||
}
|
||||
|
||||
// Initial default navigation items
|
||||
const defaultNavItems: NavItem[] = [
|
||||
{ id: 1, name: "Dashboard", icon: "mdi-view-dashboard", path: "/dashboard" },
|
||||
// Add other main menu items
|
||||
{ id: 2, name: "Verifikasi Akun", icon: "mdi-account-supervisor", path: "/verifikasiAkun/VerifikasiAkun" },
|
||||
{ id: 3, name: "Ranap Admin", icon: "mdi-bed", path: "/RanapAdmin" },
|
||||
{ id: 4, name: "Klinik Admin", icon: "mdi-hospital-box", path: "/KlinikAdmin" },
|
||||
{ id: 5, name: "Klinik Ruang Admin", icon: "mdi-hospital-marker", path: "/KlinikRuangAdmin" },
|
||||
{ id: 2, name: "Verifikasi Akun", icon: "mdi-account-check-outline", path:"/verifikasiAkun/VerifikasiAkun" },
|
||||
{
|
||||
id: 3,
|
||||
name: "Check In",
|
||||
icon: "mdi-file-document-edit-outline",
|
||||
path: "/checkinPasien/checkin"
|
||||
// badge: "3",
|
||||
},
|
||||
{ id: 4, name: "Admin Loket", icon: "mdi-account-supervisor-outline", path: "/AdminLoket" },
|
||||
{ id: 5, name: "Admin Klinik", icon: "mdi-text-box-plus-outline", path: "/AdminKlinik" },
|
||||
{ id: 6, name: "Admin Penunjang", icon: "mdi-plus-box-outline", path: "/AdminPenunjang" },
|
||||
{ id: 7, name: "Buat Antrean", icon: "mdi-account-multiple-plus-outline", path: "/BuatAntrean" },
|
||||
{ id: 8, name: "Monitoring Pasien", icon: "mdi-account-group-outline", path: "/MonitoringPasien/monitoringPasien" },
|
||||
{
|
||||
id: 6,
|
||||
name: "Anjungan",
|
||||
icon: "mdi-account-box-multiple",
|
||||
id: 9,
|
||||
name: "Layar Informasi",
|
||||
icon: "mdi-monitor-multiple",
|
||||
path: "",
|
||||
children: [
|
||||
{ id: 7, name: "Anjungan", path: "/Anjungan/Anjungan", icon: "mdi-account-box" },
|
||||
{ id: 8, name: "Admin Anjungan", path: "/Anjungan/AdminAnjungan", icon: "mdi-account-cog" },
|
||||
{ id: 10, name: "Anjungan", path: "/anjungan/anjungan", icon: "mdi-circle-small" },
|
||||
{ id: 11, name: "Klinik Ruang", path: "/anjungan/AntrianKlinik", icon: "mdi-circle-small" },
|
||||
],
|
||||
},
|
||||
{ id: 9, name: "Fast Track", icon: "mdi-clock-fast", path: "/FastTrack" },
|
||||
{ id: 10, name: "Data Pasien", icon: "mdi-account-multiple", path: "/DataPasien" },
|
||||
{
|
||||
id: 11,
|
||||
name: "Screen",
|
||||
icon: "mdi-monitor",
|
||||
{
|
||||
id: 12,
|
||||
name: "Master Data",
|
||||
icon: "mdi-cog-outline",
|
||||
path: "",
|
||||
children: [
|
||||
{ id: 12, name: "Antrian Masuk 1", path: "/Screen/Antrian Masuk 1", icon: "mdi-monitor" },
|
||||
{ id: 13, name: "Antrian Masuk 2", path: "/Screen/Antrian Masuk 2", icon: "mdi-monitor" },
|
||||
// ... more screen pages
|
||||
],
|
||||
},
|
||||
{ id: 14, name: "List Pasien", icon: "mdi-format-list-bulleted", path: "/ListPasien" },
|
||||
{
|
||||
id: 15 ,
|
||||
name: "Setting",
|
||||
icon: "mdi-cog",
|
||||
path: "",
|
||||
children: [
|
||||
{ id: 16, name: "Hak Akses", path: "/setting/HakAkses", icon: "mdi-shield-lock-outline" },
|
||||
{ id: 17, name: "User Login", path: "/setting/UserLogin", icon: "mdi-account-circle" },
|
||||
{ id: 18, name: "Master Loket", path: "/setting/MasterLoket", icon: "mdi-counter" },
|
||||
{ id: 19, name: "Master Klinik", path: "/setting/MasterKlinik", icon: "mdi-hospital" },
|
||||
{ id: 20, name: "Master Klinik Ruang", path: "/setting/MasterKlinikRuang", icon: "mdi-hospital-box" },
|
||||
{ id: 21, name: "Screen", path: "/setting/Screen", icon: "mdi-monitor" },
|
||||
{ id: 13, name: "Hak Akses", path: "/setting/HakAkses", icon: "mdi-circle-small" },
|
||||
{ id: 14, name: "User Login", path: "/setting/UserLogin", icon: "mdi-circle-small" },
|
||||
{ id: 15, name: "Master Loket", path: "/setting/masterloket", icon: "mdi-circle-small" },
|
||||
{ id: 16, name: "Master Klinik", path: "/setting/masterklinik", icon: "mdi-circle-small" },
|
||||
{ id: 17, name: "Master Klinik Ruang", path: "/setting/masterklinikruang", icon: "mdi-circle-small" },
|
||||
{ id: 18, name: "Screen", path: "/setting/screen", icon: "mdi-circle-small" },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -61,8 +57,18 @@ const defaultNavItems: NavItem[] = [
|
||||
export const useNavItemsStore = defineStore('navItems', () => {
|
||||
const navItems = useLocalStorage<NavItem[]>('navItems', defaultNavItems);
|
||||
|
||||
// === GETTER PENTING UNTUK MENGATASI MASALAH 'NULL' DI AWAL ===
|
||||
const getNavItems = computed(() => {
|
||||
// Jika navItems.value null, undefined, atau bukan array yang valid,
|
||||
// kembalikan array default.
|
||||
if (!Array.isArray(navItems.value) || navItems.value === null || navItems.value === undefined) {
|
||||
return defaultNavItems;
|
||||
}
|
||||
return navItems.value;
|
||||
});
|
||||
// =============================================================
|
||||
|
||||
function updateNavItems(newItems: NavItem[]) {
|
||||
// This will update the local storage and the state
|
||||
navItems.value = newItems.map((item, index) => ({
|
||||
...item,
|
||||
id: index + 1
|
||||
@@ -76,6 +82,7 @@ export const useNavItemsStore = defineStore('navItems', () => {
|
||||
|
||||
return {
|
||||
navItems,
|
||||
getNavItems, // Diekspor untuk digunakan di Sidebar
|
||||
updateNavItems,
|
||||
addNavItem,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user