update seed data dan fungsi pindah atau konsul klinik

This commit is contained in:
bagus-arie05
2026-01-14 15:14:07 +07:00
parent 6c80c08a83
commit 89c3549e07
5 changed files with 1620 additions and 197 deletions
+321
View File
@@ -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
File diff suppressed because it is too large Load Diff
+144 -13
View File
@@ -506,6 +506,78 @@
</v-card>
</v-dialog>
<!-- Fast Track Dialog -->
<v-dialog
v-model="showFastTrackDialog"
max-width="600"
persistent
@click:outside="showFastTrackDialog = false"
>
<v-card class="dialog-card">
<v-card-title class="dialog-header">
<v-icon class="mr-2">mdi-fast-forward</v-icon>
Formulir Fast Track
</v-card-title>
<v-divider />
<v-card-text class="pa-6">
<v-form>
<v-text-field
v-model="fastTrackForm.penanggungJawab"
label="Penanggung Jawab"
variant="outlined"
density="compact"
required
class="mb-3"
:rules="[v => !!v || 'Mohon isi Penanggung Jawab']"
/>
<v-textarea
v-model="fastTrackForm.alasanFastTrack"
label="Alasan Fast Track"
variant="outlined"
density="compact"
required
rows="3"
class="mb-3"
:rules="[v => !!v || 'Mohon isi Alasan Fast Track']"
/>
<!-- Jenis Pembayaran hanya untuk non-Eksekutif -->
<v-select
v-if="!isEksekutif"
v-model="fastTrackForm.jenisPembayaran"
:items="['BPJS', 'UMUM']"
label="Jenis Pembayaran"
variant="outlined"
density="compact"
required
class="mb-3"
:rules="[v => !!v || 'Mohon pilih Jenis Pembayaran']"
/>
</v-form>
</v-card-text>
<v-divider />
<v-card-actions class="pa-4">
<v-spacer />
<v-btn
variant="outlined"
color="neutral-600"
@click="showFastTrackDialog = false"
>
Batal
</v-btn>
<v-btn
color="success-600"
class="text-white"
variant="flat"
@click="submitFastTrackForm"
>
Lanjutkan
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Print Dialog -->
<v-dialog
v-model="showPrintDialog"
@@ -918,6 +990,7 @@ const showVisitTypeDialog = ref(false);
const showBookingFormDialog = ref(false);
const showDoctorSelectionDialog = ref(false);
const showPrintDialog = ref(false);
const showFastTrackDialog = ref(false);
const lastRegisteredPatient = ref(null);
const selectedClinic = ref(null);
const selectedDoctor = ref(null);
@@ -925,6 +998,13 @@ const snackbar = ref(false);
const snackbarText = ref("");
const snackbarColor = ref("success");
// Fast Track form data
const fastTrackForm = ref({
penanggungJawab: "",
alasanFastTrack: "",
jenisPembayaran: "UMUM", // Default untuk non-Eksekutif
});
const bookingForm = ref({
date: new Date().toISOString().substring(0, 10),
shift: "Shift 1",
@@ -1063,15 +1143,24 @@ const registerPatientDirectly = async (paymentType) => {
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;
+28 -13
View File
@@ -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;
}
+386 -36
View File
@@ -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,