301 lines
14 KiB
JavaScript
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');
|
|
const icon = fullscreenButton.querySelector('i');
|
|
if (icon) {
|
|
icon.className = isFullscreen ? 'bi bi-fullscreen-exit' : 'bi bi-arrows-fullscreen';
|
|
}
|
|
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';
|
|
const icon = audioBtn.querySelector('i');
|
|
if (icon) {
|
|
icon.className = isEnabled ? 'bi bi-megaphone-fill' : 'bi bi-megaphone';
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
}); |