update loket dan monitoring
This commit is contained in:
No files matched your search
@@ -1,19 +1,30 @@
|
||||
<template>
|
||||
<v-card
|
||||
class="pa-4 rounded-lg elevation-2 d-flex flex-column ticket-card"
|
||||
class="rounded-lg elevation-2 d-flex flex-column ticket-card"
|
||||
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>
|
||||
<!-- Header dengan background biru -->
|
||||
<v-card-title class="ticket-header">
|
||||
<div class="ticket-header-content">
|
||||
<v-icon color="white" class="mr-2">mdi-hospital-box-outline</v-icon>
|
||||
<span class="ticket-header-title">{{ cleanTitle }}</span>
|
||||
<v-chip
|
||||
v-if="extractedQueueNumber"
|
||||
size="small"
|
||||
class="ml-2 ticket-header-chip"
|
||||
color="primary-700"
|
||||
>
|
||||
Antrean: {{ extractedQueueNumber }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider class="mb-4"></v-divider>
|
||||
<!-- Content -->
|
||||
<v-card-text class="ticket-content pa-4">
|
||||
|
||||
<div class="timeline-scroll-container flex-grow-1">
|
||||
<v-timeline density="compact" align="start" line-inset="12" line-color="#FFB95F">
|
||||
<v-timeline density="compact" align="start" line-inset="12" line-color="#567EE7">
|
||||
<v-timeline-item
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
@@ -24,12 +35,12 @@
|
||||
>
|
||||
<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'"
|
||||
class="font-weight-bold"
|
||||
:class="getStepTextClass(index)"
|
||||
>
|
||||
{{ step.label }}
|
||||
</span>
|
||||
<span class="text-caption text-grey-darken-1">
|
||||
<span class="text-caption" :class="getStepDateClass(index)">
|
||||
{{ step.date !== '-' ? `${step.date}, ${step.time}` : 'Menunggu' }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -42,13 +53,14 @@
|
||||
<v-btn
|
||||
size="large"
|
||||
variant="tonal"
|
||||
color="#FFA532"
|
||||
class="mt-auto font-weight-bold reprint-button"
|
||||
color="#567EE7"
|
||||
class="mt-auto font-weight-bold reprint-button text-secondary-500"
|
||||
prepend-icon="mdi-printer"
|
||||
block
|
||||
>
|
||||
Cetak Ulang Tiket
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
@@ -59,13 +71,30 @@ const props = defineProps({
|
||||
title: String,
|
||||
steps: Array,
|
||||
color: String,
|
||||
currentStepLabel: String,
|
||||
currentStepLabel: String,
|
||||
queueNumber: String, // Nomor antrean untuk ditampilkan di chip
|
||||
});
|
||||
|
||||
const currentStepIndex = computed(() => {
|
||||
return props.steps.findIndex(step => step.label === props.currentStepLabel);
|
||||
});
|
||||
|
||||
// Extract nomor antrean dari title jika queueNumber tidak disediakan
|
||||
const extractedQueueNumber = computed(() => {
|
||||
if (props.queueNumber) {
|
||||
return props.queueNumber;
|
||||
}
|
||||
// Extract dari title format: "JIWA (Antrean: 1)" atau "RADIOLOGI (Antrean: 5)"
|
||||
const match = props.title.match(/\(Antrean:\s*(\d+)\)/);
|
||||
return match ? match[1] : null;
|
||||
});
|
||||
|
||||
// Clean title tanpa nomor antrean untuk ditampilkan di header
|
||||
const cleanTitle = computed(() => {
|
||||
// Hapus bagian "(Antrean: X)" dari title
|
||||
return props.title.replace(/\s*\(Antrean:\s*\d+\)/g, '').trim();
|
||||
});
|
||||
|
||||
const getStepIcon = (index) => {
|
||||
if (index < currentStepIndex.value) {
|
||||
return 'mdi-check';
|
||||
@@ -77,16 +106,39 @@ const getStepIcon = (index) => {
|
||||
};
|
||||
|
||||
const getStepColor = (index) => {
|
||||
// Warna berbeda berdasarkan status step (dari _colors.scss)
|
||||
if (index < currentStepIndex.value) {
|
||||
return 'success';
|
||||
return '#33A484'; // success-600 - hijau untuk completed
|
||||
} else if (index === currentStepIndex.value) {
|
||||
return 'orange-lighten-1';
|
||||
return '#FF8441'; // secondary-500 - orange untuk current
|
||||
}
|
||||
return 'grey-lighten-1';
|
||||
return '#CDD4DC'; // neutral-500 - abu-abu untuk pending
|
||||
};
|
||||
|
||||
const getStepTextClass = (index) => {
|
||||
// Warna text label sesuai dengan warna dot
|
||||
if (index < currentStepIndex.value) {
|
||||
return 'text-success-600'; // hijau untuk completed
|
||||
} else if (index === currentStepIndex.value) {
|
||||
return 'text-secondary-500'; // orange untuk current
|
||||
}
|
||||
return 'text-neutral-600'; // abu-abu untuk pending
|
||||
};
|
||||
|
||||
const getStepDateClass = (index) => {
|
||||
// Warna text date/time lebih terang
|
||||
if (index < currentStepIndex.value) {
|
||||
return 'text-success-400'; // hijau terang untuk completed
|
||||
} else if (index === currentStepIndex.value) {
|
||||
return 'text-secondary-400'; // orange terang untuk current
|
||||
}
|
||||
return 'text-neutral-500'; // abu-abu untuk pending
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
@import '@/assets/scss/_colors.scss';
|
||||
|
||||
/* =============================================== */
|
||||
/* SCROLLABLE TIMELINE STYLES (Menggunakan Tinggi Tetap) */
|
||||
/* =============================================== */
|
||||
@@ -137,6 +189,49 @@ const getStepColor = (index) => {
|
||||
min-height: 550px !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* =============================================== */
|
||||
/* TICKET HEADER */
|
||||
/* =============================================== */
|
||||
.ticket-header {
|
||||
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
|
||||
color: $neutral-100;
|
||||
padding: 16px 20px;
|
||||
box-shadow: 0 2px 8px rgba(58, 97, 201, 0.2);
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.ticket-header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ticket-header-title {
|
||||
flex: 1;
|
||||
color: $neutral-100;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ticket-header-chip {
|
||||
color: $neutral-100 !important;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* =============================================== */
|
||||
/* TICKET CONTENT */
|
||||
/* =============================================== */
|
||||
.ticket-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px !important;
|
||||
}
|
||||
|
||||
/* =============================================== */
|
||||
|
||||
+3
-1
@@ -77,6 +77,8 @@ export default defineNuxtConfig({
|
||||
keycloakClientId: process.env.KEYCLOAK_CLIENT_ID,
|
||||
keycloakClientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
|
||||
keycloakIssuer: process.env.KEYCLOAK_ISSUER,
|
||||
keycloakLogoutUri: process.env.KEYCLOAK_LOGOUT_URI, // Optional: custom logout URI
|
||||
postLogoutRedirectUri: process.env.POST_LOGOUT_REDIRECT_URI, // Optional: custom post-logout redirect URI
|
||||
public: {
|
||||
authUrl: process.env.AUTH_ORIGIN,
|
||||
// authUrl: process.env.AUTH_ORIGIN || "http://10.10.150.175:3001",
|
||||
@@ -134,4 +136,4 @@ export default defineNuxtConfig({
|
||||
noExternal: ["vuetify"],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -35,10 +35,16 @@
|
||||
>
|
||||
<!-- Loket Header -->
|
||||
<div class="loket-header-bar">
|
||||
<div class="loket-name">{{ loketData.nama }}</div>
|
||||
<div class="queue-count">
|
||||
<v-icon size="18" color="white">mdi-account-multiple</v-icon>
|
||||
<span class="count-number">{{ loketData.totalQueues }}</span>
|
||||
<div class="loket-header-content">
|
||||
<span class="loket-name">{{ loketData.nama }}</span>
|
||||
<v-chip
|
||||
size="default"
|
||||
class="ml-2 loket-header-chip"
|
||||
color="primary-700"
|
||||
>
|
||||
<v-icon start size="20" color="white">mdi-account-multiple</v-icon>
|
||||
{{ loketData.totalQueues }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,22 +52,43 @@
|
||||
<div
|
||||
:ref="el => setQueueDisplayRef(loketData.id, el)"
|
||||
class="queue-display"
|
||||
:class="{ 'has-scroll': loketData.queues.length > 20 }"
|
||||
:class="{
|
||||
'has-scroll': loketData.queues.length > 20,
|
||||
'ticker-scroll': loketData.queues.length > 0
|
||||
}"
|
||||
>
|
||||
<!-- Ticket Cards List -->
|
||||
<div v-if="loketData.queues.length > 0" class="ticket-cards-list" :style="ticketGridStyle">
|
||||
<div
|
||||
v-for="(queue, index) in loketData.queues"
|
||||
:key="`${loketData.id}-${queue.no}-${queue.barcode || index}`"
|
||||
class="ticket-card"
|
||||
:class="{ 'ticket-card-fast-track': queue.fastTrack === 'YA' }"
|
||||
<!-- Ticket Cards List - Infinite Ticker -->
|
||||
<div v-if="loketData.queues.length > 0" class="ticket-cards-wrapper">
|
||||
<div
|
||||
class="ticket-cards-list ticker-container"
|
||||
:style="ticketGridStyle"
|
||||
>
|
||||
<!-- Fast Track Badge - Pojok Kanan Atas -->
|
||||
<div v-if="queue.fastTrack === 'YA'" class="ticket-fast-track">
|
||||
<v-icon color="white" size="14">mdi-flash</v-icon>
|
||||
<!-- Original Content -->
|
||||
<div
|
||||
v-for="(queue, index) in loketData.queues"
|
||||
:key="`${loketData.id}-${queue.no}-${queue.barcode || index}`"
|
||||
class="ticket-card"
|
||||
:class="{ 'ticket-card-fast-track': queue.fastTrack === 'YA' }"
|
||||
>
|
||||
<!-- Fast Track Badge - Pojok Kanan Atas -->
|
||||
<div v-if="queue.fastTrack === 'YA'" class="ticket-fast-track">
|
||||
<v-icon color="white" size="14">mdi-flash</v-icon>
|
||||
</div>
|
||||
<div class="ticket-number">{{ queue.noAntrian?.split(" |")[0] || queue.no || '' }}</div>
|
||||
</div>
|
||||
<!-- Duplicated Content for Seamless Infinite Loop -->
|
||||
<div
|
||||
v-for="(queue, index) in loketData.queues"
|
||||
:key="`${loketData.id}-dup-${queue.no}-${queue.barcode || index}`"
|
||||
class="ticket-card"
|
||||
:class="{ 'ticket-card-fast-track': queue.fastTrack === 'YA' }"
|
||||
>
|
||||
<!-- Fast Track Badge - Pojok Kanan Atas -->
|
||||
<div v-if="queue.fastTrack === 'YA'" class="ticket-fast-track">
|
||||
<v-icon color="white" size="14">mdi-flash</v-icon>
|
||||
</div>
|
||||
<div class="ticket-number">{{ queue.noAntrian?.split(" |")[0] || queue.no || '' }}</div>
|
||||
</div>
|
||||
<div class="ticket-number">{{ queue.noAntrian?.split(" |")[0] || queue.no || '' }}</div>
|
||||
<!-- <div v-if="queue.klinik" class="ticket-klinik">{{ queue.klinik }}</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -70,8 +97,9 @@
|
||||
v-else
|
||||
class="empty-queue"
|
||||
>
|
||||
<v-icon size="40" color="grey-lighten-3">mdi-minus-circle-outline</v-icon>
|
||||
<v-icon size="64" color="grey-lighten-2" class="empty-icon">mdi-ticket-outline</v-icon>
|
||||
<div class="empty-text">Tidak Ada Antrian</div>
|
||||
<div class="empty-subtext">Antrian akan muncul di sini</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -376,61 +404,93 @@ const updateTime = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// Auto-scroll setiap 2 detik dengan loop (atas -> bawah -> atas)
|
||||
let scrollIntervals = {}
|
||||
// Ticker scroll dengan smooth continuous animation
|
||||
let tickerAnimations = {}
|
||||
|
||||
const startAutoScroll = () => {
|
||||
// Clear existing intervals
|
||||
Object.values(scrollIntervals).forEach(interval => {
|
||||
if (interval) clearInterval(interval)
|
||||
// Clear existing animations
|
||||
Object.values(tickerAnimations).forEach(animation => {
|
||||
if (animation) {
|
||||
cancelAnimationFrame(animation.frameId)
|
||||
}
|
||||
})
|
||||
scrollIntervals = {}
|
||||
tickerAnimations = {}
|
||||
|
||||
nextTick(() => {
|
||||
displayedLokets.value.forEach(loketData => {
|
||||
const container = queueDisplayRefs.value[loketData.id]
|
||||
if (container && loketData.queues.length > 0) {
|
||||
let isScrollingDown = true
|
||||
let currentScrollTop = 0
|
||||
const maxScroll = container.scrollHeight - container.clientHeight
|
||||
|
||||
// Jika konten tidak cukup untuk di-scroll, skip
|
||||
if (maxScroll <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
scrollIntervals[loketData.id] = setInterval(() => {
|
||||
const maxScroll = container.scrollHeight - container.clientHeight
|
||||
|
||||
// Jika konten tidak cukup untuk di-scroll, skip
|
||||
if (maxScroll <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Hitung ukuran scroll berdasarkan tinggi card
|
||||
// Ambil tinggi card pertama jika ada, atau gunakan default 80px (min-height ticket-card)
|
||||
let scrollAmount = 80 // Default: min-height ticket-card
|
||||
const firstCard = container.querySelector('.ticket-card')
|
||||
if (firstCard) {
|
||||
const cardRect = firstCard.getBoundingClientRect()
|
||||
const cardHeight = cardRect.height
|
||||
// Tambahkan gap (10px dari .ticket-cards-list gap)
|
||||
scrollAmount = cardHeight + 10
|
||||
}
|
||||
|
||||
if (isScrollingDown) {
|
||||
// Scroll ke bawah dengan ukuran card
|
||||
currentScrollTop += scrollAmount
|
||||
if (currentScrollTop >= maxScroll) {
|
||||
// Sudah sampai bawah, kembali ke atas dan lanjut scroll ke bawah
|
||||
currentScrollTop = 0
|
||||
// Tetap isScrollingDown = true agar langsung scroll ke bawah lagi
|
||||
}
|
||||
} else {
|
||||
// Scroll ke atas (tidak digunakan karena langsung reset ke atas)
|
||||
currentScrollTop -= scrollAmount
|
||||
if (currentScrollTop <= 0) {
|
||||
currentScrollTop = 0
|
||||
isScrollingDown = true
|
||||
// Tunggu sebentar untuk memastikan DOM sudah render dengan sempurna
|
||||
setTimeout(() => {
|
||||
// Hitung tinggi konten asli dengan lebih akurat menggunakan scrollHeight
|
||||
// Karena konten di-duplicate, scrollHeight total = 2x contentHeight
|
||||
const totalScrollHeight = container.scrollHeight
|
||||
const contentHeight = totalScrollHeight / 2
|
||||
|
||||
// Pastikan contentHeight valid
|
||||
if (contentHeight <= 0 || contentHeight > maxScroll) {
|
||||
// Fallback: hitung berdasarkan card pertama
|
||||
const firstCard = container.querySelector('.ticket-card')
|
||||
if (firstCard) {
|
||||
const cardHeight = firstCard.offsetHeight
|
||||
const gap = 10
|
||||
const totalCards = loketData.queues.length
|
||||
const gridCols = ticketGridColumns.value || 1
|
||||
const rows = Math.ceil(totalCards / gridCols)
|
||||
const calculatedHeight = (cardHeight + gap) * rows
|
||||
|
||||
if (calculatedHeight > 0) {
|
||||
contentHeight = calculatedHeight
|
||||
} else {
|
||||
return // Skip jika tidak bisa dihitung
|
||||
}
|
||||
} else {
|
||||
return // Skip jika tidak ada card
|
||||
}
|
||||
}
|
||||
|
||||
container.scrollTop = currentScrollTop
|
||||
}, 2000) // Setiap 2 detik
|
||||
let currentScrollTop = 0
|
||||
const scrollSpeed = 0.2 // pixels per frame (smooth ticker speed - diperlambat)
|
||||
|
||||
const animate = () => {
|
||||
currentScrollTop += scrollSpeed
|
||||
|
||||
// Infinite loop: gunakan modulo untuk seamless transition tanpa jitter
|
||||
// Modulo memastikan transisi smooth tanpa jump
|
||||
if (currentScrollTop >= contentHeight) {
|
||||
// Reset dengan modulo untuk seamless loop
|
||||
currentScrollTop = currentScrollTop % contentHeight
|
||||
// Pastikan tidak ada nilai negatif
|
||||
if (currentScrollTop < 0) {
|
||||
currentScrollTop = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Set scrollTop langsung (lebih smooth daripada scrollTo)
|
||||
container.scrollTop = currentScrollTop
|
||||
|
||||
tickerAnimations[loketData.id] = {
|
||||
frameId: requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
|
||||
// Start animation
|
||||
tickerAnimations[loketData.id] = {
|
||||
frameId: requestAnimationFrame(animate)
|
||||
}
|
||||
}, 150) // Delay untuk memastikan DOM dan layout sudah selesai render
|
||||
|
||||
// Start animation
|
||||
tickerAnimations[loketData.id] = {
|
||||
frameId: requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -507,11 +567,13 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
|
||||
// Clear all scroll intervals
|
||||
Object.values(scrollIntervals).forEach(interval => {
|
||||
if (interval) clearInterval(interval)
|
||||
// Clear all ticker animations
|
||||
Object.values(tickerAnimations).forEach(animation => {
|
||||
if (animation && animation.frameId) {
|
||||
cancelAnimationFrame(animation.frameId)
|
||||
}
|
||||
})
|
||||
scrollIntervals = {}
|
||||
tickerAnimations = {}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -684,43 +746,44 @@ onUnmounted(() => {
|
||||
|
||||
/* Loket Header */
|
||||
.loket-header-bar {
|
||||
background: #FFFFFF;
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
|
||||
color: var(--color-neutral-100);
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 3px solid #FF9B1B;
|
||||
box-shadow: 0 2px 8px rgba(58, 97, 201, 0.2);
|
||||
border-radius: 8px 8px 0 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.loket-name {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #212121;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.queue-count {
|
||||
.loket-header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: var(--color-primary-600);
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
min-width: 50px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.queue-count .v-icon {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.count-number {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
font-size: 16px;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loket-name {
|
||||
flex: 1;
|
||||
color: var(--color-neutral-100);
|
||||
font-weight: 700;
|
||||
font-size: 24px;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.loket-header-chip {
|
||||
color: var(--color-neutral-100) !important;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
height: 36px !important;
|
||||
padding: 0 16px !important;
|
||||
min-width: auto !important;
|
||||
}
|
||||
|
||||
.loket-header-chip .v-icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* Queue Display - Vertical Scrollable */
|
||||
@@ -732,22 +795,38 @@ onUnmounted(() => {
|
||||
overflow-x: hidden;
|
||||
background: #FFFFFF;
|
||||
min-height: 0;
|
||||
scroll-behavior: smooth;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
/* Smooth scrolling untuk menghindari jitter */
|
||||
scroll-behavior: auto;
|
||||
/* Hardware acceleration */
|
||||
transform: translateZ(0);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.queue-display.has-scroll {
|
||||
max-height: calc(100vh - 400px);
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
/* Hide scrollbar */
|
||||
.queue-display {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
/* Ticker scroll effect */
|
||||
.queue-display.ticker-scroll {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.queue-display::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari, Opera */
|
||||
.ticket-cards-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ticker-container {
|
||||
will-change: transform;
|
||||
transform: translateZ(0); /* Hardware acceleration */
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.ticket-cards-list {
|
||||
@@ -759,37 +838,47 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.ticket-card {
|
||||
background: #F5F7FA ;
|
||||
border: 1px solid #E0E0E0;
|
||||
background: var(--color-neutral-400, #E1E5EA);
|
||||
border: 1px solid var(--color-neutral-500, #CDD4DC);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
min-height: 80px;
|
||||
position: relative; /* Untuk absolute positioning fast track icon */
|
||||
gap: 8px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
min-height: 90px;
|
||||
position: relative;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ticket-card-fast-track {
|
||||
background: rgba(255, 132, 65, 0.4) !important; /* #FF8441 dengan 40% opacity (66 hex = 102/255) */
|
||||
background: var(--color-secondary-50, rgba(255, 132, 65, 0.15)) !important;
|
||||
border-color: var(--color-secondary-300, rgba(255, 132, 65, 0.4)) !important;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.ticket-card:hover {
|
||||
box-shadow: 0 2px 6px rgba(25, 118, 210, 0.2);
|
||||
border-color: var(--color-primary-600);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(58, 97, 201, 0.15);
|
||||
border-color: var(--color-primary-500, #567EE7);
|
||||
transform: translateY(-2px);
|
||||
background: var(--color-neutral-300, #F5F7FA);
|
||||
}
|
||||
|
||||
.ticket-card-fast-track:hover {
|
||||
background: var(--color-secondary-100, rgba(255, 132, 65, 0.2)) !important;
|
||||
border-color: var(--color-secondary-500, #FF8441) !important;
|
||||
}
|
||||
|
||||
.ticket-number {
|
||||
font-size: 28px;
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
color: #3556AE;
|
||||
letter-spacing: 1px;
|
||||
line-height: 1.2;
|
||||
color: var(--color-primary-600, #3A61C9);
|
||||
letter-spacing: 1.5px;
|
||||
line-height: 1.1;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.ticket-klinik {
|
||||
@@ -802,8 +891,17 @@ onUnmounted(() => {
|
||||
|
||||
.ticket-fast-track {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
background: var(--color-danger-600, #DE473B);
|
||||
border-radius: 50%;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 6px rgba(222, 71, 59, 0.4);
|
||||
z-index: 1;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
@@ -838,21 +936,35 @@ onUnmounted(() => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
padding: 80px 20px;
|
||||
text-align: center;
|
||||
min-height: 300px;
|
||||
color: var(--color-neutral-600, #89939E);
|
||||
}
|
||||
|
||||
.empty-queue .v-icon {
|
||||
color: #BDBDBD !important;
|
||||
margin-bottom: 12px;
|
||||
.empty-icon {
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.6;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.empty-queue:hover .empty-icon {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--color-neutral-700, #717171);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-subtext {
|
||||
font-size: 14px;
|
||||
color: #89939E;
|
||||
margin-top: 8px;
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
color: var(--color-neutral-600, #89939E);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* ========== FOOTER STATS ========== */
|
||||
|
||||
@@ -101,8 +101,8 @@
|
||||
:key="ticket.title"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="4"
|
||||
md="3"
|
||||
lg="3"
|
||||
>
|
||||
<MonitorPasienTicketCard
|
||||
:title="ticket.title"
|
||||
@@ -273,23 +273,7 @@ onMounted(fetchPatientData);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// Colors from Design System
|
||||
$neutral-900: #212121;
|
||||
$neutral-800: #4D4D4D;
|
||||
$neutral-700: #717171;
|
||||
$neutral-600: #89939E;
|
||||
$neutral-500: #ABBED1;
|
||||
$neutral-400: #E5F7FA;
|
||||
$neutral-300: #F5F7FA;
|
||||
$neutral-100: #FFFFFF;
|
||||
|
||||
$success-700: #1B6E53;
|
||||
$success-600: #009262;
|
||||
$success-400: #32C997;
|
||||
$success-300: #84DFC1;
|
||||
$success-200: #F1FBF8;
|
||||
|
||||
$danger-600: #E02B1D;
|
||||
@import '@/assets/scss/_colors.scss';
|
||||
|
||||
// Font Family & Weights
|
||||
$font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
@@ -306,9 +290,9 @@ $font-weight-semibold: 600;
|
||||
// PAGE HEADER
|
||||
// ============================================
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, $success-600 0%, $success-700 100%);
|
||||
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
|
||||
border-radius: 16px 16px 0 0;
|
||||
box-shadow: 0 4px 16px rgba(0, 146, 98, 0.2);
|
||||
box-shadow: 0 4px 16px rgba(58, 97, 201, 0.2);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
@@ -357,7 +341,7 @@ $font-weight-semibold: 600;
|
||||
}
|
||||
|
||||
.stat-chip {
|
||||
color: $success-600 !important;
|
||||
color: $primary-600 !important;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
@@ -369,7 +353,7 @@ $font-weight-semibold: 600;
|
||||
letter-spacing: 0.5px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: $success-600 !important;
|
||||
color: $primary-600 !important;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -392,7 +376,7 @@ $font-weight-semibold: 600;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $success-600;
|
||||
color: $primary-600;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,13 +38,27 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
// Construct the Keycloak logout URL with proper parameters
|
||||
// IMPORTANT: The post_logout_redirect_uri must be registered in Keycloak client settings
|
||||
const logoutUrl = new URL(`${config.keycloakIssuer}/protocol/openid-connect/logout`);
|
||||
// Use custom logout URI from env if provided, otherwise use default
|
||||
const logoutPath = config.keycloakLogoutUri || `${config.keycloakIssuer}/protocol/openid-connect/logout`;
|
||||
const logoutUrl = new URL(logoutPath);
|
||||
|
||||
// Debug: Log the authUrl being used
|
||||
// Debug: Log the authUrl and logout URI being used
|
||||
console.log('🔧 Using authUrl from config:', config.public.authUrl);
|
||||
if (config.keycloakLogoutUri) {
|
||||
console.log('🔧 Using custom logout URI from env:', config.keycloakLogoutUri);
|
||||
} else {
|
||||
console.log('🔧 Using default logout URI:', logoutPath);
|
||||
}
|
||||
|
||||
// Build the redirect URI - must match what's configured in Keycloak
|
||||
const postLogoutRedirectUri = `${config.public.authUrl}/LoginPage?logout=success`;
|
||||
// Use custom post-logout redirect URI from env if provided, otherwise use default
|
||||
const postLogoutRedirectUri = config.postLogoutRedirectUri || `${config.public.authUrl}/LoginPage?logout=success`;
|
||||
|
||||
if (config.postLogoutRedirectUri) {
|
||||
console.log('🔧 Using custom post-logout redirect URI from env:', config.postLogoutRedirectUri);
|
||||
} else {
|
||||
console.log('🔧 Using default post-logout redirect URI:', postLogoutRedirectUri);
|
||||
}
|
||||
|
||||
// Add required parameters for proper Keycloak logout
|
||||
logoutUrl.searchParams.set('client_id', config.keycloakClientId);
|
||||
@@ -77,8 +91,9 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
// Even if there's an error, try to provide a basic logout URL
|
||||
const config = useRuntimeConfig();
|
||||
const postLogoutRedirectUri = `${config.public.authUrl}/LoginPage?logout=success`;
|
||||
const fallbackLogoutUrl = new URL(`${config.keycloakIssuer}/protocol/openid-connect/logout`);
|
||||
const postLogoutRedirectUri = config.postLogoutRedirectUri || `${config.public.authUrl}/LoginPage?logout=success`;
|
||||
const logoutPath = config.keycloakLogoutUri || `${config.keycloakIssuer}/protocol/openid-connect/logout`;
|
||||
const fallbackLogoutUrl = new URL(logoutPath);
|
||||
fallbackLogoutUrl.searchParams.set('client_id', config.keycloakClientId);
|
||||
fallbackLogoutUrl.searchParams.set('post_logout_redirect_uri', postLogoutRedirectUri);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user