Merge branch 'Antrean-Code' of https://git.rssa.top/arie.bagus.2905/Web-Antrean into Antrean-Code
This commit is contained in:
No files matched your search
@@ -0,0 +1,474 @@
|
||||
/* assets/styles/loginpage/login.css */
|
||||
|
||||
/* Main Background */
|
||||
.login-background {
|
||||
background: linear-gradient(135deg, #FFC857 0%, #FF9B1B 25%, #FF8F00 50%, #FF6F00 75%, #E65100 100%);
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding-top: 64px;
|
||||
}
|
||||
|
||||
.login-background::before {
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: white !important;
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Floating Medical Icons */
|
||||
.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;
|
||||
}
|
||||
|
||||
.floating-medical-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.floating-medical-icon .v-icon {
|
||||
color: rgba(255, 255, 255, 0.15) !important;
|
||||
}
|
||||
|
||||
.icon-1 {
|
||||
top: 0%;
|
||||
left: 0%;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 25s;
|
||||
animation-name: dvdBounce1;
|
||||
}
|
||||
|
||||
.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 {
|
||||
z-index: 3;
|
||||
max-width: 1100px;
|
||||
margin: 2rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.white-card {
|
||||
background: white !important;
|
||||
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);
|
||||
}
|
||||
|
||||
/* Left Section Styling */
|
||||
.text-section-box {
|
||||
background: linear-gradient(135deg, #61cbdd 0%, #4194c4 50%, #2089b9 100%);
|
||||
min-height: 600px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.text-section-box::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
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%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-title-box {
|
||||
color: white;
|
||||
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);
|
||||
}
|
||||
|
||||
.hero-description-box {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
margin-top: 2rem;
|
||||
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.hospital-logo-large {
|
||||
height: 80px;
|
||||
width: auto;
|
||||
filter: drop-shadow(2px 2px 6px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
|
||||
/* Right Section Styling */
|
||||
.login-section-box {
|
||||
background: white;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.login-content {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* White Dialog Styles */
|
||||
.white-dialog {
|
||||
background: white !important;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Card Header - Dark Text */
|
||||
.welcome-title-dark {
|
||||
color: #37474F;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.login-instruction-dark {
|
||||
color: #546E7A;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* App Title - Dark */
|
||||
.app-title-dark {
|
||||
color: #0053AD;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.hospital-logo {
|
||||
height: 64px;
|
||||
width: auto;
|
||||
filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.sso-text-dark {
|
||||
color: #546E7A;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.login-btn {
|
||||
background: linear-gradient(135deg, #0053AD 0%, #0663C7 50%, #0671E0 100%) !important;
|
||||
color: white !important;
|
||||
border: none;
|
||||
box-shadow:
|
||||
0 8px 25px rgba(0, 83, 173, 0.3),
|
||||
0 4px 12px rgba(0, 83, 173, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
text-transform: none;
|
||||
font-size: 1rem;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.register-btn-dark {
|
||||
color: #0053AD !important;
|
||||
border: 2px solid #0053AD !important;
|
||||
background: transparent !important;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: none;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.register-btn-dark:hover {
|
||||
background: rgba(0, 83, 173, 0.05) !important;
|
||||
border-color: #0663C7 !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Custom Divider - Dark */
|
||||
.custom-divider-dark {
|
||||
border-color: rgba(0, 0, 0, 0.12) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Help Text and Links - Dark */
|
||||
.help-text-dark {
|
||||
color: #546E7A;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.contact-link-dark {
|
||||
color: #0053AD !important;
|
||||
text-decoration: underline;
|
||||
text-transform: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.help-link-dark {
|
||||
color: #78909C;
|
||||
font-size: 0.85rem;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.help-link-dark:hover {
|
||||
color: #0053AD;
|
||||
}
|
||||
|
||||
/* Transparent Background */
|
||||
.transparent {
|
||||
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);
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
color: #FF9B1B;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 8px;
|
||||
margin: 4px 8px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 960px) {
|
||||
.login-background {
|
||||
padding-top: 56px;
|
||||
}
|
||||
|
||||
.main-card {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.text-section-box {
|
||||
min-height: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-section-box {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.hero-title-box {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.hero-description-box {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.login-background {
|
||||
padding-top: 56px;
|
||||
}
|
||||
|
||||
.main-card {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.hero-title-box {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.text-section-box,
|
||||
.login-section-box {
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- components/AppBar.vue -->
|
||||
<template>
|
||||
<v-app-bar app color="#ff9248" dark>
|
||||
<v-app-bar-nav-icon @click="emit('toggle-rail')"></v-app-bar-nav-icon>
|
||||
+60
-39
@@ -2,31 +2,62 @@
|
||||
<v-menu
|
||||
v-model="menu"
|
||||
:close-on-content-click="false"
|
||||
location="bottom right"
|
||||
origin="top right"
|
||||
location="top end"
|
||||
origin="bottom right"
|
||||
transition="slide-y-transition"
|
||||
open-on-hover
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon v-bind="props">
|
||||
<template v-slot:activator="{ props: menuProps }">
|
||||
<v-list-item
|
||||
v-if="!rail"
|
||||
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"
|
||||
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>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<v-icon size="20">mdi-chevron-up</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-btn
|
||||
v-else
|
||||
icon
|
||||
v-bind="menuProps"
|
||||
size="large"
|
||||
variant="text"
|
||||
color="blue-grey-darken-4"
|
||||
:title="user?.name || 'Profile'"
|
||||
>
|
||||
<v-avatar size="40">
|
||||
<v-img
|
||||
:src="user?.picture || 'https://i.pravatar.cc/300?img=68'"
|
||||
<v-img
|
||||
:src="user?.picture || 'https://i.pravatar.cc/300?img=68'"
|
||||
:alt="`${user?.name || 'User'} Profile`"
|
||||
></v-img>
|
||||
</v-avatar>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-card class="rounded-lg elevation-4 pa-4" width="300">
|
||||
<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'"
|
||||
<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">
|
||||
<div class="text-subtitle-1 font-weight-bold text-blue-grey-darken-4">
|
||||
{{ user?.name || user?.preferred_username || 'User' }}
|
||||
</div>
|
||||
<div class="text-caption text-grey-darken-1">
|
||||
@@ -40,16 +71,16 @@
|
||||
<v-divider class="my-2"></v-divider>
|
||||
|
||||
<v-list dense>
|
||||
<v-list-item link class="rounded-lg" @click="handleAction('account')">
|
||||
<v-list-item link class="rounded-lg text-blue-grey-darken-4" @click="handleAction('account')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-cog</v-icon>
|
||||
<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>
|
||||
|
||||
<v-list-item link class="rounded-lg" @click="handleAction('darkMode')">
|
||||
<v-list-item link class="rounded-lg text-blue-grey-darken-4" @click="handleAction('darkMode')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-weather-night</v-icon>
|
||||
<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>
|
||||
@@ -57,23 +88,23 @@
|
||||
v-model="darkMode"
|
||||
hide-details
|
||||
density="compact"
|
||||
color="primary"
|
||||
color="orange-darken-2"
|
||||
></v-switch>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link class="rounded-lg" @click="handleAction('profile')">
|
||||
<v-list-item link class="rounded-lg text-blue-grey-darken-4" @click="handleAction('profile')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-account</v-icon>
|
||||
<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>
|
||||
|
||||
<v-divider class="my-2"></v-divider>
|
||||
|
||||
<v-list-item
|
||||
link
|
||||
class="rounded-lg text-red"
|
||||
<v-list-item
|
||||
link
|
||||
class="rounded-lg text-red"
|
||||
@click="signOut"
|
||||
:disabled="isLoggingOut"
|
||||
>
|
||||
@@ -91,12 +122,16 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { navigateTo } from '#app';
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
rail: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -105,43 +140,29 @@ const darkMode = ref(false);
|
||||
const isLoggingOut = ref(false);
|
||||
const emit = defineEmits(['logout']);
|
||||
|
||||
/**
|
||||
* Handles the logout action - delegates to parent
|
||||
*/
|
||||
const signOut = async () => {
|
||||
if (isLoggingOut.value) return;
|
||||
|
||||
isLoggingOut.value = true;
|
||||
menu.value = false;
|
||||
|
||||
|
||||
try {
|
||||
console.log('🚪 ProfilePopup signOut called...')
|
||||
emit('logout');
|
||||
// 🚨 Emits event up to SideBar to trigger useAuth().logout()
|
||||
emit('logout');
|
||||
} finally {
|
||||
isLoggingOut.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleAction = (action) => {
|
||||
console.log('Action triggered:', action);
|
||||
|
||||
switch(action) {
|
||||
case 'account':
|
||||
// Navigate to account settings
|
||||
navigateTo('/settings/account')
|
||||
break;
|
||||
case 'profile':
|
||||
// Navigate to profile page
|
||||
navigateTo('/profile')
|
||||
break;
|
||||
case 'darkMode':
|
||||
// Dark mode toggle is handled by v-model
|
||||
break;
|
||||
default:
|
||||
console.log('Unknown action:', action);
|
||||
}
|
||||
|
||||
// Close menu for navigation actions
|
||||
|
||||
if (action !== 'darkMode') {
|
||||
menu.value = false;
|
||||
}
|
||||
|
||||
+7
-18
@@ -1,3 +1,4 @@
|
||||
<!-- components/sideBar.vue -->
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
@@ -112,27 +113,15 @@
|
||||
|
||||
<v-divider class="my-4 mx-2"></v-divider>
|
||||
</v-list>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<template v-slot:append>
|
||||
<div class="pa-3">
|
||||
<ProfileMenu
|
||||
:user="user.value" :rail="rail"
|
||||
@logout="handleLogout"
|
||||
class="full-width-on-expand"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
// Import store Pinia
|
||||
import { useNavItemsStore } from "@/stores/navItems"; // Sesuaikan path ini jika perlu
|
||||
import ProfileMenu from "./ProfileMenu.vue";
|
||||
import { useNavItemsStore } from "@/stores/navItems";
|
||||
import ProfilePopup from "./ProfilePopup.vue";
|
||||
import { useAuth } from "~/composables/useAuth";
|
||||
|
||||
// State
|
||||
const drawer = ref(true);
|
||||
@@ -143,7 +132,7 @@ const hoverTimeout = ref(null);
|
||||
const user = ref({
|
||||
name: 'Adam Sulfat',
|
||||
email: '[email protected]',
|
||||
picture: 'https://i.pravatar.cc/150?img=33', // Ganti dengan URL gambar dinamis
|
||||
picture: 'https://i.pravatar.cc/150?img=33',
|
||||
id: 'a1b2c3d4e5f6g7h8'
|
||||
});
|
||||
const navItemsStore = useNavItemsStore();
|
||||
@@ -151,10 +140,10 @@ const navItemsStore = useNavItemsStore();
|
||||
const currentRoute = useRoute();
|
||||
|
||||
const currentActiveMenu = computed(() => {
|
||||
// Menggunakan Getter untuk mendapatkan array menu yang pasti valid
|
||||
|
||||
const items = navItemsStore.getNavItems;
|
||||
|
||||
// Pengecekan keamanan (walaupun Getter seharusnya sudah menjamin array)
|
||||
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
export const usePermission = () => {
|
||||
/**
|
||||
* Extract group and role from path
|
||||
* Example: "/Instalasi STIM/Devops/Superadmin"
|
||||
* → { group: "STIM", role: "superadmin" }
|
||||
*/
|
||||
const parsePath = (path: string) => {
|
||||
const parts = path.split("/").filter(Boolean);
|
||||
const group = parts[1] || ""; // "STIM"
|
||||
const role = parts.at(-1)?.toLowerCase() || ""; // "superadmin"
|
||||
return { group, role };
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch permission from backend API
|
||||
*/
|
||||
const fetchPermission = async (path: string) => {
|
||||
const { group, role } = parsePath(path);
|
||||
const url = `http://10.10.150.131:8080/api/permission?roles=${role}&groups=${group}`;
|
||||
|
||||
const { data, error } = await useFetch(url, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (error.value) {
|
||||
console.error("❌ Gagal mengambil permission:", error.value);
|
||||
throw error.value;
|
||||
}
|
||||
|
||||
console.log("✅ Permission data:", data.value);
|
||||
return data.value;
|
||||
};
|
||||
|
||||
return { parsePath, fetchPermission };
|
||||
};
|
||||
@@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<!-- <LazyAppBar @toggle-rail="rail = !rail" /> -->
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
:rail="rail"
|
||||
permanent
|
||||
app
|
||||
color="white"
|
||||
width="280"
|
||||
rail-width="72"
|
||||
>
|
||||
<div
|
||||
class="pa-4 d-flex align-center cursor-pointer"
|
||||
style="height: 64px;"
|
||||
@click="rail = !rail"
|
||||
>
|
||||
<v-icon color="#ff9248" size="32">mdi-hospital-building</v-icon>
|
||||
<v-toolbar-title v-show="!rail" class="ml-3 text-h6">
|
||||
Antrean RSSA
|
||||
</v-toolbar-title>
|
||||
</div>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-list density="default" nav class="py-2">
|
||||
<v-list-subheader v-show="!rail" class="text-caption text-grey">
|
||||
OVERVIEW
|
||||
</v-list-subheader>
|
||||
|
||||
<template v-for="item in items" :key="item.title">
|
||||
<v-menu
|
||||
v-if="item.children"
|
||||
open-on-hover
|
||||
location="end"
|
||||
:disabled="!rail"
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-list-group v-if="!rail" :value="item.title">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-list-item
|
||||
v-bind="props"
|
||||
:prepend-icon="item.icon"
|
||||
:title="item.title"
|
||||
:active="item.title === currentActiveMenu"
|
||||
rounded="lg"
|
||||
class="mx-2 my-1"
|
||||
>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<v-list-item
|
||||
v-for="child in item.children"
|
||||
:key="child.title"
|
||||
:to="child.to"
|
||||
:title="child.title"
|
||||
:active="child.to === currentRoute.path"
|
||||
rounded="lg"
|
||||
class="mx-2 my-1 pl-12"
|
||||
>
|
||||
</v-list-item>
|
||||
</v-list-group>
|
||||
|
||||
<v-list-item
|
||||
v-else
|
||||
v-bind="props"
|
||||
:prepend-icon="item.icon"
|
||||
:active="item.title === currentActiveMenu"
|
||||
rounded="lg"
|
||||
class="mx-2 my-1"
|
||||
>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<v-list class="py-2" style="min-width: 200px;">
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
{{ item.title }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider class="my-2"></v-divider>
|
||||
<v-list-item
|
||||
v-for="child in item.children"
|
||||
:key="child.title"
|
||||
:to="child.to"
|
||||
:title="child.title"
|
||||
:active="child.to === currentRoute.path"
|
||||
rounded="lg"
|
||||
class="mx-2"
|
||||
>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-list-item
|
||||
v-else
|
||||
:prepend-icon="item.icon"
|
||||
:title="!rail ? item.title : ''"
|
||||
:to="item.to"
|
||||
:active="item.to === currentRoute.path"
|
||||
rounded="lg"
|
||||
class="mx-2 my-1"
|
||||
>
|
||||
<template v-slot:append v-if="item.badge && !rail">
|
||||
<v-chip size="x-small" color="primary">{{ item.badge }}</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<v-divider class="my-4"></v-divider>
|
||||
|
||||
<v-list-subheader v-show="!rail" class="text-caption text-grey">
|
||||
ACCOUNT
|
||||
</v-list-subheader>
|
||||
|
||||
<v-list-item
|
||||
prepend-icon="mdi-cog-outline"
|
||||
:title="!rail ? 'Settings' : ''"
|
||||
to="/settings"
|
||||
rounded="lg"
|
||||
class="mx-2 my-1"
|
||||
>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
prepend-icon="mdi-logout"
|
||||
:title="!rail ? 'Log out' : ''"
|
||||
rounded="lg"
|
||||
class="mx-2 my-1"
|
||||
>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<template v-slot:append>
|
||||
<div class="pa-4 d-flex justify-center align-center">
|
||||
<v-icon color="primary" size="20">mdi-white-balance-sunny</v-icon>
|
||||
<v-switch
|
||||
v-show="!rail"
|
||||
v-model="darkMode"
|
||||
hide-details
|
||||
density="compact"
|
||||
color="primary"
|
||||
class="mx-2"
|
||||
></v-switch>
|
||||
<v-icon v-show="!rail" size="20">mdi-moon-waning-crescent</v-icon>
|
||||
</div>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<div class="pa-4">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar color="#ff9248" size="40">
|
||||
<span class="text-white">AS</span>
|
||||
</v-avatar>
|
||||
<div v-show="!rail" class="ml-3 flex-grow-1">
|
||||
<div class="text-subtitle-2 font-weight-bold">Adam Sulfat</div>
|
||||
<div class="text-caption text-grey">[email protected]</div>
|
||||
</div>
|
||||
<v-btn
|
||||
v-show="!rail"
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
>
|
||||
<v-icon size="20">mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-main>
|
||||
<v-container fluid class="pa-6">
|
||||
<slot></slot>
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
||||
<v-footer app class="bg-grey-lighten-4">
|
||||
<v-container fluid class="py-2">
|
||||
<v-row no-gutters align="center">
|
||||
<v-col cols="12" md="6">
|
||||
<span class="text-caption text-grey-darken-2">
|
||||
RSUD Dr. Saiful Anwar Malang | Jl. Jaksa Agung Suprapto No. 2 Malang | Telp: 0341-362101
|
||||
</span>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" class="text-right">
|
||||
<span class="text-caption text-grey-darken-2">
|
||||
ITIKom Antrian RSSA Ver. 0.2
|
||||
</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-footer>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { useRoute } from 'vue-router';
|
||||
// NOTE: Make sure to check if LazyAppBar is still needed,
|
||||
// as its original purpose might have been to handle the rail toggle.
|
||||
import AppBar from "../components/AppBar.vue";
|
||||
|
||||
// Use auth composable
|
||||
// Assuming useAuth() is defined elsewhere
|
||||
// const { user, isAuthenticated, isLoading, checkAuth, logout } = useAuth()
|
||||
// console.log(user)
|
||||
|
||||
// State
|
||||
const drawer = ref(true);
|
||||
const rail = ref(false);
|
||||
const darkMode = ref(false);
|
||||
|
||||
// Menu Items
|
||||
const items = ref([
|
||||
{ title: "Dashboard", icon: "mdi-view-dashboard-outline", to: "/dashboard" },
|
||||
{
|
||||
title: "Setting",
|
||||
icon: "mdi-cog-outline",
|
||||
children: [
|
||||
{ title: "Hak Akses", to: "/setting/hak-akses" },
|
||||
{ title: "User Login", to: "/setting/user-login" },
|
||||
{ title: "Master Loket", to: "/setting/master-loket" },
|
||||
{ title: "Master Klinik", to: "/setting/master-klinik" },
|
||||
{ title: "Master Klinik Ruang", to: "/setting/master-klinik-ruang" },
|
||||
{ title: "Screen", to: "/setting/screen" },
|
||||
],
|
||||
},
|
||||
{ title: "Loket Admin", icon: "mdi-account-supervisor-outline", to: "/loket-admin" },
|
||||
{ title: "Ranap Admin", icon: "mdi-bed-outline", to: "/ranap-admin" },
|
||||
{
|
||||
title: "Anjungan",
|
||||
icon: "mdi-account-box-multiple-outline",
|
||||
children: [
|
||||
{ title: "Anjungan", to: "/anjungan/anjungan" },
|
||||
{ title: "Klinik Ruang", to: "/anjungan/AntrianKlinik"},
|
||||
],
|
||||
},
|
||||
{ title: "Data Pasien", icon: "mdi-account-multiple-outline", to: "/data-pasien" },
|
||||
]);
|
||||
|
||||
const currentRoute = useRoute();
|
||||
const currentActiveMenu = computed(() => {
|
||||
const currentItem = items.value.find(item => item.to === currentRoute.path);
|
||||
if (currentItem) {
|
||||
return currentItem.title;
|
||||
}
|
||||
|
||||
for (const item of items.value) {
|
||||
if (item.children) {
|
||||
const childItem = item.children.find(child => child.to === currentRoute.path);
|
||||
if (childItem) {
|
||||
return item.title;
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Added cursor style for the new clickable header */
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.v-list-item--active {
|
||||
background-color: #e8f5e9 !important;
|
||||
color: #2e7d32 !important;
|
||||
}
|
||||
|
||||
.v-list-item--active :deep(.v-list-item__prepend) {
|
||||
color: #2e7d32 !important;
|
||||
}
|
||||
|
||||
.v-list-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.v-list-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.v-navigation-drawer {
|
||||
border-right: 1px solid #e0e0e0 !important;
|
||||
}
|
||||
</style>
|
||||
+25
-27
@@ -1,37 +1,35 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<Sidebar />
|
||||
<v-app id="inspire">
|
||||
<SideBar
|
||||
:items="navItemsStore.navItems"
|
||||
v-model:drawer="drawer"
|
||||
:rail="rail"
|
||||
@toggle-rail="rail = !rail"
|
||||
/>
|
||||
|
||||
<v-main>
|
||||
<v-container fluid class="bg-grey-lighten-4 pa-6">
|
||||
<slot></slot>
|
||||
</v-container>
|
||||
<v-main app>
|
||||
<slot />
|
||||
</v-main>
|
||||
|
||||
<v-footer app class="bg-grey-lighten-4">
|
||||
<v-container fluid class="py-2">
|
||||
<v-row no-gutters align="center">
|
||||
<v-col cols="12" md="6">
|
||||
<span class="text-caption text-grey-darken-2">
|
||||
RSUD Dr. Saiful Anwar Malang | Jl. Jaksa Agung Suprapto No. 2
|
||||
Malang | Telp: 0341-362101
|
||||
</span>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" class="text-right">
|
||||
<span class="text-caption text-grey-darken-2">
|
||||
ITIKom Antrian RSSA Ver. 0.2
|
||||
</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-footer>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Sidebar from "@/components/Sidebar.vue";
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import SideBar from "../components/SideBar.vue";
|
||||
// Ensure this path matches your store location
|
||||
import { useNavItemsStore } from '@/stores/navItems';
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth'
|
||||
})
|
||||
|
||||
// State for controlling the sidebar
|
||||
const drawer = ref(true);
|
||||
const rail = ref(true);
|
||||
|
||||
const navItemsStore = useNavItemsStore();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
/* Global styles for layout */
|
||||
</style>
|
||||
@@ -1,37 +0,0 @@
|
||||
<!-- layouts/default.vue -->
|
||||
<template>
|
||||
<v-app id="inspire">
|
||||
<AppBar @toggle-rail="rail = !rail" />
|
||||
<SideBar :items="navItemsStore.navItems" v-model:drawer="drawer" :rail="rail" />
|
||||
|
||||
<v-main app>
|
||||
<slot />
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watchEffect } from "vue";
|
||||
import AppBar from "../components/AppBar.vue";
|
||||
import SideBar from "../components/SideBar.vue";
|
||||
import { useNavItemsStore } from '@/stores/navItems'; // Import the new store
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth'
|
||||
})
|
||||
|
||||
const drawer = ref(true);
|
||||
const rail = ref(true);
|
||||
|
||||
const navItemsStore = useNavItemsStore();
|
||||
|
||||
// Your logic to check user access and filter the menu can go here
|
||||
// For example:
|
||||
// const filteredItems = computed(() => {
|
||||
// return navItemsStore.navItems.filter(item => userHasAccess(item.path));
|
||||
// });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Global styles for layout */
|
||||
</style>
|
||||
@@ -1,48 +1,3 @@
|
||||
// export default defineNuxtRouteMiddleware(async (to) => {
|
||||
// console.log('🛡️ Auth middleware triggered for:', to.path)
|
||||
|
||||
// // Skip middleware on server-side during build/generation
|
||||
// if (process.server && process.env.NODE_ENV === 'development') {
|
||||
// console.log('⏭️ Skipping auth check on server-side during development')
|
||||
// return
|
||||
// }
|
||||
|
||||
// // Allow the login page to handle its own checks
|
||||
// if (to.path === '/LoginPage') {
|
||||
// console.log('⏭️ Allowing access to LoginPage')
|
||||
// return
|
||||
// }
|
||||
|
||||
// // This is the crucial change: check for the authentication signal
|
||||
// const isAuthRedirect = to.query.authenticated === 'true';
|
||||
|
||||
// // If this is a redirect from a successful login, we need to let the route load
|
||||
// if (isAuthRedirect) {
|
||||
// console.log('⏳ Client-side is processing a new login session, allowing the route to load...');
|
||||
// // We navigate to a clean URL to remove the query parameter
|
||||
// return navigateTo({ path: to.path, query: {} }, { replace: true });
|
||||
// }
|
||||
|
||||
// try {
|
||||
// console.log('🔍 Checking authentication status...')
|
||||
|
||||
// const session = await $fetch<{ user: any } | null>('/api/auth/session').catch(() => null)
|
||||
|
||||
// if (session && session.user) {
|
||||
// console.log('✅ User is authenticated:', session.user.name || session.user.email)
|
||||
// return
|
||||
// } else {
|
||||
// console.log('❌ No valid session found, redirecting to login')
|
||||
// return navigateTo('/LoginPage')
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('❌ Auth middleware error:', error)
|
||||
// console.log('🔄 Redirecting to login due to error')
|
||||
// return navigateTo('/LoginPage')
|
||||
// }
|
||||
// })
|
||||
|
||||
|
||||
import { defineNuxtRouteMiddleware, navigateTo } from '#app';
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@ export default defineNuxtConfig({
|
||||
'@nuxt/test-utils',
|
||||
'@nuxt/ui',
|
||||
'@pinia/nuxt',
|
||||
'nuxt-qrcode',
|
||||
// Remove '@sidebase/nuxt-auth' completely
|
||||
(_options, nuxt) => {
|
||||
nuxt.hooks.hook('vite:extendConfig', (config) => {
|
||||
@@ -33,7 +34,7 @@ export default defineNuxtConfig({
|
||||
keycloakClientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
|
||||
keycloakIssuer: process.env.KEYCLOAK_ISSUER,
|
||||
public: {
|
||||
authUrl: process.env.AUTH_ORIGIN || 'http://localhost:3001' || 'http://localhost:3000'
|
||||
authUrl: process.env.AUTH_ORIGIN || 'http://localhost:3001'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Generated
+2212
-3874
File diff suppressed because it is too large
Load diff
@@ -24,7 +24,9 @@
|
||||
"dayjs": "^1.11.18",
|
||||
"eslint": "^9.32.0",
|
||||
"nuxt": "^3.17.7",
|
||||
"nuxt-qrcode": "^0.4.8",
|
||||
"pinia": "^3.0.3",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vue": "^3.5.18",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-container fluid class="pa-6 bg-grey-lighten-4">
|
||||
<v-container fluid class="pa-6 bg-white-lighten-4">
|
||||
<div class="d-flex justify-space-between align-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-h4 font-weight-bold">Dashboard</h1>
|
||||
@@ -473,4 +473,6 @@ const pieOptions = ref({
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
+109
-506
@@ -7,7 +7,7 @@
|
||||
<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>
|
||||
<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>
|
||||
@@ -92,118 +92,123 @@
|
||||
<v-icon size="114">mdi-bandage</v-icon>
|
||||
</div>
|
||||
|
||||
<!-- Centered White Box with Both Sections -->
|
||||
<v-row class="fill-height align-center justify-center">
|
||||
<!-- Left Content -->
|
||||
<v-col cols="12" md="6" class="text-section">
|
||||
<div class="hero-content">
|
||||
<div class="logo-section mb-6">
|
||||
<img
|
||||
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
|
||||
alt="Logo Rumah Sakit"
|
||||
class="mt-3 hospital-logo"
|
||||
/>
|
||||
<h1 class="hero-title">Sistem Terbaik</h1>
|
||||
<h1 class="hero-title">Untuk Pelayanan</h1>
|
||||
<h1 class="hero-title">Kesehatan</h1>
|
||||
</div>
|
||||
|
||||
<p class="hero-description">
|
||||
Tingkatkan efisiensi layanan rumah sakit dengan sistem antrean RSSA yang canggih dan intuitif.
|
||||
Dirancang dengan kesederhanaan, keamanan, dan kecepatan, memastikan perjalanan
|
||||
Anda ke platform kami semudah mungkin. Mari kita buat inovasi menjadi sederhana!
|
||||
</p>
|
||||
</div>
|
||||
</v-col>
|
||||
<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"
|
||||
class="hospital-logo-large mb-6"
|
||||
/>
|
||||
<h1 class="hero-title-box">Sistem Terbaik</h1>
|
||||
<h1 class="hero-title-box">Untuk Pelayanan</h1>
|
||||
<h1 class="hero-title-box">Kesehatan</h1>
|
||||
</div>
|
||||
|
||||
<p class="hero-description-box">
|
||||
Tingkatkan efisiensi layanan rumah sakit dengan sistem antrean RSSA yang canggih dan intuitif.
|
||||
Dirancang dengan kesederhanaan, keamanan, dan kecepatan, memastikan perjalanan
|
||||
Anda ke platform kami semudah mungkin. Mari kita buat inovasi menjadi sederhana!
|
||||
</p>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<!-- Right Login Card -->
|
||||
<v-col cols="12" md="6" class="d-flex justify-center">
|
||||
<v-card class="login-card white-card rounded-xl pa-8" max-width="450" width="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>
|
||||
<!-- 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>
|
||||
<img
|
||||
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
|
||||
alt="Logo Rumah Sakit"
|
||||
class="mt-3 hospital-logo"
|
||||
/>
|
||||
</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>
|
||||
<img
|
||||
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
|
||||
alt="Logo Rumah Sakit"
|
||||
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>
|
||||
<!-- Alert Messages -->
|
||||
<v-alert v-if="errorMessage" type="error" class="mb-4" dismissible @click:close="errorMessage = ''">
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
|
||||
<v-alert v-if="successMessage" type="success" class="mb-4">
|
||||
{{ successMessage }}
|
||||
</v-alert>
|
||||
<v-alert v-if="successMessage" type="success" class="mb-4">
|
||||
{{ successMessage }}
|
||||
</v-alert>
|
||||
|
||||
<!-- SSO Info -->
|
||||
<div class="text-center mb-6">
|
||||
<p class="sso-text-dark">Login menggunakan Single Sign-On</p>
|
||||
</div>
|
||||
<!-- 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"
|
||||
block
|
||||
rounded="lg"
|
||||
size="large"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<v-icon left>mdi-shield-key</v-icon>
|
||||
<span class="font-weight-bold">
|
||||
{{ isLoading ? 'Connecting to Keycloak...' : 'Login dengan Keycloak' }}
|
||||
</span>
|
||||
<v-icon right>mdi-arrow-right</v-icon>
|
||||
</v-btn>
|
||||
<!-- Keycloak Login Button -->
|
||||
<v-btn
|
||||
@click="handleLogin"
|
||||
class="login-btn"
|
||||
block
|
||||
rounded="lg"
|
||||
size="large"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<v-icon left>mdi-shield-key</v-icon>
|
||||
<span class="font-weight-bold">
|
||||
{{ isLoading ? 'Connecting to Keycloak...' : 'Login dengan Keycloak' }}
|
||||
</span>
|
||||
<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">
|
||||
<v-btn
|
||||
@click="showRegistrationDialog = true"
|
||||
class="register-btn-dark"
|
||||
variant="outlined"
|
||||
block
|
||||
rounded="lg"
|
||||
size="large"
|
||||
>
|
||||
<v-icon left>mdi-account-plus</v-icon>
|
||||
<span class="font-weight-bold">Daftar Akun Baru</span>
|
||||
</v-btn>
|
||||
<!-- Registration Section -->
|
||||
<v-divider class="my-6 custom-divider-dark"></v-divider>
|
||||
|
||||
<div class="text-center">
|
||||
<v-btn
|
||||
@click="showRegistrationDialog = true"
|
||||
class="register-btn-dark"
|
||||
variant="outlined"
|
||||
block
|
||||
rounded="lg"
|
||||
size="large"
|
||||
>
|
||||
<v-icon left>mdi-account-plus</v-icon>
|
||||
<span class="font-weight-bold">Daftar Akun Baru</span>
|
||||
</v-btn>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<span class="help-text-dark">
|
||||
Belum memiliki akun?
|
||||
</span>
|
||||
<br>
|
||||
<v-btn
|
||||
@click="showAdminContact = true"
|
||||
variant="text"
|
||||
color="#0053AD"
|
||||
size="small"
|
||||
class="contact-link-dark mt-1"
|
||||
>
|
||||
Hubungi Administrator
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Help -->
|
||||
<div class="text-center mt-4">
|
||||
<span class="help-link-dark">Masalah dengan kata sandi Anda?</span>
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
<span class="help-text-dark">
|
||||
Belum memiliki akun?
|
||||
</span>
|
||||
<br>
|
||||
<v-btn
|
||||
@click="showAdminContact = true"
|
||||
variant="text"
|
||||
color="#0053AD"
|
||||
size="small"
|
||||
class="contact-link-dark mt-1"
|
||||
>
|
||||
Hubungi Administrator
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Help -->
|
||||
<div class="text-center mt-4">
|
||||
<span class="help-link-dark">Masalah dengan kata sandi Anda?</span>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -463,406 +468,4 @@ Jam Operasional: Senin - Jumat, 08:00 - 16:00
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Main Background */
|
||||
.login-background {
|
||||
background: linear-gradient(135deg, #f1b464 0%, #faa22e 25%, #e49458 50%, #e46f30 75%, #e26450 100%);
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Navigation Bar */
|
||||
.navbar {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: white !important;
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Floating Medical Icons */
|
||||
.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;
|
||||
}
|
||||
|
||||
.floating-medical-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.floating-medical-icon .v-icon {
|
||||
color: rgba(255, 255, 255, 0.15) !important;
|
||||
}
|
||||
|
||||
.icon-1 {
|
||||
top: 0%;
|
||||
left: 0%;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 25s;
|
||||
animation-name: dvdBounce1;
|
||||
}
|
||||
|
||||
.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); }
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.text-section {
|
||||
padding-left: 4rem;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
color: white;
|
||||
font-size: 3rem;
|
||||
font-weight: 900;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
margin-top: 2rem;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Login Card - White Background */
|
||||
.login-card {
|
||||
z-index: 3;
|
||||
max-width: 450px;
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
.white-card {
|
||||
background: white !important;
|
||||
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);
|
||||
}
|
||||
|
||||
/* White Dialog Styles */
|
||||
.white-dialog {
|
||||
background: white !important;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Card Header - Dark Text */
|
||||
.welcome-title-dark {
|
||||
color: #37474F;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.login-instruction-dark {
|
||||
color: #546E7A;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* App Title - Dark */
|
||||
.app-title-dark {
|
||||
color: #0053AD;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.hospital-logo {
|
||||
height: 64px;
|
||||
width: auto;
|
||||
filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.sso-text-dark {
|
||||
color: #546E7A;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.login-btn {
|
||||
background: linear-gradient(135deg, #0053AD 0%, #0663C7 50%, #0671E0 100%) !important;
|
||||
color: white !important;
|
||||
border: none;
|
||||
box-shadow:
|
||||
0 8px 25px rgba(0, 83, 173, 0.3),
|
||||
0 4px 12px rgba(0, 83, 173, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
text-transform: none;
|
||||
font-size: 1rem;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.register-btn-dark {
|
||||
color: #0053AD !important;
|
||||
border: 2px solid #0053AD !important;
|
||||
background: transparent !important;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: none;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.register-btn-dark:hover {
|
||||
background: rgba(0, 83, 173, 0.05) !important;
|
||||
border-color: #0663C7 !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Custom Divider - Dark */
|
||||
.custom-divider-dark {
|
||||
border-color: rgba(0, 0, 0, 0.12) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Help Text and Links - Dark */
|
||||
.help-text-dark {
|
||||
color: #546E7A;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.contact-link-dark {
|
||||
color: #0053AD !important;
|
||||
text-decoration: underline;
|
||||
text-transform: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.help-link-dark {
|
||||
color: #78909C;
|
||||
font-size: 0.85rem;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.help-link-dark:hover {
|
||||
color: #0053AD;
|
||||
}
|
||||
|
||||
/* Transparent Background */
|
||||
.transparent {
|
||||
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);
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
color: #FF9B1B;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 8px;
|
||||
margin: 4px 8px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 960px) {
|
||||
.text-section {
|
||||
padding-left: 2rem;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.hero-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.text-section {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped src="~/assets/styles/loginpage/login.css"></style>
|
||||
@@ -0,0 +1,347 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-card
|
||||
class="elevation-8 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">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</v-container>
|
||||
</v-card>
|
||||
|
||||
<v-main class="bg-white">
|
||||
<v-container fluid class="pa-6 pt-12 pb-12">
|
||||
|
||||
<v-card class="pa-0 mb-6 elevation-4 rounded-lg">
|
||||
|
||||
<v-row class="ma-0 pa-4 align-center bg-white border-b-sm">
|
||||
<v-col cols="12" sm="6" class="py-1">
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
density="comfortable"
|
||||
label="Pencarian Pasien..."
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
single-line
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="py-1">
|
||||
<v-tabs
|
||||
v-model="filterStatus"
|
||||
color="orange-darken-1"
|
||||
align-tabs="end"
|
||||
class="pt-0"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</v-tabs>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<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' }"
|
||||
>
|
||||
<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' }}
|
||||
</v-icon>
|
||||
{{ 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-col>
|
||||
<v-col cols="12" md="4" class="py-0 text-grey-darken-1">
|
||||
Alamat: {{ patient.alamat }}
|
||||
</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>
|
||||
</v-row>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-btn
|
||||
v-if="patient.status === 'Belum Terverifikasi'"
|
||||
color="orange-darken-1"
|
||||
size="large"
|
||||
@click="openDaftarModal(patient)"
|
||||
prepend-icon="mdi-account-plus"
|
||||
variant="flat"
|
||||
class="font-weight-bold elevation-3"
|
||||
>
|
||||
DAFTARKAN
|
||||
</v-btn>
|
||||
<v-chip
|
||||
v-else
|
||||
color="blue"
|
||||
label
|
||||
size="large"
|
||||
variant="tonal"
|
||||
class="font-weight-bold"
|
||||
>
|
||||
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">
|
||||
{{ noDataText }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</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-card-text>
|
||||
<v-text-field
|
||||
v-model="tempTelepon"
|
||||
:disabled="qrCodeGenerated"
|
||||
label="No. Telepon Pasien"
|
||||
placeholder="08xxxxxxxxxx"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:color="qrCodeGenerated ? 'grey' : 'orange-darken-1'"
|
||||
:rules="[v => v.length >= 8 || 'Min. 8 digit']"
|
||||
></v-text-field>
|
||||
|
||||
<v-btn
|
||||
v-if="!qrCodeGenerated"
|
||||
:disabled="!isTeleponValid"
|
||||
color="orange-darken-1"
|
||||
block
|
||||
size="large"
|
||||
class="mt-1 font-weight-bold elevation-3"
|
||||
@click="generateQrCode"
|
||||
>
|
||||
<v-icon left>mdi-qrcode-scan</v-icon>
|
||||
Lanjutkan & Generate QR
|
||||
</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 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>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions v-if="qrCodeGenerated" class="d-flex justify-space-between pt-0">
|
||||
<v-btn
|
||||
color="orange-darken-1"
|
||||
variant="tonal"
|
||||
@click="reloadQr"
|
||||
prepend-icon="mdi-reload"
|
||||
>
|
||||
Reload
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="green-darken-1"
|
||||
variant="elevated"
|
||||
@click="completeVerification"
|
||||
prepend-icon="mdi-check-circle"
|
||||
size="large"
|
||||
class="font-weight-bold elevation-3"
|
||||
>
|
||||
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-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
// PERUBAHAN DI SINI: Import QrcodeVue
|
||||
import QrcodeVue from 'qrcode.vue';
|
||||
|
||||
// --- MOCK DATA SIMRS ---
|
||||
const patients = ref([
|
||||
{ nama: 'Budi Santoso', rm: '00123456', alamat: 'Jl. Melati No. 10', telepon: '', status: 'Belum Terverifikasi' },
|
||||
{ nama: 'Siti Aminah', rm: '00123457', alamat: 'Perum. Indah Blok A', telepon: '081234567890', status: 'Terverifikasi' },
|
||||
{ nama: 'Joko Susilo', rm: '00123458', alamat: 'Jl. Pahlawan 5', telepon: '', status: 'Belum Terverifikasi' },
|
||||
{ nama: 'Dewi Lestari', rm: '00123459', alamat: 'Gang Mawar 12', telepon: '089876543210', status: 'Terverifikasi' },
|
||||
{ nama: 'Rina Wijaya', rm: '00123460', alamat: 'Jl. Sudirman 21', telepon: '', status: 'Belum Terverifikasi' },
|
||||
]);
|
||||
|
||||
// --- STATE DAN FILTERING ---
|
||||
const filterStatus = ref(0);
|
||||
const search = ref('');
|
||||
const noDataText = computed(() => search.value ? 'Tidak ada data pasien yang cocok dengan pencarian.' : 'Tidak ada pasien dengan status ini.');
|
||||
|
||||
const unverifiedPatients = computed(() =>
|
||||
patients.value.filter((p) => p.status === 'Belum Terverifikasi')
|
||||
);
|
||||
const verifiedPatients = computed(() =>
|
||||
patients.value.filter((p) => p.status === 'Terverifikasi')
|
||||
);
|
||||
|
||||
const filteredPatients = computed(() => {
|
||||
let statusFiltered = patients.value;
|
||||
|
||||
if (filterStatus.value === 1) { // Belum Terverifikasi
|
||||
statusFiltered = unverifiedPatients.value;
|
||||
} else if (filterStatus.value === 2) { // Terverifikasi
|
||||
statusFiltered = verifiedPatients.value;
|
||||
}
|
||||
|
||||
if (!search.value) return statusFiltered;
|
||||
|
||||
const searchTerm = search.value.toLowerCase();
|
||||
return statusFiltered.filter(p =>
|
||||
p.nama.toLowerCase().includes(searchTerm) ||
|
||||
p.rm.includes(searchTerm)
|
||||
);
|
||||
});
|
||||
|
||||
// --- STATE MODAL QR CODE ---
|
||||
const isModalOpen = ref(false);
|
||||
const selectedPatient = ref({});
|
||||
const tempTelepon = ref('');
|
||||
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({
|
||||
rm: selectedPatient.value.rm,
|
||||
phone: tempTelepon.value,
|
||||
action: 'verify-account',
|
||||
timestamp: Date.now()
|
||||
})
|
||||
: '';
|
||||
});
|
||||
|
||||
|
||||
// --- LOGIC MODAL ---
|
||||
|
||||
const openDaftarModal = (patient) => {
|
||||
selectedPatient.value = patient;
|
||||
tempTelepon.value = patient.telepon || '';
|
||||
qrCodeGenerated.value = false;
|
||||
isModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
isModalOpen.value = false;
|
||||
selectedPatient.value = {};
|
||||
tempTelepon.value = '';
|
||||
qrCodeGenerated.value = false;
|
||||
};
|
||||
|
||||
const generateQrCode = () => {
|
||||
if (isTeleponValid.value) {
|
||||
console.log(`[API CALL] Generating QR for ${selectedPatient.value.nama}. QR Data: ${qrCodeData.value}`);
|
||||
setTimeout(() => {
|
||||
qrCodeGenerated.value = true;
|
||||
}, 200);
|
||||
}
|
||||
};
|
||||
|
||||
const reloadQr = () => {
|
||||
console.log('[API CALL] Reloading QR Code...');
|
||||
qrCodeGenerated.value = false;
|
||||
setTimeout(() => {
|
||||
qrCodeGenerated.value = true;
|
||||
console.log('QR Code reloaded.');
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const completeVerification = () => {
|
||||
const patientIndex = patients.value.findIndex(
|
||||
(p) => p.rm === selectedPatient.value.rm
|
||||
);
|
||||
|
||||
if (patientIndex !== -1) {
|
||||
patients.value[patientIndex].status = 'Terverifikasi';
|
||||
patients.value[patientIndex].telepon = tempTelepon.value;
|
||||
}
|
||||
|
||||
filterStatus.value = 2;
|
||||
closeModal();
|
||||
};
|
||||
</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;
|
||||
}
|
||||
|
||||
/* Style untuk Header Card (Floating & Rounded) */
|
||||
.header-banner {
|
||||
background: linear-gradient(90deg, #1565C0, #1976D2);
|
||||
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Memastikan elemen list item memiliki efek hover yang halus */
|
||||
.hover\:bg-orange-lighten-5:hover {
|
||||
background-color: var(--v-theme-orange-lighten-5) !important;
|
||||
}
|
||||
|
||||
/* Memastikan container di v-main memiliki padding atas yang cukup */
|
||||
.v-main .v-container {
|
||||
padding-top: 5rem !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;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
// /stores/permissionStore.ts
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const usePermissionStore = defineStore("permission", {
|
||||
state: () => ({
|
||||
data: null as any,
|
||||
}),
|
||||
actions: {
|
||||
async load(path: string) {
|
||||
const parts = path.split("/").filter(Boolean);
|
||||
const group = parts[1] || "";
|
||||
const role = parts.at(-1)?.toLowerCase() || "";
|
||||
const url = `http://10.10.150.131:8080/api/permission?roles=${role}&groups=${group}`;
|
||||
const { data } = await useFetch(url);
|
||||
this.data = data.value;
|
||||
},
|
||||
can(action: string) {
|
||||
return this.data?.[action] === true;
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user