feat: implement queue store with clinic API integration, WebSocket sync, and patient data management

This commit is contained in:
Fanrouver
2026-07-02 11:39:07 +07:00
parent e18ed186e1
commit 61772105fd
8 changed files with 553 additions and 541 deletions
+41 -172
View File
@@ -1,193 +1,62 @@
// composables/useQueueAPI.ts
// Composable untuk API calls terkait antrian pasien
export interface Patient {
no: number;
jamPanggil: string;
barcode: string;
noAntrian: string;
shift: string;
klinik: string;
fastTrack: string;
pembayaran: string;
status: 'anjungan' | 'pending' | 'di-loket' | 'di-klinik' | 'selesai' | 'terlambat';
processStage: 'loket' | 'klinik' | 'penunjang';
createdAt: string;
registrationType?: 'online' | 'onsite';
visitType?: string;
visitDate?: string;
}
export interface QueueAPIResponse<T = any> {
success: boolean;
data?: T;
message?: string;
error?: string;
}
import { useRuntimeConfig } from '#app';
import type { QueuePatient } from '@/types/queue';
export const useQueueAPI = () => {
const config = useRuntimeConfig();
const baseURL = config.public.apiBaseUrl || '/api/queue';
const verificationApiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
const externalApiBase = config.public.externalApiBaseUrl;
/**
* Fetch all patients from database
*/
const fetchAllPatients = async (): Promise<Patient[]> => {
const fetchRawLoketPatients = async (loketId: string | number) => {
try {
const response = await $fetch<QueueAPIResponse<Patient[]>>(`${baseURL}/patients`, {
method: 'GET',
});
if (response.success && response.data) {
return response.data;
const rawData: any = await $fetch(`${verificationApiBase}/loket/${loketId}`);
if (rawData.metadata && rawData.metadata.code !== 200) {
throw new Error(rawData.message || 'API returned error status');
}
throw new Error(response.message || 'Failed to fetch patients');
return rawData.data || [];
} catch (error: any) {
console.error('❌ Error fetching patients:', error);
throw error;
throw new Error(`HTTP error! status: ${error.response?.status || error.message}`);
}
};
/**
* Fetch single patient by ID or barcode
*/
const fetchPatient = async (idOrBarcode: string): Promise<Patient | null> => {
const fetchRawClinicPatients = async (clinicId: string | number) => {
const url = `${externalApiBase}/visit?klinik_id=${clinicId}&limit=500`;
try {
const response = await $fetch<QueueAPIResponse<Patient>>(`${baseURL}/patients/${idOrBarcode}`, {
method: 'GET',
});
const rawResponse: any = await $fetch(url);
return rawResponse?.data || [];
} catch (error: any) {
throw new Error(`HTTP error! status: ${error.response?.status || error.message}`);
}
};
if (response.success && response.data) {
return response.data;
const updateTicketStatus = async (barcode: string, statuspasien: string, statuspasien2: string, idklinikstatus: string, idklinikstatus2: string) => {
return await $fetch(`${verificationApiBase}/tiket/update`, {
method: 'POST',
body: {
barcode: barcode || "",
statuspasien,
statuspasien2,
idklinikstatus,
idklinikstatus2
}
return null;
} catch (error: any) {
console.error('❌ Error fetching patient:', error);
return null;
}
});
};
/**
* Create new patient (register from Anjungan)
*/
const createPatient = async (patientData: Partial<Patient>): Promise<Patient> => {
try {
const response = await $fetch<QueueAPIResponse<Patient>>(`${baseURL}/patients`, {
method: 'POST',
body: patientData,
});
if (response.success && response.data) {
return response.data;
const completeTicketStatus = async (idloket: string, barcode: string, statuspasien: string, idklinikstatus: string) => {
return await $fetch(`${verificationApiBase}/tiket/selesai`, {
method: 'POST',
body: {
idloket: String(idloket || ""),
barcode: barcode || "",
statuspasien,
idklinikstatus
}
throw new Error(response.message || 'Failed to create patient');
} catch (error: any) {
console.error('❌ Error creating patient:', error);
throw error;
}
};
/**
* Update patient status (check-in, process, etc)
*/
const updatePatient = async (
idOrBarcode: string,
updates: Partial<Patient>
): Promise<Patient> => {
try {
const response = await $fetch<QueueAPIResponse<Patient>>(
`${baseURL}/patients/${idOrBarcode}`,
{
method: 'PATCH',
body: updates,
}
);
if (response.success && response.data) {
return response.data;
}
throw new Error(response.message || 'Failed to update patient');
} catch (error: any) {
console.error('❌ Error updating patient:', error);
throw error;
}
};
/**
* Check-in patient (update status to di-loket)
*/
const checkInPatient = async (idOrBarcode: string): Promise<Patient> => {
return updatePatient(idOrBarcode, { status: 'di-loket' });
};
/**
* Process patient at loket (update status and processStage)
*/
const processPatientAtLoket = async (
idOrBarcode: string,
updates: { status?: string; processStage?: string }
): Promise<Patient> => {
return updatePatient(idOrBarcode, updates as Partial<Patient>);
};
/**
* Sync local state with database
*/
const syncWithDatabase = async (localPatients: Patient[]): Promise<Patient[]> => {
try {
// Fetch latest from database
const dbPatients = await fetchAllPatients();
// Merge strategy: prefer database data, but keep local if newer
const merged = new Map<string, Patient>();
// Add database patients
dbPatients.forEach(patient => {
merged.set(patient.barcode, patient);
});
// Add local patients that don't exist in DB or are newer
localPatients.forEach(localPatient => {
const existing = merged.get(localPatient.barcode);
if (!existing || new Date(localPatient.createdAt) > new Date(existing.createdAt)) {
merged.set(localPatient.barcode, localPatient);
}
});
return Array.from(merged.values());
} catch (error: any) {
console.error('❌ Error syncing with database:', error);
// Return local patients as fallback
return localPatients;
}
};
/**
* Batch sync: save multiple patients to database
*/
const batchSyncPatients = async (patients: Patient[]): Promise<boolean> => {
try {
const response = await $fetch<QueueAPIResponse>(`${baseURL}/patients/batch`, {
method: 'POST',
body: { patients },
});
return response.success || false;
} catch (error: any) {
console.error('❌ Error batch syncing patients:', error);
return false;
}
});
};
return {
fetchAllPatients,
fetchPatient,
createPatient,
updatePatient,
checkInPatient,
processPatientAtLoket,
syncWithDatabase,
batchSyncPatients,
fetchRawLoketPatients,
fetchRawClinicPatients,
updateTicketStatus,
completeTicketStatus
};
};
+237
View File
@@ -0,0 +1,237 @@
import { ref } from 'vue';
import { useRuntimeConfig } from '#app';
import { useWebSocket } from '@/composables/useWebSocket';
import type { QueuePatient } from '@/types/queue';
export interface QueueSyncDeps {
allPatients: Ref<QueuePatient[]>;
currentProcessingPatient: Ref<Record<string, QueuePatient>>;
activeLoketInterest: Ref<Record<string, number>>;
activeClinicInterest: Ref<Record<string, number>>;
globalInterestCount: Ref<number>;
fetchPatientsForLoket: (id: string | number, force?: boolean) => void;
fetchPatientsForClinic: (id: string, force?: boolean) => void;
fetchAllPatients: () => void;
}
export const useQueueSync = (deps: QueueSyncDeps) => {
const isWsConnected = ref<boolean>(false);
const wsClientId = ref<string>(`client-${Math.random().toString(36).substring(7)}`);
const lastGlobalCall = ref<any>(null);
const lastKlinikCall = ref<any>(null);
const onWsMessage = (data: any) => {
// Robust data extraction: some relays wrap data in another 'data' property
let messageData = data?.data || data;
if (messageData?.data && !messageData.callKlinikEvent && !messageData.callEvent) {
messageData = messageData.data; // Double wrap check
}
const targetLoketId = messageData?.loketId || messageData?.idloket;
const targetKlinikId = messageData?.klinikId || messageData?.idklinik;
// Handle Call Events and WS messages
if (messageData?.triggerRefresh) {
if (messageData.klinikId) {
// console.log(`🔄 [queueSync] Received refresh trigger for clinic ${messageData.klinikId}`);
// Handle current processing update if provided
if (messageData.currentProcessingUpdate) {
// console.log(`🎯 [queueSync] Applying current processing update:`, messageData.currentProcessingUpdate);
Object.keys(messageData.currentProcessingUpdate).forEach(key => {
deps.currentProcessingPatient.value[key] = messageData.currentProcessingUpdate[key];
// Also patch the status in allPatients if possible
const processingPatient = messageData.currentProcessingUpdate[key];
if (processingPatient && processingPatient.no) {
const idx = deps.allPatients.value.findIndex(p => p.no === processingPatient.no);
if (idx !== -1) {
deps.allPatients.value[idx] = { ...deps.allPatients.value[idx], status: 'di-loket' };
}
}
});
}
deps.fetchPatientsForClinic(messageData.klinikId, true);
}
}
if (messageData?.callEvent) {
lastGlobalCall.value = messageData.callEvent;
}
// Handle Klinik Call Events (cross-device sync for AntrianKlinikRuang display)
if (messageData?.callKlinikEvent) {
const ev = messageData.callKlinikEvent;
// console.log('🏥 [queueSync] Klinik call event received:', ev);
// PERSISTENCE FIX: Save to lastKlinikCall for displays to watch
lastKlinikCall.value = ev;
// Find the patient in allPatients and patch directly for immediate UI update
const idx = deps.allPatients.value.findIndex(p =>
p.processStage === 'klinik-ruang' &&
p.kodeKlinik === ev.kodeKlinik &&
(
(p.barcode && String(p.barcode) === String(ev.barcode)) ||
(p.noAntrian && p.noAntrian.split(' |')[0] === ev.noantrian)
)
);
if (idx !== -1) {
const updatedPatient = {
...deps.allPatients.value[idx],
tipeLayanan: ev.tipeLayanan,
lastCalledAt: ev.lastCalledAt || new Date().toISOString(),
lastCalledTipeLayanan: ev.tipeLayanan,
status: 'di-loket' as any,
calledPemeriksaanAwal: ev.tipeLayanan === 'Pemeriksaan Awal' ? true : deps.allPatients.value[idx].calledPemeriksaanAwal,
calledTindakan: ev.tipeLayanan === 'Tindakan' ? true : deps.allPatients.value[idx].calledTindakan
};
deps.allPatients.value[idx] = updatedPatient;
// console.log(`✅ [queueSync] Successfully patched patient ${ev.noantrian} status to di-loket (lastCalledAt: ${updatedPatient.lastCalledAt})`);
} else {
console.warn(`⚠️ [queueSync] Patient ${ev.noantrian} not found in store for clinic ${ev.kodeKlinik}.`);
}
}
// TRIGGER STRATEGIC REFRESHES
let refreshedSomething = false;
if (targetLoketId) {
deps.fetchPatientsForLoket(targetLoketId, true);
refreshedSomething = true;
}
if (targetKlinikId) {
const interestingClinics = Object.keys(deps.activeClinicInterest.value);
if (interestingClinics.includes(String(targetKlinikId)) || targetKlinikId === 'broadcast') {
const clinicToFetch = targetKlinikId === 'broadcast' ? interestingClinics[0] : targetKlinikId;
deps.fetchPatientsForClinic(clinicToFetch, true);
refreshedSomething = true;
}
}
if (deps.globalInterestCount.value > 0) {
deps.fetchAllPatients();
refreshedSomething = true;
}
// ALWAYS refresh our own active interests when a WebSocket message is received
const interestingLokets = Object.keys(deps.activeLoketInterest.value);
const interestingClinics = Object.keys(deps.activeClinicInterest.value);
if (interestingLokets.length > 0) {
interestingLokets.forEach(loketId => {
if (String(loketId) !== String(targetLoketId)) {
deps.fetchPatientsForLoket(loketId, true);
refreshedSomething = true;
}
});
}
if (interestingClinics.length > 0) {
interestingClinics.forEach(kodeKlinik => {
if (String(kodeKlinik) !== String(targetKlinikId)) {
deps.fetchPatientsForClinic(kodeKlinik, true);
refreshedSomething = true;
}
});
}
if (!refreshedSomething) {
// console.log(`🔕 [queueSync] WS trigger received but no active interest matched. Skipping.`);
}
};
const config = useRuntimeConfig();
const wsBaseUrl = config.public?.wsBaseUrl || "ws://10.10.123.135:8084/api/v1/ws";
const { connect, disconnect, sendViaPost, isConnected } = useWebSocket({
url: wsBaseUrl,
clientId: wsClientId,
fallbackPostUrl: `${config.public.externalApiBaseUrl}/ws`,
reconnectInterval: 2000,
maxReconnectAttempts: 9999,
onOpen: () => {
// console.log('✅ [queueSync] WebSocket connected');
isWsConnected.value = true;
},
onClose: () => {
// console.log('❌ [queueSync] WebSocket disconnected');
isWsConnected.value = false;
},
onError: (err: any) => {
console.error('⚠️ [queueSync] WebSocket error:', err);
isWsConnected.value = false;
},
onMessage: onWsMessage
});
let _autoSyncInterval: any = null;
const startAutoSync = () => {
if (typeof window === 'undefined') return;
if (_autoSyncInterval) return;
// console.log('🔄 [queueSync] Starting store-level auto-sync (30s interval)');
_autoSyncInterval = setInterval(async () => {
const hasLoketInterest = Object.keys(deps.activeLoketInterest.value).length > 0;
const hasClinicInterest = Object.keys(deps.activeClinicInterest.value).length > 0;
const hasGlobalInterest = deps.globalInterestCount.value > 0;
if (hasGlobalInterest) {
deps.fetchAllPatients();
} else {
if (hasLoketInterest) {
Object.keys(deps.activeLoketInterest.value).forEach(loketId => {
deps.fetchPatientsForLoket(loketId, true);
});
}
if (hasClinicInterest) {
Object.keys(deps.activeClinicInterest.value).forEach(kodeKlinik => {
deps.fetchPatientsForClinic(kodeKlinik, true);
});
}
}
}, 30000); // 30 seconds
};
const stopAutoSync = () => {
if (_autoSyncInterval) {
clearInterval(_autoSyncInterval);
_autoSyncInterval = null;
// console.log('⏹️ [queueSync] Store-level auto-sync stopped');
}
};
const initWebSocket = (customClientId: string | null = null) => {
if (isConnected.value && customClientId === wsClientId.value) {
// console.log('🔌 [queueSync] WebSocket already connected with same ID.');
startAutoSync();
return;
}
if (customClientId) {
wsClientId.value = customClientId;
disconnect();
}
// console.log(`🔌 [queueSync] Connecting to WebSocket: ${wsBaseUrl} as ${wsClientId.value}`);
connect();
startAutoSync();
};
return {
isWsConnected,
wsClientId,
lastGlobalCall,
lastKlinikCall,
initWebSocket,
disconnectWebSocket: disconnect,
sendViaPost,
startAutoSync,
stopAutoSync
};
};
+5 -5
View File
@@ -64,7 +64,7 @@ export const useWebSocket = (config: WebSocketConfig) => {
// Close existing connection if any, and CLEAN HANDLERS to prevent recursion
if (ws.value) {
if (ws.value.readyState !== WebSocket.CLOSED) {
console.log('🔌 Closing existing WebSocket before new connection...')
// console.log('🔌 Closing existing WebSocket before new connection...')
clearHandlers(ws.value)
ws.value.close()
}
@@ -75,7 +75,7 @@ export const useWebSocket = (config: WebSocketConfig) => {
ws.value = new WebSocket(connectionUrl)
ws.value.onopen = () => {
console.log('✅ WebSocket connected:', currentClientId.value)
// console.log('✅ WebSocket connected:', currentClientId.value)
isConnected.value = true
reconnectAttempts.value = 0
config.onOpen?.()
@@ -91,7 +91,7 @@ export const useWebSocket = (config: WebSocketConfig) => {
}
ws.value.onclose = () => {
console.log('❌ WebSocket closed:', currentClientId.value)
// console.log('❌ WebSocket closed:', currentClientId.value)
isConnected.value = false
config.onClose?.()
@@ -100,7 +100,7 @@ export const useWebSocket = (config: WebSocketConfig) => {
if (reconnectAttempts.value < (config.maxReconnectAttempts || 5)) {
reconnectAttempts.value++
const interval = config.reconnectInterval || 3000
console.log(`⏳ Reconnecting in ${interval}ms... Attempt ${reconnectAttempts.value}`)
// console.log(`⏳ Reconnecting in ${interval}ms... Attempt ${reconnectAttempts.value}`)
reconnectTimer.value = setTimeout(() => {
connect()
}, interval)
@@ -125,7 +125,7 @@ export const useWebSocket = (config: WebSocketConfig) => {
reconnectTimer.value = null
}
if (ws.value) {
console.log('🔌 Manual disconnect: Cleaning handlers and closing...')
// console.log('🔌 Manual disconnect: Cleaning handlers and closing...')
clearHandlers(ws.value)
ws.value.close()
ws.value = null
+50
View File
@@ -63,6 +63,56 @@ Format output:
<!-- Tambahkan entry baru di bawah baris ini, urutan terbaru di atas -->
## 2026-06-23 — Refactoring queueStore.js ke TypeScript (Phase 2: WebSocket & Typings)
**Sprint/Phase:** Phase 6 (Verifikasi Akun & Kiosk) / Refactoring Technical Debt
**Durasi:** 1 jam
**Status:** ✅ Done
### Yang Dikerjakan
- Membuat composable mandiri `composables/useQueueSync.ts` dan memindahkan lebih dari 200 baris logika *WebSocket* (`onWsMessage`, `connect`, `startAutoSync`) dari dalam *store*.
- Menjalankan *scripting* untuk me-resolve sebagian besar error peringatan `implicit any type` dan `catch(error: any)` di dalam `queueStore.ts`.
- Menerapkan injeksi dependencies dari `queueStore` ke dalam parameter `useQueueSync()` tanpa memutus *reactivity* dari Pinia state.
### Keputusan Teknis
> **Dependency Injection pada Composable:** Karena fungsionalitas WebSocket membutuhkan akses terhadap state (`allPatients`, `currentProcessingPatient`) dan actions (`fetchPatientsForLoket`, dll), maka variabel-variabel tersebut dilempar (injected) melalui parameter `deps` ke dalam fungsi `useQueueSync(deps)`. Hal ini mencegah masalah *circular dependencies* di ekosistem Nuxt/Pinia.
### Masalah & Solusi
| Masalah | Solusi | Referensi |
|---------|--------|-----------|
| Pinia-Plugin-PersistedState *type mismatch* pada `paths` config | Menambahkan flag `// @ts-ignore` untuk properti `paths` karena kompatibel secara *runtime* namun melanggar validasi tipe di rilis plugin yang terpasang. | QMD: Technical Debt |
### Besok
- Menguji secara manual interaksi antar *terminal* (Anjungan, Loket, Klinik) untuk memverifikasi fungsionalitas `useQueueSync`.
- Menulis Unit Tests (jika platform sudah siap).
---
## 2026-06-23 — Refactoring queueStore.js ke TypeScript (Phase 1)
**Sprint/Phase:** Phase 6 (Verifikasi Akun & Kiosk) / Refactoring Technical Debt
**Durasi:** 1 jam
**Status:** 🔄 In Progress
### Yang Dikerjakan
- Mengubah ekstensi file `queueStore.js` menjadi `queueStore.ts`.
- Membuat file definisi tipe antrean di `types/queue.ts` (`QueuePatient`).
- Mengekstrak raw fetch API calls (Loket, Visit, Update, Selesai Tiket) ke composable mandiri `composables/useQueueAPI.ts`.
- Melakukan transisi pada fungsi-fungsi besar (`fetchPatientsForLoket`, `callNext`, `callMultiplePatients`, `processPatient`) di dalam `queueStore.ts` untuk menggunakan `useQueueAPI` guna memangkas boilerplate fetch.
### Keputusan Teknis
> **Incremental Refactoring:** Menghindari pecahnya `queueStore` secara drastis dengan tetap mempertahankan satu *store instance* (`useQueueStore`) namun mengabstraksi beban logikanya keluar (*API* & *Sync layer*). Ini meminimalisasi *blast radius* / *breaking changes* ke komponen UI yang sudah jalan.
### Masalah & Solusi
| Masalah | Solusi | Referensi |
|---------|--------|-----------|
| Pembengkakan ukuran *store file* (>3500 baris, 145KB). | Mulai memisahkan logika pemanggilan API eksternal dan mendefinisikan strukturnya dengan TypeScript. | QMD: Technical Debt |
### Besok
- Membuat tipe yang strict untuk `state` dan seluruh *method signatures* di `queueStore.ts`.
- Mengekstraksi fungsionalitas WebSocket ke `composables/useQueueSync.ts`.
- Jika sudah ringan, memecah *store* menjadi `loketQueueStore` dan `clinicQueueStore`.
---
## 2026-06-19 — Fitur Detail Akun Verifikasi
+54
View File
@@ -0,0 +1,54 @@
import re
import sys
path = r'e:\antrean operasi\web-antrean\stores\queueStore.ts'
try:
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
# 1. Add import for useQueueSync at the top
if 'useQueueSync' not in content:
content = content.replace("import { useQueueAPI } from '@/composables/useQueueAPI';", "import { useQueueAPI } from '@/composables/useQueueAPI';\nimport { useQueueSync } from '@/composables/useQueueSync';")
# 2. Remove the WEBSOCKET INTEGRATION block
ws_pattern = re.compile(r' // WEBSOCKET INTEGRATION \(CENTRALIZED\).*? const disconnectWebSocket = \(\) => \{\n disconnect\(\);\n \};', re.DOTALL)
content = ws_pattern.sub(' // WS and AutoSync logic extracted to composables/useQueueSync.ts', content)
# 3. Add the initialization right before return
init_code = """
// Initialize Queue Sync
const queueSync = useQueueSync({
allPatients,
currentProcessingPatient,
activeLoketInterest,
activeClinicInterest,
globalInterestCount,
fetchPatientsForLoket,
fetchPatientsForClinic,
fetchAllPatients
});
const {
isWsConnected,
wsClientId,
lastGlobalCall,
lastKlinikCall,
initWebSocket,
disconnectWebSocket,
sendViaPost,
startAutoSync,
stopAutoSync
} = queueSync;
return {
"""
content = content.replace(" return {\n // State\n allPatients,", init_code + " // State\n allPatients,")
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
print("Success")
except Exception as e:
print("Error:", e)
sys.exit(1)
+14 -1
View File
@@ -94,6 +94,14 @@ export const useDoctorStore = defineStore('doctor', () => {
break; // Success!
} catch (err) {
lastError = err;
const status = err.response ? err.response.status : (err.status || 'unknown');
// Do not retry for 500 (Internal Server Error) or 404 (Not Found)
// as these are likely permanent failures for this specific clinic
if (status === 500 || status === 404) {
break;
}
if (i < retries) {
const delay = 500 * (i + 1);
console.warn(`⚠️ [doctorStore] Retry ${i+1}/${retries} for klinik ${idklinik} after ${delay}ms...`);
@@ -130,7 +138,12 @@ export const useDoctorStore = defineStore('doctor', () => {
return doctorNames;
} catch (error) {
const status = error.response ? error.response.status : (error.status || 'unknown');
console.error(`❌ [doctorStore] Gagal mengambil dokter untuk klinik ID ${idklinik} (${clinic.name}) - Status: ${status}`);
if (status === 500 || status === 404) {
console.warn(`⚠️ [doctorStore] Data dokter kosong/tidak ditemukan untuk klinik ID ${idklinik} (${clinic.name}) - Status: ${status}`);
} else {
console.error(`❌ [doctorStore] Gagal mengambil dokter untuk klinik ID ${idklinik} (${clinic.name}) - Status: ${status}`);
}
// Track failure for blacklisting
if (!failedClinics.value[idklinik]) {
+104 -363
View File
@@ -2,43 +2,48 @@
import { defineStore } from 'pinia';
import { ref, computed, watch } from 'vue';
import { useClinicStore } from './clinicStore';
import { usePenunjangStore } from './penunjangStore';
import { useLoketStore } from './loketStore';
import { usePenunjangStore } from './penunjangStore';
import { useWebSocket } from '@/composables/useWebSocket';
import { useQueueAPI } from '@/composables/useQueueAPI';
import { useQueueSync } from '@/composables/useQueueSync';
import type { QueuePatient } from '@/types/queue';
export const useQueueStore = defineStore('queue', () => {
const config = useRuntimeConfig();
const clinicStore = useClinicStore();
const penunjangStore = usePenunjangStore();
const loketStore = useLoketStore();
const queueAPI = useQueueAPI();
// ============================================
// API INTEGRATION FOR LOKET PATIENTS
// ============================================
// State untuk API patient data per loket
const allPatients = ref([]);
const apiPatientsPerLoket = ref({});
const isLoadingPatients = ref(false);
const apiPatientsError = ref(null);
const quotaUsed = ref(5);
const currentProcessingPatient = ref({});
const allPatients = ref<QueuePatient[]>([]);
const apiPatientsPerLoket = ref<Record<string, QueuePatient[]>>({});
const isLoadingPatients = ref<boolean>(false);
const apiPatientsError = ref<string | null>(null);
const quotaUsed = ref<number>(5);
const currentProcessingPatient = ref<Record<string, QueuePatient>>({});
const lastUpdated = ref(Date.now());
const lastFetchTime = ref({});
const lastGlobalFetchTime = ref(0); // Cooldown for bulk refreshes
const lastUpdated = ref<number>(Date.now());
const lastFetchTime = ref<Record<string, number>>({});
const lastGlobalFetchTime = ref<number>(0); // Cooldown for bulk refreshes
// Scoped Refresh Logic: track which lokets are currently being viewed
const activeLoketInterest = ref({}); // { [loketId]: count }
const activeClinicInterest = ref({}); // { [kodeKlinik]: count }
const globalInterestCount = ref(0); // Tracks pages that need ALL loket data (e.g. CheckInPasien)
const activeLoketInterest = ref<Record<string, number>>({}); // { [loketId]: count }
const activeClinicInterest = ref<Record<string, number>>({}); // { [kodeKlinik]: count }
const globalInterestCount = ref<number>(0); // Tracks pages that need ALL loket data (e.g. CheckInPasien)
const registerInterest = (loketId) => {
const registerInterest = (loketId: string | number) => {
if (!loketId) return;
const id = String(loketId);
activeLoketInterest.value[id] = (activeLoketInterest.value[id] || 0) + 1;
};
const unregisterInterest = (loketId) => {
const unregisterInterest = (loketId: string | number) => {
if (!loketId) return;
const id = String(loketId);
if (activeLoketInterest.value[id]) {
@@ -49,13 +54,13 @@ export const useQueueStore = defineStore('queue', () => {
}
};
const registerClinicInterest = (kodeKlinik) => {
const registerClinicInterest = (kodeKlinik: string) => {
if (!kodeKlinik) return;
const code = String(kodeKlinik);
activeClinicInterest.value[code] = (activeClinicInterest.value[code] || 0) + 1;
};
const unregisterClinicInterest = (kodeKlinik) => {
const unregisterClinicInterest = (kodeKlinik: string) => {
if (!kodeKlinik) return;
const code = String(kodeKlinik);
if (activeClinicInterest.value[code]) {
@@ -74,7 +79,7 @@ export const useQueueStore = defineStore('queue', () => {
globalInterestCount.value = Math.max(0, globalInterestCount.value - 1);
};
const fetchPatientsForClinic = async (kodeKlinik, force = false) => {
const fetchPatientsForClinic = async (kodeKlinik: string, force: boolean = false) => {
if (!kodeKlinik) return { success: false, message: 'Kode Klinik diperlukan' };
isLoadingPatients.value = true;
@@ -86,7 +91,7 @@ export const useQueueStore = defineStore('queue', () => {
const timeSinceLastFetch = now - lastFetch;
if (!force && timeSinceLastFetch < 2000) {
console.log(`⏭️ [queueStore] Skipping fetch for clinic ${kodeKlinik} (last fetched ${Math.round(timeSinceLastFetch/1000)}s ago)`);
// console.log(`⏭️ [queueStore] Skipping fetch for clinic ${kodeKlinik} (last fetched ${Math.round(timeSinceLastFetch/1000)}s ago)`);
isLoadingPatients.value = false;
return { success: true, message: 'Using cache' };
}
@@ -100,14 +105,9 @@ export const useQueueStore = defineStore('queue', () => {
throw new Error(`Klinik ID tidak ditemukan untuk kode: ${kodeKlinik}`);
}
const url = `${config.public.externalApiBaseUrl}/visit?klinik_id=${clinic.id}&limit=500`;
console.log(`🔄 [queueStore] Fetching patients for clinic ${kodeKlinik} (ID: ${clinic.id})...`);
// console.log(`🔄 [queueStore] Fetching patients for clinic ${kodeKlinik} (ID: ${clinic.id})...`);
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const rawResponse = await response.json();
const data = rawResponse?.data || [];
const data = await queueAPI.fetchRawClinicPatients(clinic.id);
const mappedClinicPatients = [];
data.forEach((visit, index) => {
@@ -270,7 +270,7 @@ export const useQueueStore = defineStore('queue', () => {
syncCountersWithState();
return { success: true, message: `${mappedClinicPatients.length} pasien dimuat` };
} catch (error) {
} catch (error: any) {
console.error(`❌ [queueStore] Error fetching clinic patients (${kodeKlinik}):`, error);
apiPatientsError.value = error.message;
return { success: false, message: error.message };
@@ -280,251 +280,6 @@ export const useQueueStore = defineStore('queue', () => {
};
// ============================================
// WEBSOCKET INTEGRATION (CENTRALIZED)
// ============================================
// ============================================
// WEBSOCKET INTEGRATION (CENTRALIZED)
// ============================================
const isWsConnected = ref(false);
const wsClientId = ref(`client-${Math.random().toString(36).substring(7)}`);
const lastGlobalCall = ref(null);
const lastKlinikCall = ref(null);
const onWsMessage = (data) => {
// Robust data extraction: some relays wrap data in another 'data' property
let messageData = data?.data || data;
if (messageData?.data && !messageData.callKlinikEvent && !messageData.callEvent) {
messageData = messageData.data; // Double wrap check
}
const targetLoketId = messageData?.loketId || messageData?.idloket;
const targetKlinikId = messageData?.klinikId || messageData?.idklinik;
// Handle Call Events and WS messages
if (messageData?.triggerRefresh) {
if (messageData.klinikId) {
console.log(`🔄 [queueStore] Received refresh trigger for clinic ${messageData.klinikId}`);
// Handle current processing update if provided
if (messageData.currentProcessingUpdate) {
console.log(`🎯 [queueStore] Applying current processing update:`, messageData.currentProcessingUpdate);
// Merge specifically for the keys that exist in the update
Object.keys(messageData.currentProcessingUpdate).forEach(key => {
currentProcessingPatient.value[key] = messageData.currentProcessingUpdate[key];
// Also patch the status in allPatients if possible
const processingPatient = messageData.currentProcessingUpdate[key];
if (processingPatient && processingPatient.no) {
const idx = allPatients.value.findIndex(p => p.no === processingPatient.no);
if (idx !== -1) {
allPatients.value[idx] = { ...allPatients.value[idx], status: 'di-loket' };
}
}
});
}
fetchPatientsForClinic(messageData.klinikId, true);
}
}
if (messageData?.callEvent) {
lastGlobalCall.value = messageData.callEvent;
}
// Handle Klinik Call Events (cross-device sync for AntrianKlinikRuang display)
if (messageData?.callKlinikEvent) {
const ev = messageData.callKlinikEvent;
console.log('🏥 [queueStore] Klinik call event received:', ev);
// PERSISTENCE FIX: Save to lastKlinikCall for displays to watch
lastKlinikCall.value = ev;
// Find the patient in allPatients and patch directly for immediate UI update
// Match by barcode (string) or target antrian number (part before |)
const idx = allPatients.value.findIndex(p =>
p.processStage === 'klinik-ruang' &&
p.kodeKlinik === ev.kodeKlinik &&
(
(p.barcode && String(p.barcode) === String(ev.barcode)) ||
(p.noAntrian && p.noAntrian.split(' |')[0] === ev.noantrian)
)
);
if (idx !== -1) {
// Create a patched object to ensure reactivity
const updatedPatient = {
...allPatients.value[idx],
tipeLayanan: ev.tipeLayanan,
lastCalledAt: ev.lastCalledAt || new Date().toISOString(),
lastCalledTipeLayanan: ev.tipeLayanan,
status: 'di-loket',
calledPemeriksaanAwal: ev.tipeLayanan === 'Pemeriksaan Awal' ? true : allPatients.value[idx].calledPemeriksaanAwal,
calledTindakan: ev.tipeLayanan === 'Tindakan' ? true : allPatients.value[idx].calledTindakan
};
allPatients.value[idx] = updatedPatient;
console.log(`✅ [queueStore] Successfully patched patient ${ev.noantrian} status to di-loket (lastCalledAt: ${updatedPatient.lastCalledAt})`);
} else {
console.warn(`⚠️ [queueStore] Patient ${ev.noantrian} not found in store for clinic ${ev.kodeKlinik}.`);
console.log('🧪 [queueStore] Available klinik-ruang patients in store:',
allPatients.value
.filter(p => p.processStage === 'klinik-ruang')
.map(p => `[${p.kodeKlinik}] ${p.noAntrian?.split(' |')[0]} / ${p.barcode}`)
);
}
}
// TRIGGER STRATEGIC REFRESHES
let refreshedSomething = false;
if (targetLoketId) {
fetchPatientsForLoket(targetLoketId, true);
refreshedSomething = true;
}
if (targetKlinikId) {
const interestingClinics = Object.keys(activeClinicInterest.value);
if (interestingClinics.includes(String(targetKlinikId)) || targetKlinikId === 'broadcast') {
const clinicToFetch = targetKlinikId === 'broadcast' ? interestingClinics[0] : targetKlinikId;
fetchPatientsForClinic(clinicToFetch, true);
refreshedSomething = true;
}
}
if (globalInterestCount.value > 0) {
fetchAllPatients();
refreshedSomething = true;
}
// ALWAYS refresh our own active interests when a WebSocket message is received,
// because shared lists (e.g. unassigned patients in 'menunggu') might have changed.
const interestingLokets = Object.keys(activeLoketInterest.value);
const interestingClinics = Object.keys(activeClinicInterest.value);
if (interestingLokets.length > 0) {
interestingLokets.forEach(loketId => {
if (String(loketId) !== String(targetLoketId)) {
fetchPatientsForLoket(loketId, true);
refreshedSomething = true;
}
});
}
if (interestingClinics.length > 0) {
interestingClinics.forEach(kodeKlinik => {
if (String(kodeKlinik) !== String(targetKlinikId)) {
fetchPatientsForClinic(kodeKlinik, true);
refreshedSomething = true;
}
});
}
if (!refreshedSomething) {
console.log(`🔕 [queueStore] WS trigger received but no active interest matched. Skipping.`);
}
};
const config = useRuntimeConfig();
const wsBaseUrl = config.public?.wsBaseUrl || "ws://10.10.123.135:8084/api/v1/ws";
const { connect, disconnect, sendViaPost, isConnected } = useWebSocket({
url: wsBaseUrl,
clientId: wsClientId,
fallbackPostUrl: `${config.public.externalApiBaseUrl}/ws`,
reconnectInterval: 2000, // 2 seconds between reconnect attempts
maxReconnectAttempts: 9999, // Effectively infinite — never give up on remote machines
onOpen: () => {
console.log('✅ [queueStore] WebSocket connected');
isWsConnected.value = true;
},
onClose: () => {
console.log('❌ [queueStore] WebSocket disconnected');
isWsConnected.value = false;
},
onError: (err) => {
console.error('⚠️ [queueStore] WebSocket error:', err);
isWsConnected.value = false;
},
onMessage: onWsMessage
});
// ============================================
// STORE-LEVEL AUTO-POLLING (cross-device sync fallback)
// ============================================
// Runs on every browser instance every 30 seconds.
// Fetches data based on whatever active interests are registered
// (lokets, clinics, or global). This ensures any device always
// has fresh data regardless of WS delivery reliability.
let _autoSyncInterval = null;
const startAutoSync = () => {
// Guard: only run on client, and only start once
if (typeof window === 'undefined') return;
if (_autoSyncInterval) return; // Already running
console.log('🔄 [queueStore] Starting store-level auto-sync (30s interval)');
_autoSyncInterval = setInterval(async () => {
const hasLoketInterest = Object.keys(activeLoketInterest.value).length > 0;
const hasClinicInterest = Object.keys(activeClinicInterest.value).length > 0;
const hasGlobalInterest = globalInterestCount.value > 0;
if (hasGlobalInterest) {
fetchAllPatients();
} else {
if (hasLoketInterest) {
Object.keys(activeLoketInterest.value).forEach(loketId => {
fetchPatientsForLoket(loketId, true);
});
}
if (hasClinicInterest) {
Object.keys(activeClinicInterest.value).forEach(kodeKlinik => {
fetchPatientsForClinic(kodeKlinik, true);
});
}
}
}, 30000); // 30 seconds
};
const stopAutoSync = () => {
if (_autoSyncInterval) {
clearInterval(_autoSyncInterval);
_autoSyncInterval = null;
console.log('⏹️ [queueStore] Store-level auto-sync stopped');
}
};
/**
* Initialize Global WebSocket
*/
const initWebSocket = (customClientId = null) => {
if (isConnected.value && customClientId === wsClientId.value) {
console.log('🔌 [queueStore] WebSocket already connected with same ID.');
// Auto-sync should still start even if WS is already connected
startAutoSync();
return;
}
if (customClientId) {
wsClientId.value = customClientId;
// Re-connect with new ID if changed
disconnect();
}
console.log(`🔌 [queueStore] Connecting to WebSocket: ${wsBaseUrl} as ${wsClientId.value}`);
connect();
// Start store-level auto-polling if not already running (client-side only).
// This guarantees cross-device sync even when WS messages are missed.
startAutoSync();
};
/**
* Disconnect Global WebSocket
*/
const disconnectWebSocket = () => {
disconnect();
isWsConnected.value = false;
};
/**
* Sync patient status to apiPatientsPerLoket for reactivity
@@ -602,12 +357,7 @@ export const useQueueStore = defineStore('queue', () => {
18: 'pemeriksaan', // PO PEMERIKSAAN
19: 'pemeriksaan', // PS PEMERIKSAAN
32: 'pending', // PE PEMERIKSAAN
33: 'terlambat', // TR PEMERIKSAAN
// String versions for robustness
"1": 'menunggu', "2": 'menunggu', "3": 'anjungan', "4": 'anjungan', "5": 'di-loket',
"6": 'di-loket', "14": 'pemeriksaan', "15": 'pemeriksaan', "28": 'pending', "29": 'terlambat',
"30": 'pending', "31": 'terlambat', "32": 'pending', "33": 'terlambat'
33: 'terlambat' // TR PEMERIKSAAN
};
/**
@@ -742,7 +492,7 @@ export const useQueueStore = defineStore('queue', () => {
/**
* Fetch patient data untuk loket tertentu dari API
*/
const fetchPatientsForLoket = async (loketId, force = false) => {
const fetchPatientsForLoket = async (loketId: string | number, force: boolean = false) => {
if (!loketId) {
console.error('loketId required for fetchPatientsForLoket');
return { success: false, message: 'ID Loket diperlukan' };
@@ -757,7 +507,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
const timeSinceLastFetch = now - lastFetch;
if (!force && timeSinceLastFetch < 2000 && apiPatientsPerLoket.value[loketId]) {
console.log(`⏭️ [queueStore] Skipping fetch for loket ${loketId} (last fetched ${Math.round(timeSinceLastFetch/1000)}s ago)`);
// console.log(`⏭️ [queueStore] Skipping fetch for loket ${loketId} (last fetched ${Math.round(timeSinceLastFetch/1000)}s ago)`);
isLoadingPatients.value = false;
return {
success: true,
@@ -773,22 +523,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
checkAndResetDaily();
try {
console.log(`🔄 [queueStore] Fetching patients for loket ${loketId}...`);
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
const response = await fetch(`${apiBase}/loket/${loketId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const rawData = await response.json();
// Check response structure
if (rawData.metadata && rawData.metadata.code !== 200) {
throw new Error(rawData.message || 'API returned error status');
}
const patientsRaw = rawData.data || [];
// console.log(`🔄 [queueStore] Fetching patients for loket ${loketId}...`);
const patientsRaw = await queueAPI.fetchRawLoketPatients(loketId);
// Fetch temporary subspesialis mapping
let subspesialisMap = {};
@@ -797,7 +533,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
if (subRes.ok) {
subspesialisMap = await subRes.json();
}
} catch (e) {
} catch (e: any) {
console.error('Failed to fetch temporary subspesialis mapping', e);
}
@@ -999,7 +735,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
// 6. RESTORE terlambat/pending status from LocalStorage (hybrid fallback)
// This ensures status persists even if API doesn't save it
allPatients.value.forEach((patient, index) => {
allPatients.value.forEach((patient: any, index: number) => {
if (patient.barcode) {
const storageKey = `patient-status-${patient.barcode}`;
const savedData = localStorage.getItem(storageKey);
@@ -1021,7 +757,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
// Clean up old data
localStorage.removeItem(storageKey);
}
} catch (e) {
} catch (e: any) {
console.error('Error parsing LocalStorage data:', e);
localStorage.removeItem(storageKey);
}
@@ -1041,7 +777,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
data: mappedPatients
};
} catch (error) {
} catch (error: any) {
console.error(`❌ [queueStore] Error fetching patients for loket ${loketId}:`, error);
apiPatientsError.value = error.message;
@@ -1066,7 +802,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
* Global fetcher for all patients across all available lokets
* Uses staggered fetching to prevent 429 Too Many Requests errors.
*/
const fetchAllPatients = async (force = false) => {
const fetchAllPatients = async (force: boolean = false) => {
// 1. Cooldown Check: Prevent global refresh spam (max once every 5 seconds)
const now = Date.now();
if (!force && now - lastGlobalFetchTime.value < 5000) {
@@ -1075,7 +811,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
}
lastGlobalFetchTime.value = now;
console.log('🔄 [queueStore] Fetching all patients for all lokets (Staggered)...');
// console.log('🔄 [queueStore] Fetching all patients for all lokets (Staggered)...');
const allLokets = loketStore.lokets || [];
if (allLokets.length === 0) {
console.warn('⚠️ [queueStore] No lokets available for fetchAllPatients');
@@ -1129,7 +865,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
/**
* Get patients for a specific loket (from API or seed data based on loket type)
*/
const getPatientsForLoket = (loketId) => {
const getPatientsForLoket = (loketId: string | number) => {
return computed(() => {
const loket = loketStore.getLoketById(parseInt(loketId));
@@ -1527,7 +1263,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
/**
* Filter strictly to only show today's patients (after 2 AM)
*/
const isTodayPatient = (patient) => {
const isTodayPatient = (patient: any) => {
if (!patient) return false;
// Status processing overrides filter (always show if currently processing)
@@ -1617,7 +1353,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
setTimeout(() => { isSyncing = false; }, 50);
}
}
} catch (e) {
} catch (e: any) {
console.error('Error hydrating from storage event:', e);
isSyncing = false;
}
@@ -1670,7 +1406,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
const resetPatients = () => {
allPatients.value = cloneSeed();
quotaUsed.value = 5;
currentProcessingPatient.value = { loket: null, klinik: null, penunjang: null };
currentProcessingPatient.value = {};
syncCountersWithState(); // Re-initialize counters after reset
};
@@ -1815,20 +1551,13 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
// POST to external API when patient is called
try {
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
fetch(`${apiBase}/tiket/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
barcode: nextPatient.barcode || "",
statuspasien: "3",
statuspasien2: "4",
idklinikstatus: "1",
idklinikstatus2: "1"
})
}).then(response => {
queueAPI.updateTicketStatus(
nextPatient.barcode || "",
"3",
"4",
"1",
"1"
).then(response => {
if (response.ok) {
console.log(`✅ Successfully posted status update for patient ${nextPatient.barcode}`);
} else {
@@ -1837,7 +1566,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
}).catch(error => {
console.error(`❌ Error posting status update for patient ${nextPatient.barcode}:`, error);
});
} catch (error) {
} catch (error: any) {
console.error(`❌ Error initiating status update for patient ${nextPatient.barcode}:`, error);
}
}
@@ -1909,7 +1638,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
const patientsToCall = menungguList.slice(0, maxCallable);
const callTimestamp = new Date().toISOString();
patientsToCall.forEach(async (patient) => {
patientsToCall.forEach(async (patient: any) => {
const index = allPatients.value.findIndex(p => p.no === patient.no);
if (index !== -1) {
const newStatus = "anjungan";
@@ -1927,27 +1656,20 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
// POST to external API when patient is called
try {
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
const response = await fetch(`${apiBase}/tiket/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
barcode: patient.barcode || "",
statuspasien: "3",
statuspasien2: "4",
idklinikstatus: "1",
idklinikstatus2: "1"
})
});
const response = await queueAPI.updateTicketStatus(
patient.barcode || "",
"3",
"4",
"1",
"1"
);
if (response.ok) {
console.log(`✅ Successfully posted status update for patient ${patient.barcode}`);
} else {
console.error(`⚠️ Failed to post status update for patient ${patient.barcode}:`, response.status);
}
} catch (error) {
} catch (error: any) {
console.error(`❌ Error posting status update for patient ${patient.barcode}:`, error);
}
}
@@ -1986,19 +1708,12 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
// POST to external API when patient finishes at loket
try {
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
fetch(`${apiBase}/tiket/selesai`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
idloket: String(patient.loketId || specificId || ""),
barcode: patient.barcode || "",
statuspasien: "9",
idklinikstatus: "2"
})
}).then(response => {
queueAPI.completeTicketStatus(
String(patient.loketId || specificId || ""),
patient.barcode || "",
"9",
"2"
).then(response => {
if (response.ok) {
console.log(`✅ [queueStore] Successfully posted selesai status for patient ${patient.barcode}`);
} else {
@@ -2007,7 +1722,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
}).catch(error => {
console.error(`❌ [queueStore] Error posting selesai status for patient ${patient.barcode}:`, error);
});
} catch (error) {
} catch (error: any) {
console.error(`❌ [queueStore] Error initiating selesai status update for patient ${patient.barcode}:`, error);
}
}
@@ -2088,7 +1803,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
console.error(`❌ [TERLAMBAT] API rejected request:`, responseData);
message = `Gagal: ${responseData.message || 'API error'}`;
}
} catch (error) {
} catch (error: any) {
console.error(`❌ [TERLAMBAT] Error:`, error);
message = `Error: ${error.message}`;
}
@@ -2145,7 +1860,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
console.error(`❌ [PENDING] API rejected request:`, responseData);
message = `Gagal: ${responseData.message || 'API error'}`;
}
} catch (error) {
} catch (error: any) {
console.error(`❌ [PENDING] Error:`, error);
message = `Error: ${error.message}`;
}
@@ -2188,7 +1903,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
}).catch(error => {
console.error(`❌ [queueStore] Error activating patient ${patient.barcode}:`, error);
});
} catch (error) {
} catch (error: any) {
console.error(`❌ [queueStore] Error initiating activation for patient ${patient.barcode}:`, error);
}
} else {
@@ -2253,7 +1968,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
}).catch(error => {
console.error(`❌ [queueStore] Error updating patient ${patient.barcode} to sedang diproses:`, error);
});
} catch (error) {
} catch (error: any) {
console.error(`❌ [queueStore] Error initiating status update for patient ${patient.barcode}:`, error);
}
} else {
@@ -2483,7 +2198,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
// 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" };
}
@@ -2820,7 +2535,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
} else {
console.log('✅ [queueStore] Successfully finished patient via API');
}
} catch (error) {
} catch (error: any) {
console.error('❌ [queueStore] Error calling finish API:', error);
// Continue with local status update even if API fails
}
@@ -2846,7 +2561,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
} catch (error) {
} catch (error: any) {
console.error('❌ [queueStore] Error calling terlambat API:', error);
}
@@ -2870,7 +2585,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
} catch (error) {
} catch (error: any) {
console.error('❌ [queueStore] Error calling pending API:', error);
}
@@ -3238,7 +2953,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
subspesialis: subSpesialis
})
});
} catch (e) {
} catch (e: any) {
console.error('Failed to save temporary subSpesialis mapping', e);
}
}
@@ -3250,7 +2965,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
patient: newPatient
};
} catch (error) {
} catch (error: any) {
console.error('❌ [queueStore] Error generating ticket via API:', error);
return {
success: false,
@@ -3292,7 +3007,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
data: result
};
} catch (error) {
} catch (error: any) {
console.error('❌ [queueStore] Error syncing check-in via API:', error);
return {
success: false,
@@ -3422,6 +3137,31 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
return { success: false, message: "Gagal memproses antrean." };
};
// Initialize Queue Sync
const queueSync = useQueueSync({
allPatients,
currentProcessingPatient,
activeLoketInterest,
activeClinicInterest,
globalInterestCount,
fetchPatientsForLoket,
fetchPatientsForClinic,
fetchAllPatients
});
const {
isWsConnected,
wsClientId,
lastGlobalCall,
lastKlinikCall,
initWebSocket,
disconnectWebSocket,
sendViaPost,
startAutoSync,
stopAutoSync
} = queueSync;
return {
// State
allPatients,
@@ -3504,12 +3244,13 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
persist: {
key: 'queue-store-state',
storage: typeof window !== 'undefined' ? localStorage : undefined,
// @ts-ignore - plugin version mismatch
paths: ['quotaUsed', 'lastUpdated'],
serializer: {
deserialize: JSON.parse,
serialize: JSON.stringify,
},
restore: (value) => {
restore: (value: any) => {
// Ensure allPatients is always an array
if (value && value.allPatients && !Array.isArray(value.allPatients)) {
value.allPatients = [];
+48
View File
@@ -0,0 +1,48 @@
export interface QueuePatient {
no: number;
barcode: string;
noAntrian: string;
jamPanggil: string;
klinik: string;
kodeKlinik: string;
klinikId?: number;
healthcareServiceId?: number;
ruang?: string;
nomorRuang?: string;
kodeRuang?: string;
pembayaran?: string;
status: 'menunggu' | 'di-loket' | 'anjungan' | 'pemeriksaan' | 'pending' | 'terlambat' | 'selesai' | 'skip' | 'processed';
processStage: 'loket' | 'klinik-ruang';
createdAt: string;
visitType?: string;
noRM?: string;
fastTrack?: "YA" | "TIDAK";
registrationType?: 'api' | 'manual';
visitId?: number;
visitCode?: string;
referencePatient?: string;
loketId?: number;
calledByAdmin?: boolean;
lastCalledAt?: string;
lastCalledTipeLayanan?: string;
calledPemeriksaanAwal?: boolean;
calledTindakan?: boolean;
tipeLayanan?: string;
idtiket?: string;
ticket?: string;
posisi?: any[];
deskripsi?: string;
manuallyMoved?: boolean;
movedAt?: number;
idvisit?: number;
visitDate?: string;
namaDokter?: string | null;
penanggungJawab?: string | null;
alasanFastTrack?: string | null;
}
export interface ApiPatientResponse {
success: boolean;
message: string;
data?: QueuePatient[];
}