change on login sidebar and dashboard
This commit is contained in:
@@ -110,7 +110,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hospital-logo {
|
.hospital-logo {
|
||||||
height: 64px;
|
height: 232px;
|
||||||
width: auto;
|
width: auto;
|
||||||
filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.1));
|
filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.1));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
:rail="rail"
|
:rail="rail"
|
||||||
permanent
|
permanent
|
||||||
app
|
app
|
||||||
class="bg-white d-flex flex-column" @update:model-value="emit('update:drawer', $event)"
|
class="bg-white"
|
||||||
|
@update:model-value="emit('update:drawer', $event)"
|
||||||
|
|
||||||
@mouseenter="handleMouseEnter"
|
@mouseenter="handleMouseEnter"
|
||||||
@mouseleave="handleMouseLeave"
|
@mouseleave="handleMouseLeave"
|
||||||
@@ -22,7 +23,8 @@
|
|||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-divider></v-divider>
|
<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-list-group
|
||||||
v-if="item.children"
|
v-if="item.children"
|
||||||
@@ -38,74 +40,53 @@
|
|||||||
active-class="bg-orange-lighten-5 text-orange-darken-2 font-weight-bold"
|
active-class="bg-orange-lighten-5 text-orange-darken-2 font-weight-bold"
|
||||||
></v-list-item>
|
></v-list-item>
|
||||||
</template>
|
</template>
|
||||||
=======
|
|
||||||
<v-list
|
|
||||||
density="compact"
|
|
||||||
nav
|
|
||||||
class="mt-2 flex-grow-1" v-model:opened="openedGroups"
|
|
||||||
>
|
|
||||||
<template v-for="item in items" :key="item.name">
|
|
||||||
<v-list-group
|
|
||||||
v-if="item.children"
|
|
||||||
:value="item.name"
|
|
||||||
:disabled="rail"
|
|
||||||
>
|
|
||||||
<template v-slot:activator="{ props: groupProps }">
|
|
||||||
<v-list-item
|
|
||||||
v-bind="groupProps"
|
|
||||||
:prepend-icon="item.icon"
|
|
||||||
:title="item.name"
|
|
||||||
color="orange-darken-2"
|
|
||||||
active-class="bg-orange-lighten-5 text-orange-darken-2 font-weight-bold"
|
|
||||||
></v-list-item>
|
|
||||||
</template>
|
|
||||||
>>>>>>> d7f3240a8226d36d66c5dc319c7a665539d5d8e3
|
|
||||||
|
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-for="child in item.children"
|
v-for="child in item.children"
|
||||||
:key="child.name"
|
:key="child.name"
|
||||||
:to="child.path"
|
:to="child.path"
|
||||||
:title="child.name"
|
:title="child.name"
|
||||||
:prepend-icon="child.icon"
|
:prepend-icon="child.icon"
|
||||||
link
|
link
|
||||||
class="pl-8"
|
class="pl-8"
|
||||||
color="orange-darken-2"
|
color="orange-darken-2"
|
||||||
active-class="bg-orange-lighten-5 text-orange-darken-2 font-weight-bold"
|
active-class="bg-orange-lighten-5 text-orange-darken-2 font-weight-bold"
|
||||||
></v-list-item>
|
></v-list-item>
|
||||||
</v-list-group>
|
</v-list-group>
|
||||||
|
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
v-else
|
v-else
|
||||||
:disabled="!rail"
|
:disabled="!rail"
|
||||||
open-on-hover
|
open-on-hover
|
||||||
location="end"
|
location="end"
|
||||||
:text="item.name"
|
:text="item.name"
|
||||||
>
|
>
|
||||||
<template #activator="{ props: tooltipProps }">
|
<template #activator="{ props: tooltipProps }">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-bind="tooltipProps"
|
v-bind="tooltipProps"
|
||||||
:prepend-icon="item.icon"
|
:prepend-icon="item.icon"
|
||||||
:title="item.name"
|
:title="item.name"
|
||||||
:to="item.path"
|
:to="item.path"
|
||||||
link
|
link
|
||||||
color="orange-darken-2"
|
color="orange-darken-2"
|
||||||
active-class="bg-orange-lighten-5 text-orange-darken-2 font-weight-bold"
|
active-class="bg-orange-lighten-5 text-orange-darken-2 font-weight-bold"
|
||||||
></v-list-item>
|
></v-list-item>
|
||||||
</template>
|
</template>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</v-list>
|
</v-list>
|
||||||
|
|
||||||
|
<v-spacer></v-spacer>
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
|
|
||||||
<v-list v-if="user" nav density="compact" class="pa-2 flex-shrink-0">
|
<v-list v-if="user" nav density="compact" class="pa-2">
|
||||||
<ProfilePopup
|
<ProfilePopup
|
||||||
:user="user"
|
:user="user"
|
||||||
@logout="handleLogout"
|
@logout="handleLogout"
|
||||||
:rail="rail"
|
:rail="rail"
|
||||||
/>
|
/>
|
||||||
</v-list>
|
</v-list>
|
||||||
<v-list v-else nav density="compact" class="flex-shrink-0">
|
<v-list v-else nav density="compact">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
@click="redirectToLogin"
|
@click="redirectToLogin"
|
||||||
prepend-icon="mdi-login"
|
prepend-icon="mdi-login"
|
||||||
@@ -117,11 +98,12 @@
|
|||||||
|
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, defineEmits, onMounted, ref, watch } from 'vue'; // IMPORT WATCH
|
import { defineProps, defineEmits, onMounted, ref, watch } from 'vue';
|
||||||
import { navigateTo } from '#app';
|
import { navigateTo, useRoute } from '#app';
|
||||||
import ProfilePopup from './ProfilePopup.vue';
|
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 { user, logout, checkAuth } = useAuth();
|
||||||
const route = useRoute(); // Access the current route
|
const route = useRoute(); // Access the current route
|
||||||
@@ -151,23 +133,40 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['update:drawer', 'toggle-rail']);
|
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 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) => {
|
watch(() => props.rail, (newRailState) => {
|
||||||
if (newRailState === true) {
|
if (newRailState === true) {
|
||||||
// If the sidebar is collapsing (rail is true), close all groups
|
|
||||||
openedGroups.value = [];
|
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
|
// --- HOVER LOGIC for Rail Expansion ---
|
||||||
const isHovering = ref(false);
|
|
||||||
|
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
if (props.rail) {
|
if (props.rail) {
|
||||||
|
|||||||
@@ -40,10 +40,15 @@ export default defineNuxtConfig({
|
|||||||
keycloakClientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
|
keycloakClientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
|
||||||
keycloakIssuer: process.env.KEYCLOAK_ISSUER,
|
keycloakIssuer: process.env.KEYCLOAK_ISSUER,
|
||||||
public: {
|
public: {
|
||||||
authUrl: process.env.AUTH_ORIGIN || 'http://localhost:3001'
|
authUrl: process.env.AUTH_ORIGIN || 'http://10.10.150.114:3001/' || 'http://localhost:3001'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
devServer: {
|
||||||
|
host: "10.10.150.114",
|
||||||
|
port: 3001,
|
||||||
|
},
|
||||||
|
|
||||||
build: {
|
build: {
|
||||||
transpile: ['vuetify']
|
transpile: ['vuetify']
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -101,14 +101,11 @@
|
|||||||
|
|
||||||
<v-row class="mb-4">
|
<v-row class="mb-4">
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-card class="pa-4 rounded-xl elevation-6">
|
<v-card class="pa-6 rounded-xl elevation-3 card-style chart-card">
|
||||||
<v-card-title class="d-flex justify-space-between align-center">
|
<v-card-title class="d-flex justify-space-between align-center px-0 pt-0 mb-4">
|
||||||
<span class="text-h6 font-weight-bold">Grafik Jumlah Antrean</span>
|
<span class="text-h6 font-weight-bold primary-text">Registration Trend</span>
|
||||||
<v-btn-toggle v-model="queueTimePeriod" mandatory divided variant="outlined" color="primary" class="text-caption">
|
<v-btn-toggle v-model="queueTimePeriod" mandatory variant="outlined" color="#D17A4A" class="text-caption rounded-lg">
|
||||||
<v-btn size="small" value="day">Hari</v-btn>
|
<v-btn size="small" value="day">Day</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-btn-toggle>
|
</v-btn-toggle>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-text class="pa-0">
|
<v-card-text class="pa-0">
|
||||||
@@ -159,9 +156,15 @@
|
|||||||
|
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-card class="pa-4 rounded-xl elevation-6">
|
<v-card class="pa-6 rounded-xl elevation-3 card-style chart-card">
|
||||||
<v-card-title class="text-h6 font-weight-bold">Realtime Ticket Queue</v-card-title>
|
<v-card-title class="text-h6 font-weight-bold primary-text px-0 pt-0 mb-4">
|
||||||
<v-card-text>
|
Realtime Check-in Velocity
|
||||||
|
<v-chip size="small" color="success" class="ml-2">
|
||||||
|
<v-icon start size="small">mdi-circle</v-icon>
|
||||||
|
Live
|
||||||
|
</v-chip>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-0">
|
||||||
<Line
|
<Line
|
||||||
ref="realtimeChart"
|
ref="realtimeChart"
|
||||||
:data="realtimeLineData"
|
:data="realtimeLineData"
|
||||||
@@ -217,18 +220,33 @@ import {
|
|||||||
LineElement,
|
LineElement,
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
|
|
||||||
// Ensure this middleware setup is correct for your framework (e.g., Nuxt)
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware:['auth']
|
middleware:['auth']
|
||||||
})
|
})
|
||||||
|
|
||||||
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, ArcElement, PointElement, LineElement);
|
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 { user, isLoading, checkAuth, logout } = useAuth()
|
||||||
const isLoggingOut = ref(false)
|
// const navigateTo = (path) => { console.log('Navigating to', path); };
|
||||||
|
// Mock user for demo purposes - replace with your actual auth
|
||||||
|
const user = ref({ name: 'Admin User', preferred_username: 'admin' });
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const checkAuth = async () => user.value;
|
||||||
|
const navigateTo = (path) => console.log('Navigate to:', path);
|
||||||
|
// ---------------------------------
|
||||||
|
|
||||||
// Dashboard data
|
// --- 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' },
|
||||||
|
]);
|
||||||
|
// --------------------
|
||||||
|
|
||||||
|
// Dashboard state
|
||||||
const currentDate = ref('');
|
const currentDate = ref('');
|
||||||
const activeYear = ref('2025');
|
const activeYear = ref('2025');
|
||||||
const queueTimePeriod = ref('day');
|
const queueTimePeriod = ref('day');
|
||||||
@@ -243,74 +261,69 @@ const mockQueueData = ref([
|
|||||||
{ date: '2025-10-18', registrants: 435, attendees: 380 },
|
{ date: '2025-10-18', registrants: 435, attendees: 380 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// --- REFACTORED REALTIME LOGIC ---
|
// --- REALTIME CHART LOGIC - FIXED ---
|
||||||
const realtimeChart = ref(null); // Ref untuk mengakses komponen Line
|
const realtimeChart = ref(null);
|
||||||
const maxDataPoints = 10;
|
const maxDataPoints = 10;
|
||||||
let realtimeInterval = null;
|
let realtimeInterval = null;
|
||||||
|
|
||||||
// Initial data structure (non-reactive for update function)
|
const realtimeLineData = ref({
|
||||||
const initialLineData = {
|
labels: Array.from({ length: maxDataPoints }, (_, i) =>
|
||||||
labels: Array.from({ length: maxDataPoints }, (_, i) =>
|
dayjs().subtract((maxDataPoints - 1 - i) * 30, 'second').format('HH:mm:ss')
|
||||||
dayjs().subtract((maxDataPoints - 1 - i) * 5, 'second').format('HH:mm:ss')
|
),
|
||||||
),
|
datasets: [
|
||||||
datasets: [
|
{
|
||||||
{
|
label: 'Check-ins/min',
|
||||||
label: 'Tickets Processed',
|
backgroundColor: 'rgba(74, 95, 122, 0.15)',
|
||||||
backgroundColor: '#FF5722',
|
borderColor: '#5FB7FF',
|
||||||
borderColor: '#FF5722',
|
data: Array.from({ length: maxDataPoints }, () => Math.floor(Math.random() * 20) + 40),
|
||||||
data: Array.from({ length: maxDataPoints }, () => Math.floor(Math.random() * 50) + 100),
|
fill: true,
|
||||||
fill: false,
|
tension: 0.3,
|
||||||
tension: 0.1,
|
borderWidth: 3,
|
||||||
},
|
pointRadius: 4,
|
||||||
],
|
pointHoverRadius: 6,
|
||||||
};
|
pointBackgroundColor: '#4A5F7A',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
const lineOptions = ref({
|
const lineOptions = ref({
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
animation: {
|
animation: { duration: 300 },
|
||||||
duration: 0 // Crucial: Disable animation for smooth realtime scrolling
|
plugins: {
|
||||||
|
legend: { display: true },
|
||||||
|
title: { display: false }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
suggestedMax: 80,
|
||||||
|
title: { display: true, text: 'Check-ins per Minute' },
|
||||||
|
grid: { color: 'rgba(0, 0, 0, 0.05)' }
|
||||||
},
|
},
|
||||||
plugins: {
|
x: {
|
||||||
legend: { display: true },
|
title: { display: true, text: 'Time (HH:MM:SS)' },
|
||||||
title: { display: false }
|
grid: { display: false }
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
beginAtZero: true,
|
|
||||||
suggestedMax: 200,
|
|
||||||
title: { display: true, text: 'Count' },
|
|
||||||
grid: { display: true }
|
|
||||||
},
|
|
||||||
x: {
|
|
||||||
title: { display: true, text: 'Time (HH:MM:SS)' },
|
|
||||||
grid: { display: false }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateRealtimeData = () => {
|
const updateRealtimeData = () => {
|
||||||
const chart = realtimeChart.value?.chart;
|
const data = realtimeLineData.value.datasets[0].data;
|
||||||
if (!chart) return;
|
const labels = realtimeLineData.value.labels;
|
||||||
|
|
||||||
// 1. Get the current data arrays
|
// Remove first data point
|
||||||
const dataArray = chart.data.datasets[0].data;
|
data.shift();
|
||||||
const labelArray = chart.data.labels;
|
labels.shift();
|
||||||
|
|
||||||
// 2. Shift (remove) the oldest data point and label
|
// Add new data point
|
||||||
dataArray.shift();
|
const newDataPoint = Math.floor(Math.random() * 20) + 40;
|
||||||
labelArray.shift();
|
const newTimeLabel = dayjs().format('HH:mm:ss');
|
||||||
|
|
||||||
// 3. Generate new data point and time label
|
data.push(newDataPoint);
|
||||||
const newDataPoint = Math.floor(Math.random() * 50) + 100;
|
labels.push(newTimeLabel);
|
||||||
const newTimeLabel = dayjs().format('HH:mm:ss');
|
|
||||||
|
console.log('Realtime chart updated:', newTimeLabel, newDataPoint);
|
||||||
// 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 REALTIME CHART LOGIC ---
|
// --- END REALTIME CHART LOGIC ---
|
||||||
|
|
||||||
@@ -318,13 +331,13 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
const sessionUser = await checkAuth()
|
const sessionUser = await checkAuth()
|
||||||
if (sessionUser) {
|
if (sessionUser) {
|
||||||
// Set current date
|
// Set date with Indonesian locale
|
||||||
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
currentDate.value = dayjs().format('dddd, DD MMMM YYYY');
|
||||||
currentDate.value = new Date().toLocaleDateString('id-ID', options);
|
console.log('Current date set:', currentDate.value);
|
||||||
|
|
||||||
// Start realtime data update interval
|
// Start realtime updates every 5 seconds
|
||||||
// Pastikan chart instance sudah siap sebelum memulai interval
|
realtimeInterval = setInterval(updateRealtimeData, 5000);
|
||||||
realtimeInterval = setInterval(updateRealtimeData, 5000);
|
console.log('Realtime interval started');
|
||||||
} else {
|
} else {
|
||||||
await navigateTo('/LoginPage');
|
await navigateTo('/LoginPage');
|
||||||
}
|
}
|
||||||
@@ -337,25 +350,10 @@ onMounted(async () => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (realtimeInterval) {
|
if (realtimeInterval) {
|
||||||
clearInterval(realtimeInterval);
|
clearInterval(realtimeInterval);
|
||||||
|
console.log('Realtime interval cleared');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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) => {
|
const changeYear = (year) => {
|
||||||
activeYear.value = year;
|
activeYear.value = year;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user