Thursday, October 16, 2025

DMV Queue & Waiting Area Announcer

DMV Queue & Waiting Area Announcer

Waiting Area Display

Open on TV for public view

Now Serving

Counter —
Welcome. Please have your documents ready. Priority tickets are called first.

Queue

TicketTypeRequestedStatus
Tickets persist locally. Use Factory Reset to clear.

Recently Served

TicketCounterTime
// DOM elements const prefixEl = $('#prefix'); const serviceEl = $('#service'); const transferTargetEl = $('#transferTarget'); const nowTicketEl = $('#nowTicket'); const nowCounterEl = $('#nowCounter'); const queueRowsEl = $('#queueRows'); const servedRowsEl = $('#servedRows'); const addCounterBtn = $('#addCounter'); const countersWrap = $('#counters'); const langEl = $('#lang'); const volumeEl = $('#volume'); const chimeEl = $('#chime'); const speakEl = $('#speak'); // Utility functions function formatTime(ts) { const d = new Date(ts); return d.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'}); } function ensurePrefixSeq(p) { if (!(p in state.seq)) state.seq[p] = 1; } function padNumber(n) { return String(n).padStart(3, '0'); } // Ticket management function issueTicket(isPriority = false) { const prefix = prefixEl.value.trim().toUpperCase() || 'A'; ensurePrefixSeq(prefix); const id = prefix + padNumber(state.seq[prefix]++); const ticket = { id, ts: Date.now(), prio: isPriority, status: 'waiting', history: [] }; state.tickets.push(ticket); save(); render(); toast(`Issued ${id}${isPriority ? ' (Priority)' : ''}`, isPriority ? 'warn' : ''); return ticket; } function getWaitingTickets() { const waiting = state.tickets.filter(t => t.status === 'waiting'); const priority = waiting.filter(t => t.prio).sort((a, b) => a.ts - b.ts); const regular = waiting.filter(t => !t.prio).sort((a, b) => a.ts - b.ts); return priority.concat(regular); } function callNext() { const counter = serviceEl.value; if (!counter) return toast('Select a counter first', 'danger'); const nextTicket = getWaitingTickets()[0]; if (!nextTicket) return toast('No waiting tickets', 'danger'); nextTicket.status = 'serving'; nextTicket.history.push({ at: Date.now(), op: 'call', counter }); state.now.ticket = nextTicket.id; state.now.counter = counter; announce(nextTicket.id, counter); save(); render(); } function recall() { if (!state.now.ticket || !state.now.counter) { return toast('Nothing to recall', 'danger'); } announce(state.now.ticket, state.now.counter, true); } function skip() { const ticket = state.tickets.find(t => t.id === state.now.ticket); if (!ticket) return toast('No active ticket', 'danger'); ticket.status = 'skipped'; ticket.history.push({ at: Date.now(), op: 'skip', counter: state.now.counter }); state.now.ticket = null; state.now.counter = null; save(); render(); } function markDone(ticketId, counterId) { const ticket = state.tickets.find(t => t.id === ticketId); if (!ticket) return; ticket.status = 'done'; ticket.history.push({ at: Date.now(), op: 'done', counter: counterId }); state.served.unshift({ id: ticket.id, counter: counterId, at: Date.now() }); state.served = state.served.slice(0, 20); if (state.now.ticket === ticketId) { state.now.ticket = null; state.now.counter = null; } save(); render(); } function transfer(toCounter) { const ticket = state.tickets.find(t => t.id === state.now.ticket); if (!ticket) return toast('No active ticket', 'danger'); ticket.history.push({ at: Date.now(), op: 'transfer', from: state.now.counter, to: toCounter }); // Keep serving state but announce at new counter state.now.counter = toCounter; announce(ticket.id, toCounter, false, 'transferred'); save(); render(); } function addCounter() { const id = 'C' + (state.counters.length + 1); state.counters.push({ id, name: 'Counter ' + (state.counters.length + 1) }); save(); render(); } function removeCounter(id) { state.counters = state.counters.filter(c => c.id !== id); if (serviceEl.value === id) { serviceEl.value = state.counters[0]?.id || ''; } save(); render(); } // Announcement system function playChime() { const key = chimeEl.value; if (key === 'none') return; const audioElement = chimes[key]; if (!audioElement) return; audioElement.volume = Number(volumeEl.value || 1); audioElement.currentTime = 0; audioElement.play().catch(err => { console.warn('Could not play chime:', err); }); } function speakText(text) { if (speakEl.value === 'off') return; // Check if speech synthesis is available if (!('speechSynthesis' in window)) { console.warn('Speech synthesis not supported'); return; } const utterance = new SpeechSynthesisUtterance(text); utterance.lang = langEl.value; utterance.volume = Number(volumeEl.value || 1); // Cancel any ongoing speech speechSynthesis.cancel(); // Speak the text speechSynthesis.speak(utterance); } function announce(ticket, counter, isRecall = false, extra = '') { playChime(); const phrase = isRecall ? 'recall' : 'now serving'; const message = `${phrase} ticket ${spellTicket(ticket)} at counter ${spellCounter(counter)} ${extra || ''}`.trim(); // Delay speech slightly to allow chime to play first setTimeout(() => speakText(message), 150); } function spellTicket(code) { // Improves clarity: A001 -> "A zero zero one" return code.split('').map(char => { if (/\d/.test(char)) { const numbers = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']; return numbers[Number(char)]; } if (/[A-Z]/.test(char)) return char; return char; }).join(' '); } function spellCounter(counter) { // Extract number from counter ID (e.g., "C1" -> "1") const match = counter.match(/\d+/); return match ? match[0] : counter; } // Rendering functions function renderCounters() { // Update selects serviceEl.innerHTML = ''; transferTargetEl.innerHTML = ''; state.counters.forEach(counter => { const serviceOption = document.createElement('option'); serviceOption.value = counter.id; serviceOption.textContent = counter.name; serviceEl.appendChild(serviceOption); const transferOption = document.createElement('option'); transferOption.value = counter.id; transferOption.textContent = counter.name; transferTargetEl.appendChild(transferOption); }); if (!serviceEl.value && state.counters[0]) { serviceEl.value = state.counters[0].id; } // Render counter cards countersWrap.innerHTML = ''; state.counters.forEach(counter => { const div = document.createElement('div'); div.className = 'row'; div.style.justifyContent = 'space-between'; div.innerHTML = `
${counter.id}
`; countersWrap.appendChild(div); }); // Bind event listeners $$('.counter-name').forEach(input => { input.addEventListener('change', e => { const id = e.target.dataset.id; const counter = state.counters.find(c => c.id === id); if (counter) { counter.name = e.target.value || counter.name; save(); renderCounters(); } }); }); $$('[data-del]').forEach(button => { button.addEventListener('click', e => removeCounter(e.target.dataset.del)); }); $$('[data-done]').forEach(button => { button.addEventListener('click', e => { const counterId = e.target.dataset.done; if (!state.now.ticket) return toast('No active ticket', 'danger'); markDone(state.now.ticket, counterId); }); }); } function renderQueue() { queueRowsEl.innerHTML = ''; const waitingTickets = state.tickets .filter(t => t.status === 'waiting' || t.status === 'serving') .sort((a, b) => { // Serving tickets first if (a.status !== b.status) return a.status === 'serving' ? -1 : 1; // Priority before regular if (a.prio !== b.prio) return a.prio ? -1 : 1; // Then by timestamp return a.ts - b.ts; }); waitingTickets.forEach(ticket => { const row = document.createElement('tr'); row.innerHTML = ` ${ticket.id} ${ticket.prio ? 'Priority' : 'Regular'} ${formatTime(ticket.ts)} ${ticket.status === 'serving' ? 'Serving' : 'Waiting'} `; queueRowsEl.appendChild(row); }); } function renderServed() { servedRowsEl.innerHTML = ''; state.served.forEach(served => { const row = document.createElement('tr'); row.innerHTML = ` ${served.id} ${served.counter} ${formatTime(served.at)} `; servedRowsEl.appendChild(row); }); } function renderNow() { nowTicketEl.textContent = state.now.ticket || '—'; if (state.now.counter) { const counter = state.counters.find(c => c.id === state.now.counter); const counterName = counter ? counter.name : `Counter ${state.now.counter.replace(/^C/, '').trim()}`; nowCounterEl.textContent = counterName; } else { nowCounterEl.textContent = 'Counter —'; } } function renderSettings() { langEl.value = state.settings.lang || 'en-US'; volumeEl.value = state.settings.volume ?? 1; chimeEl.value = state.settings.chime || 'ding'; speakEl.value = state.settings.speak || 'on'; } function render() { renderCounters(); renderQueue(); renderServed(); renderNow(); renderSettings(); } // Event listeners function initializeEventListeners() { // Ticket management $('#issueRegular').addEventListener('click', () => issueTicket(false)); $('#issuePriority').addEventListener('click', () => issueTicket(true)); $('#callNext').addEventListener('click', callNext); $('#recall').addEventListener('click', recall); $('#skip').addEventListener('click', skip); $('#transfer').addEventListener('click', () => transfer(transferTargetEl.value)); // Data management $('#clearServed').addEventListener('click', () => { state.served = []; save(); renderServed(); toast('Cleared served list'); }); $('#resetAll').addEventListener('click', () => { if (confirm('Factory reset? This clears tickets, counters, and settings.')) { localStorage.removeItem(LS_KEY); location.reload(); } }); // Counter management $('#addCounter').addEventListener('click', addCounter); // Settings langEl.addEventListener('change', () => { state.settings.lang = langEl.value; save(); }); volumeEl.addEventListener('input', () => { state.settings.volume = Number(volumeEl.value); save(); }); chimeEl.addEventListener('change', () => { state.settings.chime = chimeEl.value; save(); }); speakEl.addEventListener('change', () => { state.settings.speak = speakEl.value; save(); }); // Keyboard shortcuts document.addEventListener('keydown', e => { // Ignore if user is typing in an input field if (e.target.matches('input, textarea, select')) return; const key = e.key.toLowerCase(); if (key === 'n') callNext(); if (key === 'r') recall(); if (key === 's') skip(); }); } // Display mode for public screens function initializeDisplayMode() { const toggleBtn = $('#toggleDisplay'); let displayMode = false; toggleBtn.addEventListener('click', () => { displayMode = !displayMode; document.querySelector('aside').classList.toggle('hidden', displayMode); toggleBtn.textContent = displayMode ? 'Exit Display Mode' : 'Toggle Display Mode'; // Update ARIA attributes for accessibility document.querySelector('aside').setAttribute('aria-hidden', displayMode); }); } // Self-service kiosk function initializeKiosk() { $('#printKiosk').addEventListener('click', () => { const kioskWindow = window.open('', '_blank', 'width=430,height=700'); if (!kioskWindow) { toast('Please allow pop-ups for this site', 'danger'); return; } kioskWindow.document.write(` Take a Number
Take a Number
Please select ticket type and press one button below.
Close this window when finished.
`); kioskWindow.document.close(); }); // Handle kiosk messages window.addEventListener('message', e => { if (!e.data || !e.data.__kiosk) return; const { type, prefix } = e.data.data; prefixEl.value = prefix; issueTicket(type === 'priority'); }); } // Toast notification system function toast(message, type = '') { // Remove existing toasts $$('.toast').forEach(toast => toast.remove()); const toastElement = document.createElement('div'); toastElement.className = `toast ${type}`; toastElement.textContent = message; toastElement.setAttribute('aria-live', 'polite'); document.body.appendChild(toastElement); // Animate in requestAnimationFrame(() => { toastElement.style.transform = 'translateY(0)'; toastElement.style.opacity = '1'; }); // Remove after delay setTimeout(() => { toastElement.style.transform = 'translateY(20px)'; toastElement.style.opacity = '0'; setTimeout(() => { if (toastElement.parentNode) { toastElement.remove(); } }, 300); }, 2200); } // Initialize the application function init() { load(); initializeEventListeners(); initializeDisplayMode(); initializeKiosk(); render(); // Seed selects for existing counters on first load renderCounters(); } // Start the application when DOM is loaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.

Do you need Ethiopian Power of Attorney where your agent can preform several crucial tasks on your behalf? Such as adoption proceedings, buying movable or immovable properties, paying tax, represent you in governmental and public offices and several others tasks with our your physical presence? If your answer is yes get the Ethiopian Power of Attorney or YEBBO now on sale

Shop Amazon