pindah klinik belum service
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<v-dialog v-model="internalModel" max-width="500px">
|
||||
<v-card class="dialog-card overflow-hidden rounded-xl" elevation="0">
|
||||
<!-- Solid Blue Header -->
|
||||
<v-card-title class="bg-primary-600 text-white py-4 px-6 d-flex justify-space-between align-center">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon start icon="mdi-alert-circle-outline" class="mr-2"></v-icon>
|
||||
<span class="text-h6 font-weight-bold">Konfirmasi Selesai</span>
|
||||
</div>
|
||||
<v-btn icon="mdi-close" variant="text" size="small" @click="internalModel = false" class="text-white"></v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="dialog-content-premium pa-6 text-center">
|
||||
<div class="text-center mb-6 text-uppercase font-weight-bold text-caption text-neutral-600" style="letter-spacing: 1px;">
|
||||
Antrean Belum Dibuat
|
||||
</div>
|
||||
|
||||
<p class="text-body-1 mb-6">
|
||||
Tiket antrean ruang atau penunjang <strong>belum dibuat</strong> untuk pasien ini.
|
||||
</p>
|
||||
|
||||
<div class="comparison-container mb-6 mx-auto" style="max-width: 220px; justify-content: center;">
|
||||
<div class="comparison-item current">
|
||||
<div class="item-label text-center">PASIEN AKTIF</div>
|
||||
<div class="item-card">
|
||||
<div class="item-number text-danger-700">{{ patient?.noAntrian?.split(' |')[0] || '-' }}</div>
|
||||
<div class="item-status">Sedang Diproses</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-body-2 text-neutral-600">
|
||||
Apakah Anda yakin ingin menyelesaikan pelayanan untuk pasien ini?
|
||||
</p>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions class="pa-4 bg-light justify-center">
|
||||
<v-btn
|
||||
class="px-8 font-weight-bold rounded-lg text-none btn-batal"
|
||||
variant="outlined"
|
||||
@click="internalModel = false"
|
||||
size="large"
|
||||
>
|
||||
Batal
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="px-8 font-weight-bold ml-4 rounded-lg text-none btn-confirm"
|
||||
variant="flat"
|
||||
@click="confirm"
|
||||
size="large"
|
||||
elevation="2"
|
||||
>
|
||||
Ya, Selesaikan
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
patient: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm']);
|
||||
|
||||
const internalModel = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
});
|
||||
|
||||
const confirm = () => {
|
||||
emit('confirm');
|
||||
internalModel.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-card {
|
||||
/* Remove border to prevent white outline around blue header */
|
||||
}
|
||||
.bg-light {
|
||||
background-color: var(--color-neutral-50) !important;
|
||||
}
|
||||
|
||||
.comparison-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: white;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--color-neutral-300);
|
||||
}
|
||||
|
||||
.comparison-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
color: var(--color-neutral-600);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
background: white;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-neutral-400);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.item-number {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.item-status {
|
||||
font-size: 11px;
|
||||
color: var(--color-neutral-600);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-danger-700 {
|
||||
color: var(--color-danger-700) !important;
|
||||
}
|
||||
|
||||
.btn-batal {
|
||||
border-color: var(--color-neutral-300) !important;
|
||||
color: var(--color-neutral-700) !important;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background-color: var(--color-primary-600) !important;
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
||||
@@ -34,15 +34,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right d-flex align-center" style="gap: 8px;">
|
||||
<v-chip
|
||||
v-if="patient.ruang"
|
||||
color="primary-600"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
class="subspesialis-chip text-caption font-weight-bold"
|
||||
>
|
||||
{{ patient.ruang }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
:color="getStatusColor(patient.status)"
|
||||
size="small"
|
||||
@@ -63,6 +54,17 @@
|
||||
>
|
||||
{{ patient.klinik }}
|
||||
</v-chip>
|
||||
<!-- Sub Spesialis -->
|
||||
<v-chip
|
||||
v-if="patient.ruang"
|
||||
color="primary-600"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
class="subspesialis-chip font-weight-bold"
|
||||
style="height: 24px; font-size: 11px;"
|
||||
>
|
||||
{{ patient.ruang }}
|
||||
</v-chip>
|
||||
<!-- Tipe Layanan atau Jenis Pasien -->
|
||||
<v-chip
|
||||
v-if="displayJenisPasien"
|
||||
|
||||
@@ -487,7 +487,11 @@ const handleAction = (patient, action) => {
|
||||
padding: 8px 12px;
|
||||
background: var(--color-neutral-200);
|
||||
border-radius: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-neutral-400);
|
||||
border-left: 3px solid var(--color-primary-600);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.results-text {
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<v-dialog v-model="internalModel" max-width="500px">
|
||||
<v-card class="dialog-card overflow-hidden rounded-xl" elevation="0">
|
||||
<!-- Solid Blue Header -->
|
||||
<v-card-title class="bg-primary-600 text-white py-4 px-6 d-flex justify-space-between align-center">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon start icon="mdi-alert-circle-outline" class="mr-2"></v-icon>
|
||||
<span class="text-h6 font-weight-bold">Konfirmasi Selesai</span>
|
||||
</div>
|
||||
<v-btn icon="mdi-close" variant="text" size="small" @click="internalModel = false" class="text-white"></v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="dialog-content-premium pa-6 text-center">
|
||||
<div class="text-center mb-6 text-uppercase font-weight-bold text-caption text-neutral-600" style="letter-spacing: 1px;">
|
||||
Tindakan Diperlukan
|
||||
</div>
|
||||
|
||||
<p class="text-body-1 mb-6">
|
||||
Pasien ini <strong>sudah dibuatkan</strong> tiket antrean ruang atau penunjang, namun statusnya belum diselesaikan di Loket.
|
||||
</p>
|
||||
|
||||
<div class="comparison-container mb-6 mx-auto" style="max-width: 220px; justify-content: center;">
|
||||
<div class="comparison-item current">
|
||||
<div class="item-label text-center">PASIEN AKTIF</div>
|
||||
<div class="item-card">
|
||||
<div class="item-number text-danger-700">{{ patient?.noAntrian?.split(' |')[0] || '-' }}</div>
|
||||
<div class="item-status">Menunggu Selesai</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-body-2 text-neutral-600">
|
||||
Apakah Anda ingin menyelesaikan pasien ini sekarang dan melanjutkan ke antrean berikutnya?
|
||||
</p>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions class="pa-4 bg-light justify-center">
|
||||
<v-btn
|
||||
class="px-8 font-weight-bold rounded-lg text-none btn-batal"
|
||||
variant="outlined"
|
||||
@click="internalModel = false"
|
||||
size="large"
|
||||
>
|
||||
Batal
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="px-6 font-weight-bold ml-4 rounded-lg text-none btn-confirm"
|
||||
variant="flat"
|
||||
@click="confirm"
|
||||
size="large"
|
||||
elevation="2"
|
||||
>
|
||||
Ya, Selesaikan
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
patient: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm']);
|
||||
|
||||
const internalModel = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
});
|
||||
|
||||
const confirm = () => {
|
||||
emit('confirm');
|
||||
internalModel.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-card {
|
||||
/* No border to prevent white outline */
|
||||
}
|
||||
.bg-light {
|
||||
background-color: var(--color-neutral-50) !important;
|
||||
}
|
||||
|
||||
|
||||
.comparison-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: white;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--color-neutral-300);
|
||||
}
|
||||
|
||||
.comparison-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
color: var(--color-neutral-600);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
background: white;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-neutral-400);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.item-number {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.item-status {
|
||||
font-size: 11px;
|
||||
color: var(--color-neutral-600);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-danger-700 {
|
||||
color: var(--color-danger-700) !important;
|
||||
}
|
||||
|
||||
.btn-batal {
|
||||
border-color: var(--color-neutral-300) !important;
|
||||
color: var(--color-neutral-700) !important;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background-color: var(--color-primary-600) !important;
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
||||
@@ -126,18 +126,25 @@ export const useThermalPrint = () => {
|
||||
// Format nomor antrian (hilangkan bagian "| Onsite - barcode")
|
||||
const noAntrianDisplay = data.noAntrian.split(' |')[0];
|
||||
|
||||
// Format informasi ruang: "Poli Anak - Ruang A"
|
||||
// Format informasi ruang: "Poli Anak - Ruang A" atau "Poli Anak - Gastro-Hepatologi"
|
||||
let ruangInfo = '';
|
||||
if (data.klinik && data.nomorRuang) {
|
||||
// Konversi nomor ruang ke abjad (1 = A, 2 = B, 3 = C, dst)
|
||||
const ruangNumber = parseInt(data.nomorRuang) || 1;
|
||||
const ruangLetter = String.fromCharCode(64 + ruangNumber); // 64 = '@', 65 = 'A', 66 = 'B', dst
|
||||
ruangInfo = `${data.klinik} - Ruang ${ruangLetter}`;
|
||||
} else if (data.klinik && data.ruang) {
|
||||
// Fallback jika hanya ada nama ruang
|
||||
|
||||
// 1. Jika ada data.ruang dan bukan sekadar kata "Ruang X" (berarti ini subspesialis)
|
||||
if (data.ruang && !data.ruang.toLowerCase().startsWith('ruang')) {
|
||||
ruangInfo = `${data.klinik} - ${data.ruang}`;
|
||||
} else if (data.klinik) {
|
||||
// Hanya klinik jika tidak ada info ruang
|
||||
}
|
||||
// 2. Fallback: gunakan nomorRuang untuk dikonversi ke abjad (Ruang A, Ruang B, dll)
|
||||
else if (data.klinik && data.nomorRuang) {
|
||||
const ruangNumber = parseInt(data.nomorRuang) || 1;
|
||||
const ruangLetter = String.fromCharCode(64 + ruangNumber); // 64 = '@', 65 = 'A'
|
||||
ruangInfo = `${data.klinik} - Ruang ${ruangLetter}`;
|
||||
}
|
||||
// 3. Fallback: gunakan data.ruang yang ada
|
||||
else if (data.klinik && data.ruang) {
|
||||
ruangInfo = `${data.klinik} - ${data.ruang}`;
|
||||
}
|
||||
// 4. Default: hanya nama klinik
|
||||
else if (data.klinik) {
|
||||
ruangInfo = data.klinik;
|
||||
}
|
||||
|
||||
|
||||
+132
-33
@@ -304,6 +304,20 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Check-In Confirmation Dialog -->
|
||||
<CheckInConfirmationDialog
|
||||
v-model="showCheckInConfirmDialog"
|
||||
:patient="currentProcessingPatient"
|
||||
@confirm="confirmCheckIn"
|
||||
/>
|
||||
|
||||
<!-- Unfinished Patient Dialog -->
|
||||
<UnfinishedPatientDialog
|
||||
v-model="showUnfinishedPatientDialog"
|
||||
:patient="currentProcessingPatient"
|
||||
@confirm="confirmUnfinishedAndProcess"
|
||||
/>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<AppSnackbar
|
||||
v-model="snackbar"
|
||||
@@ -327,6 +341,8 @@ import PageHeader from "@/components/common/PageHeader.vue";
|
||||
import CurrentPatientCard from "@/components/features/queue/CurrentPatientCard.vue";
|
||||
import QueueActionsCard from "@/components/features/queue/QueueActionsCard.vue";
|
||||
import PatientDataTable from "@/components/features/queue/TabelPatientData.vue";
|
||||
import CheckInConfirmationDialog from "@/components/features/queue/CheckInConfirmationDialog.vue";
|
||||
import UnfinishedPatientDialog from "@/components/features/queue/UnfinishedPatientDialog.vue";
|
||||
import SelectionDialog from "@/components/common/SelectionDialog.vue";
|
||||
import AppSnackbar from "@/components/common/AppSnackbar.vue";
|
||||
import { useThermalPrint } from "@/composables/useThermalPrint";
|
||||
@@ -387,8 +403,11 @@ const {
|
||||
const changeKlinik = async (klinik) => {
|
||||
const result = await originalChangeKlinik(klinik);
|
||||
if (result.success) {
|
||||
// Global broadcast to update all displays and other admins
|
||||
broadcastUpdate();
|
||||
// If patient was moved to a different loket, include destination loket in broadcast
|
||||
const broadcastData = result.moved && result.patient?.loketId
|
||||
? { loketId: result.patient.loketId } // Destination loket will get notified
|
||||
: null;
|
||||
broadcastUpdate(broadcastData);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -569,10 +588,10 @@ const selectedFastTrack = ref(null);
|
||||
// Dialog Klinik Ruang
|
||||
const showKlinikRuangDialog = ref(false);
|
||||
const klinikRuangSearch = ref("");
|
||||
const activePanel = ref(null); // Tracks the open Klinik Ruang panel
|
||||
|
||||
// Confirmation Replace Dialog
|
||||
const activePanel = ref(null); // Additional Refs
|
||||
const showConfirmReplaceDialog = ref(false);
|
||||
const showCheckInConfirmDialog = ref(false);
|
||||
const showUnfinishedPatientDialog = ref(false);
|
||||
const pendingReplaceItem = ref(null);
|
||||
const pendingReplaceAction = ref(null);
|
||||
|
||||
@@ -887,11 +906,38 @@ const nextQueueInfo = computed(() => {
|
||||
|
||||
const handlePatientAction = async (action) => {
|
||||
if (currentProcessingPatient.value) {
|
||||
if (action === 'check-in') {
|
||||
// Periksa apakah tiket antrean ruang atau penunjang sudah dibuat
|
||||
let allPatientsArray = [];
|
||||
if (queueStore.allPatients) {
|
||||
allPatientsArray = Array.isArray(queueStore.allPatients)
|
||||
? queueStore.allPatients
|
||||
: (queueStore.allPatients.value || []);
|
||||
}
|
||||
|
||||
const hasGeneratedNextQueue = allPatientsArray.some(p =>
|
||||
(p.sourcePatientNo && p.sourcePatientNo === currentProcessingPatient.value.no) ||
|
||||
(p.referencePatient && p.referencePatient === currentProcessingPatient.value.noAntrian)
|
||||
);
|
||||
|
||||
if (!hasGeneratedNextQueue) {
|
||||
showCheckInConfirmDialog.value = true;
|
||||
return; // Hentikan proses, tunggu konfirmasi user
|
||||
}
|
||||
}
|
||||
|
||||
await processPatient(currentProcessingPatient.value, action);
|
||||
broadcastUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
const confirmCheckIn = async () => {
|
||||
if (currentProcessingPatient.value) {
|
||||
await processPatient(currentProcessingPatient.value, 'check-in');
|
||||
broadcastUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCall = async (count) => {
|
||||
if (count === 1) {
|
||||
callNext(loketId.value);
|
||||
@@ -932,14 +978,44 @@ const handleProcessNext = () => {
|
||||
|
||||
const confirmAndProcess = (item, action) => {
|
||||
if (currentProcessingPatient.value) {
|
||||
let allPatientsArray = [];
|
||||
if (queueStore.allPatients) {
|
||||
allPatientsArray = Array.isArray(queueStore.allPatients)
|
||||
? queueStore.allPatients
|
||||
: (queueStore.allPatients.value || []);
|
||||
}
|
||||
|
||||
const hasGeneratedNextQueue = allPatientsArray.some(p =>
|
||||
(p.sourcePatientNo && p.sourcePatientNo === currentProcessingPatient.value.no) ||
|
||||
(p.referencePatient && p.referencePatient === currentProcessingPatient.value.noAntrian)
|
||||
);
|
||||
|
||||
pendingReplaceItem.value = item;
|
||||
pendingReplaceAction.value = action;
|
||||
showConfirmReplaceDialog.value = true;
|
||||
|
||||
if (hasGeneratedNextQueue) {
|
||||
showUnfinishedPatientDialog.value = true;
|
||||
} else {
|
||||
showConfirmReplaceDialog.value = true;
|
||||
}
|
||||
} else {
|
||||
executeProcess(item, action);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmUnfinishedAndProcess = async () => {
|
||||
if (currentProcessingPatient.value) {
|
||||
// Selesaikan pasien aktif
|
||||
await processPatient(currentProcessingPatient.value, 'check-in');
|
||||
|
||||
// Proses pasien berikutnya / yang dipilih
|
||||
executeProcess(pendingReplaceItem.value, pendingReplaceAction.value);
|
||||
|
||||
pendingReplaceItem.value = null;
|
||||
pendingReplaceAction.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const executeProcess = async (item, action) => {
|
||||
if (action === "next") {
|
||||
processNextQueue();
|
||||
@@ -966,47 +1042,70 @@ const handleConfirmReplace = () => {
|
||||
|
||||
const { sendViaPost } = queueStore;
|
||||
|
||||
const broadcastUpdate = async (callData = null) => {
|
||||
// Debounce timer to prevent multiple rapid broadcasts
|
||||
let _broadcastDebounceTimer = null;
|
||||
|
||||
const broadcastUpdate = async (extraData = null) => {
|
||||
// extraData can be:
|
||||
// - null: regular refresh (debounced)
|
||||
// - { noAntrian, ... }: a CALL event (immediate, bypass debounce)
|
||||
// - { loketId }: a MOVE event (immediate, bypass debounce, notify destination)
|
||||
|
||||
const isCallEvent = extraData && extraData.noAntrian; // Patient call
|
||||
const isMoveEvent = extraData && extraData.loketId && !extraData.noAntrian; // Klinik move
|
||||
const isImmediate = isCallEvent || isMoveEvent;
|
||||
|
||||
// Debounce only plain refresh events
|
||||
if (!isImmediate) {
|
||||
if (_broadcastDebounceTimer) return; // Already pending, skip
|
||||
_broadcastDebounceTimer = setTimeout(() => {
|
||||
_broadcastDebounceTimer = null;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
try {
|
||||
const displayClientIds = [];
|
||||
|
||||
// Base broadcast IDs for this loket
|
||||
displayClientIds.push(`anjungan-loket-${loketId.value}`);
|
||||
displayClientIds.push(`anjungan-masuk-${loketId.value}`);
|
||||
// Also broadcast to clinic specific displays if needed
|
||||
// displayClientIds.push(`anjungan-klinik-${...}`);
|
||||
|
||||
// Broadcast to ALL loket admins and anjungans (including this one, for other computers opening the same loket)
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
displayClientIds.push(`admin-loket-${i}`);
|
||||
displayClientIds.push(`anjungan-loket-${i}`);
|
||||
displayClientIds.push(`anjungan-masuk-${i}`);
|
||||
}
|
||||
|
||||
console.log('📡 [AdminLoket] Broadcasting update trigger to:', displayClientIds);
|
||||
|
||||
const payload = {
|
||||
loketId: loketId.value,
|
||||
triggerRefresh: true
|
||||
};
|
||||
|
||||
// If this is a CALL event, include the patient data
|
||||
if (callData) {
|
||||
if (isCallEvent) {
|
||||
payload.callEvent = {
|
||||
...callData,
|
||||
...extraData,
|
||||
loketId: loketId.value,
|
||||
loketName: loketName.value,
|
||||
calledAt: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
// Send all broadcasts in parallel for speed
|
||||
await Promise.all(displayClientIds.map(clientId =>
|
||||
sendViaPost({
|
||||
to_client: clientId,
|
||||
data: payload
|
||||
})
|
||||
));
|
||||
// Minimal broadcast: only send to the most relevant clients
|
||||
const criticalTargets = [
|
||||
`anjungan-loket-${loketId.value}`,
|
||||
`anjungan-masuk-${loketId.value}`,
|
||||
`admin-loket-${loketId.value}`,
|
||||
];
|
||||
|
||||
// For MOVE events, also notify the destination loket
|
||||
if (isMoveEvent && extraData.loketId && String(extraData.loketId) !== String(loketId.value)) {
|
||||
const destLoketId = extraData.loketId;
|
||||
criticalTargets.push(`admin-loket-${destLoketId}`);
|
||||
criticalTargets.push(`anjungan-loket-${destLoketId}`);
|
||||
criticalTargets.push(`anjungan-masuk-${destLoketId}`);
|
||||
console.log(`📡 [AdminLoket] Patient moved: also notifying Loket ${destLoketId}`);
|
||||
}
|
||||
|
||||
console.log('📡 [AdminLoket] Broadcasting to:', criticalTargets);
|
||||
|
||||
// Send sequentially with a delay to avoid 429 Rate Limit
|
||||
for (const clientId of criticalTargets) {
|
||||
try {
|
||||
await sendViaPost({ to_client: clientId, data: payload });
|
||||
} catch (err) {
|
||||
console.warn(`Failed to broadcast to ${clientId}:`, err?.message || err);
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 80));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [AdminLoket] Error broadcasting update:', error);
|
||||
}
|
||||
|
||||
+100
-33
@@ -240,11 +240,18 @@
|
||||
<div class="chart-container">
|
||||
<ClientOnly>
|
||||
<Line
|
||||
v-if="visitTrendData.labels && visitTrendData.labels.length > 0"
|
||||
v-if="visitTrendData.labels && visitTrendData.labels.length > 0 && visitTrendData.datasets[0].data.length > 0 && visitTrendData.datasets[0].data.some(v => v > 0)"
|
||||
:data="visitTrendData"
|
||||
:options="areaChartOptions"
|
||||
class="chart-wrapper"
|
||||
/>
|
||||
<div v-else class="empty-chart-state">
|
||||
<div class="empty-state-content">
|
||||
<v-icon size="64" color="grey-lighten-2" class="mb-2">mdi-chart-timeline-variant</v-icon>
|
||||
<h4 class="text-h6 text-grey-darken-1 font-weight-medium mb-1">Belum Ada Data</h4>
|
||||
<p class="text-caption text-grey">Tren kunjungan bulanan akan tampil di sini</p>
|
||||
</div>
|
||||
</div>
|
||||
<template #fallback>
|
||||
<div class="chart-loading">
|
||||
<v-progress-circular indeterminate color="primary" size="50"></v-progress-circular>
|
||||
@@ -285,11 +292,18 @@
|
||||
<div class="chart-container">
|
||||
<ClientOnly>
|
||||
<Doughnut
|
||||
v-if="paymentStatusData.labels && paymentStatusData.labels.length > 0"
|
||||
v-if="paymentStatusData.labels && paymentStatusData.labels.length > 0 && paymentStatusData.datasets[0].data.length > 0 && paymentStatusData.datasets[0].data.some(v => v > 0)"
|
||||
:data="paymentStatusData"
|
||||
:options="doughnutOptions"
|
||||
class="chart-wrapper pie-chart"
|
||||
/>
|
||||
<div v-else class="empty-chart-state">
|
||||
<div class="empty-state-content">
|
||||
<v-icon size="64" color="grey-lighten-2" class="mb-2">mdi-chart-donut-variant</v-icon>
|
||||
<h4 class="text-h6 text-grey-darken-1 font-weight-medium mb-1">Belum Ada Data</h4>
|
||||
<p class="text-caption text-grey">Distribusi metode bayar akan tampil di sini</p>
|
||||
</div>
|
||||
</div>
|
||||
<template #fallback>
|
||||
<div class="chart-loading">
|
||||
<v-progress-circular indeterminate color="success" size="50"></v-progress-circular>
|
||||
@@ -330,11 +344,18 @@
|
||||
<div class="chart-container">
|
||||
<ClientOnly>
|
||||
<Bar
|
||||
v-if="waitingTimeData.labels && waitingTimeData.labels.length > 0"
|
||||
v-if="waitingTimeData.labels && waitingTimeData.labels.length > 0 && waitingTimeData.datasets[0].data.length > 0 && waitingTimeData.datasets[0].data.some(v => v > 0)"
|
||||
:data="waitingTimeData"
|
||||
:options="horizontalBarOptions"
|
||||
class="chart-wrapper"
|
||||
/>
|
||||
<div v-else class="empty-chart-state">
|
||||
<div class="empty-state-content">
|
||||
<v-icon size="64" color="grey-lighten-2" class="mb-2">mdi-clock-outline</v-icon>
|
||||
<h4 class="text-h6 text-grey-darken-1 font-weight-medium mb-1">Belum Ada Data</h4>
|
||||
<p class="text-caption text-grey">Data waktu tunggu poli akan tampil di sini</p>
|
||||
</div>
|
||||
</div>
|
||||
<template #fallback>
|
||||
<div class="chart-loading">
|
||||
<v-progress-circular indeterminate color="secondary" size="50"></v-progress-circular>
|
||||
@@ -379,11 +400,18 @@
|
||||
<div class="chart-container">
|
||||
<ClientOnly>
|
||||
<PolarArea
|
||||
v-if="attendanceData.labels && attendanceData.labels.length > 0"
|
||||
v-if="attendanceData.labels && attendanceData.labels.length > 0 && attendanceData.datasets[0].data.length > 0 && attendanceData.datasets[0].data.some(v => v > 0)"
|
||||
:data="attendanceData"
|
||||
:options="polarAreaOptions"
|
||||
class="chart-wrapper pie-chart"
|
||||
/>
|
||||
<div v-else class="empty-chart-state">
|
||||
<div class="empty-state-content">
|
||||
<v-icon size="64" color="grey-lighten-2" class="mb-2">mdi-account-check</v-icon>
|
||||
<h4 class="text-h6 text-grey-darken-1 font-weight-medium mb-1">Belum Ada Data</h4>
|
||||
<p class="text-caption text-grey">Statistik tingkat kehadiran akan tampil di sini</p>
|
||||
</div>
|
||||
</div>
|
||||
<template #fallback>
|
||||
<div class="chart-loading">
|
||||
<v-progress-circular indeterminate color="primary" size="50"></v-progress-circular>
|
||||
@@ -499,54 +527,55 @@ const exportOptions = ref([
|
||||
const currentDate = ref('');
|
||||
const filterDateFrom = ref(dayjs().format('YYYY-MM-DD'));
|
||||
const filterDateTo = ref(dayjs().format('YYYY-MM-DD'));
|
||||
const selectedYear = ref(2025);
|
||||
const availableYears = ref([2025, 2026, 2027]);
|
||||
const currentYear = dayjs().year();
|
||||
const selectedYear = ref(currentYear);
|
||||
const availableYears = ref([currentYear - 1, currentYear, currentYear + 1]);
|
||||
|
||||
// New Stats Data - Updated Metrics
|
||||
const stats = ref([
|
||||
{
|
||||
label: 'Pasien Hari Ini',
|
||||
value: '324',
|
||||
value: '-',
|
||||
icon: 'mdi-account-heart',
|
||||
iconSize: 24,
|
||||
iconColor: 'white',
|
||||
color: 'primary',
|
||||
change: '+18.2%',
|
||||
changeType: 'positive',
|
||||
changeIcon: 'mdi-trending-up'
|
||||
change: '-',
|
||||
changeType: 'neutral',
|
||||
changeIcon: 'mdi-minus'
|
||||
},
|
||||
{
|
||||
label: 'Antrean Aktif',
|
||||
value: '47',
|
||||
value: '-',
|
||||
icon: 'mdi-clock-fast',
|
||||
iconSize: 24,
|
||||
iconColor: 'white',
|
||||
color: 'secondary',
|
||||
change: '+5',
|
||||
changeType: 'positive',
|
||||
changeIcon: 'mdi-arrow-up'
|
||||
change: '-',
|
||||
changeType: 'neutral',
|
||||
changeIcon: 'mdi-minus'
|
||||
},
|
||||
{
|
||||
label: 'Rata-rata Tunggu',
|
||||
value: '12 min',
|
||||
value: '-',
|
||||
icon: 'mdi-timer-outline',
|
||||
iconSize: 24,
|
||||
iconColor: 'white',
|
||||
color: 'success',
|
||||
change: '-3 min',
|
||||
changeType: 'positive',
|
||||
changeIcon: 'mdi-trending-down'
|
||||
change: '-',
|
||||
changeType: 'neutral',
|
||||
changeIcon: 'mdi-minus'
|
||||
},
|
||||
{
|
||||
label: 'Tingkat Kehadiran',
|
||||
value: '89.5%',
|
||||
value: '-',
|
||||
icon: 'mdi-check-circle',
|
||||
iconSize: 24,
|
||||
iconColor: 'white',
|
||||
color: 'primary',
|
||||
change: '+2.3%',
|
||||
changeType: 'positive',
|
||||
changeIcon: 'mdi-trending-up'
|
||||
change: '-',
|
||||
changeType: 'neutral',
|
||||
changeIcon: 'mdi-minus'
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -556,7 +585,7 @@ const visitTrendData = ref({
|
||||
datasets: [
|
||||
{
|
||||
label: 'Pasien Umum',
|
||||
data: [850, 920, 880, 1050, 1120, 1080, 1200, 1150, 1080, 1190, 1250, 1300],
|
||||
data: [],
|
||||
backgroundColor: 'rgba(86, 126, 231, 0.2)',
|
||||
borderColor: colors.primary[500],
|
||||
borderWidth: 3,
|
||||
@@ -570,7 +599,7 @@ const visitTrendData = ref({
|
||||
},
|
||||
{
|
||||
label: 'Pasien BPJS',
|
||||
data: [1200, 1350, 1280, 1480, 1550, 1620, 1700, 1650, 1590, 1720, 1800, 1850],
|
||||
data: [],
|
||||
backgroundColor: 'rgba(255, 132, 65, 0.2)',
|
||||
borderColor: colors.secondary[500],
|
||||
borderWidth: 3,
|
||||
@@ -587,10 +616,10 @@ const visitTrendData = ref({
|
||||
|
||||
// Payment Status Data (Doughnut Chart)
|
||||
const paymentStatusData = ref({
|
||||
labels: ['BPJS Kesehatan', 'Umum/Tunai', 'Asuransi Swasta', 'Corporate'],
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
data: [1850, 680, 320, 280],
|
||||
data: [],
|
||||
backgroundColor: [
|
||||
colors.primary[500],
|
||||
colors.secondary[500],
|
||||
@@ -606,11 +635,11 @@ const paymentStatusData = ref({
|
||||
|
||||
// Waiting Time per Poli Data (Horizontal Bar Chart)
|
||||
const waitingTimeData = ref({
|
||||
labels: ['Poli Umum', 'Poli Anak', 'Poli Gigi', 'Poli Mata', 'Poli THT', 'Poli Jantung'],
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Waktu Tunggu (menit)',
|
||||
data: [15, 12, 8, 18, 10, 22],
|
||||
data: [],
|
||||
backgroundColor: [
|
||||
colors.primary[400],
|
||||
colors.secondary[400],
|
||||
@@ -635,10 +664,10 @@ const waitingTimeData = ref({
|
||||
|
||||
// Attendance Data (Polar Area Chart)
|
||||
const attendanceData = ref({
|
||||
labels: ['Hadir Tepat Waktu', 'Hadir Terlambat', 'Tidak Hadir', 'Batal', 'Reschedule'],
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
data: [1850, 420, 280, 150, 230],
|
||||
data: [],
|
||||
backgroundColor: [
|
||||
colors.success[400],
|
||||
colors.primary[400],
|
||||
@@ -998,8 +1027,24 @@ const refreshDashboardStats = async (filterParams = {}) => {
|
||||
|
||||
// 4. Update Monthly Trend (Map API into datasets)
|
||||
if (data.monthly_trend && data.monthly_trend.length > 0) {
|
||||
// Logic to update trend charts can be expanded here
|
||||
// For now, we'll keep the existing structure but show where real data fits
|
||||
// TODO: Map 'data.monthly_trend' to 'visitTrendData'
|
||||
// Example:
|
||||
// visitTrendData.value.labels = data.monthly_trend.map(m => m.month_name);
|
||||
// visitTrendData.value.datasets[0].data = data.monthly_trend.map(m => m.umum_count);
|
||||
// visitTrendData.value.datasets[1].data = data.monthly_trend.map(m => m.bpjs_count);
|
||||
}
|
||||
|
||||
// 5. Update Attendance Data
|
||||
if (data.attendance_stats && Object.keys(data.attendance_stats).length > 0) {
|
||||
// TODO: Map 'data.attendance_stats' to 'attendanceData'
|
||||
// Example:
|
||||
// attendanceData.value = {
|
||||
// labels: Object.keys(data.attendance_stats),
|
||||
// datasets: [{
|
||||
// ...attendanceData.value.datasets[0],
|
||||
// data: Object.values(data.attendance_stats)
|
||||
// }]
|
||||
// };
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1551,7 +1596,7 @@ const downloadBlob = (blob, filename) => {
|
||||
|
||||
/* Compact Stats Cards */
|
||||
.stats-row {
|
||||
margin-top: -20px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 12px !important;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
@@ -2082,6 +2127,28 @@ const downloadBlob = (blob, filename) => {
|
||||
}
|
||||
}
|
||||
|
||||
.empty-chart-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
border-radius: 12px;
|
||||
border: 1px dashed rgba(0, 0, 0, 0.1);
|
||||
margin: 16px;
|
||||
padding: 24px;
|
||||
|
||||
.empty-state-content {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.live-chip {
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { defineEventHandler, readBody } from 'h3';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Path to the mock JSON file
|
||||
const filePath = path.resolve(process.cwd(), 'public/data/patient_subspesialis.json');
|
||||
|
||||
// Ensure directory and file exist
|
||||
const ensureFileExists = () => {
|
||||
const dirPath = path.dirname(filePath);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(filePath)) {
|
||||
fs.writeFileSync(filePath, JSON.stringify({}, null, 2), 'utf-8');
|
||||
}
|
||||
};
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
ensureFileExists();
|
||||
|
||||
if (event.node.req.method === 'GET') {
|
||||
// Return all mappings
|
||||
try {
|
||||
const data = fs.readFileSync(filePath, 'utf-8');
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
if (event.node.req.method === 'POST') {
|
||||
// Save a new mapping
|
||||
try {
|
||||
const body = await readBody(event);
|
||||
const { barcode, subspesialis } = body;
|
||||
|
||||
if (!barcode || !subspesialis) {
|
||||
return { success: false, message: 'Barcode and subspesialis are required' };
|
||||
}
|
||||
|
||||
const fileData = fs.readFileSync(filePath, 'utf-8');
|
||||
const data = JSON.parse(fileData);
|
||||
|
||||
// Save mapping: barcode -> subspesialis object
|
||||
data[barcode] = subspesialis;
|
||||
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
return { success: true, message: 'Saved successfully' };
|
||||
} catch (e) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
}
|
||||
});
|
||||
+71
-10
@@ -784,10 +784,29 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
|
||||
|
||||
const patientsRaw = rawData.data || [];
|
||||
|
||||
// Fetch temporary subspesialis mapping
|
||||
let subspesialisMap = {};
|
||||
try {
|
||||
const subRes = await fetch('/api/patient-subspesialis');
|
||||
if (subRes.ok) {
|
||||
subspesialisMap = await subRes.json();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch temporary subspesialis mapping', e);
|
||||
}
|
||||
|
||||
// Map API data to store format
|
||||
const mappedPatients = patientsRaw.map((apiPatient, index) =>
|
||||
mapApiPatientToStoreFormat(apiPatient, index)
|
||||
).filter(p => isTodayPatient(p));
|
||||
const mappedPatients = patientsRaw.map((apiPatient, index) => {
|
||||
const mapped = mapApiPatientToStoreFormat(apiPatient, index);
|
||||
// Merge temporary subspesialis mapping
|
||||
const barcodeKey = mapped.barcode || mapped.idtiket;
|
||||
if (barcodeKey && subspesialisMap[barcodeKey]) {
|
||||
mapped.ruang = subspesialisMap[barcodeKey].namaRuang || subspesialisMap[barcodeKey].nama;
|
||||
mapped.nomorRuang = subspesialisMap[barcodeKey].nomorRuang || subspesialisMap[barcodeKey].ruang;
|
||||
mapped.kodeRuang = subspesialisMap[barcodeKey].kodeRuang || subspesialisMap[barcodeKey].id;
|
||||
}
|
||||
return mapped;
|
||||
}).filter(p => isTodayPatient(p));
|
||||
|
||||
// Deduplicate patients by idtiket
|
||||
// API returns multiple entries if patient is at multiple positions
|
||||
@@ -891,6 +910,25 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
|
||||
return true; // Keep the currently processing patient
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Protect patients manually moved to a different loket via changeKlinik.
|
||||
// The backend API doesn't know about this move yet, so API refresh would wipe the patient.
|
||||
// We protect them for 10 minutes (600000ms) to give admin time to process them.
|
||||
if (p.manuallyMoved && p.movedAt) {
|
||||
const timeSinceMoved = Date.now() - p.movedAt;
|
||||
const tenMinutes = 10 * 60 * 1000;
|
||||
if (timeSinceMoved < tenMinutes) {
|
||||
console.log(`🛡️ [queueStore] Protecting manually-moved patient ${p.noAntrian?.split(' |')[0]} (moved ${Math.round(timeSinceMoved/1000)}s ago) from API overwrite`);
|
||||
// Prevent the old loket's API data from overwriting this patient
|
||||
if (newPatientMap.has(key)) {
|
||||
newPatientMap.delete(key);
|
||||
}
|
||||
return true; // Keep the moved patient
|
||||
} else {
|
||||
// After 10 minutes, allow API to take over
|
||||
console.log(`⏰ [queueStore] manuallyMoved patient ${p.noAntrian?.split(' |')[0]} protection expired. Allowing API overwrite.`);
|
||||
}
|
||||
}
|
||||
|
||||
// If it's still in loket stage but marked as processed, and it's in the API batch,
|
||||
// we might be experiencing API lag. Keep it as processed.
|
||||
if (p.status === 'processed' && newPatientMap.has(key)) {
|
||||
@@ -2297,7 +2335,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
|
||||
if (newPatient.kodeKlinik) {
|
||||
const allLokets = loketStore.lokets || [];
|
||||
const targetLoket = allLokets.find(l =>
|
||||
l.pelayanan && Array.isArray(l.pelayanan) && l.pelayanan.includes(newPatient.kodeKlinik)
|
||||
l.pelayanan && Array.isArray(l.pelayanan) &&
|
||||
(l.pelayanan.includes(newPatient.kodeKlinik) || l.pelayanan.includes(newPatient.kodeKlinik?.split('-')[0]))
|
||||
);
|
||||
if (targetLoket) {
|
||||
newPatient.loketId = targetLoket.id;
|
||||
@@ -2672,7 +2711,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
|
||||
const getPatientsByKlinikRuang = (kodeKlinik, nomorRuang, tipeLayanan = null) => {
|
||||
return computed(() => {
|
||||
let patients = allPatients.value.filter(p =>
|
||||
p.kodeKlinik === kodeKlinik &&
|
||||
(p.kodeKlinik === kodeKlinik || (p.kodeKlinik && p.kodeKlinik.split('-')[0] === kodeKlinik)) &&
|
||||
p.nomorRuang === nomorRuang &&
|
||||
p.processStage === 'klinik-ruang'
|
||||
);
|
||||
@@ -2694,7 +2733,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
|
||||
// Call next patient for a specific room and tipeLayanan
|
||||
const callNextKlinikRuang = (kodeKlinik, nomorRuang, tipeLayanan, allowMultiple = false) => {
|
||||
const patients = allPatients.value.filter(p =>
|
||||
p.kodeKlinik === kodeKlinik &&
|
||||
(p.kodeKlinik === kodeKlinik || (p.kodeKlinik && p.kodeKlinik.split('-')[0] === kodeKlinik)) &&
|
||||
p.nomorRuang === nomorRuang &&
|
||||
p.tipeLayanan === tipeLayanan &&
|
||||
p.processStage === 'klinik-ruang' &&
|
||||
@@ -2854,7 +2893,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
|
||||
if (newKlinik.kode) {
|
||||
const allLokets = loketStore.lokets || [];
|
||||
const foundLoket = allLokets.find(l =>
|
||||
l.pelayanan && Array.isArray(l.pelayanan) && l.pelayanan.includes(newKlinik.kode)
|
||||
l.pelayanan && Array.isArray(l.pelayanan) &&
|
||||
(l.pelayanan.includes(newKlinik.kode) || l.pelayanan.includes(newKlinik.kode?.split('-')[0]))
|
||||
);
|
||||
|
||||
if (foundLoket) {
|
||||
@@ -2881,13 +2921,16 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
|
||||
}
|
||||
|
||||
// Update the patient in allPatients
|
||||
// Mark as manuallyMoved so API refresh doesn't wipe this patient from the destination loket
|
||||
const updatedPatient = {
|
||||
...oldPatient,
|
||||
klinik: newKlinik.name,
|
||||
kodeKlinik: newKlinik.kode,
|
||||
loketId: targetLoketId,
|
||||
loket: targetLoketName,
|
||||
status: newStatus
|
||||
status: newStatus,
|
||||
manuallyMoved: isMovedToDifferentLoket ? true : (oldPatient.manuallyMoved || false),
|
||||
movedAt: isMovedToDifferentLoket ? Date.now() : (oldPatient.movedAt || null)
|
||||
};
|
||||
|
||||
allPatients.value[patientIndex] = updatedPatient;
|
||||
@@ -3022,7 +3065,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
|
||||
if (!newPatient.loketId && newPatient.kodeKlinik) {
|
||||
const allLokets = loketStore.lokets || [];
|
||||
const targetLoket = allLokets.find(l =>
|
||||
l.pelayanan && Array.isArray(l.pelayanan) && l.pelayanan.includes(newPatient.kodeKlinik)
|
||||
l.pelayanan && Array.isArray(l.pelayanan) &&
|
||||
(l.pelayanan.includes(newPatient.kodeKlinik) || l.pelayanan.includes(newPatient.kodeKlinik?.split('-')[0]))
|
||||
);
|
||||
|
||||
if (targetLoket) {
|
||||
@@ -3061,7 +3105,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
|
||||
// Find loket that handles BOTH the clinic AND the payment type
|
||||
const targetLoket = allLokets.find(l => {
|
||||
if (l.source !== 'api' || l.id >= 1000) return false;
|
||||
const handlesClinic = l.pelayanan && Array.isArray(l.pelayanan) && l.pelayanan.includes(clinic.kode);
|
||||
const handlesClinic = l.pelayanan && Array.isArray(l.pelayanan) &&
|
||||
(l.pelayanan.includes(clinic.kode) || l.pelayanan.includes(clinic.kode?.split('-')[0]));
|
||||
if (handlesClinic) {
|
||||
return isPaymentCompatible(paymentTypeForMatching, l.pembayaran);
|
||||
}
|
||||
@@ -3179,6 +3224,22 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
|
||||
allPatients.value.push(newPatient);
|
||||
// NOTE: Kita tidak perlu incrementBarcodeCounter() di sini karena barcode berasal dari API
|
||||
|
||||
// Temporary: Save subSpesialis mapping to local JSON API
|
||||
if (subSpesialis) {
|
||||
try {
|
||||
await fetch('/api/patient-subspesialis', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
barcode: barcode,
|
||||
subspesialis: subSpesialis
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to save temporary subSpesialis mapping', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user