diff --git a/composables/useThermalPrint.ts b/composables/useThermalPrint.ts index d177832..b8befb9 100644 --- a/composables/useThermalPrint.ts +++ b/composables/useThermalPrint.ts @@ -5,6 +5,8 @@ export interface ThermalPrintData { noAntrian: string; barcode: string; klinik: string; + ruang?: string; + nomorRuang?: string; shift: string; pembayaran: string; tanggal: string; @@ -103,6 +105,21 @@ export const useThermalPrint = () => { // Format nomor antrian (hilangkan bagian "| Onsite - barcode") const noAntrianDisplay = data.noAntrian.split(' |')[0]; + + // Format informasi ruang: "Poli Anak - Ruang A" + let ruangInfo = ''; + if (data.klinik && data.nomorRuang) { + // Konversi nomor ruang ke abjad (1 = A, 2 = B, 3 = C, dst) + const ruangNumber = parseInt(data.nomorRuang) || 1; + const ruangLetter = String.fromCharCode(64 + ruangNumber); // 64 = '@', 65 = 'A', 66 = 'B', dst + ruangInfo = `${data.klinik} - Ruang ${ruangLetter}`; + } else if (data.klinik && data.ruang) { + // Fallback jika hanya ada nama ruang + ruangInfo = `${data.klinik} - ${data.ruang}`; + } else if (data.klinik) { + // Hanya klinik jika tidak ada info ruang + ruangInfo = data.klinik; + } const html = ` @@ -333,7 +350,7 @@ export const useThermalPrint = () => {
${noAntrianDisplay}
-
${data.klinik}
+ ${ruangInfo ? `
${ruangInfo}
` : `
${data.klinik}
`}
@@ -502,6 +519,8 @@ export const useThermalPrint = () => { noAntrian: patient.noAntrian || '', barcode: patient.barcode || '', klinik: patient.klinik || '', + ruang: patient.ruang || undefined, + nomorRuang: patient.nomorRuang || undefined, shift: patient.shift || 'Shift 1', pembayaran: patient.pembayaran || '', tanggal: tanggalKunjungan, diff --git a/pages/AdminKlinikRuang/[kodeKlinik].vue b/pages/AdminKlinikRuang/[kodeKlinik].vue index 9f485cb..a1b1872 100644 --- a/pages/AdminKlinikRuang/[kodeKlinik].vue +++ b/pages/AdminKlinikRuang/[kodeKlinik].vue @@ -9,31 +9,6 @@ theme="warning" /> - - - - - - - - - -
- Tekan Enter. (Apabila barcode depan nomor ada huruf lain, Ex: J200730100005 "Hiraukan huruf 'j' nya") -
-
-
-
-
-
- {{ getCurrentProcessingForRoom(ruang)?.noAntrian.split(" |")[0] }} + {{ getCurrentProcessingForRoom(ruang)?.noAntrian?.split(" |")[0] || '-' }}
@@ -113,24 +88,56 @@
Tidak ada pasien yang diproses
- -
+ +
-
+
-
- {{ patient.noAntrian.split(" |")[0] }} +
+ {{ patient.noAntrian?.split(" |")[0] || patient.barcode || '-' }}
-
+
+ + + mdi-check-circle + Pemeriksaan Awal + + + + mdi-check-circle + Tindakan + + + + mdi-pause-circle + Pending + +
+
Pindah Ruang mdi-play Proses - - mdi-ticket - Generate -
- - - - - Pilih Ruang - - mdi-close - - - - -
-
Pasien: {{ scannedPatient?.noAntrian?.split(" |")[0] }}
-
{{ scannedPatient?.barcode }}
-
- - - - -
- - - Batal - - Buat Antrean - - -
-
- @@ -403,14 +356,9 @@ const currentDate = ref( }) ); -const barcodeInput = ref(''); const snackbar = ref(false); const snackbarText = ref(''); const snackbarColor = ref('success'); -const showRuangDialog = ref(false); -const scannedPatient = ref(null); -const selectedRuang = ref(null); -const selectedTipeLayanan = ref('Pemeriksaan Awal'); const showDetailDialog = ref(false); const selectedPatientDetail = ref(null); const pendingPage = ref({}); @@ -424,105 +372,29 @@ const showSnackbar = (message, color = 'success') => { snackbar.value = true; }; -const handleScanBarcode = () => { - if (!barcodeInput.value.trim()) { - showSnackbar('Masukkan barcode/nomor antrian', 'error'); - return; - } - - // Find patient from queueStore - const cleanInput = String(barcodeInput.value).trim().toUpperCase(); - const numericInput = cleanInput.replace(/^[A-Z]+/, ''); - - const patient = queueStore.allPatients.find(p => { - if (p.barcode === cleanInput || p.barcode === numericInput) return true; - const noAntrianUpper = (p.noAntrian || '').toUpperCase(); - if (noAntrianUpper.includes(cleanInput) || noAntrianUpper.includes(numericInput)) return true; - return false; - }); - - if (!patient) { - showSnackbar('Pasien tidak ditemukan', 'error'); - barcodeInput.value = ''; - return; - } - - // Check if patient already has antrean klinik ruang - const existingAntrean = queueStore.allPatients.find(p => - p.referencePatient === patient.noAntrian && - p.kodeKlinik === klinikData.value?.kodeKlinik && - p.processStage === 'klinik-ruang' - ); - - if (existingAntrean) { - showSnackbar(`Pasien sudah memiliki antrean di ${existingAntrean.ruang}`, 'warning'); - barcodeInput.value = ''; - return; - } - - scannedPatient.value = patient; - selectedRuang.value = null; - selectedTipeLayanan.value = 'Pemeriksaan Awal'; - showRuangDialog.value = true; - barcodeInput.value = ''; -}; - -const confirmCreateAntrean = () => { - if (!selectedRuang.value || !selectedTipeLayanan.value || !scannedPatient.value) { - showSnackbar('Pilih ruang dan tipe layanan', 'error'); - return; - } - - const klinikRuang = { - kodeKlinik: klinikData.value.kodeKlinik, - namaKlinik: klinikData.value.namaKlinik - }; - - const result = queueStore.scanAndCreateAntreanKlinikRuang( - scannedPatient.value.barcode, - klinikRuang, - selectedRuang.value, - selectedTipeLayanan.value - ); - - if (result.success && result.patient) { - // Auto proses pasien yang baru digenerate tiket - const processResult = queueStore.processPatientKlinikRuang( - result.patient, - 'proses', - klinikData.value.kodeKlinik, - selectedRuang.value.nomorRuang - ); - showSnackbar(processResult.message, processResult.success ? 'success' : 'error'); - } else { - showSnackbar(result.message, result.success ? 'success' : 'error'); - } - - showRuangDialog.value = false; - scannedPatient.value = null; -}; - -// Get pending patients (belum generate tiket - dari AdminLoket/AdminKlinik) -// Juga include pasien yang sudah digenerate tiket tapi status pending -const getPendingPatientsForRoom = (ruang) => { +// 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 && - ( - // Belum generate tiket (belum punya tipeLayanan) - (!p.tipeLayanan && (p.processStage === 'klinik' || p.processStage === 'klinik-ruang') && - (p.status === 'waiting' || p.status === 'di-loket' || p.status === 'terlambat')) || - // Sudah generate tiket tapi status pending - (p.tipeLayanan && p.processStage === 'klinik-ruang' && p.status === 'pending') - ) + p.processStage === 'klinik-ruang' && + // Exclude pasien yang sedang diproses (current processing) + p.no !== getCurrentProcessingForRoom(ruang)?.no && + // Include semua pasien dengan status yang relevan + (p.status === 'waiting' || p.status === 'di-loket' || p.status === 'terlambat' || p.status === 'pending') ) .sort((a, b) => { - // Prioritaskan yang belum digenerate (pending status di akhir) - if (!a.tipeLayanan && b.tipeLayanan) return -1; - if (a.tipeLayanan && !b.tipeLayanan) return 1; + // Prioritaskan yang sudah digenerate/diproses & pending di atas + const aHasTiket = !!a.tipeLayanan; + const bHasTiket = !!b.tipeLayanan; + // Jika satu sudah punya tiket dan yang lain belum, yang sudah tiket di atas + if (aHasTiket && !bHasTiket) return -1; + if (!aHasTiket && bHasTiket) return 1; + + // Jika keduanya sudah punya tiket atau keduanya belum, sort by status const statusPriority = { 'di-loket': 1, 'waiting': 2, @@ -532,12 +404,18 @@ const getPendingPatientsForRoom = (ruang) => { const priorityDiff = (statusPriority[a.status] || 99) - (statusPriority[b.status] || 99); if (priorityDiff !== 0) return priorityDiff; + // Sort by time const timeA = a.jamPanggil?.split(':').map(Number) || [0, 0]; const timeB = b.jamPanggil?.split(':').map(Number) || [0, 0]; return timeA[0] * 60 + timeA[1] - (timeB[0] * 60 + timeB[1]); }); }; +// Get pending patients (untuk backward compatibility jika masih digunakan) +const getPendingPatientsForRoom = (ruang) => { + return getAllPatientsForRoom(ruang).filter(p => !p.tipeLayanan || p.status === 'pending'); +}; + const getCurrentProcessingForRoom = (ruang) => { @@ -610,7 +488,7 @@ const handleGenerateTicket = (patient, currentRuang) => { } } else { // Pasien belum punya ruang, isi field dan trigger scan (akan muncul dialog) - barcodeInput.value = patient.noAntrian.split(" |")[0] || patient.barcode; + barcodeInput.value = patient.noAntrian?.split(" |")[0] || patient.barcode || ''; handleScanBarcode(); } }; @@ -638,16 +516,17 @@ const confirmPindahRuang = () => { // Jika pasien sudah punya antrean klinik ruang (sudah digenerate tiket), update antreannya juga if (patient.processStage === 'klinik-ruang' && patient.tipeLayanan) { - // Update antrean yang sudah ada - queueStore.allPatients[patientIndex] = { - ...queueStore.allPatients[patientIndex], - ruang: selectedRuangBaru.value.namaRuang, - nomorRuang: selectedRuangBaru.value.nomorRuang, - nomorScreen: selectedRuangBaru.value.nomorScreen, - // Update noAntrian untuk reflect ruang baru - noAntrian: `${patient.noAntrian.split(" |")[0]} | ${klinikData.value.namaKlinik} - ${selectedRuangBaru.value.namaRuang} - ${patient.tipeLayanan}`, - noAntrianRuang: `${klinikData.value.namaKlinik} - ${selectedRuangBaru.value.namaRuang} | ${patient.noAntrian.split(" |")[0]}` - }; + // Update antrean yang sudah ada + const baseNoAntrian = patient.noAntrian?.split(" |")[0] || patient.barcode || ''; + queueStore.allPatients[patientIndex] = { + ...queueStore.allPatients[patientIndex], + ruang: selectedRuangBaru.value.namaRuang, + nomorRuang: selectedRuangBaru.value.nomorRuang, + nomorScreen: selectedRuangBaru.value.nomorScreen, + // Update noAntrian untuk reflect ruang baru + noAntrian: `${baseNoAntrian} | ${klinikData.value.namaKlinik} - ${selectedRuangBaru.value.namaRuang} - ${patient.tipeLayanan}`, + noAntrianRuang: `${klinikData.value.namaKlinik} - ${selectedRuangBaru.value.namaRuang} | ${baseNoAntrian}` + }; // Clear current processing dari ruang lama jika ada const oldRuang = ruangList.value.find(r => r.nomorRuang === patient.nomorRuang); @@ -668,7 +547,7 @@ const confirmPindahRuang = () => { } showSnackbar( - `Pasien ${selectedPatientForPindah.value.noAntrian.split(" |")[0]} berhasil dipindah ke ${selectedRuangBaru.value.namaRuang}`, + `Pasien ${selectedPatientForPindah.value.noAntrian?.split(" |")[0] || selectedPatientForPindah.value.barcode || '-'} berhasil dipindah ke ${selectedRuangBaru.value.namaRuang}`, 'success' ); @@ -679,27 +558,43 @@ const confirmPindahRuang = () => { const handleCallPatientByTipe = (ruang, tipeLayanan) => { const current = getCurrentProcessingForRoom(ruang); - if (!current) return; + if (!current || !current.no) { + showSnackbar('Tidak ada pasien yang sedang diproses', 'error'); + return; + } // Update tipeLayanan pasien untuk tracking panggilan terakhir const patientIndex = queueStore.allPatients.findIndex(p => p.no === current.no); - if (patientIndex !== -1) { - // Update status to di-loket untuk ditampilkan di anjungan - // Simpan tipeLayanan yang dipanggil untuk display di anjungan - queueStore.allPatients[patientIndex] = { - ...queueStore.allPatients[patientIndex], - status: 'di-loket', - tipeLayanan: tipeLayanan, // Update tipeLayanan untuk display di anjungan - lastCalledAt: new Date().toISOString(), - lastCalledTipeLayanan: tipeLayanan // Track tipe layanan terakhir yang dipanggil - }; - - // Update current processing (tetap 1 pasien, tidak dipisah per tipe) - const key = `klinik-ruang-${klinikData.value.kodeKlinik}-${ruang.nomorRuang}`; - queueStore.currentProcessingPatient[key] = queueStore.allPatients[patientIndex]; + if (patientIndex === -1) { + showSnackbar('Pasien tidak ditemukan', 'error'); + return; } - showSnackbar(`Memanggil pasien ${current.noAntrian.split(" |")[0]} untuk ${tipeLayanan}`, 'success'); + // Update tracking panggilan berdasarkan tipeLayanan + const updateData = { + ...queueStore.allPatients[patientIndex], + status: 'di-loket', + lastCalledAt: new Date().toISOString(), + lastCalledTipeLayanan: tipeLayanan + }; + + // Set penanda panggilan sesuai tipeLayanan + if (tipeLayanan === 'Pemeriksaan Awal') { + updateData.calledPemeriksaanAwal = true; + } else if (tipeLayanan === 'Tindakan') { + updateData.calledTindakan = true; + } + + queueStore.allPatients[patientIndex] = updateData; + + // Update current processing (tetap 1 pasien, tidak dipisah per tipe) + const key = `klinik-ruang-${klinikData.value.kodeKlinik}-${ruang.nomorRuang}`; + queueStore.currentProcessingPatient[key] = queueStore.allPatients[patientIndex]; + + const patientCode = queueStore.allPatients[patientIndex].noAntrian?.split(" |")[0] || + queueStore.allPatients[patientIndex].barcode || + '-'; + showSnackbar(`Memanggil pasien ${patientCode} untuk ${tipeLayanan}`, 'success'); }; const showPatientDetail = (patient) => { @@ -758,6 +653,25 @@ const handlePatientAction = (ruang, action) => { showSnackbar(result.message, result.success ? 'success' : 'error'); }; +// Handle proses pasien (untuk pasien yang belum diproses) +const handleProcessPatient = (ruang, patient) => { + const patientIndex = queueStore.allPatients.findIndex(p => p.no === patient.no); + if (patientIndex === -1) { + showSnackbar('Pasien tidak ditemukan', 'error'); + return; + } + + // Set sebagai current processing + const result = queueStore.processPatientKlinikRuang( + queueStore.allPatients[patientIndex], + 'proses', + klinikData.value.kodeKlinik, + ruang.nomorRuang + ); + + showSnackbar(result.message, result.success ? 'success' : 'error'); +}; + const handleProcessPendingPatient = (ruang, patient) => { // Proses kembali pasien yang dipending const patientIndex = queueStore.allPatients.findIndex(p => p.no === patient.no); @@ -766,10 +680,10 @@ const handleProcessPendingPatient = (ruang, patient) => { return; } - // Update status pasien dari pending menjadi di-loket + // Update status pasien dari pending menjadi waiting queueStore.allPatients[patientIndex] = { ...queueStore.allPatients[patientIndex], - status: 'di-loket' + status: 'waiting' }; // Set sebagai current processing @@ -783,12 +697,23 @@ const handleProcessPendingPatient = (ruang, patient) => { showSnackbar(result.message, result.success ? 'success' : 'error'); }; -const paginatedPendingPatients = (ruang) => { - const allPending = getPendingPatientsForRoom(ruang); +const paginatedAllPatients = (ruang) => { + const allPatients = getAllPatientsForRoom(ruang); const page = pendingPage.value[ruang.nomorRuang] || 1; const startIndex = (page - 1) * 10; const endIndex = startIndex + 10; - return allPending.slice(startIndex, endIndex); + return allPatients.slice(startIndex, endIndex); +}; + +// Helper untuk menentukan class card berdasarkan status pasien +const getPatientCardClass = (patient) => { + const baseClass = 'patient-queue-item'; + // Pasien yang sudah digenerate/diproses & pending: warna berbeda + if (patient.tipeLayanan || patient.status === 'pending') { + return `${baseClass} patient-queue-item-processed`; + } + // Pasien yang perlu digenerate: warna default + return `${baseClass} patient-queue-item-pending`; }; @@ -981,7 +906,7 @@ onMounted(() => { color: var(--color-neutral-700); } -.pending-section { +.patient-list-section { background: var(--color-warning-50); border: 1px solid var(--color-warning-200); border-radius: 8px; @@ -989,7 +914,7 @@ onMounted(() => { margin-bottom: 16px; } -.pending-queue-list { +.patient-queue-list { display: flex; flex-direction: column; gap: 6px; @@ -997,35 +922,59 @@ onMounted(() => { overflow-y: auto; } -.pending-queue-item { +.patient-queue-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; - background: var(--color-neutral-100); - border: 1px solid var(--color-neutral-300); border-radius: 6px; transition: all 0.2s ease; + gap: 8px; } -.pending-queue-item:hover { +/* Pasien yang sudah digenerate/diproses & pending */ +.patient-queue-item-processed { + background: var(--color-primary-50); + border: 1px solid var(--color-primary-200); +} + +.patient-queue-item-processed:hover { + background: var(--color-primary-100); + border-color: var(--color-primary-400); +} + +/* Pasien yang perlu digenerate */ +.patient-queue-item-pending { + background: var(--color-neutral-100); + border: 1px solid var(--color-neutral-300); +} + +.patient-queue-item-pending:hover { background: var(--color-warning-100); border-color: var(--color-warning-400); } -.pending-queue-number { +.patient-queue-number { font-size: 14px; font-weight: 700; color: var(--color-neutral-900); + min-width: 80px; } -.pending-queue-actions { +.patient-queue-info { + display: flex; + gap: 4px; + align-items: center; + flex: 1; +} + +.patient-queue-actions { display: flex; gap: 8px; align-items: center; } -.pending-queue-actions .v-btn { +.patient-queue-actions .v-btn { text-transform: none; font-weight: 600; } diff --git a/pages/AdminKlinikRuang/index.vue b/pages/AdminKlinikRuang/index.vue index db5bee7..9ad9706 100644 --- a/pages/AdminKlinikRuang/index.vue +++ b/pages/AdminKlinikRuang/index.vue @@ -58,7 +58,7 @@ const klinikRuangList = computed(() => { }); const navigateToKlinik = (kodeKlinik) => { - router.push(`/admin-klinik-ruang/${kodeKlinik}`); + router.push(`/adminklinikruang/${kodeKlinik}`); }; diff --git a/pages/AdminLoket.vue b/pages/AdminLoket.vue index e05abd4..7ca2cf5 100644 --- a/pages/AdminLoket.vue +++ b/pages/AdminLoket.vue @@ -255,9 +255,11 @@ import QueueActionsCard from "@/components/features/queue/QueueActionsCard.vue"; import PatientDataTable from "@/components/features/queue/TabelPatientData.vue"; import SelectionDialog from "@/components/common/SelectionDialog.vue"; import AppSnackbar from "@/components/common/AppSnackbar.vue"; +import { useThermalPrint } from "@/composables/useThermalPrint"; const masterStore = useMasterStore(); const queueStore = useQueueStore(); +const { printTicketFromPatient } = useThermalPrint(); const { snackbar, @@ -446,7 +448,7 @@ const closeKlinikRuangDialog = () => { klinikRuangSearch.value = ""; }; -const buatAntreanKlinikRuang = (klinikRuang, ruang) => { +const buatAntreanKlinikRuang = async (klinikRuang, ruang) => { // Pastikan currentProcessingPatient valid, jika tidak, coba dapatkan dari store let patient = currentProcessingPatient.value; if (!patient) { @@ -491,6 +493,20 @@ const buatAntreanKlinikRuang = (klinikRuang, ruang) => { "loket" ); + if (result.success && result.patient) { + // Print ticket dengan nomor antrian baru + try { + await printTicketFromPatient(result.patient); + } catch (error) { + console.error('Error printing ticket:', error); + snackbarText.value = `${result.message}. Gagal print tiket.`; + snackbarColor.value = "warning"; + snackbar.value = true; + closeKlinikRuangDialog(); + return; + } + } + snackbarText.value = result.message; snackbarColor.value = result.success ? "success" : "error"; snackbar.value = true; diff --git a/stores/queueStore.js b/stores/queueStore.js index 03d24ee..b24869c 100644 --- a/stores/queueStore.js +++ b/stores/queueStore.js @@ -570,32 +570,61 @@ export const useQueueStore = defineStore('queue', () => { const timestamp = new Date(); const barcode = patient ? patient.barcode : `250811${String(timestamp.getTime()).slice(-6)}`; + // 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(); + + // 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}`; + const newPatient = { no: newNo, jamPanggil: `${String(timestamp.getHours()).padStart(2, "0")}:${String( timestamp.getMinutes() ).padStart(2, "0")}`, barcode: barcode, - noAntrian: `KR${String(newNo).padStart(4, "0")} | ${klinikRuang.namaKlinik} - Ruang ${ruang.nomorRuang} - ${barcode}`, - shift: "Shift 1", + noAntrian: `${newNoAntrian} | ${klinikRuang.namaKlinik} - ${ruang.namaRuang}`, + noAntrianRuang: `${klinikRuang.namaKlinik} - ${ruang.namaRuang} | ${newNoAntrian}`, + shift: patient ? (patient.shift || "Shift 1") : "Shift 1", klinik: klinikRuang.namaKlinik, ruang: ruang.namaRuang, kodeKlinik: klinikRuang.kodeKlinik, nomorRuang: ruang.nomorRuang, nomorScreen: ruang.nomorScreen, - fastTrack: "TIDAK", + fastTrack: patient ? (patient.fastTrack || "TIDAK") : "TIDAK", pembayaran: patient ? patient.pembayaran : "UMUM", status: "waiting", - processStage: "klinik", + processStage: "klinik-ruang", // Set ke klinik-ruang langsung createdAt: timestamp.toISOString(), referencePatient: patient ? patient.noAntrian : null, + sourcePatientNo: patient ? patient.no : null, + // Tracking panggilan + calledPemeriksaanAwal: false, + calledTindakan: false, + lastCalledAt: null, + lastCalledTipeLayanan: null, }; allPatients.value.push(newPatient); return { success: true, - message: `Antrean ${klinikRuang.namaKlinik} Ruang ${ruang.nomorRuang} berhasil dibuat dan akan ditampilkan di layar antrian`, + message: `Antrean ${klinikRuang.namaKlinik} Ruang ${ruang.nomorRuang} berhasil dibuat: ${newNoAntrian}`, patient: newPatient, }; };