Gestão de Filas - Atendimento.
- #JavaScript

<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Sistema de Chamada de Senhas</title>
<style>
:root{
--bg:#0f1724;
--card:#0b1220;
--accent:#06b6d4;
--muted:#94a3b8;
--glass: rgba(255,255,255,0.03);
}
*{box-sizing:border-box;font-family:Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;}
body{margin:0;min-height:100vh;background:linear-gradient(180deg,#071023 0%, #071627 100%);color:#e6eef6;display:flex;gap:20px;align-items:flex-start;padding:28px;}
.app{display:grid;grid-template-columns:360px 1fr 380px;gap:20px;width:100%;max-width:1400px;margin:auto;}
.card{background:var(--card);padding:18px;border-radius:12px;box-shadow:0 6px 20px rgba(2,6,23,0.6);border:1px solid rgba(255,255,255,0.02);}
h1{font-size:18px;margin:0 0 8px;}
p.small{color:var(--muted);font-size:13px;margin:0 0 12px;}
button{appearance:none;border:0;padding:10px 14px;border-radius:10px;font-weight:600;cursor:pointer;background:var(--accent);color:#032024;}
.btn-ghost{background:transparent;border:1px solid rgba(255,255,255,0.06);color:var(--muted);padding:8px 10px;border-radius:8px;}
.controls{display:flex;flex-direction:column;gap:10px;}
.row{display:flex;gap:10px;align-items:center;}
.big-display{display:flex;flex-direction:column;align-items:center;justify-content:center;height:420px;border-radius:12px;background:linear-gradient(180deg, rgba(6,11,18,0.6), rgba(6,12,20,0.35));box-shadow: inset 0 -20px 40px rgba(0,0,0,0.4);padding:20px;}
.current-label{font-size:18px;color:var(--muted);margin-bottom:6px;}
.current-senha{font-size:120px;font-weight:800;letter-spacing:6px;margin:0;padding:0;}
.small-muted{color:var(--muted);font-size:13px;}
.history{max-height:400px;overflow:auto;padding-top:8px;display:flex;flex-direction:column;gap:6px;}
.hist-item{display:flex;justify-content:space-between;align-items:center;padding:8px;border-radius:8px;background:var(--glass);font-weight:600;}
input[type="number"]{width:100%;padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:transparent;color:inherit;}
footer{grid-column:1 / -1;text-align:center;color:var(--muted);font-size:13px;margin-top:8px;}
@media (max-width:1000px){
.app{grid-template-columns:1fr; padding:12px;}
.big-display{height:300px}
}
</style>
</head>
<body>
<div class="app">
<!-- LEFT: Controles -->
<div class="card">
<h1>Sistema de Senhas</h1>
<p class="small">Gerar e chamar senhas. Salve este arquivo como <code>index.html</code> e abra no navegador.</p>
<div class="controls">
<div class="row">
<button id="btn-gerar">Gerar Nova Senha</button>
<button id="btn-chamar" class="btn-ghost">Chamar Próxima</button>
</div>
<div class="row">
<button id="btn-rechamar" class="btn-ghost">Rechamar</button>
<button id="btn-reset" class="btn-ghost">Resetar</button>
</div>
<div>
<label class="small-muted">Prefixo da senha (opcional)</label>
<input id="input-prefix" placeholder="Ex: A, B, C" maxlength="3" />
</div>
<div>
<label class="small-muted">Incremento inicial (número atual)</label>
<input id="input-start" type="number" min="0" value="0" />
</div>
<div>
<label class="small-muted">Últimas chamadas mostradas</label>
<select id="select-limit">
<option value="5">5</option>
<option value="10" selected>10</option>
<option value="20">20</option>
</select>
</div>
<div class="row">
<button id="btn-toggle-sound" class="btn-ghost">Som: ON</button>
<button id="btn-export" class="btn-ghost">Exportar Histórico (CSV)</button>
</div>
</div>
</div>
<!-- CENTER: Display grande -->
<div class="card big-display" id="display">
<div class="current-label">SENHA ATUAL</div>
<div class="current-senha" id="senha-atual">—</div>
<div style="height:10px"></div>
<div class="small-muted" id="detalhes-atual">Nenhuma chamada ainda</div>
</div>
<!-- RIGHT: Histórico e log -->
<div class="card">
<h1>Histórico de Chamadas</h1>
<p class="small">Últimas chamadas realizadas (mais recente no topo).</p>
<div class="history" id="history"></div>
<div style="height:12px"></div>
<div class="row">
<button id="btn-clear-history" class="btn-ghost">Limpar Histórico</button>
<button id="btn-copy-current" class="btn-ghost">Copiar Senha Atual</button>
</div>
</div>
<footer>Feito com — sistema simples de chamada de senhas (local)</footer>
</div>
<!-- Sons ocultos -->
<audio id="beep" src="data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAESsAACJWAAACABAAZGF0YRAAAAAA"></audio>
<script>
// --- Estado ---
const state = {
currentNumber: 0, // número corrente (inteiro)
prefix: '', // prefixo opcional
queue: [], // senhas geradas (FIFO)
history: [], // histórico de chamadas recentes [{senha, ts}]
playSound: true,
};
// --- Elementos ---
const btnGerar = document.getElementById('btn-gerar');
const btnChamar = document.getElementById('btn-chamar');
const btnRechamar = document.getElementById('btn-rechamar');
const btnReset = document.getElementById('btn-reset');
const btnToggleSound = document.getElementById('btn-toggle-sound');
const btnExport = document.getElementById('btn-export');
const btnClearHistory = document.getElementById('btn-clear-history');
const btnCopyCurrent = document.getElementById('btn-copy-current');
const inputPrefix = document.getElementById('input-prefix');
const inputStart = document.getElementById('input-start');
const selectLimit = document.getElementById('select-limit');
const senhaAtualEl = document.getElementById('senha-atual');
const detalhesAtual = document.getElementById('detalhes-atual');
const historyEl = document.getElementById('history');
const beep = document.getElementById('beep');
// --- Utilitários ---
function formatSenha(prefix, number){
// formata com 3 dígitos (p.ex. 001)
const n = String(number).padStart(3,'0');
return (prefix ? prefix.toUpperCase() + '-' : '') + n;
}
function nowTime(){
return new Date().toLocaleString();
}
// --- Persistência mínima (localStorage) ---
function saveState(){
const s = {
currentNumber: state.currentNumber,
prefix: state.prefix,
queue: state.queue,
history: state.history,
playSound: state.playSound
};
localStorage.setItem('senhas_state_v1', JSON.stringify(s));
}
function loadState(){
try{
const raw = localStorage.getItem('senhas_state_v1');
if(!raw) return;
const s = JSON.parse(raw);
state.currentNumber = s.currentNumber || 0;
state.prefix = s.prefix || '';
state.queue = s.queue || [];
state.history = s.history || [];
state.playSound = typeof s.playSound === 'boolean' ? s.playSound : true;
}catch(e){ console.warn('load failed', e); }
}
// --- Render ---
function render(){
// display atual
const latest = state.history.length ? state.history[0] : null;
if(latest){
senhaAtualEl.textContent = latest.senha;
detalhesAtual.textContent = `Chamado em ${latest.ts}`;
} else {
senhaAtualEl.textContent = '—';
detalhesAtual.textContent = 'Nenhuma chamada ainda';
}
// histórico limitado
const limit = parseInt(selectLimit.value || '10',10);
historyEl.innerHTML = '';
state.history.slice(0, limit).forEach((h, idx) => {
const div = document.createElement('div');
div.className = 'hist-item';
div.innerHTML = `<div>${h.senha}</div><div style="font-size:12px;color:var(--muted)">${h.ts}</div>`;
historyEl.appendChild(div);
});
// controles inputs
inputPrefix.value = state.prefix;
inputStart.value = state.currentNumber;
btnToggleSound.textContent = state.playSound ? 'Som: ON' : 'Som: OFF';
saveState();
}
// --- Ações principais ---
function gerarSenha(){
// incrementa contador e adiciona à fila
state.currentNumber = Number(state.currentNumber) + 1;
const s = formatSenha(state.prefix, state.currentNumber);
state.queue.push(s);
// pequena notificação visual:
alert('Senha gerada: ' + s);
render();
}
function chamarProxima(){
// se fila vazia, gera automaticamente (opcional)
if(state.queue.length === 0){
// comportamento: gerar + chamar
state.currentNumber = Number(state.currentNumber) + 1;
const s = formatSenha(state.prefix, state.currentNumber);
// chama diretamente sem enfileirar
registrarChamada(s);
return;
}
const s = state.queue.shift();
registrarChamada(s);
}
function registrarChamada(senha){
// adiciona ao histórico (topo) com timestamp
state.history.unshift({senha, ts: nowTime()});
// manter histórico razoável
if(state.history.length > 200) state.history.length = 200;
if(state.playSound) playBeep();
render();
}
function rechamar(){
const latest = state.history[0];
if(!latest){
alert('Nenhuma senha para rechamar.');
return;
}
if(state.playSound) playBeep();
// adiciona registro de rechamada (opcional: marca "rechamada")
state.history.unshift({senha: latest.senha + ' (Rechamada)', ts: nowTime()});
render();
}
function resetar(){
if(!confirm('Deseja realmente resetar contador, fila e histórico?')) return;
state.currentNumber = Number(inputStart.value) || 0;
state.queue = [];
state.history = [];
render();
}
function playBeep(){
try{
// usa audio element simples embutido
beep.currentTime = 0;
beep.play().catch(()=>{ /* autoplay pode estar bloqueado - sem som */ });
}catch(e){}
}
// --- Eventos UI ---
btnGerar.addEventListener('click', () => {
// lê prefixo atual
state.prefix = (inputPrefix.value || '').trim().toUpperCase();
gerarSenha();
});
btnChamar.addEventListener('click', () => {
state.prefix = (inputPrefix.value || '').trim().toUpperCase();
chamarProxima();
});
btnRechamar.addEventListener('click', () => rechamar());
btnReset.addEventListener('click', () => resetar());
btnToggleSound.addEventListener('click', () => {
state.playSound = !state.playSound;
btnToggleSound.textContent = state.playSound ? 'Som: ON' : 'Som: OFF';
saveState();
});
btnExport.addEventListener('click', () => {
// exporta histórico como CSV simples
const rows = [['senha','timestamp']];
state.history.slice().reverse().forEach(h => rows.push([h.senha, h.ts]));
const csv = rows.map(r => r.map(c => `"${String(c).replace(/"/g,'""')}"`).join(',')).join('\\n');
const blob = new Blob([csv], {type:'text/csv;charset=utf-8;'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'historico_senhas.csv';
a.click();
URL.revokeObjectURL(url);
});
btnClearHistory.addEventListener('click', () => {
if(!confirm('Limpar histórico completará a lista. Confirmar?')) return;
state.history = [];
render();
});
btnCopyCurrent.addEventListener('click', async () => {
const latest = state.history[0];
if(!latest) { alert('Nenhuma senha atual'); return; }
try {
await navigator.clipboard.writeText(latest.senha);
alert('Senha copiada: ' + latest.senha);
} catch(e){
prompt('Copie a senha manualmente:', latest.senha);
}
});
// Atualiza prefix/num quando usuário digita
inputPrefix.addEventListener('input', () => {
state.prefix = (inputPrefix.value || '').trim().toUpperCase();
saveState();
});
inputStart.addEventListener('change', () => {
// altera o contador inicial sem apagar histórico (use com cuidado)
const v = Number(inputStart.value) || 0;
state.currentNumber = v;
saveState();
});
selectLimit.addEventListener('change', render);
// atalhos de teclado úteis
document.addEventListener('keydown', (e) => {
if(e.key === 'g' || e.key === 'G') { btnGerar.click(); }
if(e.key === 'n' || e.key === 'N') { btnChamar.click(); }
if(e.key === 'r' || e.key === 'R') { btnRechamar.click(); }
});
// --- Inicialização ---
loadState();
render();
// Sugestão: API mínima para controle remoto (ex.: painel separado)
// Você pode controlar chamando:
// window.remoteCall('GERAR') or window.remoteCall('CHAMAR')
window.remoteCall = function(cmd, payload){
// cmd: 'GERAR' | 'CHAMAR' | 'RECHAMAR' | 'RESET'
switch((cmd||'').toUpperCase()){
case 'GERAR': return gerarSenha();
case 'CHAMAR': return chamarProxima();
case 'RECHAMAR': return rechamar();
case 'RESET': return resetar();
default: console.warn('remoteCall comando desconhecido', cmd);
}
};
</script>
</body>
</html>
Gerenciamento de Filas



