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-sort
-
-
+
+
+ mdi-filter
+ Filter
+
+ {{ getActiveFilterCount(ruang) }}
+
+
+
@@ -246,19 +252,19 @@
-
-
+
+
-
+
mdi-account
Pilih Pasien
@@ -275,11 +281,11 @@
/>
-
+
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 @@
+
+
showFilterDialog[ruang.nomorRuang] = val"
+ max-width="600px"
+ persistent
+ >
+
+
+
+
+
+
+
+
+
+ {{ option.title }}
+
+
+
+
+
+
+
+
+
+ {{ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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,