+
mdi-clock-outline
Tidak Ada Antrian
@@ -219,19 +205,39 @@ const loketData = computed(() => {
return loketStore.getLoketById(id) || null
})
-// Get current processing patient dari AdminLoket
+// Get current processing patient dari AdminLoket (isolated by loketId)
const currentProcessingPatient = computed(() => {
- return queueStore.currentProcessingPatient?.loket || null
+ const targetId = loketId.value;
+ const key = targetId ? `loket-${targetId}` : 'loket';
+ return queueStore.currentProcessingPatient?.[key] || null;
})
// Get all patients with processStage "loket" and filter status "di-loket" (sudah check-in)
// Hanya tampilkan nomor antrian yang sudah check-in dan berstatus "di-loket"
+// Get all patients with processStage "loket"
const loketPatients = computed(() => {
- const allPatients = queueStore.getPatientsByStage('loket').value.all
- // Hanya tampilkan antrian dengan status "di-loket" (sudah check-in di admin loket)
- // Jangan tampilkan status "waiting", "menunggu", "terlambat", atau "pending"
- return allPatients.filter(p => p.status === 'di-loket')
-})
+ // Get all patients from queueStore
+ const allPatients = queueStore.allPatients?.value || queueStore.allPatients || [];
+
+ // Filter for this stage and relevant statuses
+ // Include 'di-loket' (serving/called) and 'waiting' (next in line)
+ return allPatients.filter(p =>
+ p.processStage === 'loket' &&
+ (p.status === 'di-loket' || p.status === 'waiting')
+ );
+});
+
+// Watch currentProcessingPatient to clear broadcastedPatient when patient changes
+watch(() => {
+ const targetId = loketId.value;
+ const key = targetId ? `loket-${targetId}` : 'loket';
+ return queueStore.currentProcessingPatient?.[key];
+}, (newVal, oldVal) => {
+ if (!newVal || (oldVal && newVal.no !== oldVal.no)) {
+ console.log('🔄 [Anjungan] Processing patient changed, clearing broadcasted call');
+ broadcastedPatient.value = null;
+ }
+}, { deep: true });
// Helper untuk mendapatkan waktu check-in atau waktu dipanggil
const getCheckInTime = (patient) => {
@@ -326,94 +332,106 @@ const getKlinikNameFromPatient = (patient) => {
return 'UMUM'
}
-// Display clinics with their queues - Group by klinik, filtered by loket pelayanan
-const displayedClinics = computed(() => {
- // Get pelayanan dari loket yang dipilih
+// 1. Dapatkan semua pasien yang relevan untuk loket ini (berdasarkan loketId atau pelayanan)
+const filteredPatientsForLoket = computed(() => {
const targetLoketId = loketId.value
let allowedPelayananCodes = []
if (targetLoketId && loketData.value) {
- // Ambil pelayanan dari loket (array of kode klinik seperti ['AN', 'IP', 'SR'])
allowedPelayananCodes = loketData.value.pelayanan || []
}
- // Jika tidak ada loketId atau pelayanan kosong, tampilkan semua
const shouldFilterByPelayanan = targetLoketId && allowedPelayananCodes.length > 0
- // Include currentProcessingPatient dalam list jika ada
- let allPatientsForDistribution = [...loketPatients.value]
+ // Ambil semua pasien yang berada di tahap 'loket'
+ let allPatients = loketPatients.value
- // Include currentProcessingPatient jika ada
- if (currentProcessingPatient.value) {
- const existsInList = allPatientsForDistribution.find(p => p.no === currentProcessingPatient.value.no)
- if (!existsInList) {
- allPatientsForDistribution.push(currentProcessingPatient.value)
+ // Include currentProcessingPatient jika ada dan belum ada di list
+ const targetId = loketId.value;
+ const key = targetId ? `loket-${targetId}` : 'loket';
+ const currentProc = queueStore.currentProcessingPatient?.[key]
+
+ if (currentProc) {
+ const exists = allPatients.find(p => p.no === currentProc.no)
+ if (!exists) {
+ allPatients = [...allPatients, currentProc]
}
}
-
- // Filter berdasarkan loket jika ada loketId di route
- if (targetLoketId) {
- allPatientsForDistribution = allPatientsForDistribution.filter(p => {
- const patientLoketId = p.loketId || 1
- return patientLoketId === targetLoketId ||
- patientLoketId === targetLoketId.toString() ||
- (p.loket && loketData.value && p.loket.toLowerCase().includes(loketData.value.namaLoket?.toLowerCase() || ''))
- })
- }
-
- // Group by klinik dan filter berdasarkan pelayanan loket
- const clinicsMap = new Map()
-
- allPatientsForDistribution.forEach(patient => {
- // Gunakan helper untuk mendapatkan nama klinik yang benar
- // Ini akan parse dari noAntrian jika patient.klinik tidak sesuai
- const klinikName = getKlinikNameFromPatient(patient)
+
+ // Filter based on Loket ID and assigned Services (Pelayanan)
+ return allPatients.filter(patient => {
+ // A. Filter by Loket ID (Priority)
+ // Jika pasien sudah memiliki loketId eksplisit, harus match
+ if (patient.loketId) {
+ const isMatch = patient.loketId === targetLoketId || patient.loketId === targetLoketId.toString()
+ if (isMatch) return true
+ }
- // Jika ada filter pelayanan, cek apakah klinik ini ada di pelayanan loket
+ // B. Filter by Loket Name (Priority)
+ if (patient.loket && loketData.value) {
+ if (patient.loket.toLowerCase().includes(loketData.value.namaLoket?.toLowerCase() || '')) return true
+ }
+
+ // C. Filter by assigned Services (Pelayanan)
+ // Jika tidak ada loketId eksplisit (e.g. seed/new onsite), check if clinic is handled by this loket
if (shouldFilterByPelayanan) {
- // Cari kode klinik dari nama klinik menggunakan clinicStore
+ const klinikName = getKlinikNameFromPatient(patient)
const clinic = clinicStore.getClinicByName ? clinicStore.getClinicByName(klinikName) : null
+ let isAllowed = false
if (clinic) {
- // Jika ditemukan di clinicStore, cek apakah kode-nya ada di pelayanan
- const isAllowed = allowedPelayananCodes.includes(clinic.kode)
- if (!isAllowed) return // Skip jika tidak ada di pelayanan
+ isAllowed = allowedPelayananCodes.includes(clinic.kode)
} else {
- // Jika tidak ditemukan di clinicStore, cek menggunakan masterStore
- // 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) {
+ // Fallback checks for codes in noAntrian
+ if (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 (allowedPelayananCodes.includes(kodeFromNoAntrian)) isAllowed = true
}
}
-
- if (!matchedKode) return // Skip jika tidak match
}
+
+ // Fallback: Match by clinic name if no code match found
+ if (!isAllowed && klinikName && loketData.value) {
+ const normalizedKlinik = klinikName.trim().toUpperCase();
+ isAllowed = (loketData.value._spesialisDetail || []).some(s =>
+ s.namaklinik && s.namaklinik.trim().toUpperCase() === normalizedKlinik
+ );
+ }
+
+ return isAllowed
}
-
- // Pastikan pasien memiliki klinik yang valid sebelum di-group
- if (!klinikName || klinikName.trim() === '') {
- return // Skip jika klinik tidak valid
- }
+
+ // D. If no explicit filters match and we shouldn't filter by pelayanan,
+ // we only show if it's default Loket 1 (fallback for untagged items)
+ if (!targetLoketId || targetLoketId === 1) return true
+
+ return false
+ })
+})
+
+// 2. Tampilkan klinik dengan antrean masing-masing
+const displayedClinics = computed(() => {
+ const filteredPatients = filteredPatientsForLoket.value
+ const clinicsMap = new Map()
+
+ filteredPatients.forEach(patient => {
+ const klinikName = getKlinikNameFromPatient(patient)
+ if (!klinikName || klinikName.trim() === '') return
if (!clinicsMap.has(klinikName)) {
clinicsMap.set(klinikName, [])
}
- // Pastikan pasien yang di-push memiliki klinik yang sesuai
clinicsMap.get(klinikName).push(patient)
})
// PENTING: Selalu tampilkan card untuk semua pelayanan loket,
// bahkan jika tidak ada antrian
+ const targetLoketId = loketId.value
+ const allowedPelayananCodes = (targetLoketId && loketData.value) ? (loketData.value.pelayanan || []) : []
+
if (targetLoketId && loketData.value) {
allowedPelayananCodes.forEach(kode => {
// Prioritas 1: Cari nama klinik dari _spesialisDetail (data API)
@@ -524,124 +542,58 @@ const displayedClinics = computed(() => {
// Flatten multiple call groups untuk mendapatkan semua multiple calls
const allMultipleCalls = multipleCallGroups.flat()
- // Current queue adalah yang sedang dilayani untuk klinik ini
- // 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
+ // Determine currentQueue: The patient currently being served in this clinic
let currentQueue = null
-
- // 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
+ // Get all checked-in patients for this clinic
+ const servingQueues = sortedQueues.filter(q =>
+ q.status === 'di-loket' && q.processStage === 'loket' && getKlinikNameFromPatient(q) === normalizedKlinikName
+ )
+
+ // Prioritas 1: If Hero section is calling someone for THIS clinic, show them as serving
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 =>
+ currentQueue = 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' dan status 'di-loket')
- if (heroPatient.processStage === 'loket' &&
- heroPatient.status === 'di-loket') {
- // Langsung gunakan pasien dari hero section karena sudah dipanggil dan check-in
- currentQueue = heroPatient
- }
- }
+ (q.noAntrian && heroPatient.noAntrian && q.noAntrian === heroPatient.noAntrian)
+ ) || 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
+ // Prioritas 2: Persistence - If no active Hero call for this clinic,
+ // find the patient who was explicitly called by admin and is still in servingQueues (di-loket)
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
- })
+ const calledServingQueues = servingQueues
+ .filter(q => q.calledByAdmin) // Use calledByAdmin for explicit loket call
+ .sort((a, b) => new Date(b.lastCalledAt) - new Date(a.lastCalledAt))
- 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]
+ if (calledServingQueues.length > 0) {
+ currentQueue = calledServingQueues[0]
}
}
-
- // Multiple calls untuk display - semua yang dipanggil bersamaan (termasuk yang sudah 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)
+
+ // Multiple calls for display
+ const multipleCalls = (allMultipleCalls || []).filter(q => {
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
- }
-
- // Pastikan status selalu 'di-loket' (sudah check-in di admin loket)
- if (validatedCurrentQueue && validatedCurrentQueue.status !== 'di-loket') {
- validatedCurrentQueue = null
- }
- }
-
- // Validasi allQueues: pastikan semua pasien benar-benar dari klinik ini
- // Hanya ambil yang status 'di-loket' (sudah check-in) dan dipanggil dari admin loket
- const validatedAllQueues = 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 (dibaca dari nomor antrian)
- const queueKlinik = getKlinikNameFromPatient(q)
- return q.status === 'di-loket' &&
- q.processStage === 'loket' &&
- queueKlinik === normalizedKlinikName
+ return q.no !== currentQueue?.no
})
+ // Separate di-loket grid - excluding currentQueue
+ const diLoketQueuesInGrid = servingQueues.filter(q => q.no !== currentQueue?.no);
+
return {
name: klinikName,
- currentQueue: validatedCurrentQueue,
+ currentQueue: currentQueue,
multipleCalls: multipleCalls,
- allQueues: validatedAllQueues,
- totalQueues: validatedAllQueues.length
+ diLoketQueues: diLoketQueuesInGrid,
+ waitingQueues: [], // Removed as per request
+ allQueues: diLoketQueuesInGrid,
+ totalQueues: servingQueues.length
}
})
@@ -667,9 +619,18 @@ const isInTTSWindow = (queue) => {
// Current called queue - tiket yang sedang diproses di admin loket
// Nomor antrian menjadi "dipanggil" jika diproses pada AdminLoket DAN sudah dipanggil oleh admin
const currentCalledQueue = computed(() => {
+ const targetLoketId = String(loketId.value)
+
// Prioritas 0: Broadcasted patient (Real-time from BroadcastChannel)
+ // Check if the broadcast came from this loket
if (broadcastedPatient.value) {
- return broadcastedPatient.value
+ const rawMsg = broadcastedPatient.value._rawMessage
+ const msgLoketId = rawMsg?.loketId ? String(rawMsg.loketId) : null
+
+ // Only show if it matches this anjungan's loket ID
+ if (msgLoketId === targetLoketId) {
+ return broadcastedPatient.value
+ }
}
// Prioritas 1: Pasien yang sedang diproses di admin loket (currentProcessingPatient)
@@ -677,6 +638,13 @@ const currentCalledQueue = computed(() => {
if (currentProcessingPatient.value) {
const processingPatient = currentProcessingPatient.value
+ // Cek apakah data ini milik loket ini (berdasarkan metadata loketId)
+ // Jika tidak ada loketId (misal data lama), fallback ke true untuk Admin 1
+ const patientLoketId = processingPatient.loketId ? String(processingPatient.loketId) : "1"
+ if (patientLoketId !== targetLoketId) {
+ return null
+ }
+
// Cek apakah sudah dipanggil oleh admin
if (!processingPatient.calledByAdmin) {
return null
@@ -696,58 +664,6 @@ const currentCalledQueue = computed(() => {
return null
})
-// Current multiple calls - antrian yang dipanggil bersamaan
-const currentMultipleCalls = computed(() => {
- const allMultipleCalls = displayedClinics.value
- .flatMap(klinik => (klinik.multipleCalls || []).map(queue => ({
- ...queue,
- klinikName: klinik.name
- })))
- .sort((a, b) => {
- const timeA = a.lastCalledAt ? new Date(a.lastCalledAt) : new Date(a.createdAt || 0)
- const timeB = b.lastCalledAt ? new Date(b.lastCalledAt) : new Date(b.createdAt || 0)
- return timeB - timeA
- })
-
- if (allMultipleCalls.length > 0) {
- // Group by call time (within 5 seconds)
- const groups = []
- const processed = new Set()
-
- allMultipleCalls.forEach(queue => {
- if (processed.has(queue.no)) return
-
- const callTime = queue.lastCalledAt ? new Date(queue.lastCalledAt) : new Date(queue.createdAt || 0)
- const group = [queue]
- processed.add(queue.no)
-
- allMultipleCalls.forEach(otherQueue => {
- if (!processed.has(otherQueue.no)) {
- const otherCallTime = otherQueue.lastCalledAt ? new Date(otherQueue.lastCalledAt) : new Date(otherQueue.createdAt || 0)
- const timeDiff = Math.abs(callTime - otherCallTime)
- if (timeDiff <= 5000) {
- group.push(otherQueue)
- processed.add(otherQueue.no)
- }
- }
- })
-
- if (group.length > 1) {
- groups.push(group)
- }
- })
-
- if (groups.length > 0) {
- // Return most recent group
- return groups[0].map(q => ({
- ...q,
- klinik: q.klinikName || q.klinik || 'Klinik'
- }))
- }
- }
-
- return null
-})
// Timer untuk card utama (TTS countdown)
const currentMultipleCallsTimer = computed(() => {
@@ -792,45 +708,11 @@ const getTimerText = (queue) => {
// Next 5 tickets to be called - tiket yang sudah check-in (status 'di-loket') dan belum diproses
const nextTicketsToCall = computed(() => {
- // Ambil semua tiket yang sudah check-in (status 'di-loket') dan processStage 'loket'
- const allDiLoketQueues = loketPatients.value.filter(p =>
+ // Gunakan pasien yang sudah ter-filter untuk loket ini
+ const filteredQueues = filteredPatientsForLoket.value.filter(p =>
p.status === 'di-loket' && p.processStage === 'loket'
)
- // Filter berdasarkan pelayanan loket jika ada
- const targetLoketId = loketId.value
- let filteredQueues = allDiLoketQueues
-
- if (targetLoketId && loketData.value) {
- const allowedPelayananCodes = loketData.value.pelayanan || []
- if (allowedPelayananCodes.length > 0) {
- filteredQueues = allDiLoketQueues.filter(patient => {
- // Dapatkan nama klinik dari nomor antrian
- const klinikName = getKlinikNameFromPatient(patient)
- const clinic = clinicStore.getClinicByName ? clinicStore.getClinicByName(klinikName) : null
-
- if (clinic) {
- return allowedPelayananCodes.includes(clinic.kode)
- } else {
- // Coba match dengan kode dari nomor antrian
- if (patient.noAntrian) {
- const noAntrianPart = patient.noAntrian.split(' |')[0]
- const match = noAntrianPart.match(/^([A-Z]+)/)
- if (match) {
- const kodeFromNoAntrian = match[1]
- return allowedPelayananCodes.includes(kodeFromNoAntrian)
- }
- }
- const matchedKode = allowedPelayananCodes.find(kode => {
- const k = masterStore.getKlinikByKode ? masterStore.getKlinikByKode(kode) : null
- return k && k.nama === klinikName
- })
- return !!matchedKode
- }
- })
- }
- }
-
// Urutkan berdasarkan waktu check-in (yang check-in duluan lebih dulu)
const sortedByCheckIn = filteredQueues.sort((a, b) => {
const checkInA = getCheckInTime(a)
@@ -894,7 +776,7 @@ const isCalled = (queue) => {
// Statistics
const statistics = computed(() => {
- const all = loketPatients.value
+ const all = filteredPatientsForLoket.value
const active = all.filter(p => p.status === 'di-loket').length
return {
total: all.length,
@@ -919,81 +801,67 @@ const updateTime = () => {
})
}
+// WebSocket configuration (Placeholder)
+const config = useRuntimeConfig()
+const wsBaseUrl = config.public?.wsBaseUrl || 'ws://10.10.150.100:8084/api/v1/ws'
+const anjunganClientId = computed(() => `anjungan-loket-${loketId.value}`)
+let wsInstance = null
+const isConnected = ref(false)
+
+const initWebSocket = () => {
+ console.log('🔌 WebSocket Placeholder: Connecting to', wsBaseUrl);
+ // Implementation will follow AntrianKlinikRuang style when backend is ready
+ return null;
+}
+
onMounted(() => {
- // Fetch loket data dari API di background
- loketStore.fetchLoketFromAPI(true).then(result => {
- if (result.success) {
- console.log('✅ [AntrianLoket] Loket API data loaded:', result.message);
- } else {
- console.warn('⚠️ [AntrianLoket] Failed to fetch loket from API:', result.message);
- }
- }).catch(err => {
- console.error('❌ [AntrianLoket] Error fetching loket from API:', err);
- });
+ // Fetch loket data from API to ensure fresh mapping
+ loketStore.fetchLoketFromAPI(true).catch(console.error);
- // Langsung start timer tanpa menunggu fetch
- updateTime()
- // Init BroadcastChannel
+ updateTime();
+ timeInterval = setInterval(updateTime, 1000);
+
+ // Initialize BroadcastChannel
try {
- broadcastChannel = new BroadcastChannel('antrian-loket-channel')
+ broadcastChannel = new BroadcastChannel('antrian-loket-channel');
broadcastChannel.onmessage = (event) => {
- console.log('Anjungan received broadcast:', event.data)
- if (event.data && event.data.type === 'CALL_PATIENT') {
- const { patient, loketId: senderLoketId } = event.data
+ console.log('📡 Anjungan received broadcast:', event.data);
+ if (event.data?.type === 'CALL_PATIENT') {
+ const { patient, loketId: senderId } = event.data;
+ const targetId = String(loketId.value);
- // Cek apakah event ini untuk loket ini
- // Gunakan loose equality untuk handle string vs number
- if (loketId.value) {
- if (String(loketId.value) != String(senderLoketId)) {
- console.log('Skipping broadcast for different loket:', senderLoketId)
- return
- }
+ console.log(`📞 Call from Loket ${senderId} to Anjungan ${targetId}`);
+
+ // Match loket ID strictly to ensure call isolation
+ if (targetId && String(senderId) !== targetId) {
+ console.warn(`🚫 Ignoring call: Loket ID mismatch (${senderId} vs ${targetId})`);
+ return;
}
- // Update local state untuk prioritas tampilan hero
- broadcastedPatient.value = patient
+ // Update local state for Hero display
+ broadcastedPatient.value = {
+ ...patient, // Spread patient data
+ _rawMessage: event.data // Attach metadata for filtering
+ };
- // PENTING: Update Store agar list antrian juga berubah
- // 1. Update currentProcessingPatient
- if (!queueStore.currentProcessingPatient) {
- queueStore.currentProcessingPatient = {}
+ // Sync with store using isolated key
+ if (queueStore.currentProcessingPatient) {
+ const key = targetId ? `loket-${targetId}` : 'loket';
+ queueStore.currentProcessingPatient[key] = patient;
}
- queueStore.currentProcessingPatient.loket = patient
-
- // 2. Update status patient di allPatients list jika ada
- if (queueStore.allPatients && Array.isArray(queueStore.allPatients)) {
- const idx = queueStore.allPatients.findIndex(p =>
- p.no === patient.no ||
- p.noAntrian === patient.noAntrian
- )
-
- if (idx !== -1) {
- // Update existing patient data
- // Kita update status menjadi 'di-loket' dan lastCalledAt
- const updatedPatient = {
- ...queueStore.allPatients[idx],
- ...patient,
- status: 'di-loket', // Pastikan status sinkron
- lastCalledAt: patient.lastCalledAt || new Date().toISOString()
- }
- queueStore.allPatients[idx] = updatedPatient
- } else {
- // Opsional: Jika pasien tidak ditemukan di list (misal data awal kosong), tambahkan?
- // Sebaiknya jangan sembarang nambah, tapi untuk robustnes UI boleh saja
- // queueStore.allPatients.push(patient)
- }
- }
-
- console.log('Store updated from broadcast')
}
- }
+ };
} catch (e) {
- console.error('BroadcastChannel error:', e)
+ console.error('❌ BroadcastChannel error:', e);
}
- // Start TTS checker
- timeInterval = setInterval(updateTime, 1000)
-})
+ // Future: initWebSocket()
+
+ // Ensure initial data is loaded if store is empty
+ setTimeout(() => {
+ queueStore.ensureInitialData();
+ }, 200);
+});
onUnmounted(() => {
if (timeInterval) clearInterval(timeInterval)
@@ -1279,41 +1147,6 @@ onUnmounted(() => {
margin-top: 6px;
}
-/* Multiple Calls on the Side */
-.hero-multiple-calls-side {
- display: flex;
- flex-direction: column;
- gap: 8px;
- flex: 0 0 140px;
-}
-
-.hero-call-card-side {
- background: linear-gradient(135deg, var(--color-warning-600) 0%, var(--color-warning-700) 100%);
- border-radius: 12px;
- padding: 12px 16px;
- text-align: center;
- box-shadow: 0 4px 12px rgba(255, 152, 0, 0.3);
-}
-
-.call-number-side {
- font-size: 24px;
- font-weight: 800;
- color: var(--color-neutral-100);
- letter-spacing: 2px;
- line-height: 1;
- margin-bottom: 4px;
-}
-
-.call-timer-side {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 4px;
- font-size: 11px;
- font-weight: 700;
- color: var(--color-neutral-100);
- margin-top: 4px;
-}
@keyframes pulse-highlight {
0%, 100% {
@@ -1503,6 +1336,8 @@ onUnmounted(() => {
display: grid;
gap: 10px;
margin-top: 12px;
+ grid-template-rows: auto;
+ width: 100%;
}
.queue-grid-item {
@@ -1526,6 +1361,14 @@ onUnmounted(() => {
box-shadow: 0 2px 6px rgba(255, 152, 0, 0.3);
}
+ /* Waiting queue - green or neutral highlight */
+ &.is-waiting {
+ background: var(--color-success-50);
+ border: 2px solid var(--color-success-400);
+ color: var(--color-success-800);
+ font-weight: 600;
+ }
+
/* Multiple calls - highlighted in red */
&.is-multiple {
background: var(--color-danger-100);
diff --git a/stores/loketStore.js b/stores/loketStore.js
index 0ef36b9..dd535d8 100644
--- a/stores/loketStore.js
+++ b/stores/loketStore.js
@@ -262,7 +262,7 @@ export const useLoketStore = defineStore('loket', () => {
/**
* Fetch loket data dari API backend
- * MENGGUNAKAN endpoint /api/v1/klinik/reguler untuk memetakan Loket -> Clinics
+ * MENGGUNAKAN endpoint /api/v1/klinik/loket yang sudah terstruktur per Loket
*/
const fetchLoketFromAPI = async (force = false, retryCount = 0) => {
if (activeFetchPromise && retryCount === 0) {
@@ -280,9 +280,8 @@ export const useLoketStore = defineStore('loket', () => {
apiError.value = null;
try {
- console.log(`🔄 Fetching clinics for loket config (Try ${retryCount + 1})...`);
- // Endpoint ini memberikan list klinik, setiap klinik punya list loket
- const response = await fetch('http://10.10.150.131:8089/api/v1/klinik/reguler');
+ console.log(`🔄 [loketStore] Fetching loket configuration (Try ${retryCount + 1})...`);
+ const response = await fetch('http://10.10.150.131:8089/api/v1/klinik/loket');
if (!response.ok) {
if (response.status === 429 && retryCount < 3) {
@@ -295,69 +294,93 @@ export const useLoketStore = defineStore('loket', () => {
}
const rawData = await response.json();
- const clinics = rawData.data || [];
+ const loketsRaw = rawData.data || [];
- // AGGREGATE: Build Loket Map from Clinics
- const loketMap = new Map();
-
- clinics.forEach(clinic => {
- if (clinic.loket && Array.isArray(clinic.loket)) {
- clinic.loket.forEach(l => {
- const id = parseInt(l.idloket);
- if (!loketMap.has(id)) {
- loketMap.set(id, {
- id: id,
- namaLoket: l.namaloket,
- kodeLoket: l.kodeloket,
- kuota: clinic.kuota || 100, // Fallback
- pelayanan: [],
- _spesialisDetail: [],
- pembayaran: 'JKN',
- source: 'api',
- loketAktif: true
- });
- }
-
- const loketObj = loketMap.get(id);
- // Add clinic code to pelayanan if not already there
- if (!loketObj.pelayanan.includes(clinic.code)) {
- loketObj.pelayanan.push(clinic.code);
- loketObj._spesialisDetail.push({
- idklinik: clinic.idklinik,
- namaklinik: clinic.namaklinik,
- code: clinic.code
- });
- }
- });
- }
+ // MAPPING: Convert API Structure to Store Format
+ const mappedLokets = loketsRaw.map(l => {
+ const id = parseInt(l.idloket);
+
+ // Map pelayanan (clinic codes) dan detail spesialis
+ const spesialisDetail = (l.spesialis || []).map(s => {
+ // Coba cari kode klinik dari clinicStore untuk konsistensi
+ const clinic = clinicStore.clinics.find(c =>
+ String(c.id) === String(s.idklinik) ||
+ c.name === s.namaklinik
+ );
+
+ return {
+ idklinik: s.idklinik,
+ namaklinik: s.namaklinik,
+ code: clinic ? clinic.kode : (s.kode || s.idklinik)
+ };
+ });
+
+ // Extract unique codes for 'pelayanan' field
+ const pelayananCodes = [...new Set(spesialisDetail.map(s => s.code))];
+
+ // Map payment types
+ const pembayaranLabel = (l.pembayaran || [])
+ .map(p => p.pembayaran)
+ .filter(Boolean)
+ .join(', ') || 'JKN';
+
+ return {
+ id: id,
+ namaLoket: l.namaloket,
+ kodeLoket: l.kodeloket,
+ kuota: parseInt(l.kuotaloket) || 100,
+ pelayanan: pelayananCodes,
+ _spesialisDetail: spesialisDetail,
+ pembayaran: pembayaranLabel,
+ source: 'api',
+ loketAktif: l.loketaktif ?? true,
+ jenisloket: l.jenisloket,
+ tipeloket: l.tipeloket
+ };
});
- const lokets = Array.from(loketMap.values());
-
- // Sort by nama loket (LOKET 1, 2, ...)
- lokets.sort((a, b) => {
- const numA = parseInt(a.namaLoket.match(/\d+/)?.[0] || 0);
- const numB = parseInt(b.namaLoket.match(/\d+/)?.[0] || 0);
- return numA - numB;
- });
-
- // Set No sequential
- lokets.forEach((loket, idx) => {
+ // Sort by id for stability and set sequential No
+ mappedLokets.sort((a, b) => a.id - b.id);
+ mappedLokets.forEach((loket, idx) => {
loket.no = idx + 1;
});
- apiLoketData.value = lokets;
+ console.log(`✅ [loketStore] Successfully mapped ${mappedLokets.length} lokets from API`);
+
+ apiLoketData.value = mappedLokets;
lastSyncTimestamp.value = new Date().toISOString();
return {
success: true,
- message: `${lokets.length} loket berhasil dipetakan dari klinik`,
- data: lokets
+ message: `${mappedLokets.length} loket berhasil dikonfigurasi`,
+ data: mappedLokets
};
} catch (error) {
- console.error('❌ Error fetching loket config:', error);
+ console.error('❌ [loketStore] Error fetching loket config:', error);
apiError.value = error.message;
+
+ // FALLBACK: If API fails, populate with dummy Reguler data for Dev/Offline mode
+ if (apiLoketData.value.length === 0) {
+ console.warn('⚠️ [loketStore] Using FALLBACK data due to API failure...');
+ const dummyLokets = Array.from({length: 6}, (_, i) => ({
+ id: i + 1,
+ namaLoket: `LOKET ${i + 1} REG (Mock)`,
+ kodeLoket: `L${i+1}`,
+ kuota: 100,
+ pelayanan: ['UM', 'BP', 'OB', 'AN', 'IP', 'SR', 'TH', 'MT', 'KK', 'PR'], // Mock all services
+ _spesialisDetail: [], // Empty detail
+ pembayaran: 'BPJS, Umum',
+ source: 'api', // Mimic API
+ loketAktif: true,
+ jenisloket: 'REGULER',
+ tipeloket: 'REGULER',
+ no: i + 1
+ }));
+ apiLoketData.value = dummyLokets;
+ return { success: true, message: 'Menggunakan data fallback (API Offline)', warning: true };
+ }
+
return { success: false, message: `Gagal memuat: ${error.message}` };
} finally {
isLoadingAPI.value = false;
@@ -372,6 +395,7 @@ export const useLoketStore = defineStore('loket', () => {
return {
// State
loketData,
+ lokets: loketData, // Alias for backward compatibility/clarity
availableServices,
// API State
diff --git a/stores/queueStore.js b/stores/queueStore.js
index 2a8575c..e9a19b5 100644
--- a/stores/queueStore.js
+++ b/stores/queueStore.js
@@ -198,6 +198,7 @@ export const useQueueStore = defineStore('queue', () => {
noAntrian: `F-RA001 | Online - ${seedBarcode1}`, // Counter 1: Fast Track BPJS
shift: "Shift 1",
klinik: "KANDUNGAN",
+ kodeKlinik: "KD",
fastTrack: "YA", // Fast Track Patient
pembayaran: "BPJS",
status: "waiting",
@@ -218,6 +219,7 @@ export const useQueueStore = defineStore('queue', () => {
noAntrian: `RA002 | Online - ${seedBarcode2}`, // Counter 2: Non-fast track UMUM
shift: "Shift 1",
klinik: "IPD",
+ kodeKlinik: "IP",
fastTrack: "TIDAK",
pembayaran: "UMUM",
status: "waiting",
@@ -238,6 +240,7 @@ export const useQueueStore = defineStore('queue', () => {
noAntrian: `F-RA003 | Online - ${seedBarcode3}`, // Counter 3: Fast Track BPJS
shift: "Shift 1",
klinik: "SARAF",
+ kodeKlinik: "SR",
fastTrack: "YA", // Fast Track Patient
pembayaran: "BPJS",
status: "waiting",
@@ -258,6 +261,7 @@ export const useQueueStore = defineStore('queue', () => {
noAntrian: `RA004 | Online - ${seedBarcode4}`, // Counter 4: Non-fast track UMUM
shift: "Shift 1",
klinik: "THT",
+ kodeKlinik: "TH",
fastTrack: "TIDAK",
pembayaran: "UMUM",
status: "waiting",
@@ -278,6 +282,7 @@ export const useQueueStore = defineStore('queue', () => {
noAntrian: `RA005 | Online - ${seedBarcode5}`, // Counter 5: Non-fast track UMUM
shift: "Shift 2",
klinik: "KANDUNGAN",
+ kodeKlinik: "KD",
fastTrack: "TIDAK",
pembayaran: "UMUM",
status: "waiting",
@@ -298,6 +303,7 @@ export const useQueueStore = defineStore('queue', () => {
noAntrian: `F-RA006 | Online - ${seedBarcode6}`, // Counter 6: Fast Track BPJS
shift: "Shift 1",
klinik: "IPD",
+ kodeKlinik: "IP",
fastTrack: "YA", // Fast Track Patient
pembayaran: "BPJS",
status: "waiting",
@@ -318,6 +324,7 @@ export const useQueueStore = defineStore('queue', () => {
noAntrian: `RA007 | Online - ${seedBarcode7}`, // Counter 7: Non-fast track UMUM
shift: "Shift 1",
klinik: "SARAF",
+ kodeKlinik: "SR",
fastTrack: "TIDAK",
pembayaran: "UMUM",
status: "waiting",
@@ -338,6 +345,7 @@ export const useQueueStore = defineStore('queue', () => {
noAntrian: `RA008 | Online - ${seedBarcode8}`, // Counter 8: Non-fast track UMUM
shift: "Shift 1",
klinik: "THT",
+ kodeKlinik: "TH",
fastTrack: "TIDAK",
pembayaran: "UMUM",
status: "waiting",
@@ -358,6 +366,7 @@ export const useQueueStore = defineStore('queue', () => {
noAntrian: `F-RA009 | Online - ${seedBarcode9}`, // Counter 9: Fast Track BPJS
shift: "Shift 2",
klinik: "KANDUNGAN",
+ kodeKlinik: "KD",
fastTrack: "YA", // Fast Track Patient
pembayaran: "BPJS",
status: "waiting",
@@ -378,6 +387,7 @@ export const useQueueStore = defineStore('queue', () => {
noAntrian: `RA010 | Online - ${seedBarcode10}`, // Counter 10: Non-fast track UMUM
shift: "Shift 1",
klinik: "IPD",
+ kodeKlinik: "IP",
fastTrack: "TIDAK",
pembayaran: "UMUM",
status: "waiting",
@@ -398,6 +408,7 @@ export const useQueueStore = defineStore('queue', () => {
noAntrian: `RA011 | Online - ${seedBarcode11}`, // Counter 11: Non-fast track UMUM
shift: "Shift 1",
klinik: "SARAF",
+ kodeKlinik: "SR",
fastTrack: "TIDAK",
pembayaran: "UMUM",
status: "waiting",
@@ -418,6 +429,7 @@ export const useQueueStore = defineStore('queue', () => {
noAntrian: `F-RA012 | Online - ${seedBarcode12}`, // Counter 12: Fast Track BPJS
shift: "Shift 2",
klinik: "THT",
+ kodeKlinik: "TH",
fastTrack: "YA", // Fast Track Patient
pembayaran: "BPJS",
status: "waiting",
@@ -438,6 +450,7 @@ export const useQueueStore = defineStore('queue', () => {
noAntrian: `EA013 | Online - ${seedBarcodeE1}`, // Counter 13: Non-fast track Eksekutif
shift: "Shift 1",
klinik: "KANDUNGAN",
+ kodeKlinik: "KD",
fastTrack: "TIDAK",
pembayaran: "Eksekutif",
status: "waiting",
@@ -458,6 +471,7 @@ export const useQueueStore = defineStore('queue', () => {
noAntrian: `EA014 | Online - ${seedBarcodeE2}`, // Counter 14: Non-fast track Eksekutif
shift: "Shift 1",
klinik: "IPD",
+ kodeKlinik: "IP",
fastTrack: "TIDAK",
pembayaran: "Eksekutif",
status: "waiting",
@@ -478,6 +492,7 @@ export const useQueueStore = defineStore('queue', () => {
noAntrian: `F-EA015 | Online - ${seedBarcodeE3}`, // Counter 15: Fast Track Eksekutif
shift: "Shift 2",
klinik: "SARAF",
+ kodeKlinik: "SR",
fastTrack: "YA", // Fast Track Patient
pembayaran: "Eksekutif",
status: "waiting",
@@ -517,86 +532,94 @@ export const useQueueStore = defineStore('queue', () => {
// Initialize counters from seed data to ensure numbering continues correctly
// Counter SHARED untuk semua payment group dan semua jenis pasien
- const initializeCountersFromSeed = () => {
- if (typeof window === 'undefined') return; // Skip in SSR
+ // Calculate max queue number from seeds or existing patients
+ const syncCountersWithState = () => {
+ if (typeof window === 'undefined') return;
- const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
+ const today = new Date().toISOString().split('T')[0];
+ const STORAGE_KEY_BARCODE = `barcode_counter_${today.replace(/-/g, '').substring(2)}`;
- // Parse seed data to find max counter (SHARED untuk semua)
- let maxCounter = 0;
+ // Determine the source of patients (prefer current state, fallback to seeds)
+ const sourcePatients = allPatients.value.length > 0 ? allPatients.value : seedPatients;
- seedPatients.forEach(patient => {
- if (!patient.noAntrian) return;
-
- // Extract queue number from noAntrian (format: "F-RA001 | ..." or "RA001 | ..." or "EA001 | ...")
- // Remove "F-" prefix untuk mendapatkan nomor asli
- const queueNumberMatch = patient.noAntrian.match(/^(?:F-)?([RE])([A-N])(\d+)/);
- if (!queueNumberMatch) return;
-
- const loketLetter = queueNumberMatch[2]; // A-N
- const number = parseInt(queueNumberMatch[3], 10); // 001-999
-
- // Calculate counter from loket and number
- // Loket A = 1-999, Loket B = 1000-1998, etc.
- const loketIndex = loketLetter.charCodeAt(0) - 65; // A=0, B=1, etc.
- const counter = (loketIndex * 999) + number;
-
- // Update max counter (shared untuk semua)
- if (counter > maxCounter) {
- maxCounter = counter;
+ // Find max barcode counter
+ let maxBarcodeCounter = 0;
+ sourcePatients.forEach(patient => {
+ const barcode = patient.barcode || '';
+ if (barcode.length > 5) {
+ const counterStr = barcode.substring(barcode.length - 5);
+ const counter = parseInt(counterStr, 10);
+ if (!isNaN(counter) && counter > maxBarcodeCounter) {
+ maxBarcodeCounter = counter;
+ }
}
});
+
+ // Initialize barcode counter if not exists or smaller
+ const existingBarcode = localStorage.getItem(STORAGE_KEY_BARCODE);
+ if (!existingBarcode || parseInt(existingBarcode, 10) < maxBarcodeCounter) {
+ localStorage.setItem(STORAGE_KEY_BARCODE, maxBarcodeCounter.toString());
+ }
+
+ // Existing queue counter logic...
+ const counterKeyQueue = `queue_counter_loket_shared_${today}`;
- // Initialize localStorage counter (SHARED untuk semua payment group)
- if (maxCounter > 0) {
- const counterKey = `queue_counter_loket_shared_${today}`;
- const existing = localStorage.getItem(counterKey);
- if (!existing || parseInt(existing, 10) < maxCounter) {
- localStorage.setItem(counterKey, maxCounter.toString());
- }
+ // Calculate max queue number from seeds dynamically
+ const maxQueueCounter = sourcePatients.length > 0
+ ? Math.max(...sourcePatients.map(p => p.no))
+ : 0;
+
+ const existingQueue = localStorage.getItem(counterKeyQueue);
+ // Always update if current max in memory is larger than stored
+ if (!existingQueue || parseInt(existingQueue) < maxQueueCounter) {
+ localStorage.setItem(counterKeyQueue, maxQueueCounter.toString());
}
};
- // Initialize counters from seed data
- initializeCountersFromSeed();
-
- // Initialize state - will be automatically hydrated from localStorage by pinia-plugin-persistedstate
- // If localStorage has data, it will override these defaults
- const allPatients = ref(cloneSeed());
+ // Initial state - START EMPTY to avoid clashing with hydration
+ const allPatients = ref([]);
const quotaUsed = ref(5);
- const currentProcessingPatient = ref({
- loket: null,
- klinik: null,
- penunjang: null,
- });
+ // State - currentProcessingPatient is now an object mapping 'stage-id' to patient
+ // For example: { 'loket-1': patient, 'loket-2': patient, 'klinik-3': patient }
+ const currentProcessingPatient = ref({});
// Daftar klinik untuk dropdown diambil 1 pintu dari clinicStore
- const kliniks = computed(() => {
- const baseList = typeof clinicStore.getClinicsForDropdown === 'function'
- ? clinicStore.getClinicsForDropdown()
- : [];
+ const kliniks = ref(clinicStore.clinics || []);
- // Bentuk objek disesuaikan dengan yang dipakai di useQueue (id, name, kode)
- return baseList.map((c) => ({
- id: c.id,
- name: c.name,
- kode: c.kode,
- icon: c.icon,
- available: c.available,
- }));
- });
+ // Penunjang data - reference dari penunjangStore
+ const penunjangs = ref(penunjangStore.penunjangs || []);
- // Penunjang data - reference dari penunjangStore (single source of truth)
- // Menggunakan computed untuk reactive reference
- const penunjangs = computed(() => {
- // Get penunjang list from penunjangStore dan map ke format yang diharapkan
- const penunjangList = penunjangStore.penunjangList || [];
- return penunjangList.map(p => ({
- id: p.id,
- name: p.nama || p.name, // Support both nama and name for backward compatibility
- kode: p.kode
- }));
- });
+ /**
+ * Ensures initial data exists.
+ * Only seeds if the store is empty (not hydrated from storage)
+ */
+ const ensureInitialData = () => {
+ if (allPatients.value.length === 0) {
+ console.log('🌱 Seeding queueStore with initial data...');
+ allPatients.value = cloneSeed();
+ }
+ // ALWAYS sync counters with state (hydrated or seeded) to ensure next number is correct
+ syncCountersWithState();
+ };
+
+ // CROSS-TAB SYNC: Listen for storage events to update store across tabs
+ if (typeof window !== 'undefined') {
+ window.addEventListener('storage', (event) => {
+ if (event.key === 'queue-store-state') {
+ console.log('🔄 queue-store-state changed in another tab, re-hydrating...');
+ try {
+ const newState = JSON.parse(event.newValue);
+ if (newState) {
+ if (newState.allPatients) allPatients.value = newState.allPatients;
+ if (newState.quotaUsed !== undefined) quotaUsed.value = newState.quotaUsed;
+ if (newState.currentProcessingPatient) currentProcessingPatient.value = newState.currentProcessingPatient;
+ }
+ } catch (e) {
+ console.error('Error hydrating from storage event:', e);
+ }
+ }
+ });
+ }
// Computed - Filter berdasarkan process stage dan status
const getPatientsByStage = (stage) => {
@@ -634,10 +657,13 @@ export const useQueueStore = defineStore('queue', () => {
allPatients.value = cloneSeed();
quotaUsed.value = 5;
currentProcessingPatient.value = { loket: null, klinik: null, penunjang: null };
+ syncCountersWithState(); // Re-initialize counters after reset
};
- // Actions
- const callNext = (adminType = 'loket') => {
+ // Action: Ambil Antrean Masuk dari Anjungan (Reservoir -> Waiting Room)
+ // "fungsi ini juga sesuaikan dengan idloket dan klinik id"
+ // "hanya memanggil tiket dari loket lain harus tiket menunggu yang sesuai id loket dan id kliniknya"
+ const callNext = (adminType = 'loket', specificId = null) => {
const stageMap = {
'loket': 'loket',
'klinik': 'klinik',
@@ -645,114 +671,142 @@ export const useQueueStore = defineStore('queue', () => {
};
const targetStage = stageMap[adminType];
- // Prioritaskan pasien dengan status 'menunggu' (yang belum dipanggil)
- const nextPatient = allPatients.value.find(p =>
- p.status === 'menunggu' && p.processStage === targetStage
- ) || allPatients.value.find(p =>
- p.status === 'waiting' && p.processStage === targetStage
- );
+ const targetId = specificId;
+
+ // Filter list by stage AND relevance to this specific loket/clinic/penunjang
+ const eligiblePatients = allPatients.value.filter(p => {
+ // 1. Stage Check
+ if (p.processStage !== targetStage) return false;
+
+ // 2. Relevance Check based on adminType
+ if (targetId) {
+ if (adminType === 'loket') {
+ const thisLoket = loketStore.getLoketById(parseInt(targetId));
+ // Enforce clinic mapping (pelayanan)
+ if (thisLoket && thisLoket.pelayanan && Array.isArray(thisLoket.pelayanan)) {
+ if (!thisLoket.pelayanan.includes(p.kodeKlinik)) return false;
+ }
+ // Enforce assignment if present
+ if (p.loketId && String(p.loketId) !== String(targetId)) return false;
+ }
+ else if (adminType === 'klinik') {
+ // Clinic ID match
+ if (String(p.kodeKlinik) !== String(targetId) && String(p.klinik) !== String(targetId)) return false;
+ }
+ else if (adminType === 'penunjang') {
+ // Penunjang match
+ if (String(p.klinik) !== String(targetId) && String(p.kodeKlinik) !== String(targetId)) return false;
+ }
+ }
+
+ return true;
+ });
+
+ // PRIORITAS: Hanya ambil pasien dengan status 'menunggu' (Antrean Baru dari Anjungan)
+ // "bukan untuk memanggil pasien tapi tiket baru dari anjungan yang statusnya menunggu"
+ const nextPatient = eligiblePatients.find(p => p.status === 'menunggu');
if (!nextPatient) {
- return { success: false, message: "Tidak ada pasien selanjutnya" };
+ return { success: false, message: `Tidak ada antrean baru yang sesuai untuk ${adminType} ${targetId || ''}` };
}
- // Hitung kuota yang tersedia
- const menungguCount = allPatients.value.filter(p =>
- p.status === 'menunggu' && p.processStage === targetStage
- ).length;
+ // Hitung kuota yang tersedia (khusus loket ini)
const diLoketCount = allPatients.value.filter(p =>
- p.status === 'di-loket' && p.processStage === targetStage
+ p.status === 'di-loket' &&
+ p.processStage === targetStage &&
+ (targetId && adminType === 'loket' ? String(p.loketId) === String(targetId) : true)
).length;
const availableQuota = 150 - diLoketCount;
if (availableQuota <= 0) {
return { success: false, message: "Kuota sudah penuh" };
}
-
- if (menungguCount === 0) {
- return { success: false, message: "Tidak ada pasien yang menunggu untuk dipanggil" };
- }
- // Langsung update status menjadi 'waiting' (sudah dipanggil, bisa check-in)
+ // Update status menjadi 'waiting' (Masuk ke antrean aktif)
const callTimestamp = new Date().toISOString();
const index = allPatients.value.findIndex(p => p.no === nextPatient.no);
if (index !== -1) {
allPatients.value[index] = {
...allPatients.value[index],
- status: "waiting", // Status "waiting" = sudah dipanggil, bisa check-in
- lastCalledAt: callTimestamp // Track waktu panggilan untuk multiple calls
+ status: "waiting",
+ // Assign to this specific loket if it was unassigned
+ loketId: (adminType === 'loket' && targetId) ? targetId : allPatients.value[index].loketId,
+ lastCalledAt: callTimestamp
};
}
return {
success: true,
- message: `Memanggil pasien ${nextPatient.noAntrian.split(" |")[0]}`,
+ message: `Berhasil mengambil antrean ${nextPatient.noAntrian.split(" |")[0]} ke daftar tunggu`,
};
};
- const callMultiplePatients = (count, adminType = 'loket') => {
- const stageMap = {
- 'loket': 'loket',
- 'klinik': 'klinik',
- 'penunjang': 'penunjang'
- };
-
+ const callMultiplePatients = (count, adminType = 'loket', specificId = null) => {
+ const stageMap = { 'loket': 'loket', 'klinik': 'klinik', 'penunjang': 'penunjang' };
const targetStage = stageMap[adminType];
- // Prioritaskan pasien dengan status 'menunggu' (yang belum dipanggil)
- const menungguList = allPatients.value.filter(p =>
- p.status === 'menunggu' && p.processStage === targetStage
- );
- const waitingList = allPatients.value.filter(p =>
- p.status === 'waiting' && p.processStage === targetStage
- );
-
- // Gabungkan: menunggu dulu, baru waiting
- const combinedList = [...menungguList, ...waitingList];
+ const targetId = specificId;
- if (combinedList.length === 0) {
- return { success: false, message: "Tidak ada pasien yang menunggu" };
+ const eligiblePatients = allPatients.value.filter(p => {
+ if (p.processStage !== targetStage) return false;
+ if (targetId) {
+ if (adminType === 'loket') {
+ const thisLoket = loketStore.getLoketById(parseInt(targetId));
+ if (thisLoket && thisLoket.pelayanan && Array.isArray(thisLoket.pelayanan)) {
+ if (!thisLoket.pelayanan.includes(p.kodeKlinik)) return false;
+ }
+ if (p.loketId && String(p.loketId) !== String(targetId)) return false;
+ } else if (adminType === 'klinik' || adminType === 'penunjang') {
+ if (String(p.kodeKlinik) !== String(targetId) && String(p.klinik) !== String(targetId)) return false;
+ }
+ }
+ return true;
+ });
+
+ // Hanya ambil pasien status 'menunggu'
+ const menungguList = eligiblePatients.filter(p => p.status === 'menunggu');
+
+ if (menungguList.length === 0) {
+ return { success: false, message: `Tidak ada antrean baru yang sesuai untuk ${adminType} ${targetId || ''}` };
}
// Hitung kuota yang tersedia
const diLoketCount = allPatients.value.filter(p =>
- p.status === 'di-loket' && p.processStage === targetStage
+ p.status === 'di-loket' &&
+ p.processStage === targetStage &&
+ (targetId && adminType === 'loket' ? String(p.loketId) === String(targetId) : true)
).length;
const availableQuota = 150 - diLoketCount;
- // Kuota yang bisa dipanggil = min(count yang diminta, jumlah pasien menunggu, kuota tersedia)
const maxCallable = Math.min(count, menungguList.length, availableQuota);
if (maxCallable <= 0) {
- if (menungguList.length === 0) {
- return { success: false, message: "Tidak ada pasien yang menunggu untuk dipanggil" };
- }
- if (availableQuota <= 0) {
- return { success: false, message: "Kuota sudah penuh. Tidak bisa memanggil pasien lagi" };
- }
+ if (availableQuota <= 0) return { success: false, message: "Kuota sudah penuh" };
+ return { success: false, message: "Tidak ada antrean yang bisa diambil" };
}
- const patientsToCall = combinedList.slice(0, maxCallable);
-
- // Langsung update status menjadi 'waiting'
+ const patientsToCall = menungguList.slice(0, maxCallable);
const callTimestamp = new Date().toISOString();
+
patientsToCall.forEach((patient) => {
const index = allPatients.value.findIndex(p => p.no === patient.no);
if (index !== -1) {
- allPatients.value[index] = {
- ...allPatients.value[index],
+ allPatients.value[index] = {
+ ...allPatients.value[index],
status: "waiting",
- lastCalledAt: callTimestamp // Track waktu panggilan untuk multiple calls
+ loketId: (adminType === 'loket' && targetId) ? targetId : allPatients.value[index].loketId,
+ lastCalledAt: callTimestamp
};
}
});
return {
success: true,
- message: `Memanggil ${patientsToCall.length} pasien`,
+ message: `Berhasil mengambil ${patientsToCall.length} antrean ke daftar tunggu`,
};
};
- const processPatient = (patient, action, adminType = 'loket') => {
+ const processPatient = (patient, action, adminType = 'loket', specificId = null) => {
+ const storageKey = specificId ? `${adminType}-${specificId}` : adminType;
const patientCode = patient.noAntrian.split(" |")[0];
let message = "";
@@ -793,8 +847,8 @@ export const useQueueStore = defineStore('queue', () => {
}
// Clear current processing di admin yang melakukan check-in
- if (currentProcessingPatient.value[adminType]?.no === patient.no) {
- currentProcessingPatient.value[adminType] = null;
+ if (currentProcessingPatient.value[storageKey]?.no === patient.no) {
+ currentProcessingPatient.value[storageKey] = null;
}
break;
@@ -804,8 +858,8 @@ export const useQueueStore = defineStore('queue', () => {
status: "terlambat",
calledByAdmin: false // Reset flag
};
- if (currentProcessingPatient.value[adminType]?.no === patient.no) {
- currentProcessingPatient.value[adminType] = null;
+ if (currentProcessingPatient.value[storageKey]?.no === patient.no) {
+ currentProcessingPatient.value[storageKey] = null;
}
message = `Pasien ${patientCode} ditandai terlambat`;
break;
@@ -816,8 +870,8 @@ export const useQueueStore = defineStore('queue', () => {
status: "pending",
calledByAdmin: false // Reset flag
};
- if (currentProcessingPatient.value[adminType]?.no === patient.no) {
- currentProcessingPatient.value[adminType] = null;
+ if (currentProcessingPatient.value[storageKey]?.no === patient.no) {
+ currentProcessingPatient.value[storageKey] = null;
}
message = `Pasien ${patientCode} di-pending`;
break;
@@ -862,8 +916,8 @@ export const useQueueStore = defineStore('queue', () => {
};
allPatients.value[patientIndex] = updatedPatient;
- // Set currentProcessingPatient dengan data terbaru
- currentProcessingPatient.value[adminType] = updatedPatient;
+ // Set currentProcessingPatient with isolated key
+ currentProcessingPatient.value[storageKey] = updatedPatient;
} else {
// Untuk adminType selain loket, update status jika perlu
if (shouldUpdateStatus) {
@@ -872,9 +926,9 @@ export const useQueueStore = defineStore('queue', () => {
status: "di-loket"
};
allPatients.value[patientIndex] = updatedPatient;
- currentProcessingPatient.value[adminType] = updatedPatient;
+ currentProcessingPatient.value[storageKey] = updatedPatient;
} else {
- currentProcessingPatient.value[adminType] = patient;
+ currentProcessingPatient.value[storageKey] = patient;
}
}
message = `Memproses pasien ${patientCode}`;
@@ -885,34 +939,12 @@ export const useQueueStore = defineStore('queue', () => {
return { success: true, message };
};
- const processNextQueue = (adminType = 'loket') => {
- const stageMap = {
- 'loket': 'loket',
- 'klinik': 'klinik',
- 'penunjang': 'penunjang'
- };
-
- const targetStage = stageMap[adminType];
- const nextPatient = allPatients.value.find(p =>
- p.status === 'di-loket' && p.processStage === targetStage
- );
- if (!nextPatient) {
- return { success: false, message: "Tidak ada pasien di loket yang dapat diproses" };
- }
- // Set sebagai current processing patient
- currentProcessingPatient.value[adminType] = nextPatient;
-
- return {
- success: true,
- message: `Memproses pasien ${nextPatient.noAntrian.split(" |")[0]}`,
- };
- };
-
- const callProcessingPatient = (adminType = 'loket') => {
+ const callProcessingPatient = (adminType = 'loket', specificId = null) => {
// Panggil pasien yang sedang diproses untuk ditampilkan di layar anjungan
- const processingPatient = currentProcessingPatient.value[adminType];
+ const storageKey = specificId ? `${adminType}-${specificId}` : adminType;
+ const processingPatient = currentProcessingPatient.value[storageKey];
if (!processingPatient) {
return { success: false, message: "Tidak ada pasien yang sedang diproses" };
@@ -928,7 +960,7 @@ export const useQueueStore = defineStore('queue', () => {
};
// Update currentProcessingPatient with the new data
- currentProcessingPatient.value[adminType] = allPatients.value[patientIndex];
+ currentProcessingPatient.value[storageKey] = allPatients.value[patientIndex];
return {
success: true,
@@ -956,12 +988,31 @@ export const useQueueStore = defineStore('queue', () => {
fastTrack: "TIDAK",
pembayaran: patient ? patient.pembayaran : "UMUM",
noRM: patient ? (patient.noRM || `RM-SCAN-${barcode.slice(-6)}`) : `RM-SCAN-${barcode.slice(-6)}`,
+ kodeKlinik: klinik.kode || klinik.id, // Ensure kodeKlinik is saved
status: "di-loket",
processStage: "klinik",
createdAt: timestamp.toISOString(),
referencePatient: patient ? patient.noAntrian : null,
+ loketId: null, // Initial check
};
+ // Auto-assign Loket ID based on Clinic Mapping
+ // "menyesuaikan loketnya berdasar loket id tergantung dari create tiket itu di klinik"
+ if (newPatient.kodeKlinik) {
+ const allLokets = loketStore.lokets || [];
+ const targetLoket = allLokets.find(l =>
+ l.pelayanan && Array.isArray(l.pelayanan) && l.pelayanan.includes(newPatient.kodeKlinik)
+ );
+
+ if (targetLoket) {
+ newPatient.loketId = targetLoket.id;
+ newPatient.loket = targetLoket.namaLoket;
+ console.log(`✅ Auto-assigned Ticket ${newPatient.noAntrian} to Loket ${targetLoket.id} (${targetLoket.namaLoket})`);
+ } else {
+ console.log(`⚠️ No specific Loket found for Clinic ${newPatient.kodeKlinik}, defaulting to general pool.`);
+ }
+ }
+
allPatients.value.push(newPatient);
// Increment counter setelah barcode digunakan
@@ -1348,227 +1399,167 @@ export const useQueueStore = defineStore('queue', () => {
p.processStage === 'klinik-ruang' &&
(p.status === 'waiting' || (allowMultiple && p.status === 'di-loket'))
).sort((a, b) => {
- // Sort by status priority first
- const statusPriority = {
- 'waiting': 1,
- 'di-loket': 2
- };
+ // Sort by status priority first: waiting=1, di-loket=2
+ const statusPriority = { 'waiting': 1, 'di-loket': 2 };
const priorityDiff = (statusPriority[a.status] || 99) - (statusPriority[b.status] || 99);
if (priorityDiff !== 0) return priorityDiff;
- // Then sort by queue number (extract from noAntrian)
- const numA = parseInt(a.noAntrian.match(/\d+/)?.[0] || '999');
- const numB = parseInt(b.noAntrian.match(/\d+/)?.[0] || '999');
+ const numA = parseInt(a.noAntrian?.match(/\d+/)?.[0] || '999');
+ const numB = parseInt(b.noAntrian?.match(/\d+/)?.[0] || '999');
return numA - numB;
});
if (patients.length === 0) {
- return { success: false, message: `Tidak ada antrian ${tipeLayanan} yang menunggu di ruang ini` };
+ return { success: false, message: `Tidak ada antrian ${tipeLayanan} yang menunggu` };
}
const nextPatient = patients[0];
- const patientIndex = allPatients.value.findIndex(p => p.no === nextPatient.no);
-
- if (patientIndex !== -1) {
- // If allowMultiple, we can call even if already di-loket (just update timestamp)
- // Otherwise, only update if status is waiting
- if (allowMultiple || nextPatient.status === 'waiting') {
- allPatients.value[patientIndex] = {
- ...allPatients.value[patientIndex],
- status: "di-loket",
- lastCalledAt: new Date().toISOString() // Track last call time
- };
- }
+ const index = allPatients.value.findIndex(p => p.no === nextPatient.no);
+ if (index !== -1) {
+ allPatients.value[index] = {
+ ...allPatients.value[index],
+ status: "di-loket",
+ lastCalledAt: new Date().toISOString()
+ };
}
-
- return {
- success: true,
- message: `Memanggil pasien ${nextPatient.noAntrian.split(" |")[0]} untuk ${tipeLayanan}`,
- patient: allPatients.value[patientIndex],
+ return {
+ success: true,
+ message: `Memanggil ${nextPatient.noAntrian.split(" |")[0]}`,
+ patient: allPatients.value[index]
};
};
// Process patient in klinik ruang (set as current processing)
const processPatientKlinikRuang = (patient, action, kodeKlinik, nomorRuang) => {
const patientIndex = allPatients.value.findIndex(p => p.no === patient.no);
-
- if (patientIndex === -1) {
- return { success: false, message: "Pasien tidak ditemukan" };
- }
+ if (patientIndex === -1) return { success: false, message: "Pasien tidak ditemukan" };
- const patientCode = patient.noAntrian.split(" |")[0];
- const key = `klinik-ruang-${kodeKlinik}-${nomorRuang}`;
+ const specificId = `${kodeKlinik}-${nomorRuang}`;
+ const storageKey = `klinik-ruang-${specificId}`;
+ const pCode = patient.noAntrian.split(" |")[0];
let message = "";
switch (action) {
case "proses":
- // Set as current processing for this room (1 pasien, tidak dipisah per tipe layanan)
- currentProcessingPatient.value = {
- ...currentProcessingPatient.value,
- [key]: allPatients.value[patientIndex]
- };
- message = `Memproses pasien ${patientCode}`;
+ currentProcessingPatient.value[storageKey] = allPatients.value[patientIndex];
+ message = `Memproses ${pCode}`;
break;
-
case "selesai":
- allPatients.value[patientIndex] = {
- ...allPatients.value[patientIndex],
- status: "processed"
- };
- // Clear current processing
- currentProcessingPatient.value[key] = null;
- message = `Pasien ${patientCode} selesai diproses`;
+ allPatients.value[patientIndex] = { ...allPatients.value[patientIndex], status: "processed" };
+ currentProcessingPatient.value[storageKey] = null;
+ message = `Pasien ${pCode} selesai diproses`;
break;
-
case "terlambat":
- allPatients.value[patientIndex] = {
- ...allPatients.value[patientIndex],
- status: "terlambat"
- };
- currentProcessingPatient.value[key] = null;
- message = `Pasien ${patientCode} ditandai terlambat`;
+ allPatients.value[patientIndex] = { ...allPatients.value[patientIndex], status: "terlambat" };
+ currentProcessingPatient.value[storageKey] = null;
+ message = `Pasien ${pCode} ditandai terlambat`;
break;
-
case "pending":
- allPatients.value[patientIndex] = {
- ...allPatients.value[patientIndex],
- status: "pending"
- };
- currentProcessingPatient.value[key] = null;
- message = `Pasien ${patientCode} di-pending`;
+ allPatients.value[patientIndex] = { ...allPatients.value[patientIndex], status: "pending" };
+ currentProcessingPatient.value[storageKey] = null;
+ message = `Pasien ${pCode} di-pending`;
break;
}
-
return { success: true, message };
};
- const changeKlinik = (patient, newKlinik, adminType = 'loket') => {
- const patientIndex = allPatients.value.findIndex((p) => p.no === patient.no);
-
- if (patientIndex !== -1) {
- // Update dengan cara yang Vue reactive
- allPatients.value[patientIndex] = {
- ...allPatients.value[patientIndex],
- klinik: newKlinik.name
- };
-
- // Update current processing if it's the same patient
- if (currentProcessingPatient.value[adminType]?.no === patient.no) {
- currentProcessingPatient.value[adminType] = {
- ...currentProcessingPatient.value[adminType],
- klinik: newKlinik.name
- };
- }
+ const changeKlinik = (patient, newKlinik, adminType = 'loket', specificId = null) => {
+ const patientIndex = allPatients.value.findIndex(p => p.no === patient.no);
+ if (patientIndex === -1) return { success: false, message: "Pasien tidak ditemukan" };
- return {
- success: true,
- message: `Klinik berhasil diubah ke ${newKlinik.name}`,
+ allPatients.value[patientIndex] = {
+ ...allPatients.value[patientIndex],
+ klinik: newKlinik.name,
+ kodeKlinik: newKlinik.kode
+ };
+
+ const key = specificId ? `${adminType}-${specificId}` : adminType;
+ if (currentProcessingPatient.value[key]?.no === patient.no) {
+ currentProcessingPatient.value[key] = {
+ ...currentProcessingPatient.value[key],
+ klinik: newKlinik.name,
+ kodeKlinik: newKlinik.kode
};
}
-
- return { success: false, message: "Pasien tidak ditemukan" };
+ return { success: true, message: `Klinik berhasil diubah ke ${newKlinik.name}` };
};
- const getCurrentProcessing = (adminType) => {
- return computed(() => currentProcessingPatient.value[adminType]);
+ const getCurrentProcessing = (adminType, id = null) => {
+ const key = id ? `${adminType}-${id}` : adminType;
+ return computed(() => currentProcessingPatient.value[key] || null);
};
- const setCurrentProcessing = (patient, adminType) => {
- currentProcessingPatient.value[adminType] = patient;
+ const setCurrentProcessing = (patient, adminType, id = null) => {
+ const key = id ? `${adminType}-${id}` : adminType;
+ currentProcessingPatient.value[key] = patient;
};
- // Helper function untuk generate nomor antrean baru
- // Format: R/E + Loket (A-N) + 3 digit
- // Format: R/E (jenis pelayanan Reguler/Eksekutif) + A-N (nomor loket) + 3 digit angka
- // Contoh: RA001, EA001, RB001, RB002
- // - R = Reguler (untuk pasien Reguler/BPJS)
- // - E = Eksekutif (untuk pasien Eksekutif/Grand Pavilion)
- // IMPORTANT: Counter SHARED untuk semua payment group (JKN/UMUM) dan semua jenis (fast track/non-fast track)
- // Prefix "F-" hanya untuk menandai status fast track, bukan untuk memisahkan counter
- // Setiap loket menampung maksimal 999 nomor (001-999)
const generateQueueNumber = (clinic, paymentType, isEksekutif = false) => {
- // Tentukan prefix jenis pelayanan: R untuk Reguler, E untuk Eksekutif (Grand Pavilion)
const serviceType = isEksekutif ? 'E' : 'R';
-
- // Counter SHARED untuk semua payment group dan semua jenis pasien
- // Tidak ada pemisahan counter berdasarkan payment group atau fast track
- const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
+ const today = new Date().toISOString().split('T')[0];
+ // FIXED: Use the same key as initializeCountersFromSeed
const counterKey = `queue_counter_loket_shared_${today}`;
- // Get current counter dari localStorage
let counter = 0;
if (typeof window !== 'undefined') {
const stored = localStorage.getItem(counterKey);
counter = stored ? parseInt(stored, 10) : 0;
}
- // Increment counter
counter = counter + 1;
+ const loketIndex = Math.floor((counter - 1) / 999);
- // Tentukan loket berdasarkan counter
- // Loket A untuk 1-999, B untuk 1000-1998, C untuk 2000-2997, dst sampai N (14 loket)
- // Setiap loket menampung maksimal 999 nomor (001-999)
- // Loket index: 0=A, 1=B, 2=C, ..., 13=N
- const loketIndex = Math.floor((counter - 1) / 999); // 0 untuk A, 1 untuk B, dst
-
- // Jika loket melebihi N (14 loket, index 13), wrap kembali ke A
- // Maksimal 14 loket (A-N), jadi maksimal counter per hari = 14 * 999 = 13986
- // Jika melebihi, reset counter ke 1
if (loketIndex > 13) {
counter = 1;
- if (typeof window !== 'undefined') {
- localStorage.setItem(counterKey, '1');
- }
- // Format: R/E + Loket A + 001
+ if (typeof window !== 'undefined') localStorage.setItem(counterKey, '1');
return `${serviceType}A001`;
}
- // Loket letter (A-N)
- const loketLetter = String.fromCharCode(65 + loketIndex); // 65 = 'A', 66 = 'B', dst
-
- // Nomor dalam loket (1-999, di-display sebagai 001-999)
- // counter 1-999 → loket A, nomor 001-999
- // counter 1000-1998 → loket B, nomor 001-999
+ const loketLetter = String.fromCharCode(65 + loketIndex);
const numberInLoket = ((counter - 1) % 999) + 1;
const numberPart = String(numberInLoket).padStart(3, '0');
- // Save counter back to localStorage
- if (typeof window !== 'undefined') {
- localStorage.setItem(counterKey, counter.toString());
- }
+ if (typeof window !== 'undefined') localStorage.setItem(counterKey, counter.toString());
- // Format: R/E + Loket + 3 digit
- // Contoh: RA001, RA002, ..., RA999, RB001, RB002, ...
return `${serviceType}${loketLetter}${numberPart}`;
};
// Register patient from Anjungan (onsite registration)
- const registerPatientFromAnjungan = (clinic, paymentType, visitType = 'SEKARANG', visitDate = null, shift = 'Shift 1', namaDokter = null, isFastTrack = false, fastTrackData = null) => {
+ const registerPatientFromAnjungan = (clinic, paymentType, visitType = 'SEKARANG', visitDate = null, shift = 'Shift 1', namaDokter = null, isFastTrack = false, fastTrackData = null, loketId = null, loket = null) => {
+ // 1. Validasi keberadaan (prevent duplicates)
+ // Gunakan date today untuk check-in sync
+ const timestamp = new Date();
+
+ // Generate barcode FIRST to check for existence
+ const barcode = generateBarcode([], allPatients);
+
+ // Check EXACT barcode match prevent duplicate submission
+ const duplicate = allPatients.value.find(p => p.barcode === barcode);
+ if (duplicate) {
+ return {
+ success: false,
+ message: "Pasien dengan barcode ini sudah terdaftar untuk hari ini.",
+ patient: duplicate
+ };
+ }
+
const newNo = allPatients.value.length > 0
? Math.max(...allPatients.value.map(p => p.no)) + 1
: 1;
- const timestamp = new Date();
const visitDateTime = visitDate ? new Date(visitDate) : timestamp;
const jamPanggil = visitDate
? `${String(visitDateTime.getHours()).padStart(2, "0")}:${String(visitDateTime.getMinutes()).padStart(2, "0")}`
: `${String(timestamp.getHours()).padStart(2, "0")}:${String(timestamp.getMinutes()).padStart(2, "0")}`;
- // Generate barcode dengan format: YYMMDD + 5 digit sequential
- const barcode = generateBarcode([], allPatients);
-
// Tentukan apakah pasien Eksekutif/Grand Pavilion (jika ada namaDokter, berarti Eksekutif)
const isEksekutif = namaDokter !== null && namaDokter !== undefined && namaDokter !== '';
// Generate nomor antrean dengan format baru: R/E + Loket (A-N) + 3 digit
- // Format: RA001 (Reguler), EA001 (Eksekutif/Grand Pavilion), RB001, RB002, dll
- // Untuk Fast Track, tambahkan prefix "F-" di depan: F-RA001, F-EA001, dll
const queueNumber = generateQueueNumber(clinic, paymentType, isEksekutif);
const finalQueueNumber = isFastTrack ? `F-${queueNumber}` : queueNumber;
const noAntrian = `${finalQueueNumber} | Onsite - ${barcode}`;
- // Status awal untuk pasien dari anjungan adalah "menunggu" (belum dipanggil)
- // Hanya setelah dipanggil oleh admin loket, status berubah menjadi "waiting" (bisa check-in)
const status = 'menunggu';
const newPatient = {
@@ -1578,32 +1569,45 @@ export const useQueueStore = defineStore('queue', () => {
noAntrian: noAntrian,
shift: shift,
klinik: clinic.name || clinic,
+ kodeKlinik: clinic.kode || null, // Essential for filtering
fastTrack: isFastTrack ? "YA" : "TIDAK",
pembayaran: paymentType,
- noRM: `RM-${barcode.slice(-6)}`, // Generate No. RM from barcode suffix
+ noRM: `RM-${barcode.slice(-6)}`,
status: status,
processStage: "loket",
createdAt: timestamp.toISOString(),
registrationType: 'onsite',
visitType: visitType,
visitDate: visitDate || timestamp.toISOString().substring(0, 10),
- namaDokter: namaDokter || null, // Nama dokter hanya untuk pasien Eksekutif
- calledByAdmin: false, // Flag untuk tracking apakah sudah dipanggil oleh admin
- // Fast Track data
+ namaDokter: namaDokter || null,
+ loketId: loketId, // Optional loketId
+ loket: loket, // Optional loket name
+ calledByAdmin: false,
penanggungJawab: (isFastTrack && fastTrackData) ? fastTrackData.penanggungJawab : null,
alasanFastTrack: (isFastTrack && fastTrackData) ? fastTrackData.alasanFastTrack : null,
};
+ // Auto-assign Loket ID if not provided, based on Clinic Mapping
+ // fulfill requirement: "adjust based on loket id depending on creation"
+ if (!newPatient.loketId && newPatient.kodeKlinik) {
+ const allLokets = loketStore.lokets || [];
+ const targetLoket = allLokets.find(l =>
+ l.pelayanan && Array.isArray(l.pelayanan) && l.pelayanan.includes(newPatient.kodeKlinik)
+ );
+
+ if (targetLoket) {
+ newPatient.loketId = targetLoket.id;
+ if (!newPatient.loket) newPatient.loket = targetLoket.namaLoket;
+ console.log(`✅ Auto-assigned Onsite Ticket ${newPatient.noAntrian} to Loket ${targetLoket.id} (${targetLoket.namaLoket})`);
+ }
+ }
+
allPatients.value.push(newPatient);
-
- // IMPORTANT: Increment counter SETELAH barcode benar-benar digunakan untuk membuat pasien
- // Ini mencegah counter naik meskipun barcode tidak digunakan
- // registerPatientFromAnjungan selalu membuat pasien baru, jadi selalu increment counter
incrementBarcodeCounter();
return {
success: true,
- message: `Pendaftaran ${clinic.name || clinic} untuk kunjungan ${visitType === 'SEKARANG' ? 'HARI INI' : visitDate} dengan pembayaran ${paymentType} berhasil diproses.`,
+ message: `Pendaftaran ${clinic.name || clinic} berhasil diproses.`,
patient: newPatient,
};
};
@@ -1700,12 +1704,80 @@ export const useQueueStore = defineStore('queue', () => {
};
};
- // State persistence is handled automatically by pinia-plugin-persistedstate
- // No need for manual watch or save logic
+ // FIX: Explicit implementation of processNextQueue to prevent cross-loket leakage
+ const processNextQueueCorrected = (adminType = 'loket', specificId = null) => {
+ // 1. Determine target stage and key
+ const stageMap = { 'loket': 'loket', 'klinik': 'klinik', 'penunjang': 'penunjang' };
+ const targetStage = stageMap[adminType];
+ const key = specificId ? `${adminType}-${specificId}` : adminType;
+
+ console.log(`🚀 [processNextQueue] Processing for ${key} (Stage: ${targetStage})`);
+
+ // 2. Find next patient (Status: 'menunggu' -> 'waiting')
+ // Prioritaskan yang assigned ke loket ini (loketId) jika ada match
+ let nextPatient = null;
+
+ if (adminType === 'loket' && specificId) {
+ // Cari yang spesifik untuk loket ini dulu
+ nextPatient = allPatients.value.find(p =>
+ p.status === 'menunggu' &&
+ p.processStage === targetStage &&
+ String(p.loketId) === String(specificId)
+ );
+ }
+
+ // Jika tidak ada yang spesifik, cari yang umum (menunggu & loketId null/match)
+ if (!nextPatient) {
+ nextPatient = allPatients.value.find(p =>
+ p.status === 'menunggu' &&
+ p.processStage === targetStage &&
+ (adminType !== 'loket' || !p.loketId || String(p.loketId) === String(specificId))
+ );
+ }
+
+ // Fallback: Check 'waiting' status if needed (though usually 'menunggu' is for calling)
+ if (!nextPatient) {
+ nextPatient = allPatients.value.find(p =>
+ p.status === 'waiting' &&
+ p.processStage === targetStage &&
+ (adminType !== 'loket' || String(p.loketId) === String(specificId))
+ );
+ }
+
+ if (!nextPatient) {
+ return { success: false, message: "Tidak ada antrean yang menunggu untuk diproses." };
+ }
+
+ // 3. Update Patient Status (di-loket)
+ const index = allPatients.value.findIndex(p => p.no === nextPatient.no);
+ if (index !== -1) {
+ const updatedPatient = {
+ ...allPatients.value[index],
+ status: 'di-loket',
+ loketId: specificId || allPatients.value[index].loketId, // Ensure loketId is set
+ lastCalledAt: new Date().toISOString()
+ };
+
+ allPatients.value[index] = updatedPatient;
+
+ // 4. Set Current Processing ISOLATED by key
+ currentProcessingPatient.value[key] = updatedPatient;
+
+ return {
+ success: true,
+ message: `Memproses ${nextPatient.noAntrian.split(" |")[0]}`,
+ patient: updatedPatient
+ };
+ }
+
+ return { success: false, message: "Gagal memproses antrean." };
+ };
return {
// State
allPatients,
+ resetPatients,
+ ensureInitialData,
quotaUsed,
currentProcessingPatient,
kliniks,
@@ -1729,7 +1801,7 @@ export const useQueueStore = defineStore('queue', () => {
changeKlinik,
pindahKlinikRuang,
konsultasiKlinikRuang,
- processNextQueue,
+ processNextQueue: processNextQueueCorrected,
getPatientsByStage,
getTotalPasienByStage,
getCurrentProcessing,
@@ -1739,6 +1811,7 @@ export const useQueueStore = defineStore('queue', () => {
generateQueueNumber,
generateBarcode,
incrementBarcodeCounter,
+ syncCountersWithState,
};
}, {
persist: {