image

Acesse bootcamps ilimitados e +650 cursos pra sempre

75
%OFF
Article image

SS

Sergio Silva04/12/2025 19:34
Compartilhe

Gestão de Filas - Atendimento.

  • #JavaScript


image

<!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

Compartilhe
Recomendados para você
GitHub Copilot - Código na Prática
CI&T - Backend com Java & AWS
Nexa - Machine Learning e GenAI na Prática
Comentários (0)