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 @@ -
-
+
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 @@ -