diff --git a/MAPPING_DATA_TO_ERD.md b/MAPPING_DATA_TO_ERD.md new file mode 100644 index 0000000..19f9863 --- /dev/null +++ b/MAPPING_DATA_TO_ERD.md @@ -0,0 +1,321 @@ +# Mapping Data Structure ke ERD + +Dokumen ini menjelaskan mapping antara struktur data pasien di `queueStore.js` (baris 607-622) ke tabel-tabel dalam ERD. + +## Struktur Data di Code (queueStore.js) + +```javascript +{ + no: newNo, + jamPanggil: "HH:MM", + barcode: "26011395625", + noAntrian: "KL0001 | Klinik - 26011395625", + shift: "Shift 1", + klinik: "KANDUNGAN", + fastTrack: "TIDAK" | "YA", + pembayaran: "BPJS" | "UMUM", + status: "di-loket" | "waiting" | ..., + processStage: "klinik" | "loket" | ..., + createdAt: "2026-01-13T16:51:35.502Z", + referencePatient: "UM1001 | Online - 26011395625" | null, + loket: "Loket A" (optional), + loketId: 1 (optional) +} +``` + +--- + +## Mapping ke Tabel ERD + +### 1. **data_kunjungan_antrean** (Tabel Utama Kunjungan) + +| Field Code | Field ERD | Keterangan | +|------------|-----------|------------| +| `barcode` | `Barcode` | Barcode unik pasien (string) | +| `createdAt` | `Tanggal_daftar` | Tanggal dan waktu pendaftaran antrian | +| - | `Pasien` | FK ke `data_pasien.id` (harus dicari/lookup berdasarkan barcode atau dibuat baru) | +| - | `Tanggal_periksa` | Tanggal pemeriksaan (bisa sama dengan Tanggal_daftar atau null) | +| - | `Tanggal_check_in` | Tanggal check-in (null jika belum check-in) | +| - | `Check_in` | Status check-in (boolean/flag) | +| - | `Surat_rujukan` | Surat rujukan (null jika tidak ada) | +| - | `Surat_kontrol` | Surat kontrol (null jika tidak ada) | +| - | `SEP` | SEP BPJS (null jika tidak ada) | +| - | `Status_active` | Status aktif (default: 1 = Active) | + +**Catatan:** +- `barcode` langsung map ke field `Barcode` +- `createdAt` map ke `Tanggal_daftar` +- `referencePatient` bisa digunakan untuk tracking kunjungan sebelumnya (relasi ke `data_kunjungan_antrean` lain) + +--- + +### 2. **data_kunjungan_tempat_layanan** (Detail Tempat Layanan) + +| Field Code | Field ERD | Keterangan | +|------------|-----------|------------| +| `no` | `Nomor_tiket` | Nomor urut antrian (integer) | +| `noAntrian` | - | Bisa disimpan sebagai informasi tambahan atau di-generate dari `Nomor_tiket` | +| `klinik` | `Jenis_layanan` (?) | FK ke `data_jenis_layanan.id` (harus lookup berdasarkan nama klinik) | +| `pembayaran` | `Penjamin` | FK ke `daftar_penjamin.id` (harus lookup: "BPJS" → id penjamin BPJS, "UMUM" → id penjamin UMUM) | +| `loket` | `Loket` | FK ke `data_loket.id` (harus lookup berdasarkan nama loket) | +| `loketId` | `Loket` | FK ke `data_loket.id` (jika ada, langsung gunakan) | +| `fastTrack` | `Status_fasttrack` | FK ke lookup `ID: Status_fasttrack` (1 = True jika "YA", 0 = False jika "TIDAK") | +| `fastTrack` | `Alasan_fasttrack` | Alasan fast track (string, bisa null jika "TIDAK") | +| `processStage` | `Status_kunjungan` | FK ke lookup `ID: Status_kunjungan` (harus mapping: "loket" → 1, "klinik" → 2, dll) | +| `shift` | - | Shift diambil dari `data_jenis_layanan_shift` berdasarkan jadwal | +| - | `FK_kunjunganantrean_ten` | FK ke `data_kunjungan_antrean.id` | +| - | `Jenis_kunjungan` | FK ke `daftar_jenis_kunjungan.id` (default atau lookup) | +| - | `Jenis_pelayanan` | FK ke `daftar_jenis_pelayanan.id` (bisa dari `data_jenis_layanan.Jenis_pelayanan`) | +| - | `Dokter` | FK ke `data_pegawai.id` (null jika belum ditentukan) | +| - | `Jam_awal` | Jam mulai pelayanan (bisa dari `jamPanggil` atau null) | +| - | `Jam_selesai` | Jam selesai pelayanan (null jika belum selesai) | +| - | `Tanggal_ambil_hasil` | Tanggal ambil hasil (null jika belum) | +| - | `Status_active` | Status aktif (default: 1 = Active) | + +**Mapping Status:** +- `status: "di-loket"` → `Status_kunjungan` = 1 (LOKET) +- `status: "waiting"` → `Status_kunjungan` = 2 (PENDING) atau sesuai lookup +- `processStage: "klinik"` → `Status_kunjungan` = 2 (KLINIK) +- `processStage: "loket"` → `Status_kunjungan` = 1 (LOKET) + +**Mapping FastTrack:** +- `fastTrack: "YA"` → `Status_fasttrack` = 1 (True) +- `fastTrack: "TIDAK"` → `Status_fasttrack` = 0 (False) + +--- + +### 3. **data_kunjungan_tempat_panggilan** (Riwayat Panggilan) + +| Field Code | Field ERD | Keterangan | +|------------|-----------|------------| +| `jamPanggil` | `Jam_panggilan` | Jam panggilan pasien (format: "HH:MM") | +| `status` | `Status_panggilan` | FK ke lookup `ID: Status_panggilan` (harus mapping berdasarkan context) | +| `processStage` | `Status_kunjungan` | FK ke lookup `ID: Status_kunjungan` | +| - | `Nama_tempat_panggilan` | Nama tempat panggilan (bisa dari `loket` atau `klinik`) | +| - | `FK_kunjungantempallayana` | FK ke `data_kunjungan_tempat_layanan.id` | +| - | `Status_active` | Status aktif (default: 1 = Active) | + +**Mapping Status Panggilan:** +- Panggilan di Anjungan → `Status_panggilan` = 1 (ANJUNGAN) +- Panggilan di Pendaftaran → `Status_panggilan` = 2 (PENDAFTARAN) +- Panggilan di Pelayanan → `Status_panggilan` = 3 (PELAYANAN) +- Panggilan ambil hasil → `Status_panggilan` = 4 (AMBIL HASIL) + +--- + +### 4. **data_kunjungan_tempat_layanan_detail** (Detail Status Kunjungan) + +| Field Code | Field ERD | Keterangan | +|------------|-----------|------------| +| `createdAt` | `Tanggal_detail` | Tanggal detail status (bisa dari `createdAt`) | +| `status` | `Status_kunjungan` | FK ke lookup `ID: Status_kunjungan` | +| - | `FK_kunjungantempallayana` | FK ke `data_kunjungan_tempat_layanan.id` | +| - | `Status_active` | Status aktif (default: 1 = Active) | + +**Catatan:** Tabel ini untuk tracking perubahan status kunjungan dari waktu ke waktu. + +--- + +### 5. **data_pasien** (Data Pasien - Jika Belum Ada) + +| Field Code | Field ERD | Keterangan | +|------------|-----------|------------| +| `barcode` | `Nomor_rekamedik` | Bisa digunakan sebagai nomor rekam medis sementara | +| - | `Nama` | Nama pasien (harus diinput atau dari sistem lain) | +| - | `Nomor_identitas` | NIK/KTP (harus diinput atau dari sistem lain) | +| - | `Nomor_bpjs` | Nomor BPJS (jika `pembayaran` = "BPJS") | +| - | `Status_antrean` | Status antrian pasien | +| - | `Status_active` | Status aktif (default: 1 = Active) | + +**Catatan:** Jika pasien belum ada di database, harus dibuat record baru di `data_pasien` terlebih dahulu. + +--- + +### 6. **Lookup Tables yang Digunakan** + +#### **ID: Status_fasttrack** +- 0: False +- 1: True + +#### **ID: Status_kunjungan** +- 1: LOKET +- 2: KLINIK +- 3: FARMASI +- 4: PENUNJANG +- 5: RAWAT INAP + +**Atau (versi status):** +- 1: ACTIVE +- 2: PENDING +- 3: TERLAMBAT +- 4: KONSUL +- 5: BATAL +- 6: GAGAL +- 7: AMBIL HASIL +- 8: SELESAI PELAYANAN + +#### **ID: Status_panggilan** +- 1: ANJUNGAN +- 2: PENDAFTARAN +- 3: PELAYANAN +- 4: AMBIL HASIL + +#### **ID: Status_active** +- 0: Disabled +- 1: Active + +--- + +## Alur Penyimpanan Data + +### Step 1: Cek/Create Pasien +1. Cari `data_pasien` berdasarkan `barcode` atau `Nomor_rekamedik` +2. Jika tidak ada, buat record baru di `data_pasien` + +### Step 2: Create Kunjungan Antrean +1. Insert ke `data_kunjungan_antrean`: + - `Barcode` = `barcode` + - `Pasien` = `data_pasien.id` (dari step 1) + - `Tanggal_daftar` = `createdAt` + - `Status_active` = 1 + +### Step 3: Create Tempat Layanan +1. Lookup `data_jenis_layanan.id` berdasarkan `klinik` (nama klinik) +2. Lookup `daftar_penjamin.id` berdasarkan `pembayaran` ("BPJS" atau "UMUM") +3. Lookup `data_loket.id` berdasarkan `loket` atau `loketId` +4. Insert ke `data_kunjungan_tempat_layanan`: + - `Nomor_tiket` = `no` + - `FK_kunjunganantrean_ten` = `data_kunjungan_antrean.id` (dari step 2) + - `Jenis_layanan` = `data_jenis_layanan.id` + - `Penjamin` = `daftar_penjamin.id` + - `Loket` = `data_loket.id` + - `Status_fasttrack` = 1 jika `fastTrack` = "YA", else 0 + - `Status_kunjungan` = mapping dari `processStage` atau `status` + - `Status_active` = 1 + +### Step 4: Create Panggilan (Jika Ada) +1. Insert ke `data_kunjungan_tempat_panggilan`: + - `FK_kunjungantempallayana` = `data_kunjungan_tempat_layanan.id` (dari step 3) + - `Jam_panggilan` = `jamPanggil` + - `Nama_tempat_panggilan` = `loket` atau `klinik` + - `Status_panggilan` = mapping berdasarkan context + - `Status_kunjungan` = mapping dari `processStage` + - `Status_active` = 1 + +### Step 5: Create Detail Status (Optional) +1. Insert ke `data_kunjungan_tempat_layanan_detail`: + - `FK_kunjungantempallayana` = `data_kunjungan_tempat_layanan.id` + - `Tanggal_detail` = `createdAt` + - `Status_kunjungan` = mapping dari `status` atau `processStage` + - `Status_active` = 1 + +--- + +## Field yang Tidak Langsung Map + +### Field yang Perlu Lookup/Transform: +1. **`klinik`** → Perlu lookup ke `data_jenis_layanan.id` berdasarkan nama +2. **`pembayaran`** → Perlu lookup ke `daftar_penjamin.id` berdasarkan value ("BPJS" atau "UMUM") +3. **`loket`** → Perlu lookup ke `data_loket.id` berdasarkan nama loket +4. **`shift`** → Perlu lookup ke `data_jenis_layanan_shift.id` berdasarkan shift dan jadwal +5. **`status`** → Perlu mapping ke lookup `ID: Status_kunjungan` +6. **`processStage`** → Perlu mapping ke lookup `ID: Status_kunjungan` +7. **`fastTrack`** → Perlu mapping ke lookup `ID: Status_fasttrack` (0 atau 1) + +### Field yang Hanya untuk Display/Reference: +1. **`noAntrian`** → Format display, bisa di-generate ulang dari `Nomor_tiket` dan `Barcode` +2. **`referencePatient`** → Reference ke kunjungan sebelumnya (relasi ke `data_kunjungan_antrean` lain) + +--- + +## Contoh Mapping Lengkap + +### Input Data: +```javascript +{ + no: 1, + jamPanggil: "12:49", + barcode: "26011395625", + noAntrian: "UM1001 | Online - 26011395625", + shift: "Shift 1", + klinik: "KANDUNGAN", + fastTrack: "YA", + pembayaran: "BPJS", + status: "di-loket", + processStage: "loket", + createdAt: "2026-01-13T16:51:35.502Z", + loket: "Loket A", + loketId: 1 +} +``` + +### Output ke Database: + +#### data_kunjungan_antrean: +```sql +INSERT INTO data_kunjungan_antrean ( + Barcode, + Pasien, + Tanggal_daftar, + Status_active +) VALUES ( + '26011395625', + [data_pasien.id], -- dari lookup + '2026-01-13T16:51:35.502Z', + 1 +); +``` + +#### data_kunjungan_tempat_layanan: +```sql +INSERT INTO data_kunjungan_tempat_layanan ( + Nomor_tiket, + FK_kunjunganantrean_ten, + Penjamin, -- lookup dari "BPJS" + Jenis_layanan, -- lookup dari "KANDUNGAN" + Loket, -- lookup dari "Loket A" atau langsung 1 + Status_fasttrack, -- 1 (karena "YA") + Status_kunjungan, -- 1 (karena "loket" → LOKET) + Status_active +) VALUES ( + 1, + [data_kunjungan_antrean.id], + [daftar_penjamin.id WHERE Penjamin = 'BPJS'], + [data_jenis_layanan.id WHERE Nama_jenis_layanan = 'KANDUNGAN'], + 1, + 1, + 1, + 1 +); +``` + +#### data_kunjungan_tempat_panggilan: +```sql +INSERT INTO data_kunjungan_tempat_panggilan ( + FK_kunjungantempallayana, + Jam_panggilan, + Nama_tempat_panggilan, + Status_panggilan, -- 2 (PENDAFTARAN) atau sesuai context + Status_kunjungan, -- 1 (LOKET) + Status_active +) VALUES ( + [data_kunjungan_tempat_layanan.id], + '12:49', + 'Loket A', + 2, -- PENDAFTARAN + 1, -- LOKET + 1 +); +``` + +--- + +## Catatan Penting + +1. **Foreign Key Lookups**: Banyak field yang memerlukan lookup ke tabel lain sebelum insert +2. **Status Mapping**: Field `status` dan `processStage` perlu mapping ke lookup table `ID: Status_kunjungan` +3. **Default Values**: Field yang tidak ada di code perlu diisi dengan default value atau null +4. **Relasi**: `referencePatient` bisa digunakan untuk membuat relasi ke `data_kunjungan_antrean` sebelumnya +5. **Shift**: Field `shift` perlu diambil dari `data_jenis_layanan_shift` berdasarkan jadwal dan jenis layanan + + diff --git a/pages/AdminKlinikRuang/[kodeKlinik].vue b/pages/AdminKlinikRuang/[kodeKlinik].vue index 2defa3f..a3ff996 100644 --- a/pages/AdminKlinikRuang/[kodeKlinik].vue +++ b/pages/AdminKlinikRuang/[kodeKlinik].vue @@ -32,17 +32,17 @@ class="btn-pindah-pasien" variant="flat" size="default" - @click="showPindahRuangDialog = true" + @click="showKelolaPasienDialog = true" > - mdi-swap-horizontal - Pindah Pasien + mdi-account-cog + Kelola Pasien
-
+
- - - +
+ + mdi-filter + Filter + + {{ getActiveFilterCount(ruang) }} + + +
@@ -246,19 +252,19 @@ - - + + - Pindah Pasien - + Kelola Pasien + mdi-close -
+
mdi-account Pilih Pasien @@ -275,11 +281,11 @@ />
@@ -292,11 +298,11 @@
- + mdi-check-circle
-
+
mdi-account-off
Tidak ada pasien yang tersedia @@ -305,69 +311,139 @@
- -
+ +
- mdi-door - Pilih Ruang Tujuan + mdi-cog + Pilih Aksi
Pasien: - {{ selectedPatientForPindah?.noAntrian?.split(" |")[0] || selectedPatientForPindah?.barcode || '-' }} + {{ selectedPatientForKelola?.noAntrian?.split(" |")[0] || selectedPatientForKelola?.barcode || '-' }} - ({{ selectedPatientForPindah?.ruang || '-' }}) + ({{ selectedPatientForKelola?.ruang || '-' }})
mdi-arrow-left Ubah
-
+
+
+ mdi-swap-horizontal +
Pindah Ruang
+
Pindah ke ruang lain di klinik yang sama
+
+
+ mdi-hospital-building +
Pindah Klinik
+
Pindah ke klinik ruang lain, nomor antrian tetap
+
+
+ mdi-stethoscope +
Konsultasi
+
Konsultasi ke klinik ruang lain dengan nomor antrian baru
+
+
+
+ + +
+
+ mdi-door + + {{ selectedAction === 'pindah-ruang' ? 'Pilih Ruang Tujuan' : 'Pilih Klinik Ruang Tujuan' }} + +
+
+
+ Pasien: + + {{ selectedPatientForKelola?.noAntrian?.split(" |")[0] || selectedPatientForKelola?.barcode || '-' }} + + + ({{ selectedPatientForKelola?.ruang || '-' }}) + +
+ + mdi-arrow-left + Kembali + +
+ + +
- - mdi-door -
{{ ruangOption.namaRuang }}
-
R.{{ ruangOption.nomorRuang }}
+
Ruang {{ ruangOption.nomorRuang }}
Saat Ini - +
+
+ + +
+
+
+
{{ klinikRuangOption.namaKlinik }}
+
{{ klinikRuangOption.namaRuang }} (R.{{ klinikRuangOption.nomorRuang }})
+ - mdi-check-circle - + Saat Ini +
@@ -375,18 +451,36 @@ - + mdi-close Batal mdi-swap-horizontal - Pindah Pasien + Pindah Ruang + + + mdi-hospital-building + Pindah Klinik + + + mdi-stethoscope + Konsultasi @@ -487,6 +581,21 @@
+ + +
+ Fast Track + + {{ selectedPatientDetail.fastTrack === 'YA' ? 'Ya' : 'Tidak' }} + +
+
+
@@ -510,6 +619,82 @@ + + + + + Semua Filter + + mdi-close + + + + + +
+
+ Pemeriksaan Awal +
+
+ + {{ option.title }} + +
+
+ + +
+
+ Tindakan +
+
+ + {{ option.title }} + +
+
+
+ + + mdi-refresh + Reset + + + + mdi-close + Batal + + + mdi-check + Terapkan + + +
+
+ { + if (!filterOptions.value[ruang.nomorRuang]) { + filterOptions.value[ruang.nomorRuang] = { + pemeriksaanAwal: null, // null = semua, true = sudah, false = belum + tindakan: null // null = semua, true = sudah, false = belum + }; + } +}; + +const filterOptionsList = { + pemeriksaanAwal: [ + { title: 'Semua', value: null }, + { title: 'Sudah Pemeriksaan Awal', value: true }, + { title: 'Belum Pemeriksaan Awal', value: false } + ], + tindakan: [ + { title: 'Semua', value: null }, + { title: 'Sudah Tindakan', value: true }, + { title: 'Belum Tindakan', value: false } + ] +}; // WebSocket configuration const config = useRuntimeConfig(); @@ -644,20 +849,28 @@ const getFilteredAndSortedPatientsForRoom = (ruang) => { ); } - // Apply sorting - const sortOption = sortOptions.value[ruang.nomorRuang] || 'default'; + // Initialize filter options if not exists + initializeFilterOptions(ruang); - if (sortOption === 'sudah-pawal') { - patients = patients.filter(p => p.calledPemeriksaanAwal); - } else if (sortOption === 'belum-pawal') { - patients = patients.filter(p => !p.calledPemeriksaanAwal); - } else if (sortOption === 'sudah-tindakan') { - patients = patients.filter(p => p.calledTindakan); - } else if (sortOption === 'belum-tindakan') { - patients = patients.filter(p => !p.calledTindakan); + // Apply multiple filter conditions + const filters = filterOptions.value[ruang.nomorRuang]; + if (filters) { + // Filter by Pemeriksaan Awal + if (filters.pemeriksaanAwal !== null) { + patients = patients.filter(p => + filters.pemeriksaanAwal ? p.calledPemeriksaanAwal : !p.calledPemeriksaanAwal + ); + } + + // Filter by Tindakan + if (filters.tindakan !== null) { + patients = patients.filter(p => + filters.tindakan ? p.calledTindakan : !p.calledTindakan + ); + } } - // Default sorting + // Default sorting - berdasarkan createdAt dari nomor tiket awal return patients.sort((a, b) => { // Prioritaskan yang sudah digenerate/diproses & pending di atas const aHasTiket = !!a.tipeLayanan; @@ -677,7 +890,33 @@ const getFilteredAndSortedPatientsForRoom = (ruang) => { const priorityDiff = (statusPriority[a.status] || 99) - (statusPriority[b.status] || 99); if (priorityDiff !== 0) return priorityDiff; - // Sort by time + // Sort by createdAt dari nomor tiket awal (referencePatient) + // Jika ada referencePatient, cari createdAt dari pasien awal + let createdAtA = a.createdAt; + let createdAtB = b.createdAt; + + if (a.referencePatient) { + const sourceA = queueStore.allPatients.find(p => p.noAntrian === a.referencePatient); + if (sourceA?.createdAt) { + createdAtA = sourceA.createdAt; + } + } + + if (b.referencePatient) { + const sourceB = queueStore.allPatients.find(p => p.noAntrian === b.referencePatient); + if (sourceB?.createdAt) { + createdAtB = sourceB.createdAt; + } + } + + // Sort by createdAt (yang lebih awal di atas) + if (createdAtA && createdAtB) { + const dateA = new Date(createdAtA).getTime(); + const dateB = new Date(createdAtB).getTime(); + if (dateA !== dateB) return dateA - dateB; + } + + // Fallback: 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]); @@ -783,8 +1022,8 @@ const handleGenerateTicket = (patient, currentRuang) => { } }; -// Get available patients for pindah ruang (all patients in this klinik) -const getAvailablePatientsForPindah = () => { +// Get available patients for kelola pasien (all patients in this klinik) +const getAvailablePatientsForKelola = () => { let patients = queueStore.allPatients.filter(p => p.kodeKlinik === klinikData.value?.kodeKlinik && p.processStage === 'klinik-ruang' && @@ -804,20 +1043,42 @@ const getAvailablePatientsForPindah = () => { return patients; }; -const closePindahRuangDialog = () => { - showPindahRuangDialog.value = false; - selectedPatientForPindah.value = null; +const closeKelolaPasienDialog = () => { + showKelolaPasienDialog.value = false; + selectedPatientForKelola.value = null; + selectedAction.value = null; selectedRuangBaru.value = null; + selectedKlinikRuangBaru.value = null; searchPatientQuery.value = ''; }; +// Get all available klinik ruang options (for pindah klinik and konsultasi) +const getAllKlinikRuangOptions = () => { + const ruangData = masterStore.ruangData || []; + const options = []; + + ruangData.forEach(klinikRuang => { + klinikRuang.ruangList?.forEach(ruang => { + options.push({ + kodeKlinik: klinikRuang.kodeKlinik, + namaKlinik: klinikRuang.namaKlinik, + nomorRuang: ruang.nomorRuang, + namaRuang: ruang.namaRuang, + nomorScreen: ruang.nomorScreen + }); + }); + }); + + return options; +}; + const confirmPindahRuang = () => { - if (!selectedRuangBaru.value || !selectedPatientForPindah.value) { + if (!selectedRuangBaru.value || !selectedPatientForKelola.value) { showSnackbar('Pilih ruang tujuan', 'error'); return; } - const patientIndex = queueStore.allPatients.findIndex(p => p.no === selectedPatientForPindah.value.no); + const patientIndex = queueStore.allPatients.findIndex(p => p.no === selectedPatientForKelola.value.no); if (patientIndex === -1) { showSnackbar('Pasien tidak ditemukan', 'error'); @@ -844,8 +1105,8 @@ const confirmPindahRuang = () => { const oldRuang = ruangList.value.find(r => r.nomorRuang === patient.nomorRuang); if (oldRuang) { const oldKey = `klinik-ruang-${klinikData.value.kodeKlinik}-${oldRuang.nomorRuang}`; - if (queueStore.currentProcessingPatient[oldKey] && queueStore.currentProcessingPatient[oldKey][patient.tipeLayanan]?.no === patient.no) { - queueStore.currentProcessingPatient[oldKey][patient.tipeLayanan] = null; + if (queueStore.currentProcessingPatient[oldKey]?.no === patient.no) { + queueStore.currentProcessingPatient[oldKey] = null; } } } else { @@ -859,11 +1120,77 @@ const confirmPindahRuang = () => { } showSnackbar( - `Pasien ${selectedPatientForPindah.value.noAntrian?.split(" |")[0] || selectedPatientForPindah.value.barcode || '-'} berhasil dipindah ke ${selectedRuangBaru.value.namaRuang}`, + `Pasien ${selectedPatientForKelola.value.noAntrian?.split(" |")[0] || selectedPatientForKelola.value.barcode || '-'} berhasil dipindah ke ${selectedRuangBaru.value.namaRuang}`, 'success' ); - closePindahRuangDialog(); + closeKelolaPasienDialog(); +}; + +const confirmPindahKlinik = async () => { + if (!selectedKlinikRuangBaru.value || !selectedPatientForKelola.value) { + showSnackbar('Pilih klinik ruang tujuan', 'error'); + return; + } + + const result = queueStore.pindahKlinikRuang( + selectedPatientForKelola.value, + { + kodeKlinik: selectedKlinikRuangBaru.value.kodeKlinik, + namaKlinik: selectedKlinikRuangBaru.value.namaKlinik + }, + { + nomorRuang: selectedKlinikRuangBaru.value.nomorRuang, + namaRuang: selectedKlinikRuangBaru.value.namaRuang, + nomorScreen: selectedKlinikRuangBaru.value.nomorScreen + } + ); + + if (result.success) { + showSnackbar(result.message, 'success'); + closeKelolaPasienDialog(); + } else { + showSnackbar(result.message, 'error'); + } +}; + +const confirmKonsultasi = async () => { + if (!selectedKlinikRuangBaru.value || !selectedPatientForKelola.value) { + showSnackbar('Pilih klinik ruang tujuan', 'error'); + return; + } + + const result = queueStore.konsultasiKlinikRuang( + selectedPatientForKelola.value, + { + kodeKlinik: selectedKlinikRuangBaru.value.kodeKlinik, + namaKlinik: selectedKlinikRuangBaru.value.namaKlinik + }, + { + nomorRuang: selectedKlinikRuangBaru.value.nomorRuang, + namaRuang: selectedKlinikRuangBaru.value.namaRuang, + nomorScreen: selectedKlinikRuangBaru.value.nomorScreen + }, + 'Pemeriksaan Awal' + ); + + if (result.success && result.patient) { + showSnackbar(result.message, 'success'); + + // Print tiket konsultasi + try { + const { printTicketFromPatient } = useThermalPrint(); + await printTicketFromPatient(result.patient); + showSnackbar('Tiket konsultasi berhasil dicetak', 'success'); + } catch (error) { + console.error('Error printing konsultasi ticket:', error); + showSnackbar('Tiket konsultasi berhasil dibuat, namun gagal dicetak', 'warning'); + } + + closeKelolaPasienDialog(); + } else { + showSnackbar(result.message, 'error'); + } }; // Handle call patient from list (dari card pasien di daftar) @@ -1261,19 +1588,51 @@ const getPatientCardClass = (patient) => { +// Helper functions for filter +const openFilterDialog = (ruang) => { + initializeFilterOptions(ruang); + showFilterDialog.value[ruang.nomorRuang] = true; +}; + +const closeFilterDialog = (ruang) => { + showFilterDialog.value[ruang.nomorRuang] = false; +}; + +const getActiveFilterCount = (ruang) => { + const filters = filterOptions.value[ruang.nomorRuang]; + if (!filters) return 0; + let count = 0; + if (filters.pemeriksaanAwal !== null) count++; + if (filters.tindakan !== null) count++; + return count; +}; + +const resetFilters = (ruang) => { + if (filterOptions.value[ruang.nomorRuang]) { + filterOptions.value[ruang.nomorRuang] = { + pemeriksaanAwal: null, + tindakan: null + }; + } +}; + +const applyFilters = (ruang) => { + closeFilterDialog(ruang); + // Reset pagination when filters change + pendingPage.value[ruang.nomorRuang] = 1; +}; + onMounted(() => { if (!klinikData.value) { router.push('/admin-klinik-ruang'); } - // Initialize pagination and sort options for each room + // Initialize pagination, sort options, and filter options for each room ruangList.value.forEach(ruang => { if (!pendingPage.value[ruang.nomorRuang]) { pendingPage.value[ruang.nomorRuang] = 1; } - if (!sortOptions.value[ruang.nomorRuang]) { - sortOptions.value[ruang.nomorRuang] = 'default'; - } + initializeFilterOptions(ruang); }); }); @@ -1343,23 +1702,10 @@ onMounted(() => { } .rooms-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 16px; - min-width: fit-content; -} - -.rooms-grid-scrollable { display: flex; flex-direction: row; gap: 16px; - grid-template-columns: none; -} - -.rooms-grid-scrollable .room-card { - min-width: 700px; - max-width: 700px; - flex-shrink: 0; + min-width: fit-content; } .global-search-section { @@ -1423,10 +1769,13 @@ onMounted(() => { border-radius: 12px; border: 1px solid var(--color-neutral-500); background: var(--color-neutral-100); - width: 100%; + width: 700px; + min-width: 700px; + max-width: 700px; height: fit-content; box-sizing: border-box; overflow: hidden; + flex-shrink: 0; } .room-header { @@ -1961,13 +2310,10 @@ onMounted(() => { /* Responsive untuk card */ @media (max-width: 1400px) { - .rooms-grid:not(.rooms-grid-scrollable) { - grid-template-columns: repeat(2, 1fr); - } - - .rooms-grid-scrollable .room-card { - min-width: 650px; - max-width: 650px; + .room-card { + min-width: 700px; + max-width: 700px; + width: 700px; } .patient-queue-item { @@ -2029,13 +2375,10 @@ onMounted(() => { } @media (max-width: 960px) { - .rooms-grid:not(.rooms-grid-scrollable) { - grid-template-columns: 1fr; - } - - .rooms-grid-scrollable .room-card { + .room-card { min-width: 100%; max-width: 100%; + width: 100%; } .patient-queue-item { @@ -2149,13 +2492,23 @@ onMounted(() => { } .ruang-selection-grid { - grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; } .ruang-selection-card { - min-height: 100px; - padding: 12px 8px; + min-height: 70px; + padding: 12px; + } + + .ruang-selection-name { + font-size: 13px; + line-height: 18px; + } + + .ruang-selection-number { + font-size: 11px; + line-height: 14px; } } @@ -2288,8 +2641,8 @@ onMounted(() => { .ruang-selection-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); - gap: 10px; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 12px; max-height: 400px; overflow-y: auto; } @@ -2299,21 +2652,28 @@ onMounted(() => { flex-direction: column; align-items: center; justify-content: center; - padding: 16px 12px; + padding: 16px; background: var(--color-neutral-100); - border: 2px solid var(--color-neutral-300); + border: 1px solid var(--color-neutral-400); border-radius: 8px; cursor: pointer; transition: all 0.2s ease; text-align: center; - min-height: 110px; + min-height: 80px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } .ruang-selection-card:hover:not(.ruang-selection-card-disabled) { - background: var(--color-primary-100); + background: var(--color-neutral-200); border-color: var(--color-primary-400); - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.ruang-selection-card:active:not(.ruang-selection-card-disabled) { + transform: translateY(0); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } .ruang-selection-card-selected { @@ -2322,6 +2682,13 @@ onMounted(() => { box-shadow: 0 2px 8px rgba(255, 152, 0, 0.2); } +.ruang-selection-card-selected:hover { + background: var(--color-primary-200); + border-color: var(--color-primary-700); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(255, 152, 0, 0.3); +} + .ruang-selection-card-disabled { background: var(--color-neutral-200); border-color: var(--color-neutral-400); @@ -2329,25 +2696,264 @@ onMounted(() => { opacity: 0.6; } +.ruang-selection-card-disabled:hover { + transform: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + .ruang-selection-content { display: flex; flex-direction: column; align-items: center; width: 100%; + gap: 4px; } .ruang-selection-name { - font-size: 13px; - line-height: 18px; + font-size: 14px; + line-height: 20px; font-weight: 600; color: var(--color-neutral-900); - margin-bottom: 2px; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } .ruang-selection-number { - font-size: 11px; + font-size: 12px; line-height: 16px; color: var(--color-neutral-600); + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; +} + +/* Action Selection Styles */ +.action-selection-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + margin-top: 16px; +} + +.action-selection-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 24px 16px; + background: var(--color-neutral-100); + border: 2px solid var(--color-neutral-400); + border-radius: 12px; + cursor: pointer; + transition: all 0.2s ease; + text-align: center; + min-height: 140px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; +} + +.action-selection-card:hover { + background: var(--color-neutral-200); + border-color: var(--color-primary-400); + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.action-selection-card:active { + transform: translateY(0); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.action-selection-name { + font-size: 16px; + line-height: 24px; + font-weight: 600; + color: var(--color-neutral-900); + margin-top: 8px; + margin-bottom: 4px; +} + +.action-selection-desc { + font-size: 12px; + line-height: 16px; + color: var(--color-neutral-600); + text-align: center; +} + +/* Klinik Ruang Selection Styles */ +.klinik-ruang-selection-list { + max-height: 400px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 8px; +} + +.klinik-ruang-selection-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + background: var(--color-neutral-100); + border: 2px solid var(--color-neutral-300); + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; +} + +.klinik-ruang-selection-card:hover:not(.klinik-ruang-selection-card-disabled) { + background: var(--color-neutral-200); + border-color: var(--color-primary-400); + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.klinik-ruang-selection-card-selected { + background: var(--color-primary-100); + border-color: var(--color-primary-600); + box-shadow: 0 2px 8px rgba(255, 152, 0, 0.2); +} + +.klinik-ruang-selection-card-disabled { + background: var(--color-neutral-200); + border-color: var(--color-neutral-400); + cursor: not-allowed; + opacity: 0.6; +} + +.klinik-ruang-selection-card-disabled:hover { + transform: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.klinik-ruang-selection-content { + flex: 1; +} + +.klinik-ruang-selection-name { + font-size: 16px; + line-height: 24px; + font-weight: 600; + color: var(--color-neutral-900); + margin-bottom: 4px; +} + +.klinik-ruang-selection-room { + font-size: 14px; + line-height: 20px; + color: var(--color-neutral-600); +} + +/* Filter and Sort Actions */ +.filter-sort-actions { + display: flex; + gap: 8px; + align-items: center; +} + +.btn-filter { + text-transform: none !important; + font-weight: 600 !important; + font-size: 13px !important; + line-height: 18px !important; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; + min-height: 36px !important; + padding: 6px 16px !important; + border-color: var(--color-neutral-400) !important; + color: var(--color-neutral-700) !important; + transition: all 0.2s ease !important; +} + +.btn-filter:hover { + background-color: var(--color-neutral-200) !important; + border-color: var(--color-primary-400) !important; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Filter Dialog Styles */ +.filter-dialog-card { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; +} + +.filter-dialog-content { + padding: 24px !important; + background: var(--color-neutral-300); + max-height: 600px; + overflow-y: auto; +} + +.filter-section { + margin-bottom: 24px; +} + +.filter-section:last-child { + margin-bottom: 0; +} + +.filter-section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.filter-section-title { + font-size: 16px; + line-height: 24px; + font-weight: 600; + color: var(--color-neutral-900); + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; +} + +.filter-chip-group { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.filter-chip { + font-size: 12px !important; + font-weight: 600 !important; + height: 32px !important; + border: 1px solid var(--color-neutral-500) !important; + background: var(--color-neutral-100) !important; + color: var(--color-neutral-600) !important; + transition: all 0.2s ease !important; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; + text-transform: none !important; + cursor: pointer !important; +} + +.filter-chip:hover { + background: var(--color-neutral-300) !important; +} + +.filter-chip.active-chip { + background: var(--color-secondary-600) !important; + color: var(--color-neutral-100) !important; + border-color: var(--color-secondary-600) !important; +} + +.btn-reset-filter { + text-transform: none !important; + font-weight: 500 !important; + font-size: 14px !important; + color: var(--color-neutral-600) !important; + transition: all 0.2s ease !important; +} + +.btn-reset-filter:hover { + color: var(--color-primary-600) !important; + background-color: var(--color-primary-50) !important; +} + +@media (max-width: 960px) { + .filter-chip-group { + gap: 6px; + } + + .filter-chip { + height: 28px !important; + font-size: 11px !important; + } } diff --git a/pages/Anjungan/Anjungan/[id].vue b/pages/Anjungan/Anjungan/[id].vue index 3e7dc9f..1dcd1bd 100644 --- a/pages/Anjungan/Anjungan/[id].vue +++ b/pages/Anjungan/Anjungan/[id].vue @@ -506,6 +506,78 @@ + + + + + mdi-fast-forward + Formulir Fast Track + + + + + + + + + + + + + + + + + Batal + + + Lanjutkan + + + + + { showVisitTypeDialog.value = false; - // Untuk pasien Eksekutif, dokter sudah dipilih di dialog sebelumnya - // Untuk Reguler/BPJS, tidak perlu dokter - const namaDokter = isEksekutif.value ? selectedDoctor.value : null; - - // Handle Fast Track - paymentType akan menjadi 'UMUM' atau 'BPJS' tapi dengan flag fastTrack + // Handle Fast Track - tampilkan dialog Fast Track sebelum registrasi const isFastTrack = paymentType === 'FAST_TRACK'; - const actualPaymentType = isFastTrack ? 'UMUM' : paymentType; - await registerPatient('SEKARANG', actualPaymentType, namaDokter, isFastTrack); + if (isFastTrack) { + // Reset form Fast Track + fastTrackForm.value = { + penanggungJawab: "", + alasanFastTrack: "", + jenisPembayaran: "UMUM", // Default untuk non-Eksekutif + }; + // Tampilkan dialog Fast Track + showFastTrackDialog.value = true; + } else { + // Untuk pasien Eksekutif, dokter sudah dipilih di dialog sebelumnya + // Untuk Reguler/BPJS, tidak perlu dokter + const namaDokter = isEksekutif.value ? selectedDoctor.value : null; + await registerPatient('SEKARANG', paymentType, namaDokter, false); + } }; // Fungsi ini tidak lagi digunakan karena dokter dipilih di dialog konfirmasi @@ -1089,12 +1178,12 @@ const confirmDoctorSelection = () => { registerPatient('SEKARANG', 'Eksekutif', selectedDoctor.value); }; -const registerPatient = async (visitType, paymentType, namaDokter, isFastTrack = false) => { +const registerPatient = async (visitType, paymentType, namaDokter, isFastTrack = false, fastTrackData = null) => { try { // Validasi bahwa fungsi registerPatientFromAnjungan ada if (!queueStore) { console.error('❌ queueStore is not initialized'); - showSnackbar('Error', 'Store tidak ter-initialize. Silakan refresh halaman.', 'error'); + showSnackbar('Store tidak ter-initialize. Silakan refresh halaman.', 'error'); return; } @@ -1102,7 +1191,7 @@ const registerPatient = async (visitType, paymentType, namaDokter, isFastTrack = console.error('❌ queueStore.registerPatientFromAnjungan is not a function'); console.error('queueStore:', queueStore); console.error('Available methods:', Object.keys(queueStore || {})); - showSnackbar('Error', 'Fungsi registrasi tidak tersedia. Silakan refresh halaman.', 'error'); + showSnackbar('Fungsi registrasi tidak tersedia. Silakan refresh halaman.', 'error'); return; } @@ -1114,7 +1203,8 @@ const registerPatient = async (visitType, paymentType, namaDokter, isFastTrack = null, 'Shift 1', namaDokter, - isFastTrack + isFastTrack, + fastTrackData // Pass fastTrackData (penanggungJawab, alasanFastTrack) ); if (result && result.success && result.patient) { @@ -1170,7 +1260,7 @@ const submitBooking = async () => { // Validasi bahwa fungsi registerPatientFromAnjungan ada if (!queueStore) { console.error('❌ queueStore is not initialized'); - showSnackbar('Error', 'Store tidak ter-initialize. Silakan refresh halaman.', 'error'); + showSnackbar('Store tidak ter-initialize. Silakan refresh halaman.', 'error'); return; } @@ -1178,7 +1268,7 @@ const submitBooking = async () => { console.error('❌ queueStore.registerPatientFromAnjungan is not a function'); console.error('queueStore:', queueStore); console.error('Available methods:', Object.keys(queueStore || {})); - showSnackbar('Error', 'Fungsi registrasi tidak tersedia. Silakan refresh halaman.', 'error'); + showSnackbar('Fungsi registrasi tidak tersedia. Silakan refresh halaman.', 'error'); return; } @@ -1237,6 +1327,45 @@ const skipPrint = () => { lastRegisteredPatient.value = null; }; +// Handle Fast Track dialog submission +const submitFastTrackForm = async () => { + // Validasi form + if (!fastTrackForm.value.penanggungJawab.trim()) { + showSnackbar("Mohon isi Penanggung Jawab", "error"); + return; + } + + if (!fastTrackForm.value.alasanFastTrack.trim()) { + showSnackbar("Mohon isi Alasan Fast Track", "error"); + return; + } + + // Untuk non-Eksekutif, validasi jenis pembayaran + if (!isEksekutif.value && !fastTrackForm.value.jenisPembayaran) { + showSnackbar("Mohon pilih Jenis Pembayaran", "error"); + return; + } + + // Tutup dialog Fast Track + showFastTrackDialog.value = false; + + // Untuk pasien Eksekutif, dokter sudah dipilih di dialog sebelumnya + // Untuk Reguler/BPJS, tidak perlu dokter + const namaDokter = isEksekutif.value ? selectedDoctor.value : null; + + // Tentukan paymentType berdasarkan jenis pasien + const paymentType = isEksekutif.value ? 'Eksekutif' : fastTrackForm.value.jenisPembayaran; + + // Siapkan data Fast Track + const fastTrackData = { + penanggungJawab: fastTrackForm.value.penanggungJawab.trim(), + alasanFastTrack: fastTrackForm.value.alasanFastTrack.trim(), + }; + + // Register dengan Fast Track + await registerPatient('SEKARANG', paymentType, namaDokter, true, fastTrackData); +}; + const backToList = () => { navigateTo("/anjungan/anjungan"); }; @@ -1628,6 +1757,7 @@ watch(lastRegisteredPatient, (newVal) => { word-wrap: break-word; display: -webkit-box; -webkit-line-clamp: 2; + line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; @@ -1755,6 +1885,7 @@ watch(lastRegisteredPatient, (newVal) => { line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 3; + line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; diff --git a/pages/CheckInPasien/checkIn.vue b/pages/CheckInPasien/checkIn.vue index 02bf8a5..2b38980 100644 --- a/pages/CheckInPasien/checkIn.vue +++ b/pages/CheckInPasien/checkIn.vue @@ -2154,9 +2154,11 @@ const extractQRData = (qrData: string): { barcode: string; status?: string } => // Helper function untuk extract kode dan angka dari noAntrian // Contoh: "UM0014 | Onsite - ..." -> { kode: "UM", angka: "0014" } +// Contoh: "F-RA001 | Onsite - ..." -> { kode: "RA", angka: "001" } (handle "F-" prefix) const extractKodeAndAngka = (noAntrian: string): { kode: string; angka: string } | null => { if (!noAntrian) return null; - const match = noAntrian.toUpperCase().match(/^([A-Z]+)(\d+)/); + // Handle format "F-RA001" by matching after "F-" prefix if exists + const match = noAntrian.toUpperCase().match(/^(?:F-)?([A-Z]+)(\d+)/); if (match) { return { kode: match[1], angka: match[2] }; } @@ -2165,20 +2167,26 @@ const extractKodeAndAngka = (noAntrian: string): { kode: string; angka: string } // Helper function untuk membandingkan input dengan noAntrian (kode + angka harus match) // Ini mencegah false positive ketika angka sama tapi kode beda (misalnya UM0014 vs AN0014) +// Handle format "F-RA001" dengan menghapus prefix "F-" untuk comparison const matchNoAntrian = (input: string, noAntrian: string): boolean => { if (!input || !noAntrian) return false; const cleanInput = String(input).trim().toUpperCase(); const noAntrianUpper = String(noAntrian).toUpperCase(); - // 1. Extract kode + angka dari noAntrian (misalnya "UM0014 | ..." -> kode: "UM", angka: "0014") + // Remove "F-" prefix from both input and noAntrian for comparison + const cleanInputWithoutPrefix = cleanInput.replace(/^F-/, ''); + const noAntrianWithoutPrefix = noAntrianUpper.replace(/^F-/, ''); + + // 1. Extract kode + angka dari noAntrian (misalnya "F-RA001 | ..." -> kode: "RA", angka: "001") const noAntrianParts = extractKodeAndAngka(noAntrian); if (!noAntrianParts) { // Jika noAntrian tidak punya format kode+angka, gunakan exact match saja - return noAntrianUpper === cleanInput; + // Check both with and without prefix + return noAntrianUpper === cleanInput || noAntrianWithoutPrefix === cleanInputWithoutPrefix; } - // 2. Extract kode + angka dari input (misalnya "AN002" -> kode: "AN", angka: "002") + // 2. Extract kode + angka dari input (misalnya "F-RA001" atau "RA001" -> kode: "RA", angka: "001") const inputParts = extractKodeAndAngka(cleanInput); // 3. Jika input punya kode + angka, HARUS match kode dan angka @@ -2195,33 +2203,40 @@ const matchNoAntrian = (input: string, noAntrian: string): boolean => { // 4. Jika input hanya angka (tidak punya kode), JANGAN match // Ini mencegah match "002" dengan "UM1002" atau "AN002" dengan "UM1002" - const inputAngka = cleanInput.replace(/[^0-9]/g, ''); - if (inputAngka && inputAngka === cleanInput) { + const inputAngka = cleanInputWithoutPrefix.replace(/[^0-9]/g, ''); + if (inputAngka && inputAngka === cleanInputWithoutPrefix) { // Input hanya angka, tidak match karena tidak ada kode return false; } // 5. Exact match untuk kasus lain (jika input mengandung kode tapi tidak ter-extract) - // Hanya exact match, tidak menggunakan startsWith atau includes untuk menghindari false positive - if (noAntrianUpper === cleanInput) { + // Check both with and without prefix + if (noAntrianUpper === cleanInput || noAntrianWithoutPrefix === cleanInputWithoutPrefix) { return true; } // 6. Check jika noAntrian dimulai dengan input + spasi atau pipe (untuk partial match yang aman) - // Contoh: "AN002" match dengan "AN002 | ..." atau "AN002 | Onsite - ..." + // Contoh: "F-RA001" match dengan "F-RA001 | ..." atau "RA001" match dengan "F-RA001 | ..." // Tapi TIDAK match dengan "UM1002" karena kode berbeda if (noAntrianParts && inputParts) { // Jika kedua-duanya punya kode+angka, hanya match jika kode sama if (inputParts.kode === noAntrianParts.kode) { - // Check jika noAntrian dimulai dengan format "KODEANGKA |" atau "KODEANGKA | ..." + // Check jika noAntrian dimulai dengan format "F-KODEANGKA |" atau "KODEANGKA | ..." const noAntrianPrefix = `${noAntrianParts.kode}${noAntrianParts.angka}`; const inputPrefix = `${inputParts.kode}${inputParts.angka}`; // Pastikan input prefix sama dengan noAntrian prefix if (inputPrefix === noAntrianPrefix) { - // Check jika noAntrian dimulai dengan prefix ini diikuti spasi, pipe, atau end of string - if (noAntrianUpper.startsWith(noAntrianPrefix)) { - const nextChar = noAntrianUpper[noAntrianPrefix.length]; + // Check jika noAntrian dimulai dengan prefix ini (with or without "F-") diikuti spasi, pipe, atau end of string + if (noAntrianWithoutPrefix.startsWith(noAntrianPrefix)) { + const nextChar = noAntrianWithoutPrefix[noAntrianPrefix.length]; + if (!nextChar || nextChar === ' ' || nextChar === '|') { + return true; + } + } + // Also check with "F-" prefix + if (noAntrianUpper.startsWith(`F-${noAntrianPrefix}`)) { + const nextChar = noAntrianUpper[`F-${noAntrianPrefix}`.length]; if (!nextChar || nextChar === ' ' || nextChar === '|') { return true; } diff --git a/stores/queueStore.js b/stores/queueStore.js index db76e5f..df0f222 100644 --- a/stores/queueStore.js +++ b/stores/queueStore.js @@ -113,13 +113,18 @@ export const useQueueStore = defineStore('queue', () => { const seedBarcode10 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcode10); const seedBarcode11 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcode11); const seedBarcode12 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcode12); + // Barcode untuk pasien Eksekutif + const seedBarcodeE1 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcodeE1); + const seedBarcodeE2 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcodeE2); + const seedBarcodeE3 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcodeE3); + const seedBarcodeE4 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcodeE4); const seedPatients = [ { no: 1, jamPanggil: "12:49", barcode: seedBarcode1, // Barcode unik untuk setiap pasien - noAntrian: `UM1001 | Online - ${seedBarcode1}`, + noAntrian: `F-RA001 | Online - ${seedBarcode1}`, // Counter 1: Fast Track BPJS shift: "Shift 1", klinik: "KANDUNGAN", fastTrack: "YA", // Fast Track Patient @@ -127,12 +132,18 @@ export const useQueueStore = defineStore('queue', () => { status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), + registrationType: 'online', + visitType: 'SEKARANG', + visitDate: new Date().toISOString().substring(0, 10), + namaDokter: null, + penanggungJawab: "Dr. Ahmad Wijaya", // Fast Track data + alasanFastTrack: "Pasien prioritas", // Fast Track data }, { no: 2, jamPanggil: "10:52", barcode: seedBarcode2, - noAntrian: `UM1002 | Online - ${seedBarcode2}`, + noAntrian: `RA002 | Online - ${seedBarcode2}`, // Counter 2: Non-fast track UMUM shift: "Shift 1", klinik: "IPD", fastTrack: "TIDAK", @@ -140,12 +151,18 @@ export const useQueueStore = defineStore('queue', () => { status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), + registrationType: 'online', + visitType: 'SEKARANG', + visitDate: new Date().toISOString().substring(0, 10), + namaDokter: null, + penanggungJawab: null, + alasanFastTrack: null, }, { no: 3, jamPanggil: "09:30", barcode: seedBarcode3, - noAntrian: `UM1003 | Online - ${seedBarcode3}`, + noAntrian: `F-RA003 | Online - ${seedBarcode3}`, // Counter 3: Fast Track BPJS shift: "Shift 1", klinik: "SARAF", fastTrack: "YA", // Fast Track Patient @@ -153,12 +170,18 @@ export const useQueueStore = defineStore('queue', () => { status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), + registrationType: 'online', + visitType: 'SEKARANG', + visitDate: new Date().toISOString().substring(0, 10), + namaDokter: null, + penanggungJawab: "Dr. Budi Santoso", // Fast Track data + alasanFastTrack: "Kondisi darurat", // Fast Track data }, { no: 4, jamPanggil: "14:15", barcode: seedBarcode4, - noAntrian: `UM1004 | Online - ${seedBarcode4}`, + noAntrian: `RA004 | Online - ${seedBarcode4}`, // Counter 4: Non-fast track UMUM shift: "Shift 1", klinik: "THT", fastTrack: "TIDAK", @@ -166,12 +189,18 @@ export const useQueueStore = defineStore('queue', () => { status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), + registrationType: 'online', + visitType: 'SEKARANG', + visitDate: new Date().toISOString().substring(0, 10), + namaDokter: null, + penanggungJawab: null, + alasanFastTrack: null, }, { no: 5, jamPanggil: "12:49", barcode: seedBarcode5, - noAntrian: `UM1005 | Online - ${seedBarcode5}`, + noAntrian: `RA005 | Online - ${seedBarcode5}`, // Counter 5: Non-fast track UMUM shift: "Shift 2", klinik: "KANDUNGAN", fastTrack: "TIDAK", @@ -179,12 +208,18 @@ export const useQueueStore = defineStore('queue', () => { status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), + registrationType: 'online', + visitType: 'SEKARANG', + visitDate: new Date().toISOString().substring(0, 10), + namaDokter: null, + penanggungJawab: null, + alasanFastTrack: null, }, { no: 6, jamPanggil: "10:52", barcode: seedBarcode6, - noAntrian: `UM1006 | Online - ${seedBarcode6}`, + noAntrian: `F-RA006 | Online - ${seedBarcode6}`, // Counter 6: Fast Track BPJS shift: "Shift 1", klinik: "IPD", fastTrack: "YA", // Fast Track Patient @@ -192,12 +227,18 @@ export const useQueueStore = defineStore('queue', () => { status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), + registrationType: 'online', + visitType: 'SEKARANG', + visitDate: new Date().toISOString().substring(0, 10), + namaDokter: null, + penanggungJawab: "Dr. Citra Dewi", // Fast Track data + alasanFastTrack: "Rujukan darurat", // Fast Track data }, { no: 7, jamPanggil: "09:30", barcode: seedBarcode7, - noAntrian: `UM1007 | Online - ${seedBarcode7}`, + noAntrian: `RA007 | Online - ${seedBarcode7}`, // Counter 7: Non-fast track UMUM shift: "Shift 1", klinik: "SARAF", fastTrack: "TIDAK", @@ -205,12 +246,18 @@ export const useQueueStore = defineStore('queue', () => { status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), + registrationType: 'online', + visitType: 'SEKARANG', + visitDate: new Date().toISOString().substring(0, 10), + namaDokter: null, + penanggungJawab: null, + alasanFastTrack: null, }, { no: 8, jamPanggil: "14:15", barcode: seedBarcode8, - noAntrian: `UM1008 | Online - ${seedBarcode8}`, + noAntrian: `RA008 | Online - ${seedBarcode8}`, // Counter 8: Non-fast track UMUM shift: "Shift 1", klinik: "THT", fastTrack: "TIDAK", @@ -218,12 +265,18 @@ export const useQueueStore = defineStore('queue', () => { status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), + registrationType: 'online', + visitType: 'SEKARANG', + visitDate: new Date().toISOString().substring(0, 10), + namaDokter: null, + penanggungJawab: null, + alasanFastTrack: null, }, { no: 9, jamPanggil: "12:49", barcode: seedBarcode9, - noAntrian: `UM1009 | Online - ${seedBarcode9}`, + noAntrian: `F-RA009 | Online - ${seedBarcode9}`, // Counter 9: Fast Track BPJS shift: "Shift 2", klinik: "KANDUNGAN", fastTrack: "YA", // Fast Track Patient @@ -231,12 +284,18 @@ export const useQueueStore = defineStore('queue', () => { status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), + registrationType: 'online', + visitType: 'SEKARANG', + visitDate: new Date().toISOString().substring(0, 10), + namaDokter: null, + penanggungJawab: "Dr. Dedi Kurniawan", // Fast Track data + alasanFastTrack: "Pasien VIP", // Fast Track data }, { no: 10, jamPanggil: "10:52", barcode: seedBarcode10, - noAntrian: `UM1010 | Online - ${seedBarcode10}`, + noAntrian: `RA010 | Online - ${seedBarcode10}`, // Counter 10: Non-fast track UMUM shift: "Shift 1", klinik: "IPD", fastTrack: "TIDAK", @@ -244,12 +303,18 @@ export const useQueueStore = defineStore('queue', () => { status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), + registrationType: 'online', + visitType: 'SEKARANG', + visitDate: new Date().toISOString().substring(0, 10), + namaDokter: null, + penanggungJawab: null, + alasanFastTrack: null, }, { no: 11, jamPanggil: "09:30", barcode: seedBarcode11, - noAntrian: `UM1011 | Online - ${seedBarcode11}`, + noAntrian: `RA011 | Online - ${seedBarcode11}`, // Counter 11: Non-fast track UMUM shift: "Shift 1", klinik: "SARAF", fastTrack: "TIDAK", @@ -257,12 +322,18 @@ export const useQueueStore = defineStore('queue', () => { status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), + registrationType: 'online', + visitType: 'SEKARANG', + visitDate: new Date().toISOString().substring(0, 10), + namaDokter: null, + penanggungJawab: null, + alasanFastTrack: null, }, { no: 12, jamPanggil: "14:15", barcode: seedBarcode12, - noAntrian: `UM1012 | Online - ${seedBarcode12}`, + noAntrian: `F-RA012 | Online - ${seedBarcode12}`, // Counter 12: Fast Track BPJS shift: "Shift 2", klinik: "THT", fastTrack: "YA", // Fast Track Patient @@ -270,11 +341,138 @@ export const useQueueStore = defineStore('queue', () => { status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), + registrationType: 'online', + visitType: 'SEKARANG', + visitDate: new Date().toISOString().substring(0, 10), + namaDokter: null, + 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 + shift: "Shift 1", + klinik: "KANDUNGAN", + fastTrack: "TIDAK", + pembayaran: "Eksekutif", + status: "waiting", + processStage: "loket", + createdAt: new Date().toISOString(), + registrationType: 'online', + visitType: 'SEKARANG', + visitDate: new Date().toISOString().substring(0, 10), + namaDokter: "Dr. Ahmad Wijaya, Sp.OG", + penanggungJawab: null, + alasanFastTrack: null, + }, + { + no: 14, + jamPanggil: "13:45", + barcode: seedBarcodeE2, + noAntrian: `EA014 | Online - ${seedBarcodeE2}`, // Counter 14: Non-fast track Eksekutif + shift: "Shift 1", + klinik: "IPD", + fastTrack: "TIDAK", + pembayaran: "Eksekutif", + status: "waiting", + processStage: "loket", + createdAt: new Date().toISOString(), + registrationType: 'online', + visitType: 'SEKARANG', + visitDate: new Date().toISOString().substring(0, 10), + namaDokter: "Dr. Budi Santoso, Sp.PD", + penanggungJawab: null, + alasanFastTrack: null, + }, + { + no: 15, + jamPanggil: "15:10", + barcode: seedBarcodeE3, + noAntrian: `F-EA015 | Online - ${seedBarcodeE3}`, // Counter 15: Fast Track Eksekutif + shift: "Shift 2", + klinik: "SARAF", + fastTrack: "YA", // Fast Track Patient + pembayaran: "Eksekutif", + status: "waiting", + processStage: "loket", + createdAt: new Date().toISOString(), + registrationType: 'online', + visitType: 'SEKARANG', + visitDate: new Date().toISOString().substring(0, 10), + namaDokter: "Dr. Citra Dewi, Sp.S", + penanggungJawab: "Dr. Citra Dewi", // Fast Track data + alasanFastTrack: "Pasien Eksekutif prioritas", // Fast Track data + }, + { + no: 16, + jamPanggil: "16:30", + barcode: seedBarcodeE4, + noAntrian: `EA016 | Online - ${seedBarcodeE4}`, // Counter 16: Non-fast track Eksekutif + shift: "Shift 1", + klinik: "THT", + fastTrack: "TIDAK", + pembayaran: "Eksekutif", + 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", + penanggungJawab: null, + alasanFastTrack: null, }, ]; const cloneSeed = () => seedPatients.map(p => ({ ...p })); + // Initialize counters from seed data to ensure numbering continues correctly + // Counter SHARED untuk semua payment group dan semua jenis pasien + const initializeCountersFromSeed = () => { + if (typeof window === 'undefined') return; // Skip in SSR + + const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD + + // Parse seed data to find max counter (SHARED untuk semua) + let maxCounter = 0; + + seedPatients.forEach(patient => { + if (!patient.noAntrian) return; + + // Extract queue number from noAntrian (format: "F-RA001 | ..." or "RA001 | ..." or "EA001 | ...") + // Remove "F-" prefix untuk mendapatkan nomor asli + const queueNumberMatch = patient.noAntrian.match(/^(?:F-)?([RE])([A-N])(\d+)/); + if (!queueNumberMatch) return; + + const loketLetter = queueNumberMatch[2]; // A-N + const number = parseInt(queueNumberMatch[3], 10); // 001-999 + + // Calculate counter from loket and number + // Loket A = 1-999, Loket B = 1000-1998, etc. + const loketIndex = loketLetter.charCodeAt(0) - 65; // A=0, B=1, etc. + const counter = (loketIndex * 999) + number; + + // Update max counter (shared untuk semua) + if (counter > maxCounter) { + maxCounter = counter; + } + }); + + // Initialize localStorage counter (SHARED untuk semua payment group) + if (maxCounter > 0) { + const counterKey = `queue_counter_loket_shared_${today}`; + const existing = localStorage.getItem(counterKey); + if (!existing || parseInt(existing, 10) < maxCounter) { + localStorage.setItem(counterKey, maxCounter.toString()); + } + } + }; + + // Initialize counters from seed data + initializeCountersFromSeed(); + // Initialize state - will be automatically hydrated from localStorage by pinia-plugin-persistedstate // If localStorage has data, it will override these defaults const allPatients = ref(cloneSeed()); @@ -388,13 +586,13 @@ export const useQueueStore = defineStore('queue', () => { return { success: false, message: "Tidak ada pasien yang menunggu untuk dipanggil" }; } - // Langsung update status menjadi 'waiting' + // Langsung update status menjadi 'waiting' (sudah dipanggil, bisa check-in) const callTimestamp = new Date().toISOString(); const index = allPatients.value.findIndex(p => p.no === nextPatient.no); if (index !== -1) { allPatients.value[index] = { ...allPatients.value[index], - status: "waiting", + status: "waiting", // Status "waiting" = sudah dipanggil, bisa check-in lastCalledAt: callTimestamp // Track waktu panggilan untuk multiple calls }; } @@ -748,6 +946,138 @@ 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" }; + } + + const currentPatient = allPatients.value[patientIndex]; + + // Jika pasien sudah punya antrean klinik ruang, update antreannya + if (currentPatient.processStage === 'klinik-ruang' && currentPatient.tipeLayanan) { + const baseNoAntrian = currentPatient.noAntrian?.split(" |")[0] || currentPatient.barcode || ''; + + // Update dengan klinik dan ruang baru, tapi nomor antrian tetap sama + allPatients.value[patientIndex] = { + ...currentPatient, + klinik: targetKlinikRuang.namaKlinik, + ruang: targetRuang.namaRuang, + kodeKlinik: targetKlinikRuang.kodeKlinik, + nomorRuang: targetRuang.nomorRuang, + nomorScreen: targetRuang.nomorScreen, + // Nomor antrian tetap sama + noAntrian: `${baseNoAntrian} | ${targetKlinikRuang.namaKlinik} - ${targetRuang.namaRuang} - ${currentPatient.tipeLayanan}`, + noAntrianRuang: `${targetKlinikRuang.namaKlinik} - ${targetRuang.namaRuang} | ${baseNoAntrian}` + }; + + // Clear current processing dari ruang lama jika ada + const oldKey = `klinik-ruang-${currentPatient.kodeKlinik}-${currentPatient.nomorRuang}`; + if (currentProcessingPatient.value[oldKey]?.no === currentPatient.no) { + currentProcessingPatient.value[oldKey] = null; + } + } else { + // Pasien belum digenerate tiket, hanya update ruang + allPatients.value[patientIndex] = { + ...currentPatient, + klinik: targetKlinikRuang.namaKlinik, + ruang: targetRuang.namaRuang, + kodeKlinik: targetKlinikRuang.kodeKlinik, + nomorRuang: targetRuang.nomorRuang, + nomorScreen: targetRuang.nomorScreen + }; + } + + return { + success: true, + message: `Pasien berhasil dipindah ke ${targetKlinikRuang.namaKlinik} Ruang ${targetRuang.nomorRuang}`, + patient: allPatients.value[patientIndex] + }; + }; + + // Konsultasi: pindah pasien ke klinik ruang lain dengan nomor antrian baru (prefix K- atau KF-) + const konsultasiKlinikRuang = (patient, targetKlinikRuang, targetRuang, tipeLayanan = 'Pemeriksaan Awal') => { + const patientIndex = allPatients.value.findIndex(p => p.no === patient.no); + + if (patientIndex === -1) { + return { success: false, message: "Pasien tidak ditemukan" }; + } + + const sourcePatient = allPatients.value[patientIndex]; + + // Cek apakah sudah ada antrean konsultasi di ruang tujuan + const existingKonsultasi = allPatients.value.find(p => + p.referencePatient === sourcePatient.noAntrian && + p.kodeKlinik === targetKlinikRuang.kodeKlinik && + p.nomorRuang === targetRuang.nomorRuang && + p.processStage === 'klinik-ruang' && + (p.noAntrian?.startsWith('K-') || p.noAntrian?.startsWith('KF-')) + ); + + if (existingKonsultasi) { + return { + success: false, + message: `Pasien sudah memiliki antrean konsultasi di ${targetKlinikRuang.namaKlinik} Ruang ${targetRuang.nomorRuang}` + }; + } + + // Generate queue number untuk konsultasi + const roomQueues = allPatients.value.filter(p => + p.kodeKlinik === targetKlinikRuang.kodeKlinik && + p.nomorRuang === targetRuang.nomorRuang && + p.tipeLayanan === tipeLayanan && + p.processStage === 'klinik-ruang' && + (p.noAntrian?.startsWith('K-') || p.noAntrian?.startsWith('KF-')) + ); + + const queueNumber = roomQueues.length + 1; + const prefix = tipeLayanan === 'Pemeriksaan Awal' ? 'PA' : 'TD'; + + // Tentukan prefix konsultasi: KF- untuk Fast Track, K- untuk normal + const isFastTrack = sourcePatient.fastTrack === "YA"; + const konsultasiPrefix = isFastTrack ? 'KF-' : 'K-'; + const queueNumberStr = String(queueNumber).padStart(3, "0"); + const newNoAntrian = `${konsultasiPrefix}${prefix}${queueNumberStr}`; + + const newNo = allPatients.value.length + 1; + const timestamp = new Date(); + + const newPatient = { + no: newNo, + jamPanggil: `${String(timestamp.getHours()).padStart(2, "0")}:${String( + timestamp.getMinutes() + ).padStart(2, "0")}`, + barcode: sourcePatient.barcode, + noAntrian: `${newNoAntrian} | ${targetKlinikRuang.namaKlinik} - ${targetRuang.namaRuang} - ${tipeLayanan}`, + noAntrianRuang: `${targetKlinikRuang.namaKlinik} - ${targetRuang.namaRuang} | ${newNoAntrian}`, + shift: sourcePatient.shift || "Shift 1", + klinik: targetKlinikRuang.namaKlinik, + ruang: targetRuang.namaRuang, + kodeKlinik: targetKlinikRuang.kodeKlinik, + nomorRuang: targetRuang.nomorRuang, + nomorScreen: targetRuang.nomorScreen, + tipeLayanan: tipeLayanan, + fastTrack: sourcePatient.fastTrack || "TIDAK", + pembayaran: sourcePatient.pembayaran || "UMUM", + status: "waiting", + processStage: "klinik-ruang", + createdAt: sourcePatient.createdAt || timestamp.toISOString(), // Gunakan createdAt dari pasien awal + referencePatient: sourcePatient.noAntrian, + sourcePatientNo: sourcePatient.no, + isKonsultasi: true, // Flag untuk konsultasi + }; + + allPatients.value.push(newPatient); + + return { + success: true, + message: `Antrean konsultasi berhasil dibuat: ${newNoAntrian} untuk ${targetKlinikRuang.namaKlinik} Ruang ${targetRuang.nomorRuang}`, + patient: newPatient, + }; + }; + // Scan barcode dan generate antrean klinik ruang baru const scanAndCreateAntreanKlinikRuang = (barcodeInput, klinikRuang, ruang, tipeLayanan = 'Pemeriksaan Awal') => { // Clean input - remove whitespace and handle prefix letters @@ -798,11 +1128,20 @@ export const useQueueStore = defineStore('queue', () => { p.kodeKlinik === klinikRuang.kodeKlinik && p.nomorRuang === ruang.nomorRuang && p.tipeLayanan === tipeLayanan && - p.processStage === 'klinik-ruang' + p.processStage === 'klinik-ruang' && + !p.noAntrian?.startsWith('K-') && // Exclude konsultasi + !p.noAntrian?.startsWith('KF-') // Exclude konsultasi Fast Track ); const queueNumber = roomQueues.length + 1; const prefix = tipeLayanan === 'Pemeriksaan Awal' ? 'PA' : 'TD'; + + // Tentukan prefix untuk Fast Track: F- untuk Fast Track + const isFastTrack = sourcePatient.fastTrack === "YA"; + const fastTrackPrefix = isFastTrack ? 'F-' : ''; + const queueNumberStr = String(queueNumber).padStart(3, "0"); + const newNoAntrian = `${fastTrackPrefix}${prefix}${queueNumberStr}`; + const newNo = allPatients.value.length + 1; const timestamp = new Date(); @@ -812,8 +1151,8 @@ export const useQueueStore = defineStore('queue', () => { timestamp.getMinutes() ).padStart(2, "0")}`, barcode: sourcePatient.barcode, - noAntrian: `${prefix}${String(queueNumber).padStart(3, "0")} | ${klinikRuang.namaKlinik} - ${ruang.namaRuang} - ${tipeLayanan}`, - noAntrianRuang: `${klinikRuang.namaKlinik} - ${ruang.namaRuang} | ${prefix}${String(queueNumber).padStart(3, "0")}`, + noAntrian: `${newNoAntrian} | ${klinikRuang.namaKlinik} - ${ruang.namaRuang} - ${tipeLayanan}`, + noAntrianRuang: `${klinikRuang.namaKlinik} - ${ruang.namaRuang} | ${newNoAntrian}`, shift: sourcePatient.shift || "Shift 1", klinik: klinikRuang.namaKlinik, ruang: ruang.namaRuang, @@ -825,7 +1164,7 @@ export const useQueueStore = defineStore('queue', () => { pembayaran: sourcePatient.pembayaran || "UMUM", status: "waiting", processStage: "klinik-ruang", - createdAt: timestamp.toISOString(), + createdAt: sourcePatient.createdAt || timestamp.toISOString(), // Gunakan createdAt dari pasien awal referencePatient: sourcePatient.noAntrian, sourcePatientNo: sourcePatient.no, }; @@ -1001,24 +1340,22 @@ export const useQueueStore = defineStore('queue', () => { }; // Helper function untuk generate nomor antrean baru - // Format: 2 huruf (kode klinik) + 3 digit (001-999) - // Generate nomor antrian dengan format baru untuk anjungan: R/E + Loket (A-N) + 3 digit + // Format: R/E + Loket (A-N) + 3 digit // Format: R/E (jenis pelayanan Reguler/Eksekutif) + A-N (nomor loket) + 3 digit angka // Contoh: RA001, EA001, RB001, RB002 // - R = Reguler (untuk pasien Reguler/BPJS) // - E = Eksekutif (untuk pasien Eksekutif/Grand Pavilion) - // Urutan terpisah untuk UMUM dan JKN/BPJS + // IMPORTANT: Counter SHARED untuk semua payment group (JKN/UMUM) dan semua jenis (fast track/non-fast track) + // Prefix "F-" hanya untuk menandai status fast track, bukan untuk memisahkan counter // Setiap loket menampung maksimal 999 nomor (001-999) const generateQueueNumber = (clinic, paymentType, isEksekutif = false) => { // Tentukan prefix jenis pelayanan: R untuk Reguler, E untuk Eksekutif (Grand Pavilion) const serviceType = isEksekutif ? 'E' : 'R'; - // Tentukan payment group untuk counter terpisah (UMUM atau JKN/BPJS) - const paymentGroup = paymentType === 'BPJS' || paymentType === 'JKN' ? 'JKN' : 'UMUM'; - - // Get counter untuk payment group ini per hari + // Counter SHARED untuk semua payment group dan semua jenis pasien + // Tidak ada pemisahan counter berdasarkan payment group atau fast track const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD - const counterKey = `queue_counter_loket_${paymentGroup}_${today}`; + const counterKey = `queue_counter_loket_shared_${today}`; // Get current counter dari localStorage let counter = 0; @@ -1068,7 +1405,7 @@ export const useQueueStore = defineStore('queue', () => { }; // Register patient from Anjungan (onsite registration) - const registerPatientFromAnjungan = (clinic, paymentType, visitType = 'SEKARANG', visitDate = null, shift = 'Shift 1', namaDokter = null, isFastTrack = false) => { + const registerPatientFromAnjungan = (clinic, paymentType, visitType = 'SEKARANG', visitDate = null, shift = 'Shift 1', namaDokter = null, isFastTrack = false, fastTrackData = null) => { const newNo = allPatients.value.length > 0 ? Math.max(...allPatients.value.map(p => p.no)) + 1 : 1; @@ -1087,9 +1424,9 @@ export const useQueueStore = defineStore('queue', () => { // Generate nomor antrean dengan format baru: R/E + Loket (A-N) + 3 digit // Format: RA001 (Reguler), EA001 (Eksekutif/Grand Pavilion), RB001, RB002, dll - // Untuk Fast Track, tambahkan prefix "F" di depan: FRA001, FEA001, dll + // Untuk Fast Track, tambahkan prefix "F-" di depan: F-RA001, F-EA001, dll const queueNumber = generateQueueNumber(clinic, paymentType, isEksekutif); - const finalQueueNumber = isFastTrack ? `F${queueNumber}` : queueNumber; + const finalQueueNumber = isFastTrack ? `F-${queueNumber}` : queueNumber; const noAntrian = `${finalQueueNumber} | Onsite - ${barcode}`; // Status awal untuk pasien dari anjungan adalah "menunggu" (belum dipanggil) @@ -1112,6 +1449,9 @@ export const useQueueStore = defineStore('queue', () => { visitType: visitType, visitDate: visitDate || timestamp.toISOString().substring(0, 10), namaDokter: namaDokter || null, // Nama dokter hanya untuk pasien Eksekutif + // Fast Track data + penanggungJawab: (isFastTrack && fastTrackData) ? fastTrackData.penanggungJawab : null, + alasanFastTrack: (isFastTrack && fastTrackData) ? fastTrackData.alasanFastTrack : null, }; allPatients.value.push(newPatient); @@ -1164,18 +1504,26 @@ export const useQueueStore = defineStore('queue', () => { } // Check if noAntrian includes the input (case insensitive) + // Handle format "F-RA001" by removing "F-" prefix for comparison const noAntrianUpper = (p.noAntrian || '').toUpperCase(); - if (noAntrianUpper.includes(cleanInput) || noAntrianUpper.includes(patientIdOrBarcode)) { + const noAntrianWithoutPrefix = noAntrianUpper.replace(/^F-/, ''); // Remove "F-" prefix if exists + const cleanInputWithoutPrefix = cleanInputUpper.replace(/^F-/, ''); // Remove "F-" prefix from input if exists + + if (noAntrianUpper.includes(cleanInput) || + noAntrianUpper.includes(patientIdOrBarcode) || + noAntrianWithoutPrefix.includes(cleanInputWithoutPrefix) || + noAntrianWithoutPrefix === cleanInputWithoutPrefix) { console.log('✅ Found by noAntrian:', p.noAntrian); return true; } - // Try to extract number from noAntrian (e.g., "UM0014 | Onsite - ..." -> "0014") - const noAntrianNumber = noAntrianUpper.match(/([A-Z]+)(\d+)/); - if (noAntrianNumber) { - const extractedNumber = noAntrianNumber[2]; - const inputNumber = cleanInput.replace(/[^0-9]/g, ''); - if (inputNumber && extractedNumber.includes(inputNumber) || inputNumber.includes(extractedNumber)) { + // Try to extract number from noAntrian (e.g., "F-RA001 | Onsite - ..." -> "RA001" -> "001") + // Handle format "F-RA001" by extracting after "F-" prefix + const noAntrianMatch = noAntrianUpper.match(/^(?:F-)?([A-Z]+)(\d+)/); + if (noAntrianMatch) { + const extractedNumber = noAntrianMatch[2]; + const inputNumber = cleanInputWithoutPrefix.replace(/[^0-9]/g, ''); + if (inputNumber && (extractedNumber.includes(inputNumber) || inputNumber.includes(extractedNumber))) { console.log('✅ Found by extracted number from noAntrian'); return true; } @@ -1269,6 +1617,8 @@ export const useQueueStore = defineStore('queue', () => { callNextKlinikRuang, processPatientKlinikRuang, changeKlinik, + pindahKlinikRuang, + konsultasiKlinikRuang, processNextQueue, getPatientsByStage, getTotalPasienByStage,