diff --git a/pages/AdminLoket/[id].vue b/pages/AdminLoket/[id].vue index 21d905e..2728fef 100644 --- a/pages/AdminLoket/[id].vue +++ b/pages/AdminLoket/[id].vue @@ -39,13 +39,12 @@ @open-penunjang="openPenunjangDialog" /> - @@ -55,12 +54,11 @@ - PANGGIL ANTREAN - + mdi-clock-outline - {{ menungguCount }} Antrean Menunggu + {{ filteredMenungguCount }} Antrean Menunggu @@ -81,11 +79,11 @@ v-model:selected-status="selectedStatus" v-model:search-query="searchQuery" v-model:selected-fast-track="selectedFastTrack" - :di-loket-count="diLoketCount" + :di-loket-count="filteredDiLoketCount" :diproses-count="currentProcessingPatient ? 1 : 0" - :waiting-count="waitingCount" - :terlambat-count="(terlambatPatients || []).length" - :pending-count="(pendingPatients || []).length" + :waiting-count="filteredWaitingCount" + :terlambat-count="filteredTerlambatCount" + :pending-count="filteredPendingCount" :show-diproses="false" :fast-track-options="fastTrackOptions" @action="handleTableAction" @@ -241,6 +239,7 @@ const { printTicketFromPatient } = useThermalPrint(); // Broadcast Channel let broadcastChannel = null; +let resetCheckInterval = null; const loketId = computed(() => route.params.id); const loketName = computed(() => { @@ -307,6 +306,22 @@ onMounted(async () => { // 3. Fetch specific quota/loket data from API to ensure fresh counts // This matches MasterLoket.vue implementation await fetchQuotaFromAPI(); + + // 4. Fetch patient data for this loket + await fetchPatientsForCurrentLoket(); + + // 5. Periodic check for daily reset (2 AM) + resetCheckInterval = setInterval(() => { + const didReset = queueStore.checkAndResetDaily(); + if (didReset) { + console.log('🕒 [AdminLoket] 2 AM threshold reached. Data reset performed.'); + fetchPatientsForCurrentLoket(); // Refresh after reset + } + }, 60000); // Check every minute +}); + +onUnmounted(() => { + if (resetCheckInterval) clearInterval(resetCheckInterval); }); const apiQuota = ref(null); @@ -333,6 +348,25 @@ const fetchQuotaFromAPI = async () => { } }; +// Fetch patient data for current loket +const fetchPatientsForCurrentLoket = async () => { + const targetId = parseInt(loketId.value); + const currentLoket = loketStore.getLoketById(targetId); + + // Check if loket is REGULER (not EKSEKUTIF) + const isEksekutif = currentLoket?.tipeloket === 'EKSEKUTIF' || + currentLoket?.tipeLoket === 'EKSEKUTIF' || + (currentLoket?.namaLoket || '').toUpperCase().includes('EKSEKUTIF'); + + if (!isEksekutif) { + // For REGULER loket, fetch from API + console.log(`🔄 [AdminLoket] Fetching patients for REGULER loket ${targetId}...`); + await queueStore.fetchPatientsForLoket(targetId); + } else { + console.log(`📋 [AdminLoket] Using seed data for EKSEKUTIF loket ${targetId}`); + } +}; + const currentDate = ref( new Date().toLocaleDateString("id-ID", { weekday: "long", @@ -380,6 +414,12 @@ const fastTrackOptions = computed(() => { return uniqueTracks.sort(); }); +// Loket quota from configuration +const loketQuota = computed(() => { + const currentLoket = loketStore.getLoketById(parseInt(loketId.value)); + return currentLoket?.kuota || apiQuota.value || 150; +}); + // Combine all patients with status - PRESERVE ALL PROPERTIES const allPatientsForStage = computed(() => { const currentPatientNo = currentProcessingPatient.value?.no; @@ -387,28 +427,59 @@ const allPatientsForStage = computed(() => { const currentLoket = loketStore.getLoketById(parseInt(targetLoketId)); console.log('🔍 [AdminLoket] currentLoket:', currentLoket); console.log('🔍 [AdminLoket] targetLoketId:', targetLoketId); - const allowedServices = currentLoket?.pelayanan || []; - // Helper to check if patient belongs to this loket + // Check if loket is EKSEKUTIF or REGULER + const isLoketEksekutif = currentLoket?.tipeloket === 'EKSEKUTIF' || + currentLoket?.tipeLoket === 'EKSEKUTIF' || + (currentLoket?.namaLoket || '').toUpperCase().includes('EKSEKUTIF'); + + let basePatients = []; + + if (isLoketEksekutif) { + // For EKSEKUTIF loket, use seed data (filter from allPatients) + // IMPORTANT: Do NOT include menungguPatients - they are only called via QueueActionsCard + console.log('📋 [AdminLoket] Using seed data for EKSEKUTIF loket'); + basePatients = diLoketPatients.value.concat(waitingPatients.value, terlambatPatients.value, pendingPatients.value); + } else { + // For REGULER loket, use API data + console.log('🌐 [AdminLoket] Using API data for REGULER loket'); + const apiPatients = queueStore.apiPatientsPerLoket[targetLoketId] || []; + basePatients = apiPatients; + } + + // Helper to check if patient belongs to this loket (for seed data only) const isPatientForThisLoket = (p) => { - // 0. STRICT FILTER: Payment Type vs Loket Type - const isLoketEksekutif = currentLoket?.tipeloket === 'EKSEKUTIF' || currentLoket?.jenisloket === 'EKSEKUTIF' || (currentLoket?.namaLoket || '').toUpperCase().includes('EKSEKUTIF'); - const isPatientEksekutif = (p.pembayaran || '').toUpperCase().includes('EKSEKUTIF') || (p.pembayaran || '').toUpperCase().includes('VIP'); // Check payment type + // Only check for EKSEKUTIF patients (seed data) + const isPatientEksekutif = (p.pembayaran || '').toUpperCase().includes('EKSEKUTIF') || + (p.pembayaran || '').toUpperCase().includes('VIP'); if (isLoketEksekutif) { // Loket Eksekutif HANYA melayani pasien Eksekutif if (!isPatientEksekutif) return false; + + // For EKSEKUTIF loket: accept all EKSEKUTIF patients + // EKSEKUTIF lokets typically serve ALL clinics for executive patients + // So we don't need to check kodeKlinik matching + // Only check explicit loketId assignment if present + if (p.loketId) { + return String(p.loketId) === String(targetLoketId); + } + + // If no loketId assigned, accept all EKSEKUTIF patients + return true; } else { - // Loket Reguler TIDAK melayani pasien Eksekutif + // Loket Reguler TIDAK melayani pasien Eksekutif (this shouldn't happen with API data) if (isPatientEksekutif) return false; } + // For REGULER seed data only: check loket assignment // Priority 1: Explicit loketId match if (p.loketId) { return String(p.loketId) === String(targetLoketId); } // Priority 2: Service (kodeKlinik) match + const allowedServices = currentLoket?.pelayanan || []; if (p.kodeKlinik) { return allowedServices.includes(p.kodeKlinik); } @@ -426,38 +497,102 @@ const allPatientsForStage = computed(() => { return false; }; - const diLoket = (diLoketPatients.value || []) - .filter(isPatientForThisLoket) - .map((p) => ({ - ...p, - status: p.no === currentPatientNo ? "diproses" : "diloket", - })); + // If using seed data (EKSEKUTIF), filter by loket + // If using API data (REGULER), basePatients already contains only relevant patients + let combined = []; - const terlambat = (terlambatPatients.value || []) - .filter(isPatientForThisLoket) - .map((p) => ({ - ...p, - status: "terlambat", - })); - - const pending = (pendingPatients.value || []) - .filter(isPatientForThisLoket) - .map((p) => ({ - ...p, - status: "pending", - })); - - const waiting = (waitingPatients.value || []) - .filter(isPatientForThisLoket) - .map((p) => ({ - ...p, - status: "waiting", - })); + if (isLoketEksekutif) { + // Filter seed data by loket assignment + const diLoket = (diLoketPatients.value || []) + .filter(isPatientForThisLoket) + .map((p) => ({ + ...p, + status: p.no === currentPatientNo ? "diproses" : "diloket", + })); + + const terlambat = (terlambatPatients.value || []) + .filter(isPatientForThisLoket) + .map((p) => ({ + ...p, + status: "terlambat", + })); + + const pending = (pendingPatients.value || []) + .filter(isPatientForThisLoket) + .map((p) => ({ + ...p, + status: "pending", + })); + + const waiting = (waitingPatients.value || []) + .filter(isPatientForThisLoket) + .map((p) => ({ + ...p, + status: "waiting", + })); - const combined = [...diLoket, ...waiting, ...terlambat, ...pending]; + combined = [...diLoket, ...waiting, ...terlambat, ...pending]; + } else { + // Use API data, but EXCLUDE patients with status "menunggu" + // They should only be called via QueueActionsCard, not displayed in table + combined = basePatients + .filter(p => p.status !== 'menunggu') // IMPORTANT: Filter out menunggu + .map((p) => ({ + ...p, + status: p.no === currentPatientNo ? "diproses" : (p.status || "menunggu"), + })); + } + return combined; }); +// Computed status counts based on filtered allPatientsForStage +// These counts are ONLY for patients that belong to this specific loket +const filteredWaitingCount = computed(() => { + return allPatientsForStage.value.filter(p => p.status === 'waiting').length; +}); + +const filteredDiLoketCount = computed(() => { + return allPatientsForStage.value.filter(p => p.status === 'diloket' || p.status === 'di-loket').length; +}); + +const filteredTerlambatCount = computed(() => { + return allPatientsForStage.value.filter(p => p.status === 'terlambat').length; +}); + +const filteredPendingCount = computed(() => { + return allPatientsForStage.value.filter(p => p.status === 'pending').length; +}); + +// Menunggu count from API or seed data based on loket type +const filteredMenungguCount = computed(() => { + const targetLoketId = loketId.value; + const currentLoket = loketStore.getLoketById(parseInt(targetLoketId)); + + const isLoketEksekutif = currentLoket?.tipeloket === 'EKSEKUTIF' || + currentLoket?.tipeLoket === 'EKSEKUTIF' || + (currentLoket?.namaLoket || '').toUpperCase().includes('EKSEKUTIF'); + + if (isLoketEksekutif) { + // For EKSEKUTIF, use seed data menunggu count (filtered by loket) + return menungguPatients.value.filter(p => { + const isPatientEksekutif = (p.pembayaran || '').toUpperCase().includes('EKSEKUTIF') || + (p.pembayaran || '').toUpperCase().includes('VIP'); + if (!isPatientEksekutif) return false; + + // Accept all EKSEKUTIF patients if no explicit loketId + if (p.loketId) { + return String(p.loketId) === String(targetLoketId); + } + return true; + }).length; + } else { + // For REGULER, count from API data + const apiPatients = queueStore.apiPatientsPerLoket[targetLoketId] || []; + return apiPatients.filter(p => p.status === 'menunggu').length; + } +}); + const waitingCount = computed(() => { return (waitingPatients.value || []).length; }); diff --git a/pages/AdminLoket/index.vue b/pages/AdminLoket/index.vue index c766fce..05d2450 100644 --- a/pages/AdminLoket/index.vue +++ b/pages/AdminLoket/index.vue @@ -13,27 +13,47 @@ {{ loket.namaLoket }} + - {{ loket.pembayaran }} + {{ loket.tipeLoket || loket.tipeloket || 'REGULER' }} - ID: {{ loket.id }} + + + {{ payment }} + + + +{{ loket.pembayaran.length - 2 }} + + + + - {{ loket.loketAktif ? 'Aktif' : 'Non-Aktif' }} + Tidak Aktif @@ -165,6 +185,26 @@ const navigateToLoket = (id) => { } } +.loket-card-inactive { + background: var(--color-neutral-600) !important; + cursor: not-allowed !important; + opacity: 0.7; + + &:hover { + transform: none !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important; + border-color: var(--color-neutral-400) !important; + } + + .loket-name { + color: var(--color-neutral-100) !important; + } + + .pelayanan-count { + color: var(--color-neutral-300) !important; + } +} + .loket-card-header { display: flex; align-items: center; @@ -192,29 +232,36 @@ const navigateToLoket = (id) => { margin-top: 4px; } -.chip-reguler { +// Chip Tipe Loket +.chip-tipe-reguler { + background-color: #009262 !important; + color: var(--color-neutral-100) !important; + font-weight: 600; + font-size: 11px; +} + +.chip-tipe-eksekutif { + background-color: #E67E22 !important; + color: var(--color-neutral-100) !important; + font-weight: 600; + font-size: 11px; +} + +// Chip Pembayaran +.chip-pembayaran { background-color: var(--color-primary-600) !important; color: var(--color-neutral-100) !important; font-weight: 600; font-size: 11px; } -.chip-eksekutif { - background-color: var(--color-secondary-600) !important; +.chip-more-payment { + background-color: var(--color-neutral-600) !important; color: var(--color-neutral-100) !important; font-weight: 600; font-size: 11px; } -.kode-chip { - font-size: 12px; - font-weight: 600; - color: var(--color-neutral-700); - background: var(--color-neutral-400); - padding: 2px 8px; - border-radius: 4px; -} - .loket-preview { flex: 1; } diff --git a/pages/Anjungan/Anjungan/[id].vue b/pages/Anjungan/Anjungan/[id].vue index 5bbf8fa..912337f 100644 --- a/pages/Anjungan/Anjungan/[id].vue +++ b/pages/Anjungan/Anjungan/[id].vue @@ -6,6 +6,9 @@ src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp" alt="RSUD Logo" class="header-logo" + width="40" + height="40" + style="width: 40px; height: 40px; object-fit= contain;" /> @@ -1086,18 +1089,32 @@ const registerPatient = async (visitType, paymentType, namaDokter, isFastTrack = } // Register patient to queueStore - const result = queueStore.registerPatientFromAnjungan( - selectedClinic.value, - paymentType, - visitType, - null, - 'Shift 1', - namaDokter, - isFastTrack, - fastTrackData, // Pass fastTrackData (penanggungJawab, alasanFastTrack) - null, // Stop passing anjunganId as targetLoketId (fix routing bug) - null // Stop passing anjunganName as targetLoket name - ); + let result; + if (!isEksekutif.value) { + // Create patient via API for REGULER + result = await queueStore.registerRegulerPatientViaApi( + selectedClinic.value, + paymentType, + visitType, + isFastTrack, + fastTrackData + ); + } else { + // Existing logic for Eksekutif + result = queueStore.registerPatientFromAnjungan( + selectedClinic.value, + paymentType, + visitType, + null, + 'Shift 1', + namaDokter, + isFastTrack, + fastTrackData, // Pass fastTrackData (penanggungJawab, alasanFastTrack) + null, // Stop passing anjunganId as targetLoketId (fix routing bug) + null // Stop passing anjunganName as targetLoket name + ); + } + if (result && result.success && result.patient) { const doctorInfo = namaDokter ? ` dengan dokter ${namaDokter}` : ''; @@ -1164,18 +1181,32 @@ const submitBooking = async () => { return; } - const result = queueStore.registerPatientFromAnjungan( - selectedClinic.value, - paymentType, - 'JADWAL_LAIN', - bookingForm.value.date, - bookingForm.value.shift, - namaDokter, - false, // isFastTrack - null, // fastTrackData - null, // Stop passing anjunganId as targetLoketId (fix routing bug) - null // Stop passing anjunganName as targetLoket name - ); + let result; + if (!isEksekutif.value) { + // Create patient via API for REGULER + // NOTE: API might not support date, so it will generate for today + result = await queueStore.registerRegulerPatientViaApi( + selectedClinic.value, + paymentType, + 'JADWAL_LAIN', + false, // isFastTrack + null // fastTrackData + ); + } else { + result = queueStore.registerPatientFromAnjungan( + selectedClinic.value, + paymentType, + 'JADWAL_LAIN', + bookingForm.value.date, + bookingForm.value.shift, + namaDokter, + false, // isFastTrack + null, // fastTrackData + null, // Stop passing anjunganId as targetLoketId (fix routing bug) + null // Stop passing anjunganName as targetLoket name + ); + } + if (result && result.success && result.patient) { const doctorInfo = namaDokter ? ` dengan dokter ${namaDokter}` : ''; diff --git a/pages/Anjungan/Anjungan/index.vue b/pages/Anjungan/Anjungan/index.vue index a198d3c..e39cc96 100644 --- a/pages/Anjungan/Anjungan/index.vue +++ b/pages/Anjungan/Anjungan/index.vue @@ -7,6 +7,9 @@ src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp" alt="RSUD Logo" class="header-logo" + width="40" + height="40" + style="width: 40px; height: 40px; object-fit= contain; /> diff --git a/pages/CheckInPasien/checkIn.vue b/pages/CheckInPasien/checkIn.vue index 3a35a1f..64f3f0d 100644 --- a/pages/CheckInPasien/checkIn.vue +++ b/pages/CheckInPasien/checkIn.vue @@ -1366,7 +1366,7 @@ const klinikOptions = computed(() => { const PATIENT_ID_STORAGE_KEY = 'checkin_patient_id_counter'; const QUEUE_NUMBER_STORAGE_KEY = 'checkin_queue_number_counters'; const LAST_RESET_TIME_KEY = 'checkin_last_reset_time'; -const RESET_HOUR = 22; // Jam 10 malam (22:00) +const RESET_HOUR = 2; // Jam 2 pagi const HISTORY_STORAGE_KEY = 'checkin_history'; // Check and reset daily at 10 PM (22:00) @@ -2472,7 +2472,7 @@ const onDetect = async (decodedText: string) => { const patientBarcodeForCheckIn = freshPatient.barcode || searchBarcode || decodedText; console.log('🔍 Calling checkInPatient with barcode (fresh):', patientBarcodeForCheckIn); console.log('🔍 Original QR data:', decodedText, '| Extracted barcode:', searchBarcode); - const checkInResult = queueStore.checkInPatient(patientBarcodeForCheckIn); + const checkInResult = await queueStore.checkInPatient(patientBarcodeForCheckIn); if (checkInResult.success && checkInResult.patient) { // Check-in berhasil @@ -2781,7 +2781,7 @@ const checkInManual = async () => { const patientBarcodeForCheckIn = freshPatient.barcode || searchBarcode || inputValue; console.log('🔍 Calling checkInPatient with barcode (fresh):', patientBarcodeForCheckIn); console.log('🔍 Original input:', inputValue, '| Extracted barcode:', searchBarcode); - const checkInResult = queueStore.checkInPatient(patientBarcodeForCheckIn); + const checkInResult = await queueStore.checkInPatient(patientBarcodeForCheckIn); if (checkInResult.success && checkInResult.patient) { // Check-in berhasil diff --git a/pages/Setting/MasterLoket.vue b/pages/Setting/MasterLoket.vue index 54f303b..dfc5192 100644 --- a/pages/Setting/MasterLoket.vue +++ b/pages/Setting/MasterLoket.vue @@ -59,12 +59,30 @@ - + - {{ item.pembayaran }} + {{ item.tipeLoket || item.tipeloket }} + + + + + + {{ payment }} + + + +{{ item.pembayaran.length - 2 }} @@ -181,12 +199,12 @@ @@ -403,6 +421,7 @@ const loketHeaders = ref([ { title: "Nama Loket", value: "namaLoket" }, { title: "Kuota", value: "kuota" }, { title: "Pelayanan", value: "pelayanan" }, + { title: "Tipe Loket", value: "tipeLoket" }, { title: "Pembayaran", value: "pembayaran" }, { title: "Status Loket Aktif", value: "loketAktif" }, { title: "Layar Informasi", value: "layarInformasi", sortable: false, width: "150px" }, @@ -414,7 +433,7 @@ const formData = ref({ namaLoket: '', kuota: null, statusPelayanan: '', - pembayaran: '', + tipeLoket: '', pelayanan: [], loketAktif: true, }); @@ -432,7 +451,7 @@ const openEditDialog = (item) => { namaLoket: item.namaLoket, kuota: item.kuota, statusPelayanan: item.statusPelayanan || 'RAWAT JALAN', - pembayaran: item.pembayaran, + tipeLoket: item.tipeLoket || item.tipeloket, keterangan: item.keterangan, pelayanan: [...item.pelayanan], }; @@ -450,7 +469,7 @@ const resetForm = () => { namaLoket: '', kuota: null, statusPelayanan: '', - pembayaran: '', + tipeLoket: '', keterangan: '', pelayanan: [], }; @@ -666,6 +685,14 @@ $font-weight-semibold: 600; font-weight: 500; } +.chip-payment { + background-color: #3A61C9 !important; + color: #FFFFFF !important; + font-weight: 500; + font-size: 12px; + line-height: 16px; +} + .btn-edit { background-color: $primary-600 !important; color: $neutral-100 !important; diff --git a/stores/loketStore.js b/stores/loketStore.js index dd535d8..6b3ce41 100644 --- a/stores/loketStore.js +++ b/stores/loketStore.js @@ -99,7 +99,8 @@ export const useLoketStore = defineStore('loket', () => { namaLoket: `LOKET ${i + 1} EKS`, kuota: 500, pelayanan: services, - pembayaran: 'EKSEKUTIF', + pembayaran: ['EKSEKUTIF'], // Changed to array for consistency + tipeLoket: 'EKSEKUTIF', // Add tipeLoket field for consistency keterangan: 'ONLINE', statusPelayanan: 'RAWAT JALAN', source: 'local', @@ -198,7 +199,8 @@ export const useLoketStore = defineStore('loket', () => { id: newId, no: newNo, ...loketPayload, - pembayaran: 'EKSEKUTIF', // Force EKSEKUTIF for local data + pembayaran: loketPayload.pembayaran || ['EKSEKUTIF'], // Ensure array format + tipeLoket: loketPayload.tipeLoket || 'EKSEKUTIF', // Add tipeLoket support source: 'local', // Mark as local data loketAktif: loketPayload.loketAktif ?? true, // Default aktif }; @@ -220,7 +222,10 @@ export const useLoketStore = defineStore('loket', () => { ...localLoketData.value[index], ...loketPayload, source: 'local', // Ensure source stays local - pembayaran: 'EKSEKUTIF', // Ensure pembayaran stays EKSEKUTIF + pembayaran: loketPayload.pembayaran && Array.isArray(loketPayload.pembayaran) + ? loketPayload.pembayaran + : ['EKSEKUTIF'], // Ensure array format + tipeLoket: loketPayload.tipeLoket || 'EKSEKUTIF', // Add tipeLoket support }; return { success: true, message: `Loket ${loketPayload.namaLoket} berhasil diupdate` }; } @@ -271,8 +276,18 @@ export const useLoketStore = defineStore('loket', () => { } const hasData = apiLoketData.value && apiLoketData.value.length > 0; - if (hasData && !force && retryCount === 0) { - return { success: true, message: 'Data sudah tersedia' }; + const hasCachedData = hasData && lastSyncTimestamp.value; + + // If we have cached data and not forcing refresh, use cache + if (hasCachedData && !force && retryCount === 0) { + const cacheAge = Date.now() - new Date(lastSyncTimestamp.value).getTime(); + const cacheAgeMinutes = Math.floor(cacheAge / 60000); + console.log(`📦 Using cached API data (${cacheAgeMinutes} minutes old, ${apiLoketData.value.length} items)`); + return { + success: true, + message: `Data tersedia dari cache (${cacheAgeMinutes} menit yang lalu)`, + cached: true + }; } const performFetch = async () => { @@ -318,11 +333,13 @@ export const useLoketStore = defineStore('loket', () => { // Extract unique codes for 'pelayanan' field const pelayananCodes = [...new Set(spesialisDetail.map(s => s.code))]; - // Map payment types - const pembayaranLabel = (l.pembayaran || []) + // Map payment types as array for multiple chips display + const pembayaranArray = (l.pembayaran || []) .map(p => p.pembayaran) - .filter(Boolean) - .join(', ') || 'JKN'; + .filter(Boolean); + + // If no payment types, default to ['JKN'] + const pembayaran = pembayaranArray.length > 0 ? pembayaranArray : ['JKN']; return { id: id, @@ -331,11 +348,12 @@ export const useLoketStore = defineStore('loket', () => { kuota: parseInt(l.kuotaloket) || 100, pelayanan: pelayananCodes, _spesialisDetail: spesialisDetail, - pembayaran: pembayaranLabel, + pembayaran: pembayaran, // Now an array instead of joined string + tipeLoket: l.tipeloket || 'REGULER', // Map tipeloket to tipeLoket (capital L) source: 'api', loketAktif: l.loketaktif ?? true, jenisloket: l.jenisloket, - tipeloket: l.tipeloket + tipeloket: l.tipeloket // Keep original for backward compatibility }; }); @@ -360,27 +378,40 @@ export const useLoketStore = defineStore('loket', () => { 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 }; + // FALLBACK 1: Use cached data if available + if (apiLoketData.value.length > 0) { + const cacheAge = lastSyncTimestamp.value + ? Math.floor((Date.now() - new Date(lastSyncTimestamp.value).getTime()) / 60000) + : 'unknown'; + console.warn(`⚠️ [loketStore] API failed, using cached data (${cacheAge} minutes old)`); + return { + success: true, + message: `API gagal, menggunakan data cache (${cacheAge} menit yang lalu)`, + warning: true, + cached: true + }; } + // FALLBACK 2: If no cache, populate with dummy Reguler data for Dev/Offline mode + console.warn('⚠️ [loketStore] No cache available, using FALLBACK dummy data...'); + 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'], // Changed to array for consistency + tipeLoket: 'REGULER', // Add tipeLoket for fallback data + 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; @@ -393,7 +424,11 @@ export const useLoketStore = defineStore('loket', () => { }; return { - // State + // State - Base refs (for persist) + apiLoketData, // Raw API data array (persisted) + localLoketData, // Raw local data array (persisted) + + // State - Computed loketData, lokets: loketData, // Alias for backward compatibility/clarity availableServices, @@ -417,7 +452,11 @@ export const useLoketStore = defineStore('loket', () => { persist: { key: 'loket-store-state', storage: typeof window !== 'undefined' ? localStorage : undefined, - paths: ['localLoketData'], // ONLY persist EKSEKUTIF data + paths: [ + 'localLoketData', // Persist EKSEKUTIF data (ID 1000+) + 'apiLoketData', // Persist API data (REGULER) to minimize data loss + 'lastSyncTimestamp' // Track when API data was last fetched + ], serializer: { deserialize: JSON.parse, serialize: JSON.stringify, diff --git a/stores/queueStore.js b/stores/queueStore.js index e9a19b5..e92ac54 100644 --- a/stores/queueStore.js +++ b/stores/queueStore.js @@ -10,6 +10,300 @@ export const useQueueStore = defineStore('queue', () => { const penunjangStore = usePenunjangStore(); const loketStore = useLoketStore(); + // ============================================ + // API INTEGRATION FOR LOKET PATIENTS + // ============================================ + + // State untuk API patient data per loket + const apiPatientsPerLoket = ref({}); + const isLoadingPatients = ref(false); + const apiPatientsError = ref(null); + + /** + * Sync patient status to apiPatientsPerLoket for reactivity + */ + const syncApiPatientStatus = (patient, newStatus) => { + if (!patient || patient.registrationType !== 'api') return; + + const loketId = patient.loketId; + if (loketId && apiPatientsPerLoket.value[loketId]) { + const index = apiPatientsPerLoket.value[loketId].findIndex(p => p.no === patient.no); + if (index !== -1) { + const updated = [...apiPatientsPerLoket.value[loketId]]; + updated[index] = { ...updated[index], status: newStatus }; + apiPatientsPerLoket.value[loketId] = updated; + } + } + }; + + /** + * Reference mapping for patient status from API idvisit + * idvisit 1, 2 = menunggu (CT ANJUNGAN, RT ANJUNGAN) + * idvisit 3, 4 = waiting (PG ANJUNGAN, RT CHECK-IN) + * idvisit 5, 6 = di-loket (PS CHECK-IN, TP LOKET) + */ + const PATIENT_STATUS_MAP = { + 1: 'menunggu', + 2: 'menunggu', + 3: 'waiting', + 4: 'waiting', + 5: 'di-loket', + 6: 'di-loket', + "1": 'menunggu', + "2": 'menunggu', + "3": 'waiting', + "4": 'waiting', + "5": 'di-loket', + "6": 'di-loket' + }; + + /** + * Map status from idvisit API + */ + const mapStatusFromIdVisit = (idvisit) => { + return PATIENT_STATUS_MAP[idvisit] || 'menunggu'; + }; + + /** + * Map status dari deskripsi API ke status internal + * menunggu (id 1, 2): "Cetak Tiket Antrian" atau "Ruang Tunggu Anjungan" + * waiting (id 3, 4): "Panggilan loket ke Anjungan" atau "Tunggu Pasien Check-In" + * di-loket (id 5, 6): "Pasien Sudah Check-In" atau "Tunggu Panggilan Loket" + */ + const mapStatusFromDeskripsi = (deskripsi) => { + if (!deskripsi) return 'menunggu'; + + const desc = deskripsi.toString().trim().toUpperCase(); + + // di-loket (id 5, 6): PS CHECK-IN (Pasien Sudah Check-In), RT PENDAFTARAN (Tunggu Panggilan Loket) + if (desc.includes('PASIEN SUDAH CHECK-IN') || desc.includes('TUNGGU PANGGILAN LOKET') || desc.includes('PS CHECK-IN') || desc.includes('RT PENDAFTARAN')) { + return 'di-loket'; + } + + // waiting (id 3, 4): PG ANJUNGAN (Panggilan loket ke Anjungan), RT CHECK-IN (Tunggu Pasien Check-In) + if (desc.includes('PANGGILAN LOKET KE ANJUNGAN') || desc.includes('TUNGGU PASIEN CHECK-IN') || desc.includes('PG ANJUNGAN') || desc.includes('RT CHECK-IN')) { + return 'waiting'; + } + + // menunggu (id 1, 2): CT ANJUNGAN (Cetak Tiket Antrian), RT ANJUNGAN (Ruang Tunggu Anjungan) + if (desc.includes('CETAK TIKET') || desc.includes('RUANG TUNGGU ANJUNGAN') || desc.includes('CT ANJUNGAN') || desc.includes('RT ANJUNGAN')) { + return 'menunggu'; + } + + // Default: menunggu + return 'menunggu'; + }; + + + + /** + * Map patient data dari API format ke store format + */ + const mapApiPatientToStoreFormat = (apiPatient, index) => { + // Use idtiket as unique patient number to avoid conflicts with seed data + // Seed data uses sequential numbers 1-20, so we use idtiket + 10000 offset + const uniqueNo = apiPatient.idtiket ? parseInt(apiPatient.idtiket) + 10000 : 10000 + index; + + // Handle nested positions to find the best status + let status = 'menunggu'; + let posisiStr = apiPatient.posisi || ''; + let idvisit = apiPatient.idvisit; + + if (apiPatient.posisi && Array.isArray(apiPatient.posisi) && apiPatient.posisi.length > 0) { + // Find the "best" status among all positions + // Priority: di-loket > waiting > menunggu + const statusPriority = { 'di-loket': 3, 'waiting': 2, 'menunggu': 1 }; + let bestPriority = 0; + + apiPatient.posisi.forEach(pos => { + const pStatus = pos.idvisit + ? mapStatusFromIdVisit(pos.idvisit) + : mapStatusFromDeskripsi(pos.deskripsi); + + const pPrio = statusPriority[pStatus] || 0; + if (pPrio > bestPriority) { + bestPriority = pPrio; + status = pStatus; + posisiStr = pos.posisi || pos.deskripsi || ''; + idvisit = pos.idvisit || idvisit; + } + }); + } else { + // Standard top-level status + status = apiPatient.idvisit + ? mapStatusFromIdVisit(apiPatient.idvisit) + : mapStatusFromDeskripsi(apiPatient.deskripsi); + } + + const mapped = { + no: uniqueNo, + jamPanggil: apiPatient.waktu || '', + barcode: apiPatient.barcode || '', + noAntrian: `${apiPatient.ticket || ''} | ${posisiStr}`, + shift: apiPatient.shift ? `Shift ${apiPatient.shift}` : '', + klinik: apiPatient.klinik || '', + kodeKlinik: apiPatient.klinik || '', // Will be mapped later + fastTrack: "TIDAK", + pembayaran: apiPatient.pembayaran || '', + status: status, + processStage: 'loket', + createdAt: apiPatient.tanggal || new Date().toISOString(), + registrationType: 'api', + visitType: 'SEKARANG', + visitDate: apiPatient.tanggal ? apiPatient.tanggal.split('T')[0] : new Date().toISOString().substring(0, 10), + namaDokter: null, + noRM: null, + penanggungJawab: null, + alasanFastTrack: null, + idvisit: idvisit, + idtiket: apiPatient.idtiket, + ticket: apiPatient.ticket, + posisi: apiPatient.posisi, + deskripsi: apiPatient.deskripsi, + }; + + // Map klinik name to code using clinicStore + const clinic = clinicStore.clinics.find(c => + c.name && apiPatient.klinik && + c.name.toUpperCase().includes(apiPatient.klinik.toUpperCase()) + ); + if (clinic) { + mapped.kodeKlinik = clinic.kode; + } + + return mapped; + }; + + /** + * Fetch patient data untuk loket tertentu dari API + */ + const fetchPatientsForLoket = async (loketId) => { + if (!loketId) { + console.error('loketId required for fetchPatientsForLoket'); + return { success: false, message: 'ID Loket diperlukan' }; + } + + isLoadingPatients.value = true; + apiPatientsError.value = null; + + // Check for daily reset before fetching + checkAndResetDaily(); + + try { + console.log(`🔄 [queueStore] Fetching patients for loket ${loketId}...`); + const response = await fetch(`http://10.10.150.131:8089/api/v1/loket/${loketId}`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const rawData = await response.json(); + + // Check response structure + if (rawData.metadata && rawData.metadata.code !== 200) { + throw new Error(rawData.message || 'API returned error status'); + } + + const patientsRaw = rawData.data || []; + + // Map API data to store format + const mappedPatients = patientsRaw.map((apiPatient, index) => + mapApiPatientToStoreFormat(apiPatient, index) + ).filter(p => isTodayPatient(p)); + + // Deduplicate patients by idtiket + // API returns multiple entries if patient is at multiple positions + const deduplicatedMap = new Map(); + mappedPatients.forEach(p => { + const id = p.idtiket || p.barcode || p.no; + const existing = deduplicatedMap.get(id); + + if (!existing) { + deduplicatedMap.set(id, p); + } else { + // Priority: di-loket > waiting > menunggu + const statusPriority = { 'di-loket': 3, 'waiting': 2, 'menunggu': 1 }; + const pPrio = statusPriority[p.status] || 0; + const ePrio = statusPriority[existing.status] || 0; + + if (pPrio > ePrio) { + deduplicatedMap.set(id, p); + } + } + }); + const finalPatients = Array.from(deduplicatedMap.values()); + + // Store in API-specific state + apiPatientsPerLoket.value[loketId] = finalPatients; + + // IMPORTANT: Also merge into allPatients so callMultiplePatients can find them + // Remove any existing API patients for this loket first + allPatients.value = allPatients.value.filter(p => + p.registrationType !== 'api' || String(p.loketId) !== String(loketId) + ); + + // Add new API patients with loketId assigned + const patientsWithLoketId = finalPatients.map(p => ({ + ...p, + loketId: parseInt(loketId), + registrationType: 'api' + })); + + allPatients.value.push(...patientsWithLoketId); + + console.log(`✅ [queueStore] Successfully fetched ${finalPatients.length} unique patients for loket ${loketId} (Original: ${mappedPatients.length})`); + console.log(`📊 [queueStore] Total patients in allPatients: ${allPatients.value.length}`); + + return { + success: true, + message: `${mappedPatients.length} pasien berhasil dimuat`, + data: mappedPatients + }; + + } catch (error) { + console.error(`❌ [queueStore] Error fetching patients for loket ${loketId}:`, error); + apiPatientsError.value = error.message; + + // Return empty array on error + apiPatientsPerLoket.value[loketId] = []; + + return { + success: false, + message: `Gagal memuat data pasien: ${error.message}`, + data: [] + }; + } finally { + isLoadingPatients.value = false; + } + }; + + /** + * Get patients for a specific loket (from API or seed data based on loket type) + */ + const getPatientsForLoket = (loketId) => { + return computed(() => { + const loket = loketStore.getLoketById(parseInt(loketId)); + + // If loket is EKSEKUTIF, use seed data + const isEksekutif = loket?.tipeloket === 'EKSEKUTIF' || + loket?.tipeLoket === 'EKSEKUTIF' || + (loket?.namaLoket || '').toUpperCase().includes('EKSEKUTIF'); + + if (isEksekutif) { + // Return EKSEKUTIF patients from seed data + return allPatients.value.filter(p => { + const isPembayaranEksekutif = (p.pembayaran || '').toUpperCase().includes('EKSEKUTIF') || + (p.pembayaran || '').toUpperCase().includes('VIP'); + return isPembayaranEksekutif && p.processStage === 'loket'; + }); + } + + // For REGULER loket, return API data + return apiPatientsPerLoket.value[loketId] || []; + }); + }; + + // Helper function untuk mendapatkan loket default (Loket A) const getDefaultLoket = () => { const allLokets = loketStore.loketData?.value || loketStore.loketData || []; @@ -154,18 +448,11 @@ export const useQueueStore = defineStore('queue', () => { const seedBarcode3 = getSeedBarcode(3); const seedBarcode4 = getSeedBarcode(4); const seedBarcode5 = getSeedBarcode(5); - const seedBarcode6 = getSeedBarcode(6); - const seedBarcode7 = getSeedBarcode(7); - const seedBarcode8 = getSeedBarcode(8); - const seedBarcode9 = getSeedBarcode(9); - const seedBarcode10 = getSeedBarcode(10); - const seedBarcode11 = getSeedBarcode(11); - const seedBarcode12 = getSeedBarcode(12); - // Barcode untuk pasien Eksekutif - const seedBarcodeE1 = getSeedBarcode(13); - const seedBarcodeE2 = getSeedBarcode(14); - const seedBarcodeE3 = getSeedBarcode(15); - const seedBarcodeE4 = getSeedBarcode(16); + // Barcode untuk pasien Eksekutif only (REGULER akan dari API) + const seedBarcodeE1 = getSeedBarcode(1); + const seedBarcodeE2 = getSeedBarcode(2); + const seedBarcodeE3 = getSeedBarcode(3); + const seedBarcodeE4 = getSeedBarcode(4); // IMPORTANT: Set counter ke nilai yang sesuai dengan jumlah seed data // Ini mencegah counter naik tidak terkendali saat seed data di-generate @@ -180,7 +467,7 @@ export const useQueueStore = defineStore('queue', () => { const STORAGE_KEY = `barcode_counter_${datePrefix}`; const LAST_DATE_KEY = 'barcode_last_date'; const storedCounter = localStorage.getItem(STORAGE_KEY); - const seedDataCount = 16; // Jumlah seed data + const seedDataCount = 4; // Jumlah seed data (EKSEKUTIF only) // Hanya set counter jika belum ada atau lebih kecil dari jumlah seed data // Jangan overwrite counter yang sudah lebih besar (berarti sudah ada pasien baru) @@ -190,264 +477,15 @@ export const useQueueStore = defineStore('queue', () => { } } + + // SEED DATA: ONLY EKSEKUTIF PATIENTS + // All REGULER patients will come from API const seedPatients = [ { no: 1, - jamPanggil: "12:49", - barcode: seedBarcode1, // Barcode unik untuk setiap pasien - 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", - processStage: "loket", - createdAt: new Date().toISOString(), - registrationType: 'online', - visitType: 'SEKARANG', - visitDate: new Date().toISOString().substring(0, 10), - namaDokter: null, - noRM: "RM-000001", - penanggungJawab: "Dr. Ahmad Wijaya", // Fast Track data - alasanFastTrack: "Pasien prioritas", // Fast Track data - }, - { - no: 2, - jamPanggil: "10:52", - barcode: seedBarcode2, - noAntrian: `RA002 | Online - ${seedBarcode2}`, // Counter 2: Non-fast track UMUM - shift: "Shift 1", - klinik: "IPD", - kodeKlinik: "IP", - fastTrack: "TIDAK", - pembayaran: "UMUM", - status: "waiting", - processStage: "loket", - createdAt: new Date().toISOString(), - registrationType: 'online', - visitType: 'SEKARANG', - visitDate: new Date().toISOString().substring(0, 10), - namaDokter: null, - noRM: "RM-000002", - penanggungJawab: null, - alasanFastTrack: null, - }, - { - no: 3, - jamPanggil: "09:30", - barcode: seedBarcode3, - 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", - processStage: "loket", - createdAt: new Date().toISOString(), - registrationType: 'online', - visitType: 'SEKARANG', - visitDate: new Date().toISOString().substring(0, 10), - namaDokter: null, - noRM: "RM-000003", - penanggungJawab: "Dr. Budi Santoso", // Fast Track data - alasanFastTrack: "Kondisi darurat", // Fast Track data - }, - { - no: 4, - jamPanggil: "14:15", - barcode: seedBarcode4, - noAntrian: `RA004 | Online - ${seedBarcode4}`, // Counter 4: Non-fast track UMUM - shift: "Shift 1", - klinik: "THT", - kodeKlinik: "TH", - fastTrack: "TIDAK", - pembayaran: "UMUM", - status: "waiting", - processStage: "loket", - createdAt: new Date().toISOString(), - registrationType: 'online', - visitType: 'SEKARANG', - visitDate: new Date().toISOString().substring(0, 10), - namaDokter: null, - noRM: "RM-000004", - penanggungJawab: null, - alasanFastTrack: null, - }, - { - no: 5, - jamPanggil: "12:49", - barcode: seedBarcode5, - noAntrian: `RA005 | Online - ${seedBarcode5}`, // Counter 5: Non-fast track UMUM - shift: "Shift 2", - klinik: "KANDUNGAN", - kodeKlinik: "KD", - fastTrack: "TIDAK", - pembayaran: "UMUM", - status: "waiting", - processStage: "loket", - createdAt: new Date().toISOString(), - registrationType: 'online', - visitType: 'SEKARANG', - visitDate: new Date().toISOString().substring(0, 10), - namaDokter: null, - noRM: "RM-000005", - penanggungJawab: null, - alasanFastTrack: null, - }, - { - no: 6, - jamPanggil: "10:52", - barcode: seedBarcode6, - 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", - processStage: "loket", - createdAt: new Date().toISOString(), - registrationType: 'online', - visitType: 'SEKARANG', - visitDate: new Date().toISOString().substring(0, 10), - namaDokter: null, - noRM: "RM-000006", - penanggungJawab: "Dr. Citra Dewi", // Fast Track data - alasanFastTrack: "Rujukan darurat", // Fast Track data - }, - { - no: 7, - jamPanggil: "09:30", - barcode: seedBarcode7, - noAntrian: `RA007 | Online - ${seedBarcode7}`, // Counter 7: Non-fast track UMUM - shift: "Shift 1", - klinik: "SARAF", - kodeKlinik: "SR", - fastTrack: "TIDAK", - pembayaran: "UMUM", - status: "waiting", - processStage: "loket", - createdAt: new Date().toISOString(), - registrationType: 'online', - visitType: 'SEKARANG', - visitDate: new Date().toISOString().substring(0, 10), - namaDokter: null, - noRM: "RM-000007", - penanggungJawab: null, - alasanFastTrack: null, - }, - { - no: 8, - jamPanggil: "14:15", - barcode: seedBarcode8, - noAntrian: `RA008 | Online - ${seedBarcode8}`, // Counter 8: Non-fast track UMUM - shift: "Shift 1", - klinik: "THT", - kodeKlinik: "TH", - fastTrack: "TIDAK", - pembayaran: "UMUM", - status: "waiting", - processStage: "loket", - createdAt: new Date().toISOString(), - registrationType: 'online', - visitType: 'SEKARANG', - visitDate: new Date().toISOString().substring(0, 10), - namaDokter: null, - noRM: "RM-000008", - penanggungJawab: null, - alasanFastTrack: null, - }, - { - no: 9, - jamPanggil: "12:49", - barcode: seedBarcode9, - 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", - processStage: "loket", - createdAt: new Date().toISOString(), - registrationType: 'online', - visitType: 'SEKARANG', - visitDate: new Date().toISOString().substring(0, 10), - namaDokter: null, - noRM: "RM-000009", - penanggungJawab: "Dr. Dedi Kurniawan", // Fast Track data - alasanFastTrack: "Pasien VIP", // Fast Track data - }, - { - no: 10, - jamPanggil: "10:52", - barcode: seedBarcode10, - noAntrian: `RA010 | Online - ${seedBarcode10}`, // Counter 10: Non-fast track UMUM - shift: "Shift 1", - klinik: "IPD", - kodeKlinik: "IP", - fastTrack: "TIDAK", - pembayaran: "UMUM", - status: "waiting", - processStage: "loket", - createdAt: new Date().toISOString(), - registrationType: 'online', - visitType: 'SEKARANG', - visitDate: new Date().toISOString().substring(0, 10), - namaDokter: null, - noRM: "RM-000010", - penanggungJawab: null, - alasanFastTrack: null, - }, - { - no: 11, - jamPanggil: "09:30", - barcode: seedBarcode11, - noAntrian: `RA011 | Online - ${seedBarcode11}`, // Counter 11: Non-fast track UMUM - shift: "Shift 1", - klinik: "SARAF", - kodeKlinik: "SR", - fastTrack: "TIDAK", - pembayaran: "UMUM", - status: "waiting", - processStage: "loket", - createdAt: new Date().toISOString(), - registrationType: 'online', - visitType: 'SEKARANG', - visitDate: new Date().toISOString().substring(0, 10), - namaDokter: null, - noRM: "RM-000011", - penanggungJawab: null, - alasanFastTrack: null, - }, - { - no: 12, - jamPanggil: "14:15", - barcode: seedBarcode12, - 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", - processStage: "loket", - createdAt: new Date().toISOString(), - registrationType: 'online', - visitType: 'SEKARANG', - visitDate: new Date().toISOString().substring(0, 10), - namaDokter: null, - noRM: "RM-000012", - penanggungJawab: "Dr. Eka Putri", // Fast Track data - alasanFastTrack: "Kondisi kritis", // Fast Track data - }, - { - no: 13, jamPanggil: "11:20", - barcode: seedBarcodeE1, // Barcode unik untuk pasien Eksekutif - noAntrian: `EA013 | Online - ${seedBarcodeE1}`, // Counter 13: Non-fast track Eksekutif + barcode: seedBarcodeE1, + noAntrian: `EA001 | Online - ${seedBarcodeE1}`, shift: "Shift 1", klinik: "KANDUNGAN", kodeKlinik: "KD", @@ -460,15 +498,15 @@ export const useQueueStore = defineStore('queue', () => { visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: "Dr. Ahmad Wijaya, Sp.OG", - noRM: "RM-000013", + noRM: "RM-E001", penanggungJawab: null, alasanFastTrack: null, }, { - no: 14, + no: 2, jamPanggil: "13:45", barcode: seedBarcodeE2, - noAntrian: `EA014 | Online - ${seedBarcodeE2}`, // Counter 14: Non-fast track Eksekutif + noAntrian: `EA002 | Online - ${seedBarcodeE2}`, shift: "Shift 1", klinik: "IPD", kodeKlinik: "IP", @@ -481,19 +519,19 @@ export const useQueueStore = defineStore('queue', () => { visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: "Dr. Budi Santoso, Sp.PD", - noRM: "RM-000014", + noRM: "RM-E002", penanggungJawab: null, alasanFastTrack: null, }, { - no: 15, + no: 3, jamPanggil: "15:10", barcode: seedBarcodeE3, - noAntrian: `F-EA015 | Online - ${seedBarcodeE3}`, // Counter 15: Fast Track Eksekutif + noAntrian: `F-EA003 | Online - ${seedBarcodeE3}`, shift: "Shift 2", klinik: "SARAF", kodeKlinik: "SR", - fastTrack: "YA", // Fast Track Patient + fastTrack: "YA", pembayaran: "Eksekutif", status: "waiting", processStage: "loket", @@ -502,32 +540,35 @@ export const useQueueStore = defineStore('queue', () => { visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: "Dr. Citra Dewi, Sp.S", - noRM: "RM-000015", - penanggungJawab: "Dr. Citra Dewi", // Fast Track data - alasanFastTrack: "Pasien Eksekutif prioritas", // Fast Track data + noRM: "RM-E003", + penanggungJawab: "Dr. Citra Dewi", + alasanFastTrack: "Pasien Eksekutif prioritas", }, { - no: 16, + no: 4, jamPanggil: "16:30", barcode: seedBarcodeE4, - noAntrian: `EA016 | Online - ${seedBarcodeE4}`, // Counter 16: Non-fast track Eksekutif - shift: "Shift 1", + noAntrian: `EA004 | Online - ${seedBarcodeE4}`, + shift: "Shift 2", klinik: "THT", + kodeKlinik: "TH", fastTrack: "TIDAK", - pembayaran: "Eksekutif", + pembayaran: "VIP", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), - namaDokter: "Dr. Dedi Kurniawan, Sp.THT", - noRM: "RM-000016", + namaDokter: "Dr. Eka Putri, Sp.THT", + noRM: "RM-E004", penanggungJawab: null, alasanFastTrack: null, }, ]; + + const cloneSeed = () => seedPatients.map(p => ({ ...p })); // Initialize counters from seed data to ensure numbering continues correctly @@ -589,11 +630,93 @@ export const useQueueStore = defineStore('queue', () => { // Penunjang data - reference dari penunjangStore const penunjangs = ref(penunjangStore.penunjangs || []); + const RESET_HOUR = 2; // 2 AM reset threshold + + /** + * Get logical reset threshold date object + * If current time is before 2 AM, the threshold is yesterday at 2 AM. + * If current time is after 2 AM, the threshold is today at 2 AM. + */ + const getResetThreshold = () => { + const now = new Date(); + const threshold = new Date(now); + threshold.setHours(RESET_HOUR, 0, 0, 0); + + if (now.getHours() < RESET_HOUR) { + threshold.setDate(threshold.getDate() - 1); + } + + return threshold; + }; + + /** + * Check if data needs to be reset (passed 2 AM of a new day) + */ + const checkAndResetDaily = () => { + const threshold = getResetThreshold(); + const thresholdTS = threshold.getTime(); + + // Use localStorage to persist last reset time across sessions/tabs + const lastResetStr = localStorage.getItem('queue_last_reset_time'); + const lastResetTS = lastResetStr ? parseInt(lastResetStr, 10) : 0; + + if (lastResetTS < thresholdTS) { + console.log(`🕒 [queueStore] Daily reset triggered. Threshold: ${threshold.toLocaleString()}`); + + // Clear data + allPatients.value = []; + apiPatientsPerLoket.value = {}; + currentProcessingPatient.value = {}; + + // Update reset timestamp + localStorage.setItem('queue_last_reset_time', thresholdTS.toString()); + + // Re-sync counters for the new day + syncCountersWithState(); + + return true; + } + return false; + }; + + /** + * Filter strictly to only show today's patients (after 2 AM) + */ + const isTodayPatient = (patient) => { + if (!patient) return false; + + // Status processing overrides filter (always show if currently processing) + const isProcessing = Object.values(currentProcessingPatient.value).some(p => p && p.no === patient.no); + if (isProcessing) return true; + + const createdAt = patient.createdAt ? new Date(patient.createdAt) : null; + const visitDateStr = patient.visitDate; // YYYY-MM-DD + + const threshold = getResetThreshold(); + + // Check by createdAt first + if (createdAt && createdAt < threshold) { + return false; + } + + // Check by visitDate if available + if (visitDateStr) { + const vDate = new Date(visitDateStr); + vDate.setHours(23, 59, 59, 999); // End of visit date + if (vDate < threshold) return false; + } + + return true; + }; + /** * Ensures initial data exists. * Only seeds if the store is empty (not hydrated from storage) */ const ensureInitialData = () => { + // 1. Check for daily reset first + checkAndResetDaily(); + if (allPatients.value.length === 0) { console.log('🌱 Seeding queueStore with initial data...'); allPatients.value = cloneSeed(); @@ -624,19 +747,16 @@ export const useQueueStore = defineStore('queue', () => { // Computed - Filter berdasarkan process stage dan status const getPatientsByStage = (stage) => { return computed(() => { - const patients = allPatients.value.filter(p => p.processStage === stage); + // Filter by stage AND date (today only) + const patients = allPatients.value.filter(p => p.processStage === stage && isTodayPatient(p)); // Debug log - console.log(`getPatientsByStage(${stage}):`, patients.length, 'patients'); - if (patients.length > 0) { - console.log('Sample patient properties:', Object.keys(patients[0])); - console.log('Sample fastTrack values:', patients.map(p => p.fastTrack)); - } + console.log(`getPatientsByStage(${stage}):`, patients.length, 'patients (Today)'); return { all: patients, - waiting: patients.filter(p => p.status === 'waiting'), // Pasien yang sudah dipanggil, menunggu check-in - menunggu: patients.filter(p => p.status === 'menunggu'), // Pasien yang belum dipanggil + waiting: patients.filter(p => p.status === 'waiting'), + menunggu: patients.filter(p => p.status === 'menunggu'), diLoket: patients.filter(p => p.status === 'di-loket'), terlambat: patients.filter(p => p.status === 'terlambat'), pending: patients.filter(p => p.status === 'pending'), @@ -644,10 +764,10 @@ export const useQueueStore = defineStore('queue', () => { }); }; - // Total pasien per stage + // Total pasien per stage - strictly today const getTotalPasienByStage = (stage) => { return computed(() => - allPatients.value.filter(p => p.processStage === stage).length + allPatients.value.filter(p => p.processStage === stage && isTodayPatient(p)).length ); }; @@ -730,9 +850,11 @@ export const useQueueStore = defineStore('queue', () => { ...allPatients.value[index], status: "waiting", // Assign to this specific loket if it was unassigned - loketId: (adminType === 'loket' && targetId) ? targetId : allPatients.value[index].loketId, lastCalledAt: callTimestamp }; + + // SYNC to apiPatientsPerLoket if it's an API patient + syncApiPatientStatus(allPatients.value[index], "waiting"); } return { @@ -790,12 +912,18 @@ export const useQueueStore = defineStore('queue', () => { patientsToCall.forEach((patient) => { const index = allPatients.value.findIndex(p => p.no === patient.no); if (index !== -1) { + const newStatus = "waiting"; + const newLoketId = (adminType === 'loket' && targetId) ? targetId : allPatients.value[index].loketId; + allPatients.value[index] = { ...allPatients.value[index], - status: "waiting", - loketId: (adminType === 'loket' && targetId) ? targetId : allPatients.value[index].loketId, + status: newStatus, + loketId: newLoketId, lastCalledAt: callTimestamp }; + + // SYNC to apiPatientsPerLoket if it's an API patient + syncApiPatientStatus(allPatients.value[index], newStatus); } }); @@ -827,6 +955,7 @@ export const useQueueStore = defineStore('queue', () => { processStage: "klinik", calledByAdmin: false // Reset flag saat pindah stage }; + syncApiPatientStatus(allPatients.value[patientIndex], "di-loket"); message = `Pasien ${patientCode} berhasil check in dan masuk ke Tabel Loket Klinik`; } // Jika check-in di klinik, selesai @@ -835,6 +964,7 @@ export const useQueueStore = defineStore('queue', () => { ...allPatients.value[patientIndex], status: "processed" }; + syncApiPatientStatus(allPatients.value[patientIndex], "processed"); message = `Pasien ${patientCode} berhasil check in di Klinik`; } // Jika check-in di penunjang, selesai @@ -843,6 +973,7 @@ export const useQueueStore = defineStore('queue', () => { ...allPatients.value[patientIndex], status: "processed" }; + syncApiPatientStatus(allPatients.value[patientIndex], "processed"); message = `Pasien ${patientCode} berhasil check in di Penunjang`; } @@ -1612,11 +1743,178 @@ export const useQueueStore = defineStore('queue', () => { }; }; + /** + * Register REGULER patient via API + * POST to http://10.10.150.131:8089/api/v1/tiket/generate + */ + const registerRegulerPatientViaApi = async (clinic, paymentType, visitType = 'SEKARANG', isFastTrack = false, fastTrackData = null) => { + try { + console.log('🔄 [queueStore] Generating ticket via API for REGULER patient...'); + + // 1. Find appropriate idloket + const allLokets = loketStore.lokets || []; + const targetLoket = allLokets.find(l => + l.pelayanan && Array.isArray(l.pelayanan) && l.pelayanan.includes(clinic.kode) + ); + + const idloket = targetLoket ? String(targetLoket.id) : "2"; // Default fallback to "2" if not found + + // 2. Prepare API Body + const body = { + idloket: idloket, + dokter: "", + idklinik: String(clinic.id), + namaklinik: clinic.name, + statuspasien: "1", + statuspasien2: "2", + idklinikstatus: "1" + }; + + console.log('📤 [queueStore] API Body:', body); + + // 3. Call API + const response = await fetch('http://10.10.150.131:8089/api/v1/tiket/generate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }); + + if (!response.ok) { + throw new Error(`API error! status: ${response.status}`); + } + + const result = await response.json(); + console.log('📥 [queueStore] API Response:', result); + + if (result.metadata && result.metadata.code !== 200) { + return { success: false, message: result.message || 'Gagal generate barcode via API' }; + } + + const apiData = result.data; + if (!apiData || !apiData.barcode) { + return { success: false, message: 'Data barcode tidak valid dari API' }; + } + + // 4. Map API response to Store Format + const timestamp = new Date(); + const newNo = allPatients.value.length > 0 + ? Math.max(...allPatients.value.map(p => p.no)) + 1 + : 1; + + // Gunakan waktu dari API jika tersedia + const jamPanggil = apiData.waktutiket || + `${String(timestamp.getHours()).padStart(2, "0")}:${String(timestamp.getMinutes()).padStart(2, "0")}`; + + const barcode = apiData.barcode; + // Penomoran tiket menggunakan "ticket" dari API + const queueNumber = apiData.ticket || apiData.barcode; + const finalQueueNumber = isFastTrack ? `F-${queueNumber}` : queueNumber; + const noAntrian = `${finalQueueNumber} | Onsite - ${barcode}`; + + const newPatient = { + no: newNo, + jamPanggil: jamPanggil, + barcode: barcode, + noAntrian: noAntrian, + shift: apiData.shift || 'Shift 1', + klinik: clinic.name, + kodeKlinik: clinic.kode, + fastTrack: isFastTrack ? "YA" : "TIDAK", + pembayaran: paymentType, + noRM: `RM-${barcode.slice(-6)}`, + status: 'menunggu', + processStage: "loket", + createdAt: apiData.tangaltiket ? `${apiData.tangaltiket}T${apiData.waktutiket || '00:00:00'}` : timestamp.toISOString(), + registrationType: 'onsite', + visitType: visitType, + visitDate: apiData.tangaltiket || timestamp.toISOString().substring(0, 10), + namaDokter: null, + loketId: parseInt(idloket), + loket: targetLoket ? targetLoket.namaLoket : null, + calledByAdmin: false, + penanggungJawab: (isFastTrack && fastTrackData) ? fastTrackData.penanggungJawab : null, + alasanFastTrack: (isFastTrack && fastTrackData) ? fastTrackData.alasanFastTrack : null, + // Additional API fields + idvisit: 1, // Default menunggu + idtiket: apiData.id + }; + + allPatients.value.push(newPatient); + // NOTE: Kita tidak perlu incrementBarcodeCounter() di sini karena barcode berasal dari API + + console.log(`✅ [queueStore] Successfully registered REGULER patient via API: ${noAntrian}`); + + return { + success: true, + message: `Pendaftaran ${clinic.name} berhasil via API.`, + patient: newPatient + }; + + } catch (error) { + console.error('❌ [queueStore] Error generating ticket via API:', error); + return { + success: false, + message: `Gagal pendaftaran API: ${error.message}` + }; + } + }; + + + /** + * Check-in patient via API + * POST to http://10.10.150.131:8089/api/v1/tiket/checkin + */ + const checkInPatientViaApi = async (barcode) => { + try { + console.log(`🔄 [queueStore] Syncing check-in via API for barcode: ${barcode}...`); + + const body = { + barcode: barcode, + statuspasien: "5", + statuspasien2: "6", + idklinikstatus: "1", + idklinikstatus2: "2" + }; + + console.log('📤 [queueStore] Check-in API Body:', body); + + const response = await fetch('http://10.10.150.131:8089/api/v1/tiket/checkin', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }); + + if (!response.ok) { + throw new Error(`API error! status: ${response.status}`); + } + + const result = await response.json(); + console.log('📥 [queueStore] Check-in API Response:', result); + + return { + success: true, + data: result + }; + + } catch (error) { + console.error('❌ [queueStore] Error syncing check-in via API:', error); + return { + success: false, + message: `Gagal sync check-in API: ${error.message}` + }; + } + }; + + // Check-in patient (update status from waiting to di-loket) // IMPORTANT: Hanya menggunakan EXACT barcode match untuk menghindari false positive // Format barcode: YYMMDD + 5 digit (contoh: 26011500001) // Jangan gunakan fallback ke noAntrian atau no karena bisa menyebabkan false positive - const checkInPatient = (patientIdOrBarcode) => { + const checkInPatient = async (patientIdOrBarcode) => { console.log('🔍 checkInPatient called with:', patientIdOrBarcode); console.log('📊 Total patients in store:', allPatients.value.length); @@ -1694,8 +1992,16 @@ export const useQueueStore = defineStore('queue', () => { ...allPatients.value[patientIndex], status: "di-loket", }; + syncApiPatientStatus(allPatients.value[patientIndex], "di-loket"); - console.log('✅ Check-in successful, patient status updated to di-loket'); + console.log('✅ Check-in successful locally, patient status updated to di-loket'); + + // Sync to API + const apiSyncResult = await checkInPatientViaApi(allPatients.value[patientIndex].barcode); + if (!apiSyncResult.success) { + console.warn('⚠️ Check-in API sync failed:', apiSyncResult.message); + // We still return true because local state is updated, but we could also return failure if critical + } return { success: true, @@ -1759,6 +2065,7 @@ export const useQueueStore = defineStore('queue', () => { }; allPatients.value[index] = updatedPatient; + syncApiPatientStatus(allPatients.value[index], 'di-loket'); // 4. Set Current Processing ISOLATED by key currentProcessingPatient.value[key] = updatedPatient; @@ -1783,6 +2090,11 @@ export const useQueueStore = defineStore('queue', () => { kliniks, penunjangs, + // API Patient State + apiPatientsPerLoket, + isLoadingPatients, + apiPatientsError, + // Computed totalPasien, @@ -1812,12 +2124,24 @@ export const useQueueStore = defineStore('queue', () => { generateBarcode, incrementBarcodeCounter, syncCountersWithState, + + // API Patient Actions + fetchPatientsForLoket, + getPatientsForLoket, + mapStatusFromDeskripsi, + mapApiPatientToStoreFormat, + registerRegulerPatientViaApi, + checkInPatientViaApi, + checkAndResetDaily, + isTodayPatient, + getResetThreshold, }; + }, { persist: { key: 'queue-store-state', storage: typeof window !== 'undefined' ? localStorage : undefined, - paths: ['allPatients', 'quotaUsed', 'currentProcessingPatient'], + paths: ['allPatients', 'quotaUsed', 'currentProcessingPatient', 'apiPatientsPerLoket'], serializer: { deserialize: JSON.parse, serialize: JSON.stringify,