perbaikan loket

This commit is contained in:
Fanrouver
2026-01-13 10:57:45 +07:00
parent a000ba0905
commit 0a8e11a28d
5 changed files with 460 additions and 184 deletions

View File

@@ -30,6 +30,10 @@
</div>
<div class="call-number">{{ currentCalledQueue.noAntrian.split(' |')[0] }}</div>
<div class="call-klinik">{{ currentCalledQueue.klinik || 'Klinik' }}</div>
<div v-if="currentMultipleCallsTimer" class="call-timer-main">
<v-icon size="20" color="white">mdi-timer</v-icon>
<span>{{ currentMultipleCallsTimer }}</span>
</div>
</div>
<!-- Multiple Calls Cards (Small) on the right -->
@@ -61,13 +65,14 @@
class="next-ticket-card"
:class="{
'is-multiple-call': ticket.isMultipleCall,
'highlight-called': isCalled(ticket)
'highlight-called': isCalled(ticket),
'is-next-in-queue': idx === 0 && !isCalled(ticket)
}"
>
<div class="next-ticket-number">{{ ticket.noAntrian.split(' |')[0] }}</div>
<div class="next-ticket-klinik">{{ ticket.klinik || 'Klinik' }}</div>
<div v-if="ticket.loket" class="next-ticket-loket">{{ ticket.loket }}</div>
<div v-if="ticket.isMultipleCall && getTimerText(ticket)" class="next-ticket-timer">
<div v-if="getTimerText(ticket)" class="next-ticket-timer">
<v-icon size="14" color="warning">mdi-timer</v-icon>
<span>{{ getTimerText(ticket) }}</span>
</div>
@@ -94,14 +99,18 @@
<!-- Queue Content -->
<div class="klinik-content">
<!-- Current Serving Queue (Large) -->
<div v-if="klinik.currentQueue" class="current-serving-section">
<div class="current-serving-section">
<div class="current-label">SEDANG DILAYANI</div>
<div
v-if="klinik.currentQueue"
class="current-number-large"
:class="{ 'highlight-called': isCalled(klinik.currentQueue) }"
>
{{ klinik.currentQueue.noAntrian.split(' |')[0] }}
</div>
<div v-else class="current-waiting-text">
MENUNGGU PANGGILAN
</div>
</div>
<!-- All Queues Grid (Small) - Shows all queues including current and multiple calls -->
@@ -117,7 +126,9 @@
:class="{
'is-current': queue.no === klinik.currentQueue?.no,
'is-multiple': klinik.multipleCalls?.some(mc => mc.no === queue.no),
'highlight-called': isCalled(queue)
'highlight-called': isCalled(queue),
'is-terlambat': queue.status === 'terlambat',
'is-pending': queue.status === 'pending'
}"
>
{{ queue.noAntrian.split(' |')[0] }}
@@ -207,15 +218,15 @@ const currentProcessingPatient = computed(() => {
return queueStore.currentProcessingPatient?.loket || null
})
// Get all patients with processStage "loket" and filter status "waiting" (sudah dipanggil), "di-loket" (sudah check-in), dan "pending"
// Get all patients with processStage "loket" and filter status "di-loket" (sudah check-in), "terlambat", dan "pending"
const loketPatients = computed(() => {
const allPatients = queueStore.getPatientsByStage('loket').value.all
// Tampilkan antrian dengan status:
// - "waiting" (sudah dipanggil oleh admin loket, sudah muncul di AntrianLoket, bisa check-in)
// - "di-loket" (sudah check-in)
// - "terlambat" (terlambat)
// - "pending" (pending)
// Jangan tampilkan status "menunggu" (belum dipanggil)
return allPatients.filter(p => p.status === 'waiting' || p.status === 'di-loket' || p.status === 'pending')
// Jangan tampilkan status "waiting" atau "menunggu" (belum check-in)
return allPatients.filter(p => p.status === 'di-loket' || p.status === 'terlambat' || p.status === 'pending')
})
// Helper untuk mendapatkan waktu check-in atau waktu dipanggil
@@ -236,6 +247,81 @@ const getCheckInTime = (patient) => {
return new Date(patient.createdAt || 0)
}
// Helper untuk mendapatkan nama klinik dari pasien
// Jika patient.klinik tidak ada atau tidak valid, coba parse dari noAntrian
const getKlinikNameFromPatient = (patient) => {
// Prioritas 1: Parse dari noAntrian (RA001 -> Radioterapi, AN001 -> Anak, dll)
// Ini prioritas tertinggi karena noAntrian adalah sumber kebenaran
if (patient.noAntrian) {
const noAntrianPart = patient.noAntrian.split(' |')[0] // Ambil bagian sebelum " |"
const match = noAntrianPart.match(/^([A-Z]+)/) // Extract prefix (RA, AN, EA, dll)
if (match) {
const kode = match[1]
// Mapping khusus untuk kode yang berbeda (RA -> RT untuk Radioterapi)
// Cek semua kemungkinan kode di clinicStore
let allClinics = []
if (clinicStore.getAllClinics) {
allClinics = typeof clinicStore.getAllClinics === 'function'
? clinicStore.getAllClinics()
: (clinicStore.getAllClinics.value || [])
}
let matchedClinic = null
// Coba match langsung dengan kode
matchedClinic = allClinics.find(c => c.kode === kode)
// Jika tidak match, coba mapping khusus
if (!matchedClinic) {
// Mapping khusus: RA -> RT (Radioterapi)
if (kode === 'RA') {
matchedClinic = allClinics.find(c => c.kode === 'RT' && c.name === 'RADIOTERAPI')
}
}
if (matchedClinic && matchedClinic.name) {
return matchedClinic.name.trim()
}
// Fallback ke masterStore jika clinicStore tidak ada
const klinikData = masterStore.getKlinikByKode ? masterStore.getKlinikByKode(kode) : null
if (klinikData && klinikData.nama) {
return klinikData.nama.trim()
}
// Jika masih tidak match, coba dengan mapping RA -> RT
if (kode === 'RA') {
const rtData = masterStore.getKlinikByKode ? masterStore.getKlinikByKode('RT') : null
if (rtData && rtData.nama) {
return rtData.nama.trim()
}
}
}
}
// Prioritas 2: Gunakan kodeKlinik jika ada
if (patient.kodeKlinik) {
const clinic = clinicStore.getClinicByKode ? clinicStore.getClinicByKode(patient.kodeKlinik) : null
if (clinic && clinic.name) {
return clinic.name.trim()
}
// Fallback ke masterStore
const klinikData = masterStore.getKlinikByKode ? masterStore.getKlinikByKode(patient.kodeKlinik) : null
if (klinikData && klinikData.nama) {
return klinikData.nama.trim()
}
}
// Prioritas 3: Gunakan patient.klinik jika ada dan valid
if (patient.klinik && patient.klinik.trim() !== '') {
return patient.klinik.trim()
}
// Fallback: UMUM
return 'UMUM'
}
// Display clinics with their queues - Group by klinik, filtered by loket pelayanan
const displayedClinics = computed(() => {
// Get pelayanan dari loket yang dipilih
@@ -253,7 +339,8 @@ const displayedClinics = computed(() => {
// Include currentProcessingPatient dalam list jika ada
let allPatientsForDistribution = [...loketPatients.value]
if (currentProcessingPatient.value && (currentProcessingPatient.value.status === 'waiting' || currentProcessingPatient.value.status === 'di-loket' || currentProcessingPatient.value.status === 'pending')) {
// Include currentProcessingPatient jika ada
if (currentProcessingPatient.value) {
const existsInList = allPatientsForDistribution.find(p => p.no === currentProcessingPatient.value.no)
if (!existsInList) {
allPatientsForDistribution.push(currentProcessingPatient.value)
@@ -274,7 +361,9 @@ const displayedClinics = computed(() => {
const clinicsMap = new Map()
allPatientsForDistribution.forEach(patient => {
const klinikName = patient.klinik || 'UMUM'
// Gunakan helper untuk mendapatkan nama klinik yang benar
// Ini akan parse dari noAntrian jika patient.klinik tidak sesuai
const klinikName = getKlinikNameFromPatient(patient)
// Jika ada filter pelayanan, cek apakah klinik ini ada di pelayanan loket
if (shouldFilterByPelayanan) {
@@ -287,17 +376,35 @@ const displayedClinics = computed(() => {
if (!isAllowed) return // Skip jika tidak ada di pelayanan
} else {
// Jika tidak ditemukan di clinicStore, cek menggunakan masterStore
const matchedKode = allowedPelayananCodes.find(kode => {
// Juga cek dari noAntrian jika ada (RA001 -> RA -> Radioterapi)
let matchedKode = allowedPelayananCodes.find(kode => {
const k = masterStore.getKlinikByKode ? masterStore.getKlinikByKode(kode) : null
return k && k.nama === klinikName
})
// Jika belum match, coba parse dari noAntrian
if (!matchedKode && patient.noAntrian) {
const noAntrianPart = patient.noAntrian.split(' |')[0]
const match = noAntrianPart.match(/^([A-Z]+)/)
if (match) {
const kodeFromNoAntrian = match[1]
matchedKode = allowedPelayananCodes.find(kode => kode === kodeFromNoAntrian)
}
}
if (!matchedKode) return // Skip jika tidak match
}
}
// Pastikan pasien memiliki klinik yang valid sebelum di-group
if (!klinikName || klinikName.trim() === '') {
return // Skip jika klinik tidak valid
}
if (!clinicsMap.has(klinikName)) {
clinicsMap.set(klinikName, [])
}
// Pastikan pasien yang di-push memiliki klinik yang sesuai
clinicsMap.get(klinikName).push(patient)
})
@@ -344,14 +451,14 @@ const displayedClinics = computed(() => {
sortedQueues.forEach((queue, index) => {
if (processed.has(queue.no)) return
if (queue.status === 'waiting' || queue.status === 'di-loket') {
if (queue.status === 'di-loket' || queue.status === 'terlambat' || queue.status === 'pending') {
const callTime = queue.lastCalledAt ? new Date(queue.lastCalledAt) : (queue.createdAt ? new Date(queue.createdAt) : null)
if (callTime) {
const group = [queue]
processed.add(queue.no)
sortedQueues.forEach((otherQueue, otherIndex) => {
if (otherIndex !== index && !processed.has(otherQueue.no) && (otherQueue.status === 'waiting' || otherQueue.status === 'di-loket')) {
if (otherIndex !== index && !processed.has(otherQueue.no) && (otherQueue.status === 'di-loket' || otherQueue.status === 'terlambat' || otherQueue.status === 'pending')) {
const otherCallTime = otherQueue.lastCalledAt ? new Date(otherQueue.lastCalledAt) : (otherQueue.createdAt ? new Date(otherQueue.createdAt) : null)
if (otherCallTime) {
const timeDiff = Math.abs(callTime - otherCallTime)
@@ -375,65 +482,223 @@ const displayedClinics = computed(() => {
const allMultipleCalls = multipleCallGroups.flat()
// Current queue adalah yang sedang dilayani untuk klinik ini
// Hanya ambil yang status 'di-loket' (yang sedang dilayani)
// Prioritas 1: Pasien yang sedang dipanggil di hero section (currentCalledQueue) jika sesuai dengan klinik ini
// Prioritas 2: Pasien yang status 'di-loket' (yang sedang dilayani)
// PASTIKAN hanya mengambil dari pasien yang klinik-nya sesuai dengan card ini
let currentQueue = null
// Cari yang sedang di-loket (sedang dilayani)
const diLoketQueues = sortedQueues.filter(q => q.status === 'di-loket')
if (diLoketQueues.length > 0) {
// Ambil yang paling lama (berdasarkan waktu check-in atau createdAt)
currentQueue = diLoketQueues.sort((a, b) => {
const checkInA = getCheckInTime(a)
const checkInB = getCheckInTime(b)
return checkInA - checkInB // Yang lebih lama lebih dulu
})[0]
// Normalize nama klinik sekali di awal scope
const normalizedKlinikName = klinikName.trim()
// Prioritas 1: Cek apakah pasien yang sedang dipanggil di hero section sesuai dengan klinik ini
// Jika pasien dipanggil di hero section, tampilkan di "SEDANG DILAYANI" di card klinik yang sesuai
// Filter berdasarkan nama klinik: jika RADIOTERAPI dipanggil, tampilkan di card RADIOTERAPI, dst
if (currentCalledQueue.value) {
const heroPatient = currentCalledQueue.value
const heroKlinik = getKlinikNameFromPatient(heroPatient)
// Pastikan nama klinik dari hero section sesuai dengan card ini
if (heroKlinik === normalizedKlinikName) {
// Cari pasien di sortedQueues berdasarkan no, noAntrian, atau barcode
const heroInQueues = sortedQueues.find(q =>
q.no === heroPatient.no ||
(q.noAntrian && heroPatient.noAntrian && q.noAntrian === heroPatient.noAntrian) ||
(q.barcode && heroPatient.barcode && q.barcode === heroPatient.barcode)
)
if (heroInQueues) {
// Gunakan pasien dari sortedQueues (data lebih lengkap)
// Pastikan pasien ini sesuai dengan filter (processStage 'loket')
if (heroInQueues.processStage === 'loket') {
currentQueue = heroInQueues
}
} else {
// Jika tidak ada di sortedQueues, gunakan langsung dari hero section
// Pastikan pasien sesuai dengan filter (processStage 'loket')
// Status bisa 'di-loket', 'terlambat', 'pending', atau 'pending-call' (sudah dipanggil)
// Status 'waiting' tidak ditampilkan di antrian loket
if (heroPatient.processStage === 'loket' &&
heroPatient.status !== 'waiting' &&
heroPatient.status !== 'menunggu') {
// Langsung gunakan pasien dari hero section karena sudah dipanggil
currentQueue = heroPatient
}
}
}
}
// Prioritas 2: Cari yang sedang di-loket (sedang dilayani) dan pastikan klinik-nya sesuai
// HANYA ambil yang dipanggil dari admin loket (processStage masih 'loket')
// Gunakan helper untuk mendapatkan nama klinik yang benar dari pasien
if (!currentQueue) {
const diLoketQueues = sortedQueues.filter(q => {
// Pastikan:
// 1. Status 'di-loket' (sudah check-in di admin loket)
// 2. processStage masih 'loket' (masih di admin loket, belum pindah ke klinik)
// 3. Klinik-nya sesuai dengan card ini
const patientKlinik = getKlinikNameFromPatient(q)
return q.status === 'di-loket' &&
q.processStage === 'loket' &&
patientKlinik === normalizedKlinikName
})
if (diLoketQueues.length > 0) {
// Ambil yang paling lama (berdasarkan waktu check-in atau createdAt)
currentQueue = diLoketQueues.sort((a, b) => {
const checkInA = getCheckInTime(a)
const checkInB = getCheckInTime(b)
return checkInA - checkInB // Yang lebih lama lebih dulu
})[0]
}
}
// Multiple calls untuk display - semua yang dipanggil bersamaan (termasuk yang sudah di-loket)
const multipleCalls = allMultipleCalls.filter(q =>
q.no !== currentQueue?.no || currentQueue?.status !== 'di-loket'
)
// Pastikan hanya dari klinik ini
const multipleCalls = allMultipleCalls.filter(q => {
// Pastikan dari klinik yang sama (gunakan helper untuk mendapatkan nama klinik yang benar)
const queueKlinik = getKlinikNameFromPatient(q)
if (queueKlinik !== normalizedKlinikName) return false
// Exclude currentQueue jika ada
return q.no !== currentQueue?.no || currentQueue?.status !== 'di-loket'
})
// Validasi akhir: pastikan currentQueue benar-benar dari klinik ini dan dari admin loket
let validatedCurrentQueue = currentQueue
if (validatedCurrentQueue) {
// Pastikan klinik-nya sesuai
const queueKlinik = getKlinikNameFromPatient(validatedCurrentQueue)
if (queueKlinik !== normalizedKlinikName) {
// Jika tidak match, set ke null untuk menghindari menampilkan tiket yang salah
validatedCurrentQueue = null
}
// Pastikan processStage masih 'loket' (dipanggil dari admin loket)
if (validatedCurrentQueue && validatedCurrentQueue.processStage !== 'loket') {
validatedCurrentQueue = null
}
// Jika currentQueue berasal dari hero section (currentCalledQueue), tidak perlu filter status
// karena pasien yang dipanggil di hero section sudah valid
const isFromHero = currentCalledQueue.value &&
(validatedCurrentQueue.no === currentCalledQueue.value.no ||
validatedCurrentQueue.noAntrian === currentCalledQueue.value.noAntrian)
// Jika bukan dari hero section, pastikan status 'di-loket' (sudah check-in di admin loket)
if (validatedCurrentQueue && !isFromHero && validatedCurrentQueue.status !== 'di-loket') {
validatedCurrentQueue = null
}
}
// Validasi allQueues: pastikan semua pasien benar-benar dari klinik ini
// Ambil yang status 'di-loket', 'terlambat', atau 'pending' dan dipanggil dari admin loket
const validatedAllQueues = sortedQueues.filter(q => {
// Pastikan:
// 1. Status 'di-loket', 'terlambat', atau 'pending' (sudah dipanggil/di-loket di admin loket)
// 2. processStage masih 'loket' (masih di admin loket, belum pindah ke klinik)
// 3. Klinik-nya sesuai dengan card ini
const queueKlinik = getKlinikNameFromPatient(q)
const isValidStatus = q.status === 'di-loket' || q.status === 'terlambat' || q.status === 'pending'
return isValidStatus &&
q.processStage === 'loket' &&
queueKlinik === normalizedKlinikName
})
return {
name: klinikName,
currentQueue: currentQueue,
currentQueue: validatedCurrentQueue,
multipleCalls: multipleCalls,
allQueues: sortedQueues,
totalQueues: sortedQueues.length
allQueues: validatedAllQueues,
totalQueues: validatedAllQueues.length
}
})
return clinics.sort((a, b) => a.name.localeCompare(b.name))
})
// Current called queue - antrian yang sedang diproses atau paling baru dipanggil
// Helper untuk cek apakah tiket masih dalam TTS window (15 detik)
const isInTTSWindow = (queue) => {
if (!queue) return false
const _ = currentTime.value // Force reactivity
const callTime = queue.lastCalledAt ? new Date(queue.lastCalledAt) :
(queue.pendingCallAt ? new Date(queue.pendingCallAt) : null)
if (!callTime) return false
const ttsDuration = 15000 // 15 detik
const elapsed = Date.now() - callTime.getTime()
const remaining = ttsDuration - elapsed
return remaining > 0
}
// Current called queue - tiket yang sedang diproses di admin loket atau sedang dalam TTS window
const currentCalledQueue = computed(() => {
// Prioritas 1: Antrian yang sedang diproses di AdminLoket (currentProcessingPatient)
if (currentProcessingPatient.value && (currentProcessingPatient.value.status === 'waiting' || currentProcessingPatient.value.status === 'di-loket' || currentProcessingPatient.value.status === 'pending')) {
return {
...currentProcessingPatient.value,
klinik: currentProcessingPatient.value.klinik || 'Klinik'
// Prioritas 1: Pasien yang sedang diproses di admin loket
if (currentProcessingPatient.value) {
const processingPatient = currentProcessingPatient.value
// Pastikan pasien memiliki noAntrian dan klinik
if (processingPatient.noAntrian) {
return {
...processingPatient,
klinik: processingPatient.klinik || getKlinikNameFromPatient(processingPatient) || 'Klinik'
}
}
}
// Prioritas 2: Antrian yang paling baru dipanggil dari displayedClinics
const allCurrentQueues = displayedClinics.value
.filter(klinik => klinik.currentQueue)
.map(klinik => ({
...klinik.currentQueue,
klinikName: klinik.name
}))
.sort((a, b) => {
const dateA = new Date(a.lastCalledAt || a.createdAt || 0)
const dateB = new Date(b.lastCalledAt || b.createdAt || 0)
return dateB - dateA
})
// Prioritas 2: Ambil semua tiket yang sudah dipanggil (status 'pending-call' saja, bukan 'waiting')
// Status 'waiting' tidak ditampilkan di antrian loket
const allCalledQueues = queueStore.allPatients.filter(p =>
p.status === 'pending-call' && (p.lastCalledAt || p.pendingCallAt)
)
if (allCurrentQueues.length > 0) {
const patient = allCurrentQueues[0]
// Filter berdasarkan pelayanan loket jika ada
const targetLoketId = loketId.value
let filteredQueues = allCalledQueues
if (targetLoketId && loketData.value) {
const allowedPelayananCodes = loketData.value.pelayanan || []
if (allowedPelayananCodes.length > 0) {
filteredQueues = allCalledQueues.filter(patient => {
const klinikName = patient.klinik || 'UMUM'
const clinic = clinicStore.getClinicByName ? clinicStore.getClinicByName(klinikName) : null
if (clinic) {
return allowedPelayananCodes.includes(clinic.kode)
} else {
const matchedKode = allowedPelayananCodes.find(kode => {
const k = masterStore.getKlinikByKode ? masterStore.getKlinikByKode(kode) : null
return k && k.nama === klinikName
})
return !!matchedKode
}
})
}
}
// Urutkan berdasarkan waktu panggilan (yang dipanggil duluan lebih dulu)
const sortedByCallTime = filteredQueues.sort((a, b) => {
const timeA = new Date(a.lastCalledAt || a.pendingCallAt || 0)
const timeB = new Date(b.lastCalledAt || b.pendingCallAt || 0)
return timeA - timeB
})
// Cari tiket pertama yang masih dalam TTS window
for (const queue of sortedByCallTime) {
if (isInTTSWindow(queue)) {
return {
...queue,
klinik: queue.klinik || 'Klinik'
}
}
}
// Jika tidak ada yang dalam TTS window, ambil yang paling baru dipanggil
if (sortedByCallTime.length > 0) {
const latest = sortedByCallTime[sortedByCallTime.length - 1]
return {
...patient,
klinik: patient.klinikName || patient.klinik || 'Klinik'
...latest,
klinik: latest.klinik || 'Klinik'
}
}
@@ -493,14 +758,14 @@ const currentMultipleCalls = computed(() => {
return null
})
// Timer untuk multiple calls
// Timer untuk card utama (TTS countdown)
const currentMultipleCallsTimer = computed(() => {
if (!currentMultipleCalls.value || currentMultipleCalls.value.length === 0) return null
if (!currentCalledQueue.value) return null
const _ = currentTime.value // Force reactivity
const firstCall = currentMultipleCalls.value[0]
const callTime = firstCall.lastCalledAt ? new Date(firstCall.lastCalledAt) : (firstCall.createdAt ? new Date(firstCall.createdAt) : null)
const callTime = currentCalledQueue.value.lastCalledAt ? new Date(currentCalledQueue.value.lastCalledAt) :
(currentCalledQueue.value.pendingCallAt ? new Date(currentCalledQueue.value.pendingCallAt) : null)
if (!callTime) return null
const ttsDuration = 15000 // 15 detik
@@ -545,82 +810,22 @@ const getTimerText = (queue) => {
return `${seconds}s`
}
// Next 5 tickets to be called (untuk urutan text-to-speech ketika ada multiple loket memanggil bersamaan)
// Next 5 tickets to be called - tiket yang sudah dipanggil tapi menunggu giliran TTS
const nextTicketsToCall = computed(() => {
// Ambil semua antrian dari SEMUA loket yang memiliki status 'waiting' atau 'pending-call' (sudah dipanggil atau akan dipanggil)
// Ini untuk menampilkan urutan text-to-speech ketika ada 2+ loket memanggil bersamaan
const allWaitingFromAllLokets = queueStore.allPatients.filter(p =>
(p.status === 'waiting' || p.status === 'pending-call') && (p.lastCalledAt || p.pendingCallAt)
// Ambil semua tiket yang sudah dipanggil (status 'pending-call' saja, bukan 'waiting')
// Status 'waiting' tidak ditampilkan di antrian loket
const allCalledQueues = queueStore.allPatients.filter(p =>
p.status === 'pending-call' && (p.lastCalledAt || p.pendingCallAt)
)
// Group berdasarkan waktu panggilan (dalam 5 detik = multiple calls)
const callGroups = []
const processed = new Set()
allWaitingFromAllLokets.forEach((queue) => {
if (processed.has(queue.no)) return
// Gunakan lastCalledAt atau pendingCallAt untuk waktu panggilan
const callTime = new Date(queue.lastCalledAt || queue.pendingCallAt)
const group = [queue]
processed.add(queue.no)
// Cari semua tiket yang dipanggil dalam 5 detik
allWaitingFromAllLokets.forEach((otherQueue) => {
if (!processed.has(otherQueue.no) && otherQueue.no !== queue.no) {
const otherCallTime = new Date(otherQueue.lastCalledAt || otherQueue.pendingCallAt)
const timeDiff = Math.abs(callTime - otherCallTime)
if (timeDiff <= 5000) {
group.push(otherQueue)
processed.add(otherQueue.no)
}
}
})
if (group.length > 1) {
// Sort dalam group berdasarkan waktu panggilan (siapa yang dipanggil duluan)
group.sort((a, b) => {
const timeA = new Date(a.lastCalledAt || a.pendingCallAt)
const timeB = new Date(b.lastCalledAt || b.pendingCallAt)
return timeA - timeB
})
callGroups.push(...group)
}
})
// Jika ada multiple calls, ambil 5 teratas dari yang dipanggil bersamaan
// Urutkan berdasarkan waktu panggilan untuk text-to-speech
if (callGroups.length > 0) {
// Ambil 5 teratas dari multiple calls, urutkan berdasarkan waktu panggilan
const sortedMultipleCalls = callGroups
.sort((a, b) => {
const timeA = new Date(a.lastCalledAt || a.pendingCallAt)
const timeB = new Date(b.lastCalledAt || b.pendingCallAt)
return timeA - timeB
})
.slice(0, 5)
return sortedMultipleCalls.map(queue => ({
...queue,
isMultipleCall: true,
klinik: queue.klinik || 'Klinik',
loket: queue.loket || 'Loket'
}))
}
// Jika tidak ada multiple calls, tampilkan 5 tiket tercepat yang akan dipanggil
const menungguQueues = loketPatients.value.filter(p => p.status === 'menunggu')
const waitingQueues = loketPatients.value.filter(p => p.status === 'waiting')
const allWaitingQueues = [...menungguQueues, ...waitingQueues]
// Filter berdasarkan pelayanan loket jika ada
const targetLoketId = loketId.value
let filteredQueues = allWaitingQueues
let filteredQueues = allCalledQueues
if (targetLoketId && loketData.value) {
const allowedPelayananCodes = loketData.value.pelayanan || []
if (allowedPelayananCodes.length > 0) {
filteredQueues = allWaitingQueues.filter(patient => {
filteredQueues = allCalledQueues.filter(patient => {
const klinikName = patient.klinik || 'UMUM'
const clinic = clinicStore.getClinicByName ? clinicStore.getClinicByName(klinikName) : null
@@ -637,36 +842,24 @@ const nextTicketsToCall = computed(() => {
}
}
// Sort berdasarkan prioritas
const sortedQueues = filteredQueues.sort((a, b) => {
const statusPriority = { 'menunggu': 1, 'waiting': 2 }
const statusDiff = (statusPriority[a.status] || 99) - (statusPriority[b.status] || 99)
if (statusDiff !== 0) return statusDiff
const createdAtA = new Date(a.createdAt || 0)
const createdAtB = new Date(b.createdAt || 0)
if (createdAtA.getTime() !== createdAtB.getTime()) {
return createdAtA - createdAtB
}
if (a.no !== b.no) {
return a.no - b.no
}
const checkInA = getCheckInTime(a)
const checkInB = getCheckInTime(b)
return checkInA - checkInB
// Urutkan berdasarkan waktu panggilan (yang dipanggil duluan lebih dulu)
const sortedByCallTime = filteredQueues.sort((a, b) => {
const timeA = new Date(a.lastCalledAt || a.pendingCallAt || 0)
const timeB = new Date(b.lastCalledAt || b.pendingCallAt || 0)
return timeA - timeB
})
// Exclude current called queue
// Exclude current called queue (yang sedang dalam TTS window)
const currentCalledNo = currentCalledQueue.value?.no
const filteredForNext = sortedQueues.filter(q => q.no !== currentCalledNo)
const waitingForTTS = sortedByCallTime.filter(q => q.no !== currentCalledNo)
// Ambil 5 teratas
return filteredForNext.slice(0, 5).map(queue => ({
// Ambil 5 tiket berikutnya yang sudah dipanggil tapi TTS-nya belum dimulai
// (yang akan bergantian menjadi card utama ketika TTS selesai)
return waitingForTTS.slice(0, 5).map(queue => ({
...queue,
isMultipleCall: false,
klinik: queue.klinik || 'Klinik'
isMultipleCall: true, // Semua di section ini adalah tiket yang sudah dipanggil
klinik: queue.klinik || 'Klinik',
loket: queue.loket || 'Loket'
}))
})
@@ -855,12 +1048,12 @@ onUnmounted(() => {
/* ========== HERO CALL SECTION (LARGE) ========== */
.hero-call-section {
background: linear-gradient(135deg, var(--color-danger-600) 0%, var(--color-danger-700) 100%);
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
border-radius: 20px;
padding: 32px;
flex: 1;
text-align: center;
box-shadow: 0 12px 32px rgba(224, 21, 7, 0.35);
box-shadow: 0 12px 32px rgba(33, 150, 243, 0.35);
animation: pulse-highlight 2s ease-in-out infinite;
}
@@ -894,6 +1087,21 @@ onUnmounted(() => {
letter-spacing: 1px;
}
.call-timer-main {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 24px;
font-weight: 700;
color: var(--color-neutral-100);
margin-top: 16px;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.15);
border-radius: 12px;
backdrop-filter: blur(10px);
}
/* ========== NEXT TICKETS SECTION ========== */
.next-tickets-section {
background: var(--color-neutral-100);
@@ -964,6 +1172,14 @@ onUnmounted(() => {
animation: highlight-flash 1s ease-in-out infinite;
box-shadow: 0 2px 8px rgba(244, 67, 54, 0.4);
}
/* Next in queue - akan naik ke card utama setelah TTS selesai */
&.is-next-in-queue {
border-color: var(--color-primary-500);
background: var(--color-primary-50);
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.3);
border-width: 3px;
}
}
.next-ticket-number {
@@ -1036,23 +1252,12 @@ onUnmounted(() => {
margin-top: 4px;
}
/* ========== HERO CALL SECTION (OLD - KEEP FOR REFERENCE) ========== */
.hero-call-section {
background: linear-gradient(135deg, var(--color-danger-600) 0%, var(--color-danger-700) 100%);
border-radius: 20px;
padding: 32px;
margin-bottom: 20px;
text-align: center;
box-shadow: 0 12px 32px rgba(224, 21, 7, 0.35);
animation: pulse-highlight 2s ease-in-out infinite;
}
@keyframes pulse-highlight {
0%, 100% {
box-shadow: 0 12px 32px rgba(224, 21, 7, 0.35);
box-shadow: 0 12px 32px rgba(33, 150, 243, 0.35);
}
50% {
box-shadow: 0 12px 48px rgba(224, 21, 7, 0.55);
box-shadow: 0 12px 48px rgba(33, 150, 243, 0.55);
}
}
@@ -1192,6 +1397,16 @@ onUnmounted(() => {
}
}
.current-waiting-text {
font-size: 48px;
font-weight: 700;
color: var(--color-neutral-500);
letter-spacing: 2px;
line-height: 1;
margin-top: 10px;
opacity: 0.7;
}
/* Multiple Calls Row (Small chips) */
.multiple-calls-row {
display: flex;
@@ -1274,6 +1489,25 @@ onUnmounted(() => {
color: var(--color-danger-700);
box-shadow: 0 2px 8px rgba(244, 67, 54, 0.4);
}
/* Terlambat - highlighted in orange */
&.is-terlambat {
background: var(--color-warning-50);
border: 3px solid var(--color-warning-500);
color: var(--color-warning-800);
font-weight: 800;
box-shadow: 0 2px 6px rgba(255, 152, 0, 0.3);
}
/* Pending - highlighted in grey */
&.is-pending {
background: var(--color-neutral-100);
border: 3px solid var(--color-neutral-400);
color: var(--color-neutral-600);
font-weight: 800;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
opacity: 0.8;
}
}

View File

@@ -33,12 +33,12 @@
</div>
<div class="loket-tags">
<v-chip
v-for="(pelayanan, idx) in (loket.pelayanan || []).slice(0, 3)"
v-for="(pelayananKode, idx) in (loket.pelayanan || []).slice(0, 3)"
:key="idx"
size="small"
class="ma-1 chip-preview"
>
{{ pelayanan }}
{{ getKlinikName(pelayananKode) }}
</v-chip>
<v-chip
v-if="(loket.pelayanan || []).length > 3"
@@ -91,6 +91,7 @@
<script setup>
import { computed } from 'vue';
import { useLoketStore } from '@/stores/loketStore';
import { useMasterStore } from '@/stores/masterStore';
import { useRoute } from '#app';
definePageMeta({
@@ -98,6 +99,7 @@ definePageMeta({
});
const loketStore = useLoketStore();
const masterStore = useMasterStore();
const route = useRoute();
// Helper function: Convert nomor loket ke huruf (1 -> A, 2 -> B, dst)
@@ -145,6 +147,11 @@ const navigateToLoket = (loketId) => {
const navigateToSettings = () => {
navigateTo('/setting/masterloket');
};
// Helper function untuk mendapatkan nama klinik dari kode
const getKlinikName = (kode) => {
return masterStore.getKlinikNameByKode ? masterStore.getKlinikNameByKode(kode) : kode;
};
</script>
<style scoped lang="scss">

View File

@@ -119,28 +119,46 @@ export const useLoketStore = defineStore('loket', () => {
// Computed - Available services (reference dari clinicStore)
// Mengambil data dari clinicStore untuk dropdown pelayanan
// Menggunakan getAllClinics computed untuk memastikan reactivity
const availableServices = computed(() => {
try {
// Akses getAllClinics computed dari clinicStore
// Karena kita di dalam computed, Vue akan handle reactivity
// Gunakan getAllClinics computed dari clinicStore
// Ini akan otomatis reactive ketika clinics berubah
const clinicsComputed = clinicStore.getAllClinics;
// getAllClinics adalah computed, jadi kita perlu .value untuk mendapatkan array
const clinicsArray = clinicsComputed?.value || [];
if (!clinicsComputed) {
console.warn('clinicStore.getAllClinics is not available');
return [];
}
// getAllClinics adalah computed, akses .value untuk mendapatkan array
// Vue akan otomatis track dependency ini
const clinicsArray = clinicsComputed.value || [];
// Pastikan clinicsArray adalah array
if (!Array.isArray(clinicsArray) || clinicsArray.length === 0) {
if (!Array.isArray(clinicsArray)) {
console.warn('getAllClinics.value is not an array:', typeof clinicsArray, clinicsArray);
return [];
}
if (clinicsArray.length === 0) {
console.warn('No clinics available from clinicStore');
return [];
}
// Map ke format yang dibutuhkan form: { id, nama, kode }
// id menggunakan kode untuk kompatibilitas dengan form yang menggunakan item-value="id"
return clinicsArray.map(c => ({
id: c.kode, // Menggunakan kode sebagai id (sesuai dengan item-value="id" di form)
nama: c.name,
kode: c.kode,
}));
return clinicsArray.map(c => {
if (!c || !c.kode || !c.name) {
console.warn('Invalid clinic data:', c);
return null;
}
return {
id: c.kode, // Menggunakan kode sebagai id (sesuai dengan item-value="id" di form)
nama: c.name,
kode: c.kode,
};
}).filter(Boolean); // Filter out null values
} catch (error) {
console.error('Error getting available services from clinicStore:', error);
return [];

View File

@@ -127,7 +127,12 @@ export const useMasterStore = defineStore('master', () => {
const loketData = computed(() => loketStore.loketData);
// availableServices adalah computed dari loketStore
// Pinia computed sudah handle reactivity dengan baik, jadi akses langsung
const availableServices = computed(() => loketStore.availableServices);
// loketStore.availableServices adalah computed, jadi kita bisa langsung reference
const availableServices = computed(() => {
// loketStore.availableServices adalah computed, akses .value untuk mendapatkan array
const services = loketStore.availableServices;
return services?.value || services || [];
});
// Actions - Loket (delegate to loketStore)
const addLoket = (loketPayload) => loketStore.addLoket(loketPayload);

View File

@@ -602,29 +602,41 @@ export const useQueueStore = defineStore('queue', () => {
// Ambil data terbaru dari array
const patient = allPatients.value[patientIndex];
// Jika pasien memiliki status "pending" atau "terlambat", ubah menjadi "di-loket"
// agar pasien masuk ke kategori diLoketPatients dan ditampilkan sebagai "diproses"
const currentStatus = patient.status;
const shouldUpdateStatus = currentStatus === "pending" || currentStatus === "terlambat";
// Jika adminType adalah 'loket', pastikan ada loket assignment
if (adminType === 'loket') {
// Pastikan antrian yang diproses memiliki loket assignment
const currentLoket = patient.loket || getDefaultLoket();
const currentLoketId = patient.loketId || 1;
// Update patient dengan loket assignment (jika belum ada)
if (!patient.loket || !patient.loketId) {
// Update patient dengan loket assignment dan status (jika perlu)
const updatedPatient = {
...patient,
loket: patient.loket || currentLoket,
loketId: patient.loketId || currentLoketId,
// Ubah status menjadi "di-loket" jika sebelumnya "pending" atau "terlambat"
...(shouldUpdateStatus && { status: "di-loket" })
};
allPatients.value[patientIndex] = updatedPatient;
// Set currentProcessingPatient dengan data terbaru
currentProcessingPatient.value[adminType] = updatedPatient;
} else {
// Untuk adminType selain loket, update status jika perlu
if (shouldUpdateStatus) {
const updatedPatient = {
...patient,
loket: currentLoket,
loketId: currentLoketId
status: "di-loket"
};
allPatients.value[patientIndex] = updatedPatient;
// Set currentProcessingPatient dengan loket assignment
currentProcessingPatient.value[adminType] = updatedPatient;
} else {
// Jika sudah ada loket assignment, langsung set currentProcessingPatient
currentProcessingPatient.value[adminType] = patient;
}
} else {
// Untuk adminType selain loket, langsung set currentProcessingPatient
currentProcessingPatient.value[adminType] = patient;
}
message = `Memproses pasien ${patientCode}`;
break;