feat: implement queue display and management system for loket and clinic modules voice over
This commit is contained in:
@@ -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})`);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user