diff --git a/nuxt.config.ts b/nuxt.config.ts index 46186a2..ba45147 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -117,6 +117,9 @@ export default defineNuxtConfig({ '/stats-api/**': { proxy: 'http://10.10.150.100:8084/api/v1/**' }, + '/visit-api/**': { + proxy: 'http://10.10.150.100:8084/api/v1/**' + }, }, vite: { diff --git a/pages/AdminKlinikRuang/[kodeKlinik].vue b/pages/AdminKlinikRuang/[kodeKlinik].vue index d3d6b02..5466137 100644 --- a/pages/AdminKlinikRuang/[kodeKlinik].vue +++ b/pages/AdminKlinikRuang/[kodeKlinik].vue @@ -890,6 +890,25 @@ const showFilterDialog = ref({}); const filterOptions = ref({}); const klinikRuangSearch = ref(''); +// API Patient Data +const apiPatients = ref([]); +const isLoadingPatients = ref(false); +const apiError = ref(null); + +// Helper: Check if a patient is from today +const isTodayPatient = (patient) => { + if (!patient.createdAt && !patient.jamPanggil) return false; + + const patientDate = new Date(patient.createdAt || patient.jamPanggil); + const today = new Date(); + + return ( + patientDate.getDate() === today.getDate() && + patientDate.getMonth() === today.getMonth() && + patientDate.getFullYear() === today.getFullYear() + ); +}; + // Initialize filter options for each room const initializeFilterOptions = (ruang) => { if (!filterOptions.value[ruang.nomorRuang]) { @@ -933,17 +952,454 @@ const showSnackbar = (message, color = 'success') => { snackbar.value = true; }; -// Get all patients for room (hanya pasien dari processStage 'klinik-ruang') -const getAllPatientsForRoom = (ruang) => { - return queueStore.allPatients - .filter(p => - p.kodeKlinik === klinikData.value?.kodeKlinik && - p.nomorRuang === ruang.nomorRuang && - p.ruang === ruang.namaRuang && - p.processStage === 'klinik-ruang' && - // Include semua pasien dengan status yang relevan - (p.status === 'anjungan' || p.status === 'di-loket' || p.status === 'terlambat' || p.status === 'pending') +/** + * Fetch patients from API for the clinic + * Uses filters: klinik_id, klinik_ruang_id, active, limit + */ +const fetchPatientsFromAPI = async () => { + if (!klinikData.value) { + console.warn('⚠️ Klinik data not available'); + return; + } + + isLoadingPatients.value = true; + apiError.value = null; + + try { + // Get klinik_id from clinicStore - find clinic by kodeKlinik + const clinic = clinicStore.clinics.find(c => + c.kode === klinikData.value.kodeKlinik && + (!jenisLayanan.value || c.jenisLayanan === jenisLayanan.value) ); + + if (!clinic || !clinic.id) { + console.warn('⚠️ Klinik ID not found for', klinikData.value.kodeKlinik); + console.log('Available clinics:', clinicStore.clinics.map(c => ({ kode: c.kode, id: c.id, jenisLayanan: c.jenisLayanan }))); + isLoadingPatients.value = false; + return; + } + + const klinikId = clinic.id; + + // Build API URL with filters - using Nuxt proxy to avoid CORS + const baseUrl = '/visit-api/visit'; + const params = new URLSearchParams({ + active: '1', + klinik_id: klinikId.toString(), + limit: '500' + }); + + // If there are rooms, fetch data for each room + // For now, we'll fetch all patients for the clinic and filter by room on the client side + const url = `${baseUrl}?${params.toString()}`; + + console.log('🔄 Fetching patients from:', url); + console.log('📋 Using clinic ID:', klinikId, 'for clinic code:', klinikData.value.kodeKlinik); + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const rawResponse = await response.json(); + console.log('📦 Raw API Response:', rawResponse); + + // Extract data array from response wrapper + const data = rawResponse?.data || []; + const message = rawResponse?.message || ''; + + console.log('📋 API Message:', message); + console.log('📊 Data array length:', data.length); + + // Process the response data + if (Array.isArray(data) && data.length > 0) { + // Map API data to our internal format + apiPatients.value = []; + + data.forEach((visit, index) => { + // Each visit can have multiple healthcare_services (sub-services/rooms) + const healthcareServices = visit.healthcare_services || []; + + if (healthcareServices.length > 0) { + healthcareServices.forEach(service => { + // Convert room ID to string for nomorRuang matching + const roomId = service.fk_ms_sub_healthcare_service_id; + const roomIdString = roomId ? String(roomId) : null; + + // Extract status from visit_statuses array (use latest status) + const visitStatuses = visit.visit_statuses || []; + const latestStatus = visitStatuses.length > 0 + ? visitStatuses[visitStatuses.length - 1] + : null; + + // Map status from API or default to 'di-loket' (patients just arrived from loket) + // Status 'pemeriksaan' will be set explicitly when patient is transferred from loket + let patientStatus = 'di-loket'; // Default for klinik-ruang patients + if (latestStatus && latestStatus.desc) { + const desc = latestStatus.desc.toLowerCase(); + if (desc.includes('pemeriksaan') || desc.includes('sedang diproses')) { + patientStatus = 'pemeriksaan'; + } else if (desc.includes('check-in') || desc.includes('loket')) { + patientStatus = 'di-loket'; + } + } + + const mappedPatient = { + no: visit.id || (10000 + index), + barcode: visit.visit_code || service.ticket || '', // Prioritize visit_code + noAntrian: service.ticket || visit.visit_code || '', + jamPanggil: service.check_in_datetime || visit.registration_datetime || '', + klinik: service.healthcare_service_name || klinikData.value.namaKlinik, + kodeKlinik: klinikData.value.kodeKlinik, + klinikId: service.fk_ms_healthcare_service_id || klinikId, + ruang: service.sub_healthcare_service_name || '', + nomorRuang: roomIdString, // Use string version of room ID + pembayaran: service.payment_type_name || visit.payment_type_name || '', // Payment type from service level + status: patientStatus, // Status from API or default 'di-loket' + processStage: 'klinik-ruang', + createdAt: visit.registration_datetime || new Date().toISOString(), + visitType: visit.visit_type_name || 'ONSITE', + noRM: visit.norm || '', + fastTrack: 'TIDAK', + registrationType: 'api', + visitId: visit.id, + visitCode: visit.visit_code, + // Store original API data for reference + _apiData: { visit, service } + }; + + // Only add if patient is from today + if (isTodayPatient(mappedPatient)) { + apiPatients.value.push(mappedPatient); + } + }); + } else { + // If no healthcare_services, create one entry for the visit + // Extract status from visit_statuses + const visitStatuses = visit.visit_statuses || []; + const latestStatus = visitStatuses.length > 0 + ? visitStatuses[visitStatuses.length - 1] + : null; + + let patientStatus = 'di-loket'; + if (latestStatus && latestStatus.desc) { + const desc = latestStatus.desc.toLowerCase(); + if (desc.includes('pemeriksaan') || desc.includes('sedang diproses')) { + patientStatus = 'pemeriksaan'; + } else if (desc.includes('check-in') || desc.includes('loket')) { + patientStatus = 'di-loket'; + } + } + + const mappedPatient = { + no: visit.id || (10000 + index), + barcode: visit.visit_code || '', // Use visit_code for barcode + noAntrian: visit.visit_code || '', + jamPanggil: visit.registration_datetime || '', + klinik: klinikData.value.namaKlinik, + kodeKlinik: klinikData.value.kodeKlinik, + klinikId: klinikId, + ruang: '', + nomorRuang: null, + pembayaran: visit.payment_type_name || '', // Payment type from visit level + status: patientStatus, // Status from API or default 'di-loket' + processStage: 'klinik-ruang', + createdAt: visit.registration_datetime || new Date().toISOString(), + visitType: visit.visit_type_name || 'ONSITE', + noRM: visit.norm || '', + fastTrack: 'TIDAK', + registrationType: 'api', + visitId: visit.id, + visitCode: visit.visit_code, + _apiData: { visit } + }; + + // Only add if patient is from today + if (isTodayPatient(mappedPatient)) { + apiPatients.value.push(mappedPatient); + } + } + }); + + const totalVisits = data.length; + const totalPatientEntries = apiPatients.value.length; + + console.log(`✅ Loaded ${totalPatientEntries} patient entries from ${totalVisits} visits (today only)`); + console.log('📋 Sample patient data:', apiPatients.value.slice(0, 2).map(p => ({ + ticket: p.noAntrian, + ruang: p.ruang, + nomorRuang: p.nomorRuang, + pembayaran: p.pembayaran, + kodeKlinik: p.kodeKlinik, + createdAt: p.createdAt + }))); + + // Merge with queueStore.allPatients to maintain consistency + mergeApiPatientsToStore(); + } else { + console.warn('⚠️ Unexpected API response format:', data); + apiPatients.value = []; + } + } catch (error) { + console.error('❌ Error fetching patients:', error); + apiError.value = error.message; + showSnackbar(`Gagal memuat data pasien: ${error.message}`, 'error'); + } finally { + isLoadingPatients.value = false; + } +}; + +/** + * Fetch patients for a specific room + */ +const fetchPatientsForRoom = async (ruang) => { + if (!klinikData.value || !ruang) { + console.warn('⚠️ Klinik data or room not available'); + return; + } + + isLoadingPatients.value = true; + apiError.value = null; + + try { + // Get klinik_id from masterStore ruangData + const ruangData = masterStore.ruangData || []; + const currentKlinik = ruangData.find(r => + r.kodeKlinik === klinikData.value.kodeKlinik && + (!jenisLayanan.value || r.jenisLayanan === jenisLayanan.value) + ); + + if (!currentKlinik || !currentKlinik.idKlinik) { + console.warn('⚠️ Klinik ID not found'); + isLoadingPatients.value = false; + return; + } + + const klinikId = currentKlinik.idKlinik; + + // Build API URL with filters including room ID - using Nuxt proxy to avoid CORS + const baseUrl = '/visit-api/visit'; + const params = new URLSearchParams({ + active: '1', + klinik_id: klinikId.toString(), + limit: '500' + }); + + // Add room filter if room has an ID + if (ruang.idRuang) { + params.append('klinik_ruang_id', ruang.idRuang.toString()); + } + + const url = `${baseUrl}?${params.toString()}`; + + console.log('🔄 Fetching patients for room from:', url); + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const rawResponse = await response.json(); + + // Extract data array from response wrapper + const data = rawResponse?.data || []; + + // Process and merge the response data + if (Array.isArray(data) && data.length > 0) { + const roomPatients = []; + + data.forEach((visit, index) => { + const healthcareServices = visit.healthcare_services || []; + + if (healthcareServices.length > 0) { + healthcareServices.forEach(service => { + // Convert room ID to string for nomorRuang matching + const roomId = service.fk_ms_sub_healthcare_service_id; + const roomIdString = roomId ? String(roomId) : null; + + // Extract status from visit_statuses + const visitStatuses = visit.visit_statuses || []; + const latestStatus = visitStatuses.length > 0 + ? visitStatuses[visitStatuses.length - 1] + : null; + + let patientStatus = 'di-loket'; + if (latestStatus && latestStatus.desc) { + const desc = latestStatus.desc.toLowerCase(); + if (desc.includes('pemeriksaan') || desc.includes('sedang diproses')) { + patientStatus = 'pemeriksaan'; + } else if (desc.includes('check-in') || desc.includes('loket')) { + patientStatus = 'di-loket'; + } + } + + const mappedPatient = { + no: visit.id || (10000 + index), + barcode: visit.visit_code || service.ticket || '', // Prioritize visit_code + noAntrian: service.ticket || visit.visit_code || '', + jamPanggil: service.check_in_datetime || visit.registration_datetime || '', + klinik: service.healthcare_service_name || klinikData.value.namaKlinik, + kodeKlinik: klinikData.value.kodeKlinik, + klinikId: service.fk_ms_healthcare_service_id || klinikId, + ruang: ruang.namaRuang, + nomorRuang: roomIdString, // Use string version of room ID + nomorScreen: ruang.nomorScreen, + pembayaran: service.payment_type_name || visit.payment_type_name || '', // Payment type from service level + status: patientStatus, // Status from API or default 'di-loket' + processStage: 'klinik-ruang', + createdAt: visit.registration_datetime || new Date().toISOString(), + visitType: visit.visit_type_name || 'ONSITE', + noRM: visit.norm || '', + fastTrack: 'TIDAK', + registrationType: 'api', + visitId: visit.id, + visitCode: visit.visit_code, + _apiData: { visit, service } + }; + + // Only add if patient is from today + if (isTodayPatient(mappedPatient)) { + roomPatients.push(mappedPatient); + } + }); + } else { + // Extract status from visit_statuses + const visitStatuses = visit.visit_statuses || []; + const latestStatus = visitStatuses.length > 0 + ? visitStatuses[visitStatuses.length - 1] + : null; + + let patientStatus = 'di-loket'; + if (latestStatus && latestStatus.desc) { + const desc = latestStatus.desc.toLowerCase(); + if (desc.includes('pemeriksaan') || desc.includes('sedang diproses')) { + patientStatus = 'pemeriksaan'; + } else if (desc.includes('check-in') || desc.includes('loket')) { + patientStatus = 'di-loket'; + } + } + + const mappedPatient = { + no: visit.id || (10000 + index), + barcode: visit.visit_code || '', // Use visit_code for barcode + noAntrian: visit.visit_code || '', + jamPanggil: visit.registration_datetime || '', + klinik: klinikData.value.namaKlinik, + kodeKlinik: klinikData.value.kodeKlinik, + klinikId: klinikId, + ruang: ruang.namaRuang, + nomorRuang: ruang.nomorRuang, + nomorScreen: ruang.nomorScreen, + pembayaran: visit.payment_type_name || '', // Payment type from visit level + status: patientStatus, // Status from API or default 'di-loket' + processStage: 'klinik-ruang', + createdAt: visit.registration_datetime || new Date().toISOString(), + visitType: visit.visit_type_name || 'ONSITE', + noRM: visit.norm || '', + fastTrack: 'TIDAK', + registrationType: 'api', + visitId: visit.id, + visitCode: visit.visit_code, + _apiData: { visit } + }; + + // Only add if patient is from today + if (isTodayPatient(mappedPatient)) { + roomPatients.push(mappedPatient); + } + } + }); + + // Update apiPatients for this specific room + // Remove old patients for this room and add new ones + apiPatients.value = [ + ...apiPatients.value.filter(p => p.nomorRuang !== ruang.nomorRuang), + ...roomPatients + ]; + + console.log(`✅ Loaded ${roomPatients.length} patients for room ${ruang.namaRuang}`); + + mergeApiPatientsToStore(); + } + } catch (error) { + console.error('❌ Error fetching patients for room:', error); + apiError.value = error.message; + } finally { + isLoadingPatients.value = false; + } +}; + +/** + * Merge API patients into queueStore.allPatients + */ +const mergeApiPatientsToStore = () => { + if (apiPatients.value.length === 0) return; + + // Build a Set of unique identifiers from API patients for fast lookup + const apiPatientIds = new Set( + apiPatients.value.map(p => p.visitId || p.visitCode || p.barcode || p.noAntrian).filter(Boolean) + ); + + // Remove duplicates: remove any patient (API or not) that matches visitId, visitCode, barcode, or noAntrian + queueStore.allPatients = queueStore.allPatients.filter(p => { + const patientId = p.visitId || p.visitCode || p.barcode || p.noAntrian; + const isDuplicate = apiPatientIds.has(patientId); + + if (isDuplicate) { + console.log('🗑️ Removing duplicate patient:', { + ticket: p.noAntrian, + barcode: p.barcode, + visitId: p.visitId, + registrationType: p.registrationType + }); + } + + return !isDuplicate; + }); + + // Add new API patients + queueStore.allPatients.push(...apiPatients.value); + + console.log(`📊 Total patients in store: ${queueStore.allPatients.length}`); +}; + +// Get all patients for room (menggunakan data dari API) +const getAllPatientsForRoom = (ruang) => { + // Debug logging + console.log('🔍 Filtering patients for room:', { + ruangName: ruang.namaRuang, + ruangNomor: ruang.nomorRuang, + klinikCode: klinikData.value?.kodeKlinik + }); + + // Prioritize API patients, fallback to queueStore patients + const patients = queueStore.allPatients + .filter(p => { + const matches = + p.kodeKlinik === klinikData.value?.kodeKlinik && + p.nomorRuang === ruang.nomorRuang && + p.processStage === 'klinik-ruang' && + // Include semua pasien dengan status yang relevan (including pemeriksaan for API patients) + (p.status === 'anjungan' || p.status === 'pemeriksaan' || p.status === 'di-loket' || p.status === 'terlambat' || p.status === 'pending'); + + if (!matches && p.kodeKlinik === klinikData.value?.kodeKlinik) { + console.log('❌ Patient did not match room:', { + ticket: p.noAntrian, + patientRoom: p.nomorRuang, + expectedRoom: ruang.nomorRuang, + stage: p.processStage, + status: p.status + }); + } + + return matches; + }); + + console.log(` ✅ Found ${patients.length} patients for room ${ruang.namaRuang}`); + + return patients; }; // Get filtered and sorted patients for room @@ -1794,6 +2250,12 @@ onMounted(async () => { await clinicStore.fetchRegulerClinics(); // Then sync rooms await ruangStore.fetchRuangFromAPI(); + + // 2. Fetch patient data from API + if (klinikData.value) { + console.log('📋 Fetching patient data for clinic:', klinikData.value.kodeKlinik); + await fetchPatientsFromAPI(); + } } catch (error) { console.error('❌ Error syncing data in AdminKlinikRuang:', error); } diff --git a/pages/AdminLoket/[id].vue b/pages/AdminLoket/[id].vue index 40b3991..3b46825 100644 --- a/pages/AdminLoket/[id].vue +++ b/pages/AdminLoket/[id].vue @@ -617,7 +617,17 @@ const anjunganCount = computed(() => { const nextQueueInfo = computed(() => { const currentPatientNo = currentProcessingPatient.value?.no; - const nextPatient = (diLoketPatients.value || []).find(p => p.no !== currentPatientNo) || (diLoketPatients.value || [])[0]; + const targetLoketId = parseInt(loketId.value); + + // Filter diLoketPatients to only show patients for THIS loket + const loketFilteredPatients = (diLoketPatients.value || []).filter(p => { + // Only show patients assigned to this specific loket + return p.loketId && String(p.loketId) === String(targetLoketId); + }); + + // Find next patient (excluding current processing patient) + const nextPatient = loketFilteredPatients.find(p => p.no !== currentPatientNo) || loketFilteredPatients[0]; + if (nextPatient) { return `Antrian berikutnya: ${nextPatient.noAntrian.split(" |")[0]}`; } @@ -750,11 +760,132 @@ const buatAntreanKlinikRuang = async (klinikRuang, ruang) => { return; } + // POST to external visit ticket API FIRST to get ticket number + let apiTicketNumber = null; + let apiCallSuccess = false; + + try { + // Map payment type: BPJS/JKN -> 2, UMUM/others -> 1 + const paymentTypeId = (patient.pembayaran || '').toUpperCase().includes('BPJS') || + (patient.pembayaran || '').toUpperCase().includes('JKN') + ? 2 : 1; + + // Map service type: Reguler -> 1, Eksekutif/Grand Pav -> 2 + const serviceTypeId = (patient.pembayaran || '').toUpperCase().includes('EKSEKUTIF') || + (patient.pembayaran || '').toUpperCase().includes('VIP') || + (patient.pembayaran || '').toUpperCase().includes('GRAND') + ? 2 : 1; + + // Get idruangan from ruang object, ensure it's a number or null + let subHealthcareServiceId = null; + if (ruang.kodeRuang) { + const parsed = Number(ruang.kodeRuang); + subHealthcareServiceId = isNaN(parsed) ? null : parsed; + } else if (ruang.idruangan) { + const parsed = Number(ruang.idruangan); + subHealthcareServiceId = isNaN(parsed) ? null : parsed; + } + + // Ensure healthcare_service_id is a number - MUST use actual clinic ID from clinicStore + const actualClinic = clinicStore.clinics.find(c => c.kode === klinikRuang.kodeKlinik); + const healthcareServiceId = actualClinic ? Number(actualClinic.id) : null; + + if (!healthcareServiceId) { + console.error('❌ Could not find clinic ID for kode:', klinikRuang.kodeKlinik); + snackbarText.value = "Gagal membuat antrean ruang: ID klinik tidak ditemukan"; + snackbarColor.value = "error"; + snackbar.value = true; + closeKlinikRuangDialog(); + return; + } + + const visitTicketBody = { + visit_code: Number(patient.barcode), + healthcare_service_id: healthcareServiceId, + sub_healthcare_service_id: subHealthcareServiceId, + payment_type_id: paymentTypeId, + visit_status_id: [14, 15], + visit_type_id: 1, + service_type_id: serviceTypeId, + healthcare_type_id: 2 + }; + + console.log('📤 Sending visit ticket to API:', visitTicketBody); + + const visitResponse = await fetch('http://10.10.150.100:8084/api/v1/visit/ticket/klinik', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(visitTicketBody) + }); + + if (visitResponse.ok) { + const visitResult = await visitResponse.json(); + console.log('✅ Visit ticket created successfully:', visitResult); + + // Extract ticket number from API response + if (visitResult.healthcare_service && visitResult.healthcare_service.ticket) { + apiTicketNumber = visitResult.healthcare_service.ticket; + apiCallSuccess = true; + console.log('🎫 Using API ticket number:', apiTicketNumber); + } else { + console.error('❌ API response missing ticket number'); + snackbarText.value = "Gagal membuat antrean ruang: Nomor tiket tidak ditemukan di response API"; + snackbarColor.value = "error"; + snackbar.value = true; + closeKlinikRuangDialog(); + return; + } + } else { + // Parse error response + let errorMessage = "Gagal membuat antrean ruang"; + try { + const errorData = await visitResponse.json(); + console.error('⚠️ Visit ticket API error:', errorData); + + // Check if it's a duplicate ticket error + if (errorData.message && errorData.message.toLowerCase().includes('already exists')) { + errorMessage = `Tiket klinik ruang sudah ada untuk pasien ini. ${errorData.message}`; + } else if (errorData.message) { + errorMessage = `Gagal membuat antrean ruang: ${errorData.message}`; + } else if (errorData.error) { + errorMessage = `Gagal membuat antrean ruang: ${errorData.error}`; + } + } catch (parseError) { + const errorText = await visitResponse.text(); + console.error('⚠️ Visit ticket API returned error:', visitResponse.status, errorText); + errorMessage = `Gagal membuat antrean ruang (Status: ${visitResponse.status})`; + } + + snackbarText.value = errorMessage; + snackbarColor.value = "error"; + snackbar.value = true; + closeKlinikRuangDialog(); + return; + } + } catch (apiError) { + console.error('❌ Error sending visit ticket to API:', apiError); + snackbarText.value = "Gagal membuat antrean ruang: Kesalahan koneksi ke API"; + snackbarColor.value = "error"; + snackbar.value = true; + closeKlinikRuangDialog(); + return; + } + + // Only create room queue if API call was successful + if (!apiCallSuccess || !apiTicketNumber) { + console.error('❌ Cannot create room queue without API ticket number'); + return; + } + + // Create room queue with API ticket number const result = queueStore.createAntreanKlinikRuang( klinikRuang, ruang, patient, - "loket" + "loket", + apiTicketNumber // Pass API ticket number ); if (result.success && result.patient) { @@ -769,6 +900,7 @@ const buatAntreanKlinikRuang = async (klinikRuang, ruang) => { return; } } + snackbarText.value = result.message; snackbarColor.value = result.success ? "success" : "error"; diff --git a/stores/queueStore.js b/stores/queueStore.js index 2b61cd4..ee1bda8 100644 --- a/stores/queueStore.js +++ b/stores/queueStore.js @@ -169,7 +169,7 @@ export const useQueueStore = defineStore('queue', () => { processStage: 'loket', createdAt: apiPatient.tanggal || new Date().toISOString(), registrationType: 'api', - visitType: 'SEKARANG', + visitType: 'Onsite', visitDate: apiPatient.tanggal ? apiPatient.tanggal.split('T')[0] : new Date().toISOString().substring(0, 10), namaDokter: null, noRM: null, @@ -325,13 +325,21 @@ export const useQueueStore = defineStore('queue', () => { }); // 4. Remove existing patients that collide with new ones (to be replaced) - // AND remove stale 'api' patients for this loket + // This ensures API data ALWAYS takes precedence over local/seed data + // Remove ANY patient (local, seed, or api) with matching barcode/idtiket allPatients.value = allPatients.value.filter(p => { const key = p.idtiket ? `id-${p.idtiket}` : `bc-${p.barcode}`; - // Remove if it's being replaced by new batch + // Remove if it's being replaced by new API batch (matches by barcode or idtiket) if (newPatientMap.has(key)) return false; + // Also check if barcode matches any new API patient (for cases where local has no idtiket) + // This prevents duplicates like barcode "2602050017" appearing as both BPJS (local) and JKN (API) + if (p.barcode) { + const barcodeMatches = patientsWithLoketId.some(newP => newP.barcode === p.barcode); + if (barcodeMatches) return false; + } + // Remove if it's a stale 'api' patient for this loket (that wasn't in the new batch) if (p.registrationType === 'api' && String(p.loketId) === String(loketId)) return false; @@ -1164,6 +1172,32 @@ export const useQueueStore = defineStore('queue', () => { }; syncApiPatientStatus(allPatients.value[patientIndex], "di-loket"); message = `Pasien ${patientCode} berhasil check in dan masuk ke Tabel Loket Klinik`; + + // POST to external API when patient finishes at loket + try { + fetch('http://10.10.150.131:8089/api/v1/tiket/selesai', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + idloket: String(patient.loketId || specificId || ""), + barcode: patient.barcode || "", + statuspasien: "9", + idklinikstatus: "2" + }) + }).then(response => { + if (response.ok) { + console.log(`✅ [queueStore] Successfully posted selesai status for patient ${patient.barcode}`); + } else { + console.error(`⚠️ [queueStore] Failed to post selesai status for patient ${patient.barcode}:`, response.status); + } + }).catch(error => { + console.error(`❌ [queueStore] Error posting selesai status for patient ${patient.barcode}:`, error); + }); + } catch (error) { + console.error(`❌ [queueStore] Error initiating selesai status update for patient ${patient.barcode}:`, error); + } } // Jika check-in di klinik, selesai else if (adminType === 'klinik') { @@ -1402,7 +1436,7 @@ export const useQueueStore = defineStore('queue', () => { }; }; - const createAntreanKlinikRuang = (klinikRuang, ruang, patient = null, adminType = 'klinik') => { + const createAntreanKlinikRuang = (klinikRuang, ruang, patient = null, adminType = 'klinik', apiTicketNumber = null) => { const newNo = allPatients.value.length + 1; const timestamp = new Date(); const barcode = patient ? patient.barcode : generateBarcode([], allPatients); @@ -1410,24 +1444,32 @@ export const useQueueStore = defineStore('queue', () => { // Generate nomor antrian baru dengan format: [huruf pertama poli + urutan abjad ruang + nomor antrian ruang] // Contoh: "Anak" ruang 1 = "AA001" (A dari Anak, A dari ruang 1, 001 nomor antrian) - // 1. Ambil huruf pertama dari nama klinik/poli - const firstLetter = klinikRuang.namaKlinik.charAt(0).toUpperCase(); + let newNoAntrian; - // 2. Konversi nomor ruang ke abjad (1 = A, 2 = B, 3 = C, dst) - const ruangNumber = parseInt(ruang.nomorRuang) || 1; - const ruangLetter = String.fromCharCode(64 + ruangNumber); // 64 = '@', 65 = 'A', 66 = 'B', dst - - // 3. Hitung nomor antrian ruang (dimulai dari 1, maksimal 3 digit) - const roomQueues = allPatients.value.filter(p => - p.kodeKlinik === klinikRuang.kodeKlinik && - p.nomorRuang === ruang.nomorRuang && - p.processStage === 'klinik-ruang' - ); - const queueNumber = roomQueues.length + 1; - const queueNumberStr = String(queueNumber).padStart(3, "0"); - - // 4. Format nomor antrian: AA001, AB002, dst - const newNoAntrian = `${firstLetter}${ruangLetter}${queueNumberStr}`; + // Use API ticket number if available, otherwise generate locally + if (apiTicketNumber) { + newNoAntrian = apiTicketNumber; // Use ticket from API (e.g., "PK001") + console.log('🎫 Using API ticket number:', newNoAntrian); + } else { + // 1. Ambil huruf pertama dari nama klinik/poli + const firstLetter = klinikRuang.namaKlinik.charAt(0).toUpperCase(); + + // 2. Konversi nomor ruang ke abjad (1 = A, 2 = B, 3 = C, dst) + const ruangNumber = parseInt(ruang.nomorRuang) || 1; + const ruangLetter = String.fromCharCode(64 + ruangNumber); // 64 = '@', 65 = 'A', 66 = 'B', dst + + // 3. Hitung nomor antrian ruang (dimulai dari 1, maksimal 3 digit) + const roomQueues = allPatients.value.filter(p => + p.kodeKlinik === klinikRuang.kodeKlinik && + p.nomorRuang === ruang.nomorRuang && + p.processStage === 'klinik-ruang' + ); + const queueNumber = roomQueues.length + 1; + const queueNumberStr = String(queueNumber).padStart(3, "0"); + + // 4. Format nomor antrian: AA001, AB002, dst + newNoAntrian = `${firstLetter}${ruangLetter}${queueNumberStr}`; + } const newPatient = { no: newNo, @@ -1475,7 +1517,6 @@ export const useQueueStore = defineStore('queue', () => { // Pindah pasien ke klinik ruang lain dengan nomor antrian tetap const pindahKlinikRuang = (patient, targetKlinikRuang, targetRuang) => { - const patientIndex = allPatients.value.findIndex(p => p.no === patient.no); if (patientIndex === -1) { return { success: false, message: "Pasien tidak ditemukan" };