39414-vm/assets/js/main.js
2026-04-02 11:25:04 +00:00

301 lines
14 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
const clinicSelects = document.querySelectorAll('.js-clinic-select');
clinicSelects.forEach((clinicSelect) => {
const form = clinicSelect.closest('form');
if (!form) return;
const doctorSelect = form.querySelector('.js-doctor-select');
if (!doctorSelect) return;
const syncDoctors = () => {
const clinicId = clinicSelect.value;
Array.from(doctorSelect.options).forEach((option) => {
if (!option.value) {
option.hidden = false;
return;
}
const visible = option.dataset.clinicId === clinicId;
option.hidden = !visible;
if (!visible && option.selected) {
doctorSelect.value = '';
}
});
};
clinicSelect.addEventListener('change', syncDoctors);
syncDoctors();
});
const ticketPrintButton = document.querySelector('.js-print-ticket');
if (ticketPrintButton) {
ticketPrintButton.addEventListener('click', () => window.print());
}
document.querySelectorAll('.js-app-toast').forEach((toastNode) => {
if (window.bootstrap && bootstrap.Toast) {
bootstrap.Toast.getOrCreateInstance(toastNode, { delay: 3200 }).show();
}
});
const locale = document.body.dataset.locale || 'en';
const timeZone = document.body.dataset.timezone || 'UTC';
const liveClock = document.querySelector('.js-live-clock');
if (liveClock) {
const renderClock = () => {
const now = new Date();
const opts = { hour: '2-digit', minute: '2-digit' };
if (timeZone) opts.timeZone = timeZone;
try {
liveClock.textContent = now.toLocaleTimeString(locale === 'ar' ? 'ar-SA' : 'en-US', opts);
} catch (e) {
opts.timeZone = undefined;
liveClock.textContent = now.toLocaleTimeString(locale === 'ar' ? 'ar-SA' : 'en-US', opts);
}
};
renderClock();
window.setInterval(renderClock, 1000 * 30);
}
if (document.body.dataset.page === 'display') {
const fullscreenButton = document.querySelector('.js-fullscreen-toggle');
const syncFullscreenButton = () => {
if (!fullscreenButton) return;
const isFullscreen = !!document.fullscreenElement;
fullscreenButton.title = isFullscreen
? (fullscreenButton.dataset.labelExit || 'Exit full display')
: (fullscreenButton.dataset.labelEnter || 'Full display');
fullscreenButton.innerHTML = isFullscreen
? '<i class="bi bi-fullscreen-exit"></i>'
: '<i class="bi bi-arrows-fullscreen"></i>';
fullscreenButton.setAttribute('aria-pressed', isFullscreen ? 'true' : 'false');
};
if (fullscreenButton && document.fullscreenEnabled) {
fullscreenButton.addEventListener('click', async () => {
try {
if (document.fullscreenElement) {
await document.exitFullscreen();
window.localStorage.setItem('hospitalQueue:autoFullscreen', 'false');
} else {
await document.documentElement.requestFullscreen();
window.localStorage.setItem('hospitalQueue:autoFullscreen', 'true');
}
} catch (error) {
console.warn('Fullscreen toggle failed', error);
}
});
document.addEventListener('fullscreenchange', syncFullscreenButton);
syncFullscreenButton();
if (window.localStorage.getItem('hospitalQueue:autoFullscreen') === 'true') {
const autoFs = async () => {
if (!document.fullscreenElement) {
try { await document.documentElement.requestFullscreen(); } catch (e) {}
}
document.removeEventListener('click', autoFs);
document.removeEventListener('keydown', autoFs);
document.removeEventListener('touchstart', autoFs);
};
try {
document.documentElement.requestFullscreen().catch(() => {
document.addEventListener('click', autoFs);
document.addEventListener('keydown', autoFs);
document.addEventListener('touchstart', autoFs);
});
} catch (e) {
document.addEventListener('click', autoFs);
document.addEventListener('keydown', autoFs);
document.addEventListener('touchstart', autoFs);
}
}
} else if (fullscreenButton) {
fullscreenButton.hidden = true;
}
let audioCtx = null;
const initAudio = () => {
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!audioCtx && AudioContext) {
audioCtx = new AudioContext();
}
if (audioCtx && audioCtx.state === 'suspended') {
audioCtx.resume();
}
} catch (e) {
console.warn('AudioContext init failed', e);
}
};
let availableVoices = [];
if ('speechSynthesis' in window) {
const updateVoices = () => { availableVoices = window.speechSynthesis.getVoices(); };
updateVoices();
if (window.speechSynthesis.onvoiceschanged !== undefined) {
window.speechSynthesis.onvoiceschanged = updateVoices;
}
}
const audioBtn = document.querySelector('.js-audio-toggle');
if (audioBtn) {
const updateAudioBtnState = () => {
const isEnabled = window.localStorage.getItem('hospitalQueue:audioEnabled') === 'true';
if (isEnabled) {
audioBtn.innerHTML = '<i class="bi bi-megaphone-fill"></i>';
} else {
audioBtn.innerHTML = '<i class="bi bi-megaphone"></i>';
}
audioBtn.setAttribute('aria-pressed', isEnabled.toString());
const videoPlayer = document.getElementById('adsVideoPlayer');
if (videoPlayer) {
videoPlayer.muted = !isEnabled;
}
};
updateAudioBtnState();
audioBtn.addEventListener('click', () => {
const isEnabled = window.localStorage.getItem('hospitalQueue:audioEnabled') === 'true';
const nextState = !isEnabled;
window.localStorage.setItem('hospitalQueue:audioEnabled', nextState.toString());
updateAudioBtnState();
if (nextState) {
initAudio();
if ('speechSynthesis' in window) {
const primeUtterance = new SpeechSynthesisUtterance('');
window.speechSynthesis.speak(primeUtterance);
}
}
});
}
const playChime = () => {
try {
if (!audioCtx) initAudio();
if (!audioCtx) return;
if (audioCtx.state === 'suspended') audioCtx.resume();
const playTone = (freq, startTime, duration) => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, audioCtx.currentTime + startTime);
gain.gain.setValueAtTime(0, audioCtx.currentTime + startTime);
gain.gain.linearRampToValueAtTime(0.5, audioCtx.currentTime + startTime + 0.05);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + startTime + duration);
osc.start(audioCtx.currentTime + startTime);
osc.stop(audioCtx.currentTime + startTime + duration);
};
playTone(523.25, 0, 0.4); // C5
playTone(659.25, 0.3, 0.6); // E5
} catch (e) {
console.warn('AudioContext failed', e);
}
};
const checkAnnouncements = () => {
const cards = Array.from(document.querySelectorAll('.announcement-card'));
const latest = cards[0];
const audioEnabled = window.localStorage.getItem('hospitalQueue:audioEnabled') === 'true';
if (latest) {
const announcementKey = latest.dataset.announcementKey || '';
const storageKey = `hospitalQueue:lastAnnouncement:${locale}`;
const storedKey = window.localStorage.getItem(storageKey) || '';
if (announcementKey && announcementKey !== storedKey) {
window.localStorage.setItem(storageKey, announcementKey);
if (audioEnabled) {
const videoPlayer = document.getElementById('adsVideoPlayer');
if (videoPlayer) videoPlayer.volume = 0.1;
playChime();
setTimeout(() => {
const text = locale === 'ar' ? (latest.dataset.announcementAr || '') : (latest.dataset.announcementEn || '');
if (text) {
// Use Google Translate TTS (outside high-quality voice API)
const tl = locale === 'ar' ? 'ar' : 'en';
const gTtsUrl = 'https://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&tl=' + tl + '&q=' + encodeURIComponent(text);
const audio = new Audio(gTtsUrl);
audio.onended = () => { if (videoPlayer) videoPlayer.volume = 1.0; };
audio.onerror = () => {
// Fallback to built-in if external TTS fails
console.warn("External TTS failed, falling back to built-in speech");
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = locale === 'ar' ? 'ar-SA' : 'en-US';
const voices = availableVoices.length > 0 ? availableVoices : window.speechSynthesis.getVoices();
const langPrefix = locale === 'ar' ? 'ar' : 'en';
const langVoices = voices.filter(v => v.lang.toLowerCase().startsWith(langPrefix));
if (langVoices.length > 0) {
const bestVoice = langVoices.find(v =>
v.name.includes('Google') || v.name.includes('Natural') || v.name.includes('Premium') || v.name.includes('Online')
) || langVoices.find(v => v.name.includes('Microsoft')) || langVoices[0];
if (bestVoice) utterance.voice = bestVoice;
}
utterance.onend = () => { if (videoPlayer) videoPlayer.volume = 1.0; };
utterance.onerror = () => { if (videoPlayer) videoPlayer.volume = 1.0; };
window.speechSynthesis.speak(utterance);
} else {
if (videoPlayer) videoPlayer.volume = 1.0;
}
};
// Attempt to play external audio
const playPromise = audio.play();
if (playPromise !== undefined) {
playPromise.catch(audio.onerror);
}
} else {
if (videoPlayer) videoPlayer.volume = 1.0;
}
}, 1200); // play voice after chime finishes
}
}
}
};
checkAnnouncements();
const autoRefreshSeconds = parseInt(document.querySelector('[data-auto-refresh]')?.dataset.autoRefresh || '0', 10);
if (autoRefreshSeconds > 0) {
window.setInterval(async () => {
try {
const response = await fetch(window.location.href, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (response.ok) {
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newSections = doc.querySelector('#queueSections');
const currentSections = document.querySelector('#queueSections');
if (newSections && currentSections) {
currentSections.innerHTML = newSections.innerHTML;
}
const oldTicker = document.querySelector('.news-ticker-container');
const newTicker = doc.querySelector('.news-ticker-container');
if (oldTicker && newTicker) {
oldTicker.innerHTML = newTicker.innerHTML;
}
checkAnnouncements();
}
} catch (e) {
console.error('Auto-refresh failed', e);
}
}, autoRefreshSeconds * 1000);
}
}
});