|
|
|
@@ -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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|