feat: implement queue display and management system for loket and clinic modules voice over

This commit is contained in:
Fanrouver
2026-06-09 08:51:15 +07:00
parent f81dd57a16
commit 8b9c4725de
4 changed files with 145 additions and 12 deletions
+11 -1
View File
@@ -357,6 +357,12 @@ const ruangStore = useRuangStore();
const { printTicketFromPatient } = useThermalPrint();
const config = useRuntimeConfig();
useHead({
script: [
{ src: "https://code.responsivevoice.org/responsivevoice.js?key=ZeMK8Joo" }
]
});
// Broadcast Channel
let broadcastChannel = null;
let resetCheckInterval = null;
@@ -1018,7 +1024,11 @@ const confirmUnfinishedAndProcess = async () => {
const executeProcess = async (item, action) => {
if (action === "next") {
processNextQueue();
await processNextQueue();
// Auto-call setelah memproses antrean selanjutnya
setTimeout(() => {
handleCallPatient();
}, 300);
} else {
await processPatient(item, action);
// If action is process, auto-call the patient after a short delay
@@ -160,6 +160,11 @@ useHead({
name: 'viewport',
content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'
}
],
script: [
{
src: 'https://code.responsivevoice.org/responsivevoice.js?key=ZeMK8Joo'
}
]
})
@@ -230,6 +235,43 @@ const currentDate = ref('')
let timeInterval = null
const broadcastedKlinikPatient = ref(null)
let lastPlayedTime = 0;
let lastPlayedNo = '';
const playCallVoice = (patient, tipeLayanan = null, customRoomName = null) => {
if (!patient || (!patient.noantrian && !patient.noAntrian)) return;
const now = Date.now();
const noAntrianFull = patient.noantrian || patient.noAntrian;
// Cegah double-play berbarengan dalam waktu 2 detik
if (noAntrianFull === lastPlayedNo && (now - lastPlayedTime < 2000)) {
return;
}
lastPlayedNo = noAntrianFull;
lastPlayedTime = now;
if (window.responsiveVoice) {
const noAntrianRaw = noAntrianFull.split(" |")[0];
const formattedNo = noAntrianRaw.split('').join(' ');
// For clinic, destination should include Ruang and Tipe Layanan
// Contoh: "Ruang 1, untuk Pemeriksaan Awal"
const ruang = customRoomName || patient.ruang || patient.namaRuang || (patient.nomorRuang ? `Ruang ${patient.nomorRuang}` : 'Klinik');
const layananText = tipeLayanan ? `untuk ${tipeLayanan}` : '';
const destination = `${ruang}, ${layananText}`;
const textToSpeak = `Nomor antrean, ${formattedNo}, silahkan menuju ke, ${destination}`;
if (window.responsiveVoice.isPlaying()) {
window.responsiveVoice.cancel();
}
window.responsiveVoice.speak(textToSpeak, "Indonesian Female", { pitch: 1, rate: 0.85, volume: 1 });
}
};
// Watch for Klinik Calls (Cross-Device Sync) from queueStore
watch(() => queueStore.lastKlinikCall, (newCall) => {
if (!newCall || !newCall.noantrian) return;
@@ -274,6 +316,9 @@ watch(() => queueStore.lastKlinikCall, (newCall) => {
ruang: existingPatient?.ruang || existingPatient?.namaRuang || `Ruang ${newCall.nomorRuang || '1'}`,
_source: 'websocket'
};
// Mainkan suara panggilan
playCallVoice(broadcastedKlinikPatient.value, newCall.tipeLayanan, broadcastedKlinikPatient.value.ruang);
} else {
console.log(`⏭️ [Anjungan] Skipping call for clinic ${callKode} (this screen is for ${myKode})`);
}
+79 -11
View File
@@ -209,6 +209,9 @@ useHead({
name: 'viewport',
content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'
}
],
script: [
{ src: 'https://code.responsivevoice.org/responsivevoice.js?key=ZeMK8Joo' }
]
})
@@ -229,6 +232,45 @@ let pollInterval = null
let broadcastChannel = null
const broadcastedPatient = ref(null)
let lastPlayedTime = 0;
let lastPlayedNo = '';
const playCallVoice = (patient, customLoketName = null) => {
console.log("🔊 playCallVoice called for:", patient?.noAntrian);
if (!patient || !patient.noAntrian) return;
const now = Date.now();
// Cegah double-play berbarengan (dari WS dan BroadcastChannel) dalam waktu 2 detik
if (patient.noAntrian === lastPlayedNo && (now - lastPlayedTime < 2000)) {
console.log("🔊 Skipped double play (debounce)");
return;
}
lastPlayedNo = patient.noAntrian;
lastPlayedTime = now;
console.log("🔊 responsiveVoice object exists?", !!window.responsiveVoice);
if (window.responsiveVoice) {
const noAntrianRaw = patient.noAntrian.split(" |")[0];
const formattedNo = noAntrianRaw.split('').join(' ');
const destination = customLoketName || patient.loketName || loketData.value?.namaLoket || 'Loket';
const textToSpeak = `Nomor antrean, ${formattedNo}, silahkan menuju ke, ${destination}`;
console.log("🔊 Speaking:", textToSpeak);
try {
if (window.responsiveVoice.isPlaying()) {
window.responsiveVoice.cancel();
}
window.responsiveVoice.speak(textToSpeak, "Indonesian Female", { pitch: 1, rate: 0.85, volume: 1 });
} catch (e) {
console.error("🔊 ResponsiveVoice Error:", e);
}
} else {
console.error("🔊 ResponsiveVoice is NOT loaded yet!");
}
};
// Get loket ID from route
const loketId = computed(() => {
const id = route.params.id
@@ -685,16 +727,26 @@ const isInTTSWindow = (queue) => {
// Nomor antrian menjadi "dipanggil" jika diproses pada AdminLoket DAN sudah dipanggil oleh admin
const currentCalledQueue = computed(() => {
const targetLoketId = String(loketId.value)
const CALL_DISPLAY_DURATION = 60000 // 60 seconds
const now = new Date()
// Force reactivity to time passing
const _ = currentTime.value
// Prioritas 0: Broadcasted patient (Real-time from BroadcastChannel)
// Check if the broadcast came from this loket
if (broadcastedPatient.value) {
const rawMsg = broadcastedPatient.value._rawMessage
const msgLoketId = rawMsg?.loketId ? String(rawMsg.loketId) : null
// Only show if it matches this anjungan's loket ID
if (msgLoketId === targetLoketId) {
return broadcastedPatient.value
const callTime = new Date(broadcastedPatient.value.lastCalledAt || now)
const timeDiff = now.getTime() - callTime.getTime()
if (timeDiff <= CALL_DISPLAY_DURATION) {
return broadcastedPatient.value
} else {
broadcastedPatient.value = null // Clear if expired
}
}
}
@@ -705,10 +757,15 @@ const currentCalledQueue = computed(() => {
const patientLoketId = processingPatient.loketId ? String(processingPatient.loketId) : "1"
if (patientLoketId === targetLoketId && processingPatient.calledByAdmin && processingPatient.noAntrian && processingPatient.status === 'di-loket') {
const klinikName = getKlinikNameFromPatient(processingPatient)
return {
...processingPatient,
klinik: klinikName || processingPatient.klinik || 'Klinik'
const callTime = processingPatient.lastCalledAt ? new Date(processingPatient.lastCalledAt) : now
const timeDiff = now.getTime() - callTime.getTime()
if (timeDiff <= CALL_DISPLAY_DURATION) {
const klinikName = getKlinikNameFromPatient(processingPatient)
return {
...processingPatient,
klinik: klinikName || processingPatient.klinik || 'Klinik'
}
}
}
}
@@ -717,10 +774,15 @@ const currentCalledQueue = computed(() => {
const allPatientsList = filteredPatientsForLoket.value
const dbActive = allPatientsList.find(p => p.idvisit === 8 || p.idvisit === 7)
if (dbActive) {
const klinikName = getKlinikNameFromPatient(dbActive)
return {
...dbActive,
klinik: klinikName || dbActive.klinik || 'Klinik'
const callTime = dbActive.lastCalledAt ? new Date(dbActive.lastCalledAt) : now
const timeDiff = now.getTime() - callTime.getTime()
if (timeDiff <= CALL_DISPLAY_DURATION) {
const klinikName = getKlinikNameFromPatient(dbActive)
return {
...dbActive,
klinik: klinikName || dbActive.klinik || 'Klinik'
}
}
}
@@ -914,6 +976,9 @@ watch(() => queueStore.lastGlobalCall, (newCall) => {
...newCall,
_source: 'websocket'
};
// Mainkan suara panggilan
playCallVoice(newCall, newCall.loketName);
// Sync with store using isolated key
if (queueStore.currentProcessingPatient) {
@@ -1016,6 +1081,9 @@ onMounted(async () => {
...patient, // Spread patient data
_rawMessage: event.data // Attach metadata for filtering
};
// Mainkan suara panggilan
playCallVoice(patient, event.data.loketName);
// Sync with store using isolated key
if (queueStore.currentProcessingPatient) {
+10
View File
@@ -531,6 +531,11 @@ export const useQueueStore = defineStore('queue', () => {
*/
const syncApiPatientStatus = (patient, newStatus) => {
if (!patient) return;
// Bersihkan LocalStorage fallback jika status sudah bukan pending/terlambat
if (newStatus !== 'pending' && newStatus !== 'terlambat' && patient.barcode) {
localStorage.removeItem(`patient-status-${patient.barcode}`);
}
// Allow syncing if strictly 'api' OR if we can find a matching ID in the API store
// This handles cases where we have an 'onsite' patient locally that corresponds to an API patient
@@ -2224,6 +2229,10 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
allPatients.value[patientIndex] = updatedPatient;
// Set currentProcessingPatient with isolated key
currentProcessingPatient.value[storageKey] = updatedPatient;
if (shouldUpdateStatus) {
syncApiPatientStatus(updatedPatient, "di-loket");
}
// POST to external API when patient is being processed (sedang diproses)
try {
@@ -2261,6 +2270,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
};
allPatients.value[patientIndex] = updatedPatient;
currentProcessingPatient.value[storageKey] = updatedPatient;
syncApiPatientStatus(updatedPatient, "di-loket");
} else {
currentProcessingPatient.value[storageKey] = patient;
}