Index.html

XML Detected Guest 1 Views Size: 75.17 KB Posted on: Oct 6, 26 @ 12:10 PM
  1. <!DOCTYPE html>
  2. <html lang="pt-BR">
  3. <head>
  4.   <meta charset="UTF-8">
  5.   <meta name="viewport" content="width=device-width, initial-scale=1">
  6.   <meta name="theme-color" content="#102347">
  7.   <title>FilaFácil</title>
  8.   <style>
  9.     :root {
  10.       --navy: #102347;
  11.       --blue: #2563eb;
  12.       --ink: #172b4d;
  13.       --muted: #71809b;
  14.       --line: #e4eaf3;
  15.       --bg: #f4f7fc;
  16.       --green: #159b69;
  17.       --yellow: #d99100;
  18.       --red: #e83b4f;
  19.       --shadow: 0 10px 30px #142a520a;
  20.     }
  21.  
  22.     * { box-sizing: border-box; margin: 0; padding: 0; }
  23.  
  24.     body {
  25.       background: var(--bg);
  26.       color: var(--ink);
  27.       font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
  28.       min-height: 100vh;
  29.     }
  30.  
  31.     button, input, select { font: inherit; }
  32.     button { cursor: pointer; }
  33.  
  34.     /* ========== LAYOUT ATENDENTE ========== */
  35.     .app {
  36.       min-height: 100vh;
  37.       display: grid;
  38.       grid-template-columns: 245px minmax(0, 1fr);
  39.     }
  40.  
  41.     .sidebar {
  42.       background: linear-gradient(180deg, #102347, #0d1b36);
  43.       color: #eaf0ff;
  44.       padding: 26px 16px 20px;
  45.       display: flex;
  46.       flex-direction: column;
  47.       gap: 24px;
  48.     }
  49.  
  50.     .brand {
  51.       display: flex;
  52.       align-items: center;
  53.       gap: 12px;
  54.       padding: 0 9px;
  55.     }
  56.  
  57.     .logo {
  58.       width: 43px;
  59.       height: 43px;
  60.       border-radius: 13px;
  61.       background: linear-gradient(145deg, #60a5fa, #2563eb);
  62.       display: grid;
  63.       place-items: center;
  64.       font-size: 23px;
  65.       font-weight: 900;
  66.       color: white;
  67.     }
  68.  
  69.     .brand h1 { font-size: 19px; margin: 0; }
  70.     .brand p {
  71.       font-size: 10px;
  72.       color: #a9bad9;
  73.       margin: 4px 0 0;
  74.       letter-spacing: 1px;
  75.     }
  76.  
  77.     .nav { display: grid; gap: 7px; }
  78.  
  79.     .nav button {
  80.       display: flex;
  81.       gap: 13px;
  82.       align-items: center;
  83.       border: 0;
  84.       border-radius: 11px;
  85.       background: transparent;
  86.       color: #c5d2eb;
  87.       padding: 14px;
  88.       text-align: left;
  89.       transition: background .15s;
  90.     }
  91.  
  92.     .nav button:hover,
  93.     .nav button.active {
  94.       background: #244d9b;
  95.       color: white;
  96.     }
  97.  
  98.     .nav span {
  99.       font-size: 18px;
  100.       width: 20px;
  101.       text-align: center;
  102.     }
  103.  
  104.     .guiche-selector {
  105.       background: #0d1f3a;
  106.       border: 1px solid #24466e;
  107.       border-radius: 12px;
  108.       padding: 14px;
  109.     }
  110.  
  111.     .guiche-selector label {
  112.       font-size: 10px;
  113.       font-weight: 800;
  114.       letter-spacing: 1px;
  115.       color: #9db5d5;
  116.       text-transform: uppercase;
  117.       display: block;
  118.       margin-bottom: 8px;
  119.     }
  120.  
  121.     .guiche-selector select {
  122.       width: 100%;
  123.       background: #102b4d;
  124.       border: 1px solid #2a4d78;
  125.       color: white;
  126.       border-radius: 8px;
  127.       padding: 10px;
  128.       font-size: 14px;
  129.       font-weight: 700;
  130.     }
  131.  
  132.     .side-links {
  133.       display: grid;
  134.       gap: 8px;
  135.       margin-top: 8px;
  136.     }
  137.  
  138.     .side-links a {
  139.       color: #9db5d5;
  140.       font-size: 12px;
  141.       text-decoration: none;
  142.       padding: 8px 12px;
  143.       border-radius: 8px;
  144.       background: #0d1f3a;
  145.       border: 1px solid #1e3a5f;
  146.       display: flex;
  147.       align-items: center;
  148.       gap: 8px;
  149.       transition: .15s;
  150.     }
  151.  
  152.     .side-links a:hover {
  153.       background: #163056;
  154.       color: white;
  155.     }
  156.  
  157.     .side-bottom {
  158.       margin-top: auto;
  159.       padding: 10px;
  160.       color: #bdcbe3;
  161.       font-size: 12px;
  162.     }
  163.  
  164.     .online {
  165.       display: flex;
  166.       align-items: center;
  167.       gap: 9px;
  168.       margin-bottom: 12px;
  169.     }
  170.  
  171.     .dot {
  172.       width: 8px;
  173.       height: 8px;
  174.       border-radius: 50%;
  175.       background: #32d296;
  176.     }
  177.  
  178.     main { min-width: 0; }
  179.  
  180.     .topbar {
  181.       height: 72px;
  182.       background: #ffffffeb;
  183.       border-bottom: 1px solid var(--line);
  184.       display: flex;
  185.       align-items: center;
  186.       justify-content: space-between;
  187.       padding: 0 32px;
  188.       color: var(--muted);
  189.       font-size: 13px;
  190.     }
  191.  
  192.     .avatar {
  193.       display: inline-grid;
  194.       place-items: center;
  195.       width: 35px;
  196.       height: 35px;
  197.       border-radius: 50%;
  198.       background: #e6edfa;
  199.       color: #31528c;
  200.       font-weight: 800;
  201.       margin-right: 9px;
  202.     }
  203.  
  204.     .content {
  205.       padding: 30px 32px;
  206.       max-width: 1600px;
  207.       margin: auto;
  208.     }
  209.  
  210.     .heading {
  211.       display: flex;
  212.       justify-content: space-between;
  213.       align-items: center;
  214.       gap: 15px;
  215.       margin-bottom: 25px;
  216.     }
  217.  
  218.     .eyebrow {
  219.       color: var(--blue);
  220.       font-size: 10px;
  221.       font-weight: 800;
  222.       letter-spacing: 1.5px;
  223.       text-transform: uppercase;
  224.       margin-bottom: 7px;
  225.     }
  226.  
  227.     h2 {
  228.       font-size: clamp(25px, 3vw, 33px);
  229.       letter-spacing: -.8px;
  230.       margin: 0;
  231.       color: #12264a;
  232.     }
  233.  
  234.     .subtitle {
  235.       margin: 8px 0 0;
  236.       color: var(--muted);
  237.       font-size: 14px;
  238.     }
  239.  
  240.     .btn {
  241.       border: 0;
  242.       border-radius: 10px;
  243.       padding: 12px 16px;
  244.       font-size: 12px;
  245.       font-weight: 800;
  246.       display: inline-flex;
  247.       align-items: center;
  248.       justify-content: center;
  249.       gap: 8px;
  250.       transition: .2s;
  251.     }
  252.  
  253.     .primary {
  254.       background: var(--blue);
  255.       color: white;
  256.       box-shadow: 0 7px 15px #2563eb25;
  257.     }
  258.  
  259.     .primary:hover {
  260.       background: #1d4ed8;
  261.       transform: translateY(-1px);
  262.     }
  263.  
  264.     .soft { background: #edf3ff; color: #2454aa; }
  265.     .danger { background: #ffe8ec; color: #ce2d42; }
  266.     .success { background: #e2f7ee; color: #087f51; }
  267.  
  268.     .stats {
  269.       display: grid;
  270.       grid-template-columns: repeat(4, minmax(0, 1fr));
  271.       gap: 15px;
  272.       margin-bottom: 22px;
  273.     }
  274.  
  275.     .stat, .card {
  276.       background: white;
  277.       border: 1px solid var(--line);
  278.       border-radius: 17px;
  279.       box-shadow: var(--shadow);
  280.       min-width: 0;
  281.     }
  282.  
  283.     .stat {
  284.       padding: 18px;
  285.       display: flex;
  286.       align-items: center;
  287.       gap: 13px;
  288.     }
  289.  
  290.     .stat-icon {
  291.       width: 47px;
  292.       height: 47px;
  293.       border-radius: 14px;
  294.       display: grid;
  295.       place-items: center;
  296.       font-size: 21px;
  297.       flex-shrink: 0;
  298.     }
  299.  
  300.     .blue { background: #eaf1ff; color: var(--blue); }
  301.     .green { background: #e2f7ee; color: var(--green); }
  302.     .purple { background: #f0eaff; color: #7c3aed; }
  303.     .red { background: #ffe8ec; color: var(--red); }
  304.  
  305.     .num { font-size: 27px; font-weight: 850; line-height: 1; }
  306.     .label { font-size: 12px; color: var(--muted); margin-top: 6px; }
  307.  
  308.     .workspace {
  309.       display: grid;
  310.       grid-template-columns: minmax(0, 1.55fr) minmax(290px, .9fr);
  311.       gap: 21px;
  312.       align-items: start;
  313.     }
  314.  
  315.     .card { overflow: hidden; }
  316.  
  317.     .card-head {
  318.       padding: 19px 21px;
  319.       display: flex;
  320.       justify-content: space-between;
  321.       align-items: center;
  322.       gap: 10px;
  323.       border-bottom: 1px solid var(--line);
  324.     }
  325.  
  326.     h3 { font-size: 16px; margin: 0; font-weight: 800; }
  327.  
  328.     .pill {
  329.       background: #edf3ff;
  330.       color: #2454aa;
  331.       padding: 5px 9px;
  332.       border-radius: 30px;
  333.       font-size: 11px;
  334.       font-weight: 800;
  335.     }
  336.  
  337.     .tools {
  338.       display: flex;
  339.       gap: 9px;
  340.       padding: 14px 18px;
  341.       border-bottom: 1px solid var(--line);
  342.       flex-wrap: wrap;
  343.     }
  344.  
  345.     .tools input { flex: 1; min-width: 140px; }
  346.  
  347.     .tools input,
  348.     .tools select,
  349.     .field select {
  350.       border: 1px solid var(--line);
  351.       border-radius: 9px;
  352.       padding: 10px 11px;
  353.       background: #fbfcff;
  354.       color: var(--ink);
  355.       outline: none;
  356.       font-size: 12px;
  357.     }
  358.  
  359.     .tools input:focus,
  360.     .field select:focus {
  361.       border-color: #8bb0ff;
  362.       box-shadow: 0 0 0 3px #2563eb18;
  363.     }
  364.  
  365.     .table-wrap { overflow-x: auto; }
  366.  
  367.     table {
  368.       width: 100%;
  369.       border-collapse: collapse;
  370.       text-align: left;
  371.     }
  372.  
  373.     th {
  374.       background: #f8faff;
  375.       color: #7a89a4;
  376.       font-size: 10px;
  377.       text-transform: uppercase;
  378.       letter-spacing: .6px;
  379.       padding: 13px 15px;
  380.       white-space: nowrap;
  381.     }
  382.  
  383.     td {
  384.       font-size: 12px;
  385.       padding: 15px;
  386.       border-top: 1px solid #edf1f7;
  387.       white-space: nowrap;
  388.     }
  389.  
  390.     tbody tr:hover { background: #fbfcff; }
  391.  
  392.     .pos {
  393.       display: inline-grid;
  394.       place-items: center;
  395.       width: 29px;
  396.       height: 29px;
  397.       border-radius: 10px;
  398.       font-weight: 800;
  399.     }
  400.  
  401.     .pos.alta { background: #ffe8ec; color: #ce2d42; }
  402.     .pos.media { background: #fff3d2; color: #a66b00; }
  403.     .pos.baixa { background: #e2f7ee; color: #087f51; }
  404.  
  405.     .id { color: #244a8f; font-weight: 800; font-size: 15px; }
  406.  
  407.     .priority {
  408.       display: inline-block;
  409.       border-radius: 30px;
  410.       padding: 5px 9px;
  411.       font-size: 10px;
  412.       font-weight: 800;
  413.     }
  414.  
  415.     .priority.alta { background: #ffe8ec; color: #ce2d42; }
  416.     .priority.media { background: #fff3d2; color: #a66b00; }
  417.     .priority.baixa { background: #e2f7ee; color: #087f51; }
  418.  
  419.     .status { font-size: 11px; color: #71809b; }
  420.     .status.atendimento { color: var(--blue); font-weight: 700; }
  421.     .status.resolvido { color: var(--green); }
  422.     .status.cancelado { color: var(--red); }
  423.  
  424.     .guiche-badge {
  425.       display: inline-block;
  426.       background: #eaf1ff;
  427.       color: #1d4ed8;
  428.       font-size: 10px;
  429.       font-weight: 800;
  430.       padding: 3px 8px;
  431.       border-radius: 6px;
  432.     }
  433.  
  434.     .row-actions { display: flex; gap: 6px; flex-wrap: wrap; }
  435.  
  436.     .row-action {
  437.       border: 0;
  438.       background: #edf3ff;
  439.       color: #2454aa;
  440.       border-radius: 7px;
  441.       padding: 7px 9px;
  442.       font-size: 12px;
  443.       font-weight: 700;
  444.     }
  445.  
  446.     .row-action.danger { background: #ffe8ec; color: #ce2d42; }
  447.     .row-action.success { background: #e2f7ee; color: #087f51; }
  448.  
  449.     .empty {
  450.       padding: 30px;
  451.       text-align: center;
  452.       color: var(--muted);
  453.       font-size: 13px;
  454.     }
  455.  
  456.     .right { display: grid; gap: 20px; }
  457.     .inner { padding: 18px; }
  458.  
  459.     .next {
  460.       display: flex;
  461.       align-items: center;
  462.       gap: 12px;
  463.       padding: 15px;
  464.       border: 1px solid #ffd5dc;
  465.       background: linear-gradient(120deg, #fff2f4, #fff9fa);
  466.       border-radius: 12px;
  467.     }
  468.  
  469.     .next-num {
  470.       width: 52px;
  471.       height: 52px;
  472.       border-radius: 13px;
  473.       background: var(--red);
  474.       color: white;
  475.       display: grid;
  476.       place-items: center;
  477.       font-weight: 900;
  478.       font-size: 16px;
  479.       flex-shrink: 0;
  480.     }
  481.  
  482.     .next-info { flex: 1; min-width: 0; }
  483.     .next-info strong { font-size: 14px; }
  484.     .next-info p {
  485.       font-size: 12px;
  486.       color: var(--muted);
  487.       margin: 5px 0 0;
  488.     }
  489.  
  490.     .actions {
  491.       display: grid;
  492.       grid-template-columns: 1fr 1fr;
  493.       gap: 9px;
  494.       margin-top: 12px;
  495.     }
  496.  
  497.     .actions button { padding: 11px 8px; font-size: 11px; }
  498.  
  499.     .field {
  500.       display: grid;
  501.       gap: 7px;
  502.       margin-bottom: 14px;
  503.     }
  504.  
  505.     .field label {
  506.       font-size: 11px;
  507.       font-weight: 800;
  508.       color: #334867;
  509.     }
  510.  
  511.     .form-body { padding: 19px; }
  512.     .form-body > .btn { width: 100%; }
  513.  
  514.     .note {
  515.       font-size: 10px;
  516.       line-height: 1.5;
  517.       color: #8794aa;
  518.       margin: 12px 0 0;
  519.     }
  520.  
  521.     .footer {
  522.       text-align: center;
  523.       color: #9aa7bc;
  524.       font-size: 10px;
  525.       padding: 22px 0 0;
  526.     }
  527.  
  528.     .toast {
  529.       position: fixed;
  530.       right: 22px;
  531.       bottom: 22px;
  532.       background: #142b4f;
  533.       color: white;
  534.       padding: 13px 17px;
  535.       border-radius: 11px;
  536.       box-shadow: 0 10px 35px #0003;
  537.       font-size: 13px;
  538.       opacity: 0;
  539.       transform: translateY(8px);
  540.       transition: .2s;
  541.       pointer-events: none;
  542.       z-index: 100;
  543.     }
  544.  
  545.     .toast.show {
  546.       opacity: 1;
  547.       transform: translateY(0);
  548.     }
  549.  
  550.     .mobile { display: none; }
  551.  
  552.     .sound-settings {
  553.       margin-top: 14px;
  554.       padding: 13px;
  555.       border: 1px solid var(--line);
  556.       border-radius: 12px;
  557.       background: #f8faff;
  558.     }
  559.  
  560.     .sound-option {
  561.       display: flex;
  562.       align-items: center;
  563.       gap: 8px;
  564.       font-size: 12px;
  565.       color: #334867;
  566.       margin: 7px 0;
  567.     }
  568.  
  569.     .sound-option input[type="checkbox"] {
  570.       width: 16px;
  571.       height: 16px;
  572.       accent-color: var(--blue);
  573.     }
  574.  
  575.     #soundVolume {
  576.       width: 100%;
  577.       accent-color: var(--blue);
  578.       cursor: pointer;
  579.     }
  580.  
  581.     #testSound { background: #fff4d6; color: #875700; }
  582.     #repeatCall { background: #e8f8f0; color: #087f51; }
  583.  
  584.     button:focus-visible,
  585.     input:focus-visible,
  586.     select:focus-visible {
  587.       outline: 3px solid #60a5fa;
  588.       outline-offset: 2px;
  589.     }
  590.  
  591.     .view { display: none; }
  592.     .view.active { display: block; }
  593.     .view-workspace { display: none; }
  594.     .view-workspace.active { display: grid; }
  595.  
  596.     /* Mini telão no painel */
  597.     .screen-panel {
  598.       background: #07172c;
  599.       color: #fff;
  600.       border-radius: 16px;
  601.       padding: 22px;
  602.       margin-bottom: 22px;
  603.       display: grid;
  604.       grid-template-columns: minmax(0, 1.2fr) minmax(220px, .8fr);
  605.       gap: 20px;
  606.       align-items: center;
  607.     }
  608.  
  609.     .screen-label {
  610.       font-size: 11px;
  611.       letter-spacing: 1.5px;
  612.       color: #9db5d5;
  613.       font-weight: 800;
  614.       text-transform: uppercase;
  615.     }
  616.  
  617.     .screen-current {
  618.       margin-top: 10px;
  619.       background: #102b4d;
  620.       border: 1px solid #24466e;
  621.       border-radius: 14px;
  622.       padding: 20px;
  623.       text-align: center;
  624.     }
  625.  
  626.     .screen-number {
  627.       font-size: clamp(48px, 7vw, 80px);
  628.       line-height: 1;
  629.       font-weight: 900;
  630.       letter-spacing: 5px;
  631.       color: #53b7ff;
  632.       font-variant-numeric: tabular-nums;
  633.     }
  634.  
  635.     .screen-guiche {
  636.       margin-top: 10px;
  637.       font-size: 16px;
  638.       color: #7dd3fc;
  639.       font-weight: 700;
  640.     }
  641.  
  642.     .screen-upcoming {
  643.       display: grid;
  644.       grid-template-columns: repeat(3, minmax(0, 1fr));
  645.       gap: 8px;
  646.       margin-top: 12px;
  647.     }
  648.  
  649.     .screen-upcoming-item {
  650.       background: #122642;
  651.       border: 1px solid #24415f;
  652.       border-radius: 10px;
  653.       padding: 12px 5px;
  654.       text-align: center;
  655.       font-size: clamp(16px, 2vw, 24px);
  656.       font-weight: 850;
  657.       color: #fff;
  658.     }
  659.  
  660.     .screen-empty {
  661.       color: #9db5d5;
  662.       font-size: 13px;
  663.       padding: 12px 0;
  664.     }
  665.  
  666.     .screen-panel .screen-muted {
  667.       color: #9db5d5;
  668.       font-size: 12px;
  669.       margin-top: 8px;
  670.     }
  671.  
  672.     .screen-current.called {
  673.       animation: callPulse 1.1s ease-in-out 2;
  674.       border-color: #53b7ff;
  675.       box-shadow: 0 0 0 3px #53b7ff30;
  676.     }
  677.  
  678.     .screen-number.called-number {
  679.       color: #fff;
  680.       text-shadow: 0 0 22px #39aaff;
  681.     }
  682.  
  683.     @keyframes callPulse {
  684.       0%, 100% { transform: scale(1); }
  685.       50% { transform: scale(1.012); background: #17385f; }
  686.     }
  687.  
  688.     /* ========== TELÃO FULLSCREEN ========== */
  689.     body.mode-telao {
  690.       background: #030b16;
  691.       overflow: hidden;
  692.     }
  693.  
  694.     .telao {
  695.       display: none;
  696.       min-height: 100vh;
  697.       background: radial-gradient(ellipse at center, #0a1a30 0%, #030b16 70%);
  698.       color: white;
  699.       padding: 40px 60px;
  700.       flex-direction: column;
  701.     }
  702.  
  703.     body.mode-telao .telao { display: flex; }
  704.     body.mode-telao .app { display: none; }
  705.     body.mode-telao .kiosk { display: none; }
  706.  
  707.     .telao-header {
  708.       display: flex;
  709.       justify-content: space-between;
  710.       align-items: center;
  711.       margin-bottom: 30px;
  712.     }
  713.  
  714.     .telao-brand {
  715.       display: flex;
  716.       align-items: center;
  717.       gap: 16px;
  718.     }
  719.  
  720.     .telao-brand .logo {
  721.       width: 56px;
  722.       height: 56px;
  723.       font-size: 28px;
  724.     }
  725.  
  726.     .telao-brand h1 {
  727.       font-size: 28px;
  728.       font-weight: 800;
  729.     }
  730.  
  731.     .telao-brand p {
  732.       font-size: 13px;
  733.       color: #6b8ab0;
  734.       letter-spacing: 2px;
  735.     }
  736.  
  737.     .telao-clock {
  738.       font-size: 22px;
  739.       font-weight: 600;
  740.       color: #8ba4c4;
  741.       font-variant-numeric: tabular-nums;
  742.     }
  743.  
  744.     .telao-main {
  745.       flex: 1;
  746.       display: grid;
  747.       grid-template-columns: 1.4fr 1fr;
  748.       gap: 40px;
  749.       align-items: center;
  750.     }
  751.  
  752.     .telao-current {
  753.       background: linear-gradient(145deg, #0d2138, #0a1929);
  754.       border: 2px solid #1e3a5f;
  755.       border-radius: 28px;
  756.       padding: 50px 40px;
  757.       text-align: center;
  758.       position: relative;
  759.       overflow: hidden;
  760.     }
  761.  
  762.     .telao-current::before {
  763.       content: "";
  764.       position: absolute;
  765.       inset: 0;
  766.       background: radial-gradient(circle at 50% 0%, #2563eb22, transparent 60%);
  767.       pointer-events: none;
  768.     }
  769.  
  770.     .telao-label {
  771.       font-size: 16px;
  772.       letter-spacing: 3px;
  773.       color: #6b8ab0;
  774.       font-weight: 800;
  775.       text-transform: uppercase;
  776.       margin-bottom: 20px;
  777.     }
  778.  
  779.     .telao-number {
  780.       font-size: clamp(100px, 16vw, 180px);
  781.       font-weight: 900;
  782.       line-height: 1;
  783.       letter-spacing: 8px;
  784.       color: #53b7ff;
  785.       font-variant-numeric: tabular-nums;
  786.       text-shadow: 0 0 60px #2563eb55;
  787.     }
  788.  
  789.     .telao-guiche {
  790.       margin-top: 24px;
  791.       font-size: 36px;
  792.       font-weight: 800;
  793.       color: #7dd3fc;
  794.       letter-spacing: 2px;
  795.     }
  796.  
  797.     .telao-status {
  798.       margin-top: 12px;
  799.       font-size: 16px;
  800.       color: #6b8ab0;
  801.     }
  802.  
  803.     .telao-current.called {
  804.       animation: telaoPulse 1.2s ease-in-out 3;
  805.       border-color: #53b7ff;
  806.       box-shadow: 0 0 0 4px #53b7ff30, 0 0 80px #2563eb33;
  807.     }
  808.  
  809.     .telao-number.called-number {
  810.       color: #fff;
  811.       text-shadow: 0 0 40px #39aaff, 0 0 80px #2563eb;
  812.     }
  813.  
  814.     @keyframes telaoPulse {
  815.       0%, 100% { transform: scale(1); }
  816.       50% { transform: scale(1.015); background: linear-gradient(145deg, #122a48, #0d2138); }
  817.     }
  818.  
  819.     .telao-side {
  820.       display: flex;
  821.       flex-direction: column;
  822.       gap: 28px;
  823.     }
  824.  
  825.     .telao-box {
  826.       background: #0a1929;
  827.       border: 1px solid #1e3a5f;
  828.       border-radius: 20px;
  829.       padding: 28px;
  830.     }
  831.  
  832.     .telao-box h3 {
  833.       font-size: 14px;
  834.       letter-spacing: 2px;
  835.       color: #6b8ab0;
  836.       text-transform: uppercase;
  837.       margin-bottom: 18px;
  838.     }
  839.  
  840.     .telao-next-list {
  841.       display: grid;
  842.       gap: 12px;
  843.     }
  844.  
  845.     .telao-next-item {
  846.       display: flex;
  847.       align-items: center;
  848.       justify-content: space-between;
  849.       background: #0d2138;
  850.       border: 1px solid #1e3a5f;
  851.       border-radius: 12px;
  852.       padding: 16px 20px;
  853.     }
  854.  
  855.     .telao-next-num {
  856.       font-size: 32px;
  857.       font-weight: 900;
  858.       color: white;
  859.       font-variant-numeric: tabular-nums;
  860.     }
  861.  
  862.     .telao-next-pri {
  863.       font-size: 12px;
  864.       font-weight: 800;
  865.       padding: 4px 10px;
  866.       border-radius: 20px;
  867.     }
  868.  
  869.     .telao-next-pri.alta { background: #3b1218; color: #f87171; }
  870.     .telao-next-pri.media { background: #3b2e0a; color: #fbbf24; }
  871.     .telao-next-pri.baixa { background: #0a2e1c; color: #34d399; }
  872.  
  873.     .telao-empty {
  874.       color: #4a6a8a;
  875.       font-size: 16px;
  876.       text-align: center;
  877.       padding: 20px;
  878.     }
  879.  
  880.     .telao-footer {
  881.       text-align: center;
  882.       margin-top: 30px;
  883.       color: #3a5a7a;
  884.       font-size: 13px;
  885.     }
  886.  
  887.     /* ========== KIOSK ========== */
  888.     body.mode-kiosk {
  889.       background: linear-gradient(160deg, #0c1a32, #102347 50%, #0a1628);
  890.       overflow: hidden;
  891.     }
  892.  
  893.     .kiosk {
  894.       display: none;
  895.       min-height: 100vh;
  896.       flex-direction: column;
  897.       align-items: center;
  898.       justify-content: center;
  899.       padding: 40px 24px;
  900.       color: white;
  901.       text-align: center;
  902.     }
  903.  
  904.     body.mode-kiosk .kiosk { display: flex; }
  905.     body.mode-kiosk .app { display: none; }
  906.     body.mode-kiosk .telao { display: none; }
  907.  
  908.     .kiosk-brand {
  909.       display: flex;
  910.       align-items: center;
  911.       gap: 16px;
  912.       margin-bottom: 40px;
  913.     }
  914.  
  915.     .kiosk-brand .logo {
  916.       width: 60px;
  917.       height: 60px;
  918.       font-size: 30px;
  919.     }
  920.  
  921.     .kiosk-brand h1 {
  922.       font-size: 32px;
  923.       font-weight: 800;
  924.     }
  925.  
  926.     .kiosk-brand p {
  927.       font-size: 14px;
  928.       color: #8ba4c4;
  929.       letter-spacing: 2px;
  930.     }
  931.  
  932.     .kiosk-card {
  933.       background: rgba(255,255,255,0.06);
  934.       border: 1px solid rgba(255,255,255,0.12);
  935.       border-radius: 28px;
  936.       padding: 48px 40px;
  937.       max-width: 520px;
  938.       width: 100%;
  939.       backdrop-filter: blur(12px);
  940.     }
  941.  
  942.     .kiosk-card h2 {
  943.       font-size: 28px;
  944.       margin-bottom: 8px;
  945.       color: white;
  946.     }
  947.  
  948.     .kiosk-card .sub {
  949.       color: #8ba4c4;
  950.       font-size: 15px;
  951.       margin-bottom: 36px;
  952.     }
  953.  
  954.     .kiosk-priorities {
  955.       display: grid;
  956.       gap: 14px;
  957.     }
  958.  
  959.     .kiosk-pri-btn {
  960.       border: 2px solid transparent;
  961.       border-radius: 16px;
  962.       padding: 22px 24px;
  963.       font-size: 18px;
  964.       font-weight: 800;
  965.       display: flex;
  966.       align-items: center;
  967.       justify-content: space-between;
  968.       transition: .2s;
  969.       color: white;
  970.     }
  971.  
  972.     .kiosk-pri-btn.alta {
  973.       background: linear-gradient(135deg, #7f1d1d, #991b1b);
  974.       border-color: #f87171;
  975.     }
  976.  
  977.     .kiosk-pri-btn.media {
  978.       background: linear-gradient(135deg, #1e3a5f, #2563eb);
  979.       border-color: #60a5fa;
  980.     }
  981.  
  982.     .kiosk-pri-btn.baixa {
  983.       background: linear-gradient(135deg, #064e3b, #059669);
  984.       border-color: #34d399;
  985.     }
  986.  
  987.     .kiosk-pri-btn:hover {
  988.       transform: scale(1.03);
  989.       box-shadow: 0 12px 40px #0005;
  990.     }
  991.  
  992.     .kiosk-pri-btn span.hint {
  993.       font-size: 13px;
  994.       font-weight: 600;
  995.       opacity: .7;
  996.     }
  997.  
  998.     .kiosk-result {
  999.       display: none;
  1000.       flex-direction: column;
  1001.       align-items: center;
  1002.       gap: 12px;
  1003.     }
  1004.  
  1005.     .kiosk-result.show { display: flex; }
  1006.  
  1007.     .kiosk-result-label {
  1008.       font-size: 14px;
  1009.       letter-spacing: 2px;
  1010.       color: #8ba4c4;
  1011.       text-transform: uppercase;
  1012.       font-weight: 700;
  1013.     }
  1014.  
  1015.     .kiosk-result-num {
  1016.       font-size: clamp(72px, 18vw, 120px);
  1017.       font-weight: 900;
  1018.       color: #53b7ff;
  1019.       letter-spacing: 6px;
  1020.       font-variant-numeric: tabular-nums;
  1021.       text-shadow: 0 0 40px #2563eb66;
  1022.       line-height: 1;
  1023.     }
  1024.  
  1025.     .kiosk-result-pri {
  1026.       font-size: 16px;
  1027.       font-weight: 700;
  1028.       margin-top: 4px;
  1029.       padding: 6px 14px;
  1030.       border-radius: 20px;
  1031.     }
  1032.  
  1033.     .kiosk-result-pri.alta { background: #3b1218; color: #f87171; }
  1034.     .kiosk-result-pri.media { background: #1e3a5f; color: #60a5fa; }
  1035.     .kiosk-result-pri.baixa { background: #0a2e1c; color: #34d399; }
  1036.  
  1037.     .kiosk-result-msg {
  1038.       font-size: 15px;
  1039.       color: #6b8ab0;
  1040.       margin-top: 8px;
  1041.     }
  1042.  
  1043.     .kiosk-again {
  1044.       margin-top: 28px;
  1045.       background: transparent;
  1046.       border: 2px solid #2a4d78;
  1047.       color: #8ba4c4;
  1048.       border-radius: 12px;
  1049.       padding: 14px 28px;
  1050.       font-size: 14px;
  1051.       font-weight: 700;
  1052.       transition: .2s;
  1053.     }
  1054.  
  1055.     .kiosk-again:hover {
  1056.       border-color: #53b7ff;
  1057.       color: white;
  1058.     }
  1059.  
  1060.     .kiosk-footer {
  1061.       margin-top: 40px;
  1062.       color: #3a5a7a;
  1063.       font-size: 13px;
  1064.     }
  1065.  
  1066.  
  1067.     .priority.preferencial, .pos.preferencial, .telao-next-pri.preferencial,
  1068.     .kiosk-result-pri.preferencial { background: #ffe8ec; color: #ce2d42; }
  1069.     .priority.comum, .pos.comum, .telao-next-pri.comum,
  1070.     .kiosk-result-pri.comum { background: #eaf1ff; color: #2454aa; }
  1071.     .priority.atacado, .pos.atacado, .telao-next-pri.atacado,
  1072.     .kiosk-result-pri.atacado { background: #e2f7ee; color: #087f51; }
  1073.     .kiosk-pri-btn.preferencial { background: linear-gradient(135deg, #7f1d1d, #991b1b); border-color: #f87171; }
  1074.     .kiosk-pri-btn.comum { background: linear-gradient(135deg, #1e3a5f, #2563eb); border-color: #60a5fa; }
  1075.     .kiosk-pri-btn.atacado { background: linear-gradient(135deg, #064e3b, #059669); border-color: #34d399; }
  1076.  
  1077.     /* Responsive */
  1078.     @media (prefers-reduced-motion: reduce) {
  1079.       *, *::before, *::after {
  1080.         animation-duration: .01ms !important;
  1081.         animation-iteration-count: 1 !important;
  1082.         transition: none !important;
  1083.       }
  1084.     }
  1085.  
  1086.     @media (max-width: 1100px) {
  1087.       .app { grid-template-columns: 210px minmax(0, 1fr); }
  1088.       .content { padding: 24px 20px; }
  1089.       .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  1090.       .workspace { grid-template-columns: 1fr; }
  1091.       .right { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  1092.       .telao-main { grid-template-columns: 1fr; }
  1093.     }
  1094.  
  1095.     @media (max-width: 700px) {
  1096.       .app { display: block; }
  1097.  
  1098.       .sidebar {
  1099.         position: fixed;
  1100.         z-index: 10;
  1101.         inset: 0 auto 0 0;
  1102.         width: 255px;
  1103.         transform: translateX(-105%);
  1104.         transition: .25s;
  1105.         box-shadow: 15px 0 45px #0003;
  1106.       }
  1107.  
  1108.       .sidebar.open { transform: translateX(0); }
  1109.  
  1110.       .topbar { height: 60px; padding: 0 15px; }
  1111.  
  1112.       .mobile {
  1113.         display: inline-block;
  1114.         border: 0;
  1115.         background: none;
  1116.         font-size: 21px;
  1117.         color: var(--ink);
  1118.         margin-right: 9px;
  1119.       }
  1120.  
  1121.       .content { padding: 22px 13px; }
  1122.       .heading { align-items: flex-start; }
  1123.       .heading .btn { font-size: 0; padding: 12px; }
  1124.       .heading .btn span { font-size: 20px; }
  1125.       .stats { gap: 9px; }
  1126.       .stat { padding: 12px; display: block; }
  1127.       .stat-icon { width: 38px; height: 38px; margin-bottom: 10px; }
  1128.       .num { font-size: 23px; }
  1129.       .label { font-size: 10px; }
  1130.       .right { grid-template-columns: 1fr; }
  1131.       .card-head { padding: 16px; }
  1132.       .tools { padding: 11px; }
  1133.       .tools select { max-width: 105px; }
  1134.       th, td { padding: 12px; }
  1135.       .role { display: none; }
  1136.       .screen-panel { grid-template-columns: 1fr; padding: 16px; }
  1137.       .screen-number { font-size: 52px; }
  1138.  
  1139.       .telao { padding: 24px 16px; }
  1140.       .telao-number { font-size: 80px; letter-spacing: 4px; }
  1141.       .telao-guiche { font-size: 24px; }
  1142.  
  1143.       .kiosk-card { padding: 32px 20px; }
  1144.       .kiosk-pri-btn { font-size: 16px; padding: 18px; }
  1145.     }
  1146.  
  1147.     /* ========== IMPRESSAO ELGIN TM-20 ========== */
  1148.     .printer-card {
  1149.       margin-top: 14px;
  1150.       padding: 13px;
  1151.       border: 1px solid var(--line);
  1152.       border-radius: 12px;
  1153.       background: #f8faff;
  1154.     }
  1155.  
  1156.     .printer-status {
  1157.       display: flex;
  1158.       align-items: center;
  1159.       gap: 9px;
  1160.       font-size: 12px;
  1161.       color: #334867;
  1162.       font-weight: 700;
  1163.     }
  1164.  
  1165.     .printer-status .printer-dot {
  1166.       width: 9px;
  1167.       height: 9px;
  1168.       border-radius: 50%;
  1169.       background: #159b69;
  1170.       box-shadow: 0 0 0 4px #159b6920;
  1171.     }
  1172.  
  1173.     .printer-info {
  1174.       margin-top: 7px;
  1175.       color: #8794aa;
  1176.       font-size: 10px;
  1177.       line-height: 1.5;
  1178.     }
  1179.  
  1180.     .print-btn {
  1181.       border: 0;
  1182.       background: #edf3ff;
  1183.       color: #2454aa;
  1184.       border-radius: 7px;
  1185.       padding: 7px 9px;
  1186.       font-size: 12px;
  1187.       font-weight: 800;
  1188.       cursor: pointer;
  1189.     }
  1190.  
  1191.     .print-btn:hover { background: #dce8ff; }
  1192.  
  1193.     .kiosk-print {
  1194.       width: 100%;
  1195.       margin-top: 10px;
  1196.       border: 0;
  1197.       border-radius: 12px;
  1198.       padding: 13px;
  1199.       background: #eaf1ff;
  1200.       color: #1d4ed8;
  1201.       font-weight: 850;
  1202.       cursor: pointer;
  1203.     }
  1204.  
  1205.     .kiosk-print:hover { background: #dce8ff; }
  1206.  
  1207.     @media print {
  1208.       body * { visibility: hidden !important; }
  1209.       .print-ticket, .print-ticket * { visibility: visible !important; }
  1210.       .print-ticket {
  1211.         display: block !important;
  1212.         position: absolute;
  1213.         left: 0;
  1214.         top: 0;
  1215.         width: 72mm;
  1216.         margin: 0;
  1217.         padding: 0;
  1218.         color: #000;
  1219.         background: #fff;
  1220.         font-family: Arial, sans-serif;
  1221.         text-align: center;
  1222.       }
  1223.     }
  1224.  
  1225.   </style>
  1226. </head>
  1227. <body>
  1228.   <!-- ========== PAINEL DO ATENDENTE ========== -->
  1229.   <div class="app" id="appPanel">
  1230.     <aside class="sidebar" id="sidebar">
  1231.       <div class="brand">
  1232.         <div class="logo">✓</div>
  1233.         <div>
  1234.           <h1>FilaFácil</h1>
  1235.           <p>GESTÃO DE ATENDIMENTO</p>
  1236.         </div>
  1237.       </div>
  1238.  
  1239.       <div class="guiche-selector">
  1240.         <label for="guicheSelect">Meu guichê</label>
  1241.         <select id="guicheSelect">
  1242.           <option value="1">Guichê 01</option>
  1243.           <option value="2">Guichê 02</option>
  1244.           <option value="3">Guichê 03</option>
  1245.           <option value="4">Guichê 04</option>
  1246.           <option value="5">Guichê 05</option>
  1247.           <option value="6">Guichê 06</option>
  1248.         </select>
  1249.       </div>
  1250.  
  1251.       <nav class="nav" aria-label="Navegação">
  1252.         <button class="active" data-view="inicio"><span>⌂</span>Visão geral</button>
  1253.         <button data-view="novo"><span>+</span>Nova senha</button>
  1254.         <button data-view="fila"><span>♧</span>Fila de atendimento</button>
  1255.         <button data-view="todos"><span>☷</span>Todas as senhas</button>
  1256.       </nav>
  1257.  
  1258.       <div class="side-links">
  1259.         <a href="?view=telao" target="_blank" rel="noopener">🖥 Abrir Telão (TV)</a>
  1260.         <a href="?view=kiosk" target="_blank" rel="noopener">🎫 Abrir Kiosk (cliente)</a>
  1261.       </div>
  1262.  
  1263.       <div class="printer-card">
  1264.       <div class="printer-status">
  1265.         <span class="printer-dot"></span>
  1266.         <span>Elgin TM-20 preparada</span>
  1267.       </div>
  1268.       <p class="printer-info">Impressão térmica em 80 mm. O driver da impressora deve estar instalado no Windows.</p>
  1269.       <button class="btn soft" id="testPrinter" type="button" style="width:100%;margin-top:10px;font-size:11px;padding:9px 10px;">🖨️ Testar impressão</button>
  1270.     </div>
  1271.  
  1272.     <div class="side-bottom">
  1273.         <div class="online"><span class="dot"></span>Sistema online</div>
  1274.         <div id="dateText"></div>
  1275.       </div>
  1276.     </aside>
  1277.  
  1278.     <main>
  1279.       <header class="topbar">
  1280.         <div>
  1281.           <button class="mobile" id="mobileMenu" aria-label="Abrir menu">☰</button>
  1282.           Central de operações / <strong id="breadcrumb">Visão geral</strong>
  1283.         </div>
  1284.         <div>
  1285.           <span class="avatar">AT</span>
  1286.           <span class="role">Atendente · <strong id="topGuiche">Guichê 01</strong></span>
  1287.         </div>
  1288.       </header>
  1289.  
  1290.       <div class="content">
  1291.         <section class="heading">
  1292.           <div>
  1293.             <div class="eyebrow">Painel de controle</div>
  1294.             <h2 id="pageTitle">Fila de Atendimento</h2>
  1295.             <p class="subtitle" id="subtitle">Acompanhe as senhas e organize os atendimentos por prioridade.</p>
  1296.           </div>
  1297.           <button class="btn primary" id="newTop"><span>+</span>Nova senha</button>
  1298.         </section>
  1299.  
  1300.         <section class="stats" aria-label="Resumo">
  1301.           <article class="stat">
  1302.             <div class="stat-icon blue">▣</div>
  1303.             <div>
  1304.               <div class="num" id="total">0</div>
  1305.               <div class="label">Total de senhas</div>
  1306.             </div>
  1307.           </article>
  1308.           <article class="stat">
  1309.             <div class="stat-icon green">♙</div>
  1310.             <div>
  1311.               <div class="num" id="waiting">0</div>
  1312.               <div class="label">Na fila</div>
  1313.             </div>
  1314.           </article>
  1315.           <article class="stat">
  1316.             <div class="stat-icon purple">◷</div>
  1317.             <div>
  1318.               <div class="num" id="serving">0</div>
  1319.               <div class="label">Em atendimento</div>
  1320.             </div>
  1321.           </article>
  1322.           <article class="stat">
  1323.             <div class="stat-icon red">✓</div>
  1324.             <div>
  1325.               <div class="num" id="resolved">0</div>
  1326.               <div class="label">Resolvidos</div>
  1327.             </div>
  1328.           </article>
  1329.         </section>
  1330.  
  1331.         <!-- Mini telão no painel -->
  1332.         <section class="screen-panel" id="screenPanel" aria-label="Prévia do telão">
  1333.           <div>
  1334.             <div class="screen-label">Senha chamada</div>
  1335.             <div class="screen-current">
  1336.               <div class="screen-number" id="screenCurrent">----</div>
  1337.               <div class="screen-guiche" id="screenCurrentGuiche"></div>
  1338.               <div class="screen-muted" id="screenCurrentStatus">O próximo atendimento aparecerá aqui.</div>
  1339.             </div>
  1340.           </div>
  1341.           <div>
  1342.             <div class="screen-label">Próximas senhas</div>
  1343.             <div class="screen-upcoming" id="screenUpcoming"></div>
  1344.             <div class="screen-muted">As senhas são exibidas de 0001 a 9999.</div>
  1345.           </div>
  1346.         </section>
  1347.  
  1348.         <!-- VIEW: Início / Fila -->
  1349.         <section class="workspace view-workspace active" id="viewInicio">
  1350.           <article class="card" id="queueCard">
  1351.             <div class="card-head">
  1352.               <h3>☷ Fila de atendimento</h3>
  1353.               <span class="pill" id="queuePill">0 senhas</span>
  1354.             </div>
  1355.             <div class="tools">
  1356.               <input id="search" type="search" placeholder="Buscar por número..." aria-label="Buscar senha">
  1357.               <select id="filter" aria-label="Filtrar prioridade">
  1358.                 <option value="todas">Tipos de atendimento</option>
  1359.                 <option value="preferencial">Preferencial</option>
  1360.                 <option value="atacado">Atacado</option>
  1361.                 <option value="comum">Comum</option>
  1362.               </select>
  1363.             </div>
  1364.             <div class="table-wrap">
  1365.               <table>
  1366.                 <thead>
  1367.                   <tr>
  1368.                     <th>Pos.</th>
  1369.                     <th>Senha</th>
  1370.                     <th>Prioridade</th>
  1371.                     <th>Status</th>
  1372.                     <th>Ação</th>
  1373.                   </tr>
  1374.                 </thead>
  1375.                 <tbody id="rows"></tbody>
  1376.               </table>
  1377.               <div class="empty" id="empty" hidden>Nenhuma senha na fila.</div>
  1378.             </div>
  1379.           </article>
  1380.  
  1381.           <div class="right">
  1382.             <article class="card">
  1383.               <div class="card-head">
  1384.                 <h3>🏆 Meu atendimento</h3>
  1385.               </div>
  1386.               <div class="inner">
  1387.                 <div class="next" id="nextBox"></div>
  1388.                 <div class="actions">
  1389.                   <button class="btn primary" id="callNext" type="button">▶ Chamar próximo</button>
  1390.                   <button class="btn soft" id="soundToggle" type="button" aria-pressed="true">🔊 Som ligado</button>
  1391.                   <button class="btn soft" id="testSound" type="button">🔔 Testar som</button>
  1392.                   <button class="btn soft" id="repeatCall" type="button">↻ Repetir chamada</button>
  1393.                   <button class="btn success" id="resolve" type="button" disabled>✓ Resolver</button>
  1394.                   <button class="btn danger" id="cancelCurrent" type="button" disabled>✕ Cancelar</button>
  1395.                 </div>
  1396.                 <div class="sound-settings" aria-label="Configurações de áudio">
  1397.                   <label class="sound-option">
  1398.                     <input id="voiceToggle" type="checkbox" checked> Anunciar número da senha por voz
  1399.                   </label>
  1400.                   <label class="sound-option" for="soundVolume">
  1401.                     Volume do alerta <strong id="volumeValue">80%</strong>
  1402.                   </label>
  1403.                   <input id="soundVolume" type="range" min="10" max="100" value="80">
  1404.                   <p class="note">O navegador pode exigir um clique para liberar o som.</p>
  1405.                 </div>
  1406.               </div>
  1407.             </article>
  1408.  
  1409.             <article class="card" id="newCard">
  1410.               <div class="card-head">
  1411.                 <h3>+ Gerar nova senha</h3>
  1412.               </div>
  1413.               <form class="form-body" id="ticketForm">
  1414.                 <div class="field">
  1415.                   <label for="priority">Tipo de atendimento</label>
  1416.                   <select id="priority" required>
  1417.                     <option value="comum">Comum</option>
  1418.                     <option value="atacado">Atacado</option>
  1419.                     <option value="preferencial">Preferencial</option>
  1420.                   </select>
  1421.                 </div>
  1422.                 <button class="btn primary" type="submit">+ Gerar senha</button>
  1423.                 <p class="note">Preferencial tem prioridade de chamada; os demais seguem a ordem definida e a chegada.</p>
  1424.               </form>
  1425.             </article>
  1426.           </div>
  1427.         </section>
  1428.  
  1429.         <!-- VIEW: Todos -->
  1430.         <section class="view" id="viewTodos">
  1431.           <article class="card">
  1432.             <div class="card-head">
  1433.               <h3>☷ Todas as senhas</h3>
  1434.               <div style="display:flex;gap:8px;align-items:center;">
  1435.                 <span class="pill" id="allPill">0 senhas</span>
  1436.                 <button class="btn soft" id="clearResolved" type="button" style="font-size:11px;padding:8px 12px;">
  1437.                   Limpar resolvidos
  1438.                 </button>
  1439.               </div>
  1440.             </div>
  1441.             <div class="tools">
  1442.               <input id="searchAll" type="search" placeholder="Buscar por número..." aria-label="Buscar todas">
  1443.               <select id="filterStatus" aria-label="Filtrar status">
  1444.                 <option value="todos">Todos os status</option>
  1445.                 <option value="fila">Na fila</option>
  1446.                 <option value="atendimento">Em atendimento</option>
  1447.                 <option value="resolvido">Resolvidos</option>
  1448.                 <option value="cancelado">Cancelados</option>
  1449.               </select>
  1450.               <select id="filterPriorityAll" aria-label="Filtrar prioridade">
  1451.                 <option value="todas">Tipos de atendimento</option>
  1452.                 <option value="preferencial">Preferencial</option>
  1453.                 <option value="atacado">Atacado</option>
  1454.                 <option value="comum">Comum</option>
  1455.               </select>
  1456.             </div>
  1457.             <div class="table-wrap">
  1458.               <table>
  1459.                 <thead>
  1460.                   <tr>
  1461.                     <th>Senha</th>
  1462.                     <th>Prioridade</th>
  1463.                     <th>Status</th>
  1464.                     <th>Guichê</th>
  1465.                     <th>Horário</th>
  1466.                     <th>Ações</th>
  1467.                   </tr>
  1468.                 </thead>
  1469.                 <tbody id="rowsAll"></tbody>
  1470.               </table>
  1471.               <div class="empty" id="emptyAll" hidden>Nenhuma senha encontrada.</div>
  1472.             </div>
  1473.           </article>
  1474.         </section>
  1475.  
  1476.         <!-- VIEW: Nova senha -->
  1477.         <section class="view" id="viewNovo">
  1478.           <div style="max-width:420px;margin:0 auto;">
  1479.             <article class="card">
  1480.               <div class="card-head">
  1481.                 <h3>+ Gerar nova senha</h3>
  1482.               </div>
  1483.               <form class="form-body" id="ticketFormNovo">
  1484.                 <div class="field">
  1485.                   <label for="priorityNovo">Tipo de atendimento</label>
  1486.                   <select id="priorityNovo" required>
  1487.                     <option value="comum">Comum</option>
  1488.                     <option value="atacado">Atacado</option>
  1489.                     <option value="preferencial">Preferencial</option>
  1490.                   </select>
  1491.                 </div>
  1492.                 <button class="btn primary" type="submit">+ Gerar senha</button>
  1493.                 <p class="note">Preferencial tem prioridade de chamada; os demais seguem a ordem definida e a chegada.</p>
  1494.               </form>
  1495.             </article>
  1496.           </div>
  1497.         </section>
  1498.  
  1499.         <div class="footer">FilaFácil · Painel de gerenciamento de senhas</div>
  1500.       </div>
  1501.     </main>
  1502.   </div>
  1503.  
  1504.   <!-- ========== TELÃO (TV) ========== -->
  1505.   <div class="telao" id="telaoView">
  1506.     <div class="telao-header">
  1507.       <div class="telao-brand">
  1508.         <div class="logo">✓</div>
  1509.         <div>
  1510.           <h1>FilaFácil</h1>
  1511.           <p>PAINEL DE SENHAS</p>
  1512.         </div>
  1513.       </div>
  1514.       <div class="telao-clock" id="telaoClock">--:--</div>
  1515.     </div>
  1516.  
  1517.     <div class="telao-main">
  1518.       <div class="telao-current" id="telaoCurrent">
  1519.         <div class="telao-label">Senha chamada</div>
  1520.         <div class="telao-number" id="telaoNumber">----</div>
  1521.         <div class="telao-guiche" id="telaoGuiche"></div>
  1522.         <div class="telao-status" id="telaoStatus">Dirija-se ao guichê indicado</div>
  1523.       </div>
  1524.  
  1525.       <div class="telao-side">
  1526.         <div class="telao-box">
  1527.           <h3>Próximas senhas</h3>
  1528.           <div class="telao-next-list" id="telaoNextList">
  1529.             <div class="telao-empty">Nenhuma senha aguardando</div>
  1530.           </div>
  1531.         </div>
  1532.         <div class="telao-box">
  1533.           <h3>Em atendimento agora</h3>
  1534.           <div class="telao-next-list" id="telaoActiveList">
  1535.             <div class="telao-empty">Nenhum atendimento ativo</div>
  1536.           </div>
  1537.         </div>
  1538.       </div>
  1539.     </div>
  1540.  
  1541.     <div class="telao-footer">FilaFácil · Aguarde ser chamado</div>
  1542.   </div>
  1543.  
  1544.   <!-- ========== KIOSK (CLIENTE) ========== -->
  1545.   <div class="kiosk" id="kioskView">
  1546.     <div class="kiosk-brand">
  1547.       <div class="logo">✓</div>
  1548.       <div>
  1549.         <h1>FilaFácil</h1>
  1550.         <p>RETIRE SUA SENHA</p>
  1551.       </div>
  1552.     </div>
  1553.  
  1554.     <div class="kiosk-card">
  1555.       <!-- Escolha de prioridade (gera direto) -->
  1556.       <div id="kioskChoose">
  1557.         <h2>Como podemos ajudar?</h2>
  1558.         <p class="sub">Toque na prioridade para gerar sua senha</p>
  1559.         <div class="kiosk-priorities">
  1560.           <button class="kiosk-pri-btn preferencial" data-pri="preferencial">
  1561.             Preferencial
  1562.             <span class="hint">Atendimento prioritário</span>
  1563.           </button>
  1564.           <button class="kiosk-pri-btn comum" data-pri="comum">
  1565.             Comum
  1566.             <span class="hint">Atendimento padrão</span>
  1567.           </button>
  1568.           <button class="kiosk-pri-btn atacado" data-pri="atacado">
  1569.             Atacado
  1570.             <span class="hint">Compras no atacado</span>
  1571.           </button>
  1572.         </div>
  1573.       </div>
  1574.  
  1575.       <!-- Resultado -->
  1576.       <div class="kiosk-result" id="kioskResult">
  1577.         <div class="kiosk-result-label">Sua senha é</div>
  1578.         <div class="kiosk-result-num" id="kioskNum">----</div>
  1579.         <div class="kiosk-result-pri" id="kioskResultPri"></div>
  1580.         <div class="kiosk-result-msg">Aguarde ser chamado no painel</div>
  1581.         <button class="kiosk-print" id="kioskPrint" type="button">🖨️ Imprimir senha</button>
  1582.         <button class="kiosk-again" id="kioskAgain">Pegar outra senha</button>
  1583.       </div>
  1584.     </div>
  1585.  
  1586.     <div class="kiosk-footer">FilaFácil · Toque na tela para começar</div>
  1587.   </div>
  1588.  
  1589.   <div class="toast" id="toast" role="status" aria-live="polite"></div>
  1590.  
  1591.   <script>
  1592.     // ========== CONFIG & STATE ==========
  1593.    const KEY = "filafacil_tickets_v5";
  1594.     const OLD_KEYS = ["filafacil_tickets_v4", "filafacil_tickets_v3", "filafacil_tickets_v2", "filafacil_tickets_v1"];
  1595.     const MAX_SENHA = 9999;
  1596.     const weight = { preferencial: 0, atacado: 1, comum: 2 };
  1597.     const labels = { preferencial: "Preferencial", atacado: "Atacado", comum: "Comum" };
  1598.  
  1599.     const sample = [
  1600.       { id: 3, priority: "preferencial", status: "fila", time: "14:20", order: 1, senha: 1, guiche: null },
  1601.       { id: 7, priority: "comum", status: "fila", time: "14:25", order: 2, senha: 2, guiche: null },
  1602.       { id: 10, priority: "comum", status: "fila", time: "14:28", order: 3, senha: 3, guiche: null },
  1603.       { id: 5, priority: "atacado", status: "fila", time: "14:31", order: 4, senha: 4, guiche: null },
  1604.       { id: 8, priority: "atacado", status: "fila", time: "14:35", order: 5, senha: 5, guiche: null }
  1605.     ];
  1606.  
  1607.     const $ = id => document.getElementById(id);
  1608.  
  1609.     const params = new URLSearchParams(window.location.search);
  1610.     const viewMode = params.get("view") || "painel";
  1611.  
  1612.     let tickets = carregarTickets();
  1613.     let myGuiche = Number(localStorage.getItem("filafacil_guiche") || 1);
  1614.     let current = null;
  1615.     let toastTimer;
  1616.     let currentView = "inicio";
  1617.  
  1618.     function syncCurrent() {
  1619.       const mine = tickets.find(t => t.status === "atendimento" && Number(t.guiche) === myGuiche);
  1620.       current = mine ? mine.id : null;
  1621.     }
  1622.     syncCurrent();
  1623.  
  1624.     // ========== DATA ==========
  1625.     function carregarTickets() {
  1626.       try {
  1627.         let salvo = localStorage.getItem(KEY);
  1628.         if (!salvo) {
  1629.           for (const k of OLD_KEYS) {
  1630.             salvo = localStorage.getItem(k);
  1631.             if (salvo) break;
  1632.           }
  1633.         }
  1634.         const dadosBrutos = salvo ? JSON.parse(salvo) : sample.map(t => ({ ...t }));
  1635.         if (!Array.isArray(dadosBrutos)) throw new Error("Formato inválido");
  1636.  
  1637.         const dados = dadosBrutos.filter(t => t && typeof t === "object").map(t => ({ ...t }));
  1638.         let proximaSenha = Math.max(0, ...dados.map(t => Number(t.senha) || 0));
  1639.         let proximaOrdem = Math.max(0, ...dados.map(t => Number(t.order) || 0));
  1640.  
  1641.         dados.forEach(t => {
  1642.           if (!Number.isInteger(Number(t.senha)) || Number(t.senha) < 1 || Number(t.senha) > MAX_SENHA) {
  1643.             if (proximaSenha < MAX_SENHA) t.senha = ++proximaSenha;
  1644.            else t.senha = null;
  1645.          } else {
  1646.            t.senha = Number(t.senha);
  1647.          }
  1648.          if (!Number.isFinite(Number(t.order))) t.order = ++proximaOrdem;
  1649.          if (t.status === "aberto") t.status = "fila";
  1650.          if (!["fila", "atendimento", "resolvido", "cancelado"].includes(t.status)) t.status = "fila";
  1651.          if (["alta"].includes(t.priority)) t.priority = "preferencial";
  1652.          else if (["baixa"].includes(t.priority)) t.priority = "atacado";
  1653.          else if (["media"].includes(t.priority)) t.priority = "comum";
  1654.          if (!["preferencial", "atacado", "comum"].includes(t.priority)) t.priority = "comum";
  1655.          if (t.guiche === undefined) t.guiche = null;
  1656.          // Remove campos antigos se existirem
  1657.          delete t.title;
  1658.          delete t.description;
  1659.        });
  1660.        return dados;
  1661.      } catch {
  1662.        return sample.map(t => ({ ...t }));
  1663.       }
  1664.     }
  1665.  
  1666.     function formatarSenha(ticket) {
  1667.       return ticket && Number.isInteger(Number(ticket.senha))
  1668.        ? String(ticket.senha).padStart(4, "0")
  1669.        : "----";
  1670.     }
  1671.  
  1672.     function formatarGuiche(n) {
  1673.       if (!n) return "";
  1674.       return `Guichê ${String(n).padStart(2, "0")}`;
  1675.     }
  1676.  
  1677.     function safe(s) {
  1678.       return String(s ?? "").replace(/[&<>"']/g, c => ({
  1679.        "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"
  1680.       }[c]));
  1681.     }
  1682.  
  1683.     function queue() {
  1684.       return tickets
  1685.         .filter(t => t.status === "fila")
  1686.         .sort((a, b) =>
  1687.           (weight[a.priority] ?? 99) - (weight[b.priority] ?? 99) ||
  1688.           a.order - b.order || a.id - b.id
  1689.         );
  1690.     }
  1691.  
  1692.     function save() {
  1693.       try {
  1694.         localStorage.setItem(KEY, JSON.stringify(tickets));
  1695.       } catch {
  1696.         if (viewMode === "painel") toast("Não foi possível salvar os dados.");
  1697.       }
  1698.     }
  1699.  
  1700.     function toast(message) {
  1701.       const el = $("toast");
  1702.       if (!el) return;
  1703.       el.textContent = message;
  1704.       el.classList.add("show");
  1705.       clearTimeout(toastTimer);
  1706.       toastTimer = setTimeout(() => el.classList.remove("show"), 2800);
  1707.     }
  1708.  
  1709.     // ========== RENDER PAINEL ==========
  1710.     function render() {
  1711.       if (viewMode !== "painel") return;
  1712.  
  1713.       const q = queue();
  1714.       const active = tickets.filter(t => t.status === "atendimento");
  1715.       const done = tickets.filter(t => t.status === "resolvido");
  1716.  
  1717.       $("total").textContent = tickets.length;
  1718.       $("waiting").textContent = q.length;
  1719.       $("serving").textContent = active.length;
  1720.       $("resolved").textContent = done.length;
  1721.       $("queuePill").textContent = `${q.length} ${q.length === 1 ? "senha" : "senhas"}`;
  1722.       $("allPill").textContent = `${tickets.length} senhas`;
  1723.  
  1724.       const term = $("search").value.trim().toLowerCase();
  1725.       const filter = $("filter").value;
  1726.       const visible = q.filter(t =>
  1727.         formatarSenha(t).includes(term) &&
  1728.        (filter === "todas" || t.priority === filter)
  1729.      );
  1730.  
  1731.       $("rows").innerHTML = visible.map((t, i) => `
  1732.         <tr>
  1733.           <td><span class="pos ${safe(t.priority)}">${i + 1}</span></td>
  1734.           <td><span class="id">${formatarSenha(t)}</span></td>
  1735.           <td><span class="priority ${safe(t.priority)}">${labels[t.priority] || "Média"}</span></td>
  1736.           <td><span class="status">Na fila</span></td>
  1737.           <td>
  1738.             <div class="row-actions">
  1739.               <button class="row-action" data-call="${t.id}">➜ Chamar</button>
  1740.               <button class="row-action danger" data-cancel="${t.id}">✕</button>
  1741.             </div>
  1742.           </td>
  1743.         </tr>
  1744.       `).join("");
  1745.       $("empty").hidden = visible.length > 0;
  1746.  
  1747.       syncCurrent();
  1748.       const now = tickets.find(t => t.id === current && t.status === "atendimento");
  1749.       const next = q[0];
  1750.       const display = now || next;
  1751.  
  1752.       $("nextBox").innerHTML = display
  1753.         ? `<div class="next-num" style="${now ? "background:var(--blue)" : ""}">${formatarSenha(display)}</div>
  1754.            <div class="next-info">
  1755.              <strong>${now ? "Em atendimento" : "Próxima senha"}</strong>
  1756.              <p>${now ? formatarGuiche(myGuiche) : "Aguardando chamada"}</p>
  1757.            </div>
  1758.            <span class="priority ${safe(display.priority)}">${labels[display.priority] || "Média"}</span>`
  1759.         : `<div class="next-num" style="background:#91a0b8">—</div>
  1760.            <div class="next-info"><strong>Fila vazia</strong><p>Gere uma senha ou aguarde o kiosk.</p></div>`;
  1761.  
  1762.       const lastCalled = tickets
  1763.         .filter(t => t.status === "atendimento")
  1764.         .sort((a, b) => (b.calledAt || 0) - (a.calledAt || 0))[0];
  1765.  
  1766.       if (lastCalled) {
  1767.         $("screenCurrent").textContent = formatarSenha(lastCalled);
  1768.         $("screenCurrentGuiche").textContent = formatarGuiche(lastCalled.guiche);
  1769.         $("screenCurrentStatus").textContent = "Dirija-se ao atendimento.";
  1770.       } else {
  1771.         $("screenCurrent").textContent = "----";
  1772.         $("screenCurrentGuiche").textContent = "";
  1773.         $("screenCurrentStatus").textContent = "Nenhuma senha está sendo atendida.";
  1774.       }
  1775.  
  1776.       const upcoming = q.slice(0, 3);
  1777.       $("screenUpcoming").innerHTML = upcoming.length
  1778.         ? upcoming.map(t => `<div class="screen-upcoming-item">${formatarSenha(t)}</div>`).join("")
  1779.         : '<div class="screen-empty">Nenhuma senha aguardando.</div>';
  1780.  
  1781.       $("callNext").disabled = !next || !!now;
  1782.       $("callNext").style.opacity = (!next || !!now) ? ".55" : "1";
  1783.       $("resolve").disabled = !now;
  1784.       $("resolve").style.opacity = now ? "1" : ".55";
  1785.       $("cancelCurrent").disabled = !now;
  1786.       $("cancelCurrent").style.opacity = now ? "1" : ".55";
  1787.  
  1788.       renderAllTable();
  1789.       save();
  1790.     }
  1791.  
  1792.     function renderAllTable() {
  1793.       if (viewMode !== "painel") return;
  1794.       const term = $("searchAll").value.trim().toLowerCase();
  1795.       const statusFilter = $("filterStatus").value;
  1796.       const priorityFilter = $("filterPriorityAll").value;
  1797.  
  1798.       const list = tickets
  1799.         .filter(t => {
  1800.           const matchTerm = formatarSenha(t).includes(term);
  1801.           const matchStatus = statusFilter === "todos" || t.status === statusFilter;
  1802.           const matchPriority = priorityFilter === "todas" || t.priority === priorityFilter;
  1803.           return matchTerm && matchStatus && matchPriority;
  1804.         })
  1805.         .sort((a, b) => b.order - a.order);
  1806.  
  1807.       const statusLabel = {
  1808.         fila: "Na fila",
  1809.         atendimento: "Em atendimento",
  1810.         resolvido: "Resolvido",
  1811.         cancelado: "Cancelado"
  1812.       };
  1813.  
  1814.       $("rowsAll").innerHTML = list.map(t => {
  1815.         let actions = "";
  1816.         if (t.status === "fila") {
  1817.           actions = `
  1818.             <button class="row-action" data-call="${t.id}">➜ Chamar</button>
  1819.             <button class="print-btn" data-print="${t.id}" title="Imprimir senha na Elgin TM-20">🖨️</button>
  1820.             <button class="row-action danger" data-cancel="${t.id}">✕</button>`;
  1821.         } else if (t.status === "atendimento") {
  1822.           actions = `
  1823.             <button class="row-action success" data-resolve="${t.id}">✓ Resolver</button>
  1824.             <button class="row-action" data-requeue="${t.id}">↩ Fila</button>
  1825.             <button class="print-btn" data-print="${t.id}" title="Imprimir senha na Elgin TM-20">🖨️</button>
  1826.             <button class="row-action danger" data-cancel="${t.id}">✕</button>`;
  1827.         } else {
  1828.           actions = `
  1829.             <button class="row-action" data-requeue="${t.id}">↩ Reabrir</button>
  1830.             <button class="print-btn" data-print="${t.id}" title="Imprimir senha na Elgin TM-20">🖨️</button>`;
  1831.         }
  1832.  
  1833.         return `
  1834.           <tr>
  1835.             <td><span class="id">${formatarSenha(t)}</span></td>
  1836.             <td><span class="priority ${safe(t.priority)}">${labels[t.priority] || "Média"}</span></td>
  1837.             <td><span class="status ${safe(t.status)}">${statusLabel[t.status] || t.status}</span></td>
  1838.             <td>${t.guiche ? `<span class="guiche-badge">${formatarGuiche(t.guiche)}</span>` : "—"}</td>
  1839.             <td>${safe(t.time || "—")}</td>
  1840.             <td><div class="row-actions">${actions}</div></td>
  1841.           </tr>`;
  1842.       }).join("");
  1843.  
  1844.       $("emptyAll").hidden = list.length > 0;
  1845.     }
  1846.  
  1847.     // ========== RENDER TELÃO ==========
  1848.     function renderTelao() {
  1849.       if (viewMode !== "telao") return;
  1850.  
  1851.       const q = queue();
  1852.       const active = tickets
  1853.         .filter(t => t.status === "atendimento")
  1854.         .sort((a, b) => (b.calledAt || 0) - (a.calledAt || 0));
  1855.  
  1856.       const last = active[0];
  1857.       const numEl = $("telaoNumber");
  1858.       const curEl = $("telaoCurrent");
  1859.  
  1860.       if (last) {
  1861.         const prevNum = numEl.textContent;
  1862.         const newNum = formatarSenha(last);
  1863.         numEl.textContent = newNum;
  1864.         $("telaoGuiche").textContent = formatarGuiche(last.guiche);
  1865.         $("telaoStatus").textContent = "Dirija-se ao guichê indicado";
  1866.  
  1867.         if (prevNum !== newNum && prevNum !== "----") {
  1868.          curEl.classList.remove("called");
  1869.           numEl.classList.remove("called-number");
  1870.           void curEl.offsetWidth;
  1871.           curEl.classList.add("called");
  1872.           numEl.classList.add("called-number");
  1873.         }
  1874.       } else {
  1875.         numEl.textContent = "----";
  1876.         $("telaoGuiche").textContent = "";
  1877.         $("telaoStatus").textContent = "Aguarde ser chamado";
  1878.       }
  1879.  
  1880.       const nextList = $("telaoNextList");
  1881.       if (q.length === 0) {
  1882.         nextList.innerHTML = '<div class="telao-empty">Nenhuma senha aguardando</div>';
  1883.       } else {
  1884.         nextList.innerHTML = q.slice(0, 5).map(t => `
  1885.           <div class="telao-next-item">
  1886.             <span class="telao-next-num">${formatarSenha(t)}</span>
  1887.             <span class="telao-next-pri ${safe(t.priority)}">${labels[t.priority] || "Média"}</span>
  1888.           </div>
  1889.         `).join("");
  1890.       }
  1891.  
  1892.       const activeList = $("telaoActiveList");
  1893.       if (active.length === 0) {
  1894.         activeList.innerHTML = '<div class="telao-empty">Nenhum atendimento ativo</div>';
  1895.       } else {
  1896.         activeList.innerHTML = active.map(t => `
  1897.           <div class="telao-next-item">
  1898.             <span class="telao-next-num">${formatarSenha(t)}</span>
  1899.             <span style="color:#7dd3fc;font-weight:700;font-size:14px;">${formatarGuiche(t.guiche)}</span>
  1900.           </div>
  1901.         `).join("");
  1902.       }
  1903.  
  1904.       $("telaoClock").textContent = new Intl.DateTimeFormat("pt-BR", {
  1905.         hour: "2-digit", minute: "2-digit", second: "2-digit"
  1906.       }).format(new Date());
  1907.     }
  1908.  
  1909.     // ========== SOM ==========
  1910.     let somAtivo = localStorage.getItem("filafacil_som") !== "off";
  1911.     let audioContext = null;
  1912.     let volumeSom = Number(localStorage.getItem("filafacil_volume") || 80) / 100;
  1913.     let vozAtiva = localStorage.getItem("filafacil_voz") !== "off";
  1914.  
  1915.     function atualizarBotaoSom() {
  1916.       if (viewMode !== "painel") return;
  1917.       const botao = $("soundToggle");
  1918.       if (!botao) return;
  1919.       botao.textContent = somAtivo ? "🔊 Som ligado" : "🔇 Som desligado";
  1920.       botao.setAttribute("aria-pressed", String(somAtivo));
  1921.       $("soundVolume").value = Math.round(volumeSom * 100);
  1922.       $("volumeValue").textContent = `${Math.round(volumeSom * 100)}%`;
  1923.       $("voiceToggle").checked = vozAtiva;
  1924.     }
  1925.  
  1926.     function tocarTom(ctx, frequencia, inicio, duracao, ganho, tipo = "sine") {
  1927.       const oscilador = ctx.createOscillator();
  1928.       const envelope = ctx.createGain();
  1929.       oscilador.type = tipo;
  1930.       oscilador.frequency.setValueAtTime(frequencia, inicio);
  1931.       envelope.gain.setValueAtTime(0.0001, inicio);
  1932.       envelope.gain.exponentialRampToValueAtTime(Math.max(0.001, ganho), inicio + 0.025);
  1933.       envelope.gain.exponentialRampToValueAtTime(0.0001, inicio + duracao);
  1934.       oscilador.connect(envelope);
  1935.       envelope.connect(ctx.destination);
  1936.       oscilador.start(inicio);
  1937.       oscilador.stop(inicio + duracao + 0.03);
  1938.     }
  1939.  
  1940.     function falarSenha(ticket, demonstracao = false) {
  1941.       if (!vozAtiva || !("speechSynthesis" in window)) return;
  1942.       try {
  1943.         window.speechSynthesis.cancel();
  1944.         let texto;
  1945.         if (demonstracao || !ticket) {
  1946.           texto = "Atenção, chamada de senha. Dirija-se ao atendimento.";
  1947.         } else {
  1948.           const g = ticket.guiche ? ` guichê ${ticket.guiche}` : "";
  1949.           texto = `Atenção. Senha ${String(ticket.senha).split("").join(" ")}. Dirija-se ao${g}.`;
  1950.         }
  1951.         const fala = new SpeechSynthesisUtterance(texto);
  1952.         fala.lang = "pt-BR";
  1953.         fala.rate = 0.88;
  1954.         fala.pitch = 1.0;
  1955.         fala.volume = Math.max(0.1, Math.min(1, volumeSom));
  1956.         const vozes = window.speechSynthesis.getVoices();
  1957.         const vozPt = vozes.find(v => v.lang && v.lang.toLowerCase().startsWith("pt-br"))
  1958.          || vozes.find(v => v.lang && v.lang.toLowerCase().startsWith("pt"));
  1959.         if (vozPt) fala.voice = vozPt;
  1960.         window.speechSynthesis.speak(fala);
  1961.       } catch (erro) {
  1962.         console.warn("Voz indisponível", erro);
  1963.       }
  1964.     }
  1965.  
  1966.     function tocarSomChamada(ticket = null, demonstracao = false) {
  1967.       if (!somAtivo && viewMode === "painel") return;
  1968.       try {
  1969.         const AudioContextClass = window.AudioContext || window.webkitAudioContext;
  1970.         if (AudioContextClass) {
  1971.           audioContext = audioContext || new AudioContextClass();
  1972.           const tocar = () => {
  1973.             const ctx = audioContext;
  1974.             const agora = ctx.currentTime + 0.04;
  1975.             const base = 0.22 * (viewMode === "telao" ? 0.9 : volumeSom);
  1976.             const notas = [
  1977.               { f: 660, t: 0.00, d: 0.25 },
  1978.               { f: 880, t: 0.24, d: 0.28 },
  1979.               { f: 1175, t: 0.52, d: 0.42 }
  1980.             ];
  1981.             notas.forEach(n => {
  1982.               tocarTom(ctx, n.f, agora + n.t, n.d, base, "triangle");
  1983.               tocarTom(ctx, n.f * 2, agora + n.t, n.d * 0.65, base * 0.22, "sine");
  1984.             });
  1985.             if (vozAtiva || viewMode === "telao") {
  1986.               window.setTimeout(() => falarSenha(ticket, demonstracao), 780);
  1987.             }
  1988.           };
  1989.           if (audioContext.state === "suspended") {
  1990.             audioContext.resume().then(tocar).catch(() => {});
  1991.           } else {
  1992.             tocar();
  1993.           }
  1994.         } else {
  1995.           falarSenha(ticket, demonstracao);
  1996.         }
  1997.       } catch (erro) {
  1998.         falarSenha(ticket, demonstracao);
  1999.       }
  2000.     }
  2001.  
  2002.     function destacarTelãoMini() {
  2003.       if (viewMode !== "painel") return;
  2004.       const painel = document.querySelector(".screen-current");
  2005.       const numero = $("screenCurrent");
  2006.       if (!painel || !numero) return;
  2007.       painel.classList.remove("called");
  2008.       numero.classList.remove("called-number");
  2009.       void painel.offsetWidth;
  2010.       painel.classList.add("called");
  2011.       numero.classList.add("called-number");
  2012.     }
  2013.  
  2014.     // ========== AÇÕES ==========
  2015.     function call(id = null) {
  2016.       if (tickets.some(t => t.status === "atendimento" && Number(t.guiche) === myGuiche)) {
  2017.        toast("Resolva ou cancele a senha atual do seu guichê antes de chamar outra.");
  2018.         return;
  2019.       }
  2020.  
  2021.       const ticket = id
  2022.         ? tickets.find(t => t.id === Number(id) && t.status === "fila")
  2023.        : queue()[0];
  2024.  
  2025.       if (!ticket) {
  2026.         toast("Não há senhas aguardando atendimento.");
  2027.         return;
  2028.       }
  2029.  
  2030.       ticket.status = "atendimento";
  2031.       ticket.guiche = myGuiche;
  2032.       ticket.calledAt = Date.now();
  2033.       current = ticket.id;
  2034.       render();
  2035.       destacarTelãoMini();
  2036.       tocarSomChamada(ticket);
  2037.       toast(`Senha ${formatarSenha(ticket)} chamada no ${formatarGuiche(myGuiche)}.`);
  2038.     }
  2039.  
  2040.     function resolveTicket(id = null) {
  2041.       const ticket = id
  2042.         ? tickets.find(t => t.id === Number(id))
  2043.         : tickets.find(t => t.id === current && t.status === "atendimento");
  2044.  
  2045.       if (!ticket || ticket.status !== "atendimento") {
  2046.         toast("Não há senha em atendimento para resolver.");
  2047.         return;
  2048.       }
  2049.  
  2050.       ticket.status = "resolvido";
  2051.       if (ticket.id === current) current = null;
  2052.       render();
  2053.       toast(`Senha ${formatarSenha(ticket)} resolvida.`);
  2054.     }
  2055.  
  2056.     function cancelTicket(id) {
  2057.       const ticket = tickets.find(t => t.id === Number(id));
  2058.       if (!ticket) return;
  2059.       if (ticket.status === "resolvido") {
  2060.         toast("Esta senha já está resolvida.");
  2061.         return;
  2062.       }
  2063.       ticket.status = "cancelado";
  2064.       ticket.guiche = null;
  2065.       if (ticket.id === current) current = null;
  2066.       render();
  2067.       toast(`Senha ${formatarSenha(ticket)} cancelada.`);
  2068.     }
  2069.  
  2070.     function requeueTicket(id) {
  2071.       const ticket = tickets.find(t => t.id === Number(id));
  2072.       if (!ticket) return;
  2073.       ticket.status = "fila";
  2074.       ticket.guiche = null;
  2075.       ticket.calledAt = null;
  2076.       if (ticket.id === current) current = null;
  2077.       render();
  2078.       toast(`Senha ${formatarSenha(ticket)} devolvida à fila.`);
  2079.     }
  2080.  
  2081.     // ========== IMPRESSORA ELGIN TM-20 ==========
  2082.     const IMPRESSORA = {
  2083.       nome: "Elgin TM-20",
  2084.       largura: "80mm"
  2085.     };
  2086.  
  2087.     function escaparImpressao(valor) {
  2088.       return String(valor ?? "")
  2089.         .replace(/&/g, "&amp;")
  2090.         .replace(/</g, "&lt;")
  2091.        .replace(/>/g, "&gt;")
  2092.         .replace(/"/g, "&quot;")
  2093.         .replace(/'/g, "&#039;");
  2094.     }
  2095.  
  2096.     function imprimirSenha(ticket) {
  2097.       if (!ticket) {
  2098.         toast("Senha não encontrada para impressão.");
  2099.         return;
  2100.       }
  2101.  
  2102.       const janela = window.open("", "_blank", "width=420,height=650");
  2103.       if (!janela) {
  2104.         toast("Permita pop-ups para imprimir a senha.");
  2105.         return;
  2106.       }
  2107.  
  2108.       const numero = escaparImpressao(formatarSenha(ticket));
  2109.       const tipo = escaparImpressao(labels[ticket.priority] || "Comum");
  2110.       const horario = escaparImpressao(ticket.time || new Intl.DateTimeFormat("pt-BR", {
  2111.         hour: "2-digit", minute: "2-digit"
  2112.       }).format(new Date()));
  2113.  
  2114.       janela.document.write(`<!DOCTYPE html>
  2115. <html lang="pt-BR">
  2116. <head>
  2117. <meta charset="UTF-8">
  2118. <title>${numero} - ${escaparImpressao(IMPRESSORA.nome)}</title>
  2119. <style>
  2120. @page { size: 80mm auto; margin: 0; }
  2121. * { box-sizing: border-box; }
  2122. html, body { margin: 0; padding: 0; background: #fff; }
  2123. body { width: 80mm; font-family: Arial, sans-serif; color: #000; text-align: center; }
  2124. .ticket { width: 72mm; margin: 0 auto; padding: 5mm 0 7mm; }
  2125. .brand { font-size: 17px; font-weight: 900; letter-spacing: .5px; }
  2126. .subtitle { margin-top: 2mm; font-size: 9px; font-weight: 700; letter-spacing: 1.4px; }
  2127. .line { border-top: 1px dashed #000; margin: 4mm 0; }
  2128. .label { font-size: 11px; font-weight: 700; }
  2129. .number { margin: 2mm 0; font-size: 38px; line-height: 1; font-weight: 900; letter-spacing: 2px; }
  2130. .type { font-size: 16px; font-weight: 900; }
  2131. .time { margin-top: 3mm; font-size: 10px; }
  2132. .footer { margin-top: 5mm; font-size: 9px; }
  2133. </style>
  2134. </head>
  2135. <body>
  2136.   <main class="ticket">
  2137.     <div class="brand">FilaFácil</div>
  2138.     <div class="subtitle">SENHA DE ATENDIMENTO</div>
  2139.     <div class="line"></div>
  2140.     <div class="label">SUA SENHA</div>
  2141.     <div class="number">${numero}</div>
  2142.     <div class="type">${tipo}</div>
  2143.     <div class="time">Emitida às ${horario}</div>
  2144.     <div class="line"></div>
  2145.     <div class="footer">Aguarde sua chamada no painel.</div>
  2146.   </main>
  2147. <script>
  2148. window.addEventListener('load', () => {
  2149.   setTimeout(() => { window.print(); }, 250);
  2150. });
  2151. window.addEventListener('afterprint', () => {
  2152.   setTimeout(() => window.close(), 150);
  2153. });
  2154. <\/script>
  2155. </body>
  2156. </html>`);
  2157.       janela.document.close();
  2158.     }
  2159.  
  2160.     function testarImpressora() {
  2161.       const teste = { senha: "0001", priority: "comum", time: new Intl.DateTimeFormat("pt-BR", {
  2162.         hour: "2-digit", minute: "2-digit"
  2163.       }).format(new Date()) };
  2164.       imprimirSenha(teste);
  2165.       toast(`Teste preparado para ${IMPRESSORA.nome}.`);
  2166.     }
  2167.  
  2168.     function criarTicket(priority, formEl) {
  2169.       if (!Object.prototype.hasOwnProperty.call(weight, priority)) {
  2170.         priority = "comum";
  2171.       }
  2172.  
  2173.       const usadas = new Set(tickets.map(t => Number(t.senha)).filter(n => n >= 1 && n <= MAX_SENHA));
  2174.       let senha = 1;
  2175.       while (senha <= MAX_SENHA && usadas.has(senha)) senha++;
  2176.  
  2177.      if (senha > MAX_SENHA) {
  2178.         if (viewMode === "painel") toast("Todas as senhas de 0001 a 9999 já foram utilizadas.");
  2179.         return null;
  2180.       }
  2181.  
  2182.       const id = tickets.reduce((max, t) => Math.max(max, Number(t.id) || 0), 0) + 1;
  2183.       const order = tickets.reduce((max, t) => Math.max(max, Number(t.order) || 0), 0) + 1;
  2184.       const time = new Intl.DateTimeFormat("pt-BR", {
  2185.         hour: "2-digit", minute: "2-digit"
  2186.       }).format(new Date());
  2187.  
  2188.       const novo = {
  2189.         id, senha, priority,
  2190.         status: "fila", time, order, guiche: null, calledAt: null
  2191.       };
  2192.  
  2193.       tickets.push(novo);
  2194.       if (formEl) formEl.reset();
  2195.       if (viewMode === "painel") {
  2196.         $("filter").value = "todas";
  2197.         $("search").value = "";
  2198.         render();
  2199.         toast(`Senha ${formatarSenha(novo)} gerada com sucesso.`);
  2200.       } else {
  2201.         save();
  2202.       }
  2203.       return novo;
  2204.     }
  2205.  
  2206.     // ========== KIOSK ==========
  2207.     function initKiosk() {
  2208.       document.body.classList.add("mode-kiosk");
  2209.       document.title = "FilaFácil · Retire sua senha";
  2210.  
  2211.       const choose = $("kioskChoose");
  2212.       const result = $("kioskResult");
  2213.  
  2214.       document.querySelectorAll(".kiosk-pri-btn").forEach(btn => {
  2215.         btn.addEventListener("click", () => {
  2216.           const priority = btn.dataset.pri;
  2217.           const novo = criarTicket(priority, null);
  2218.           if (!novo) return;
  2219.  
  2220.           choose.style.display = "none";
  2221.           result.classList.add("show");
  2222.           $("kioskNum").textContent = formatarSenha(novo);
  2223.           const priEl = $("kioskResultPri");
  2224.           priEl.textContent = labels[novo.priority];
  2225.           priEl.className = "kiosk-result-pri " + novo.priority;
  2226.           $("kioskPrint").onclick = () => imprimirSenha(novo);
  2227.         });
  2228.       });
  2229.  
  2230.       $("kioskAgain").addEventListener("click", () => {
  2231.         result.classList.remove("show");
  2232.         choose.style.display = "";
  2233.       });
  2234.     }
  2235.  
  2236.     // ========== TELÃO ==========
  2237.     function initTelao() {
  2238.       document.body.classList.add("mode-telao");
  2239.       document.title = "FilaFácil · Telão de Senhas";
  2240.       renderTelao();
  2241.  
  2242.       setInterval(() => {
  2243.         tickets = carregarTickets();
  2244.         renderTelao();
  2245.       }, 1000);
  2246.  
  2247.       window.addEventListener("storage", e => {
  2248.         if (e.key === KEY) {
  2249.           tickets = carregarTickets();
  2250.           renderTelao();
  2251.         }
  2252.       });
  2253.  
  2254.       document.body.addEventListener("click", () => {
  2255.         if (audioContext && audioContext.state === "suspended") {
  2256.          audioContext.resume();
  2257.         }
  2258.       }, { once: true });
  2259.     }
  2260.  
  2261.     // ========== PAINEL ==========
  2262.     function initPainel() {
  2263.       $("guicheSelect").value = String(myGuiche);
  2264.       const testPrinter = $("testPrinter");
  2265.       if (testPrinter) testPrinter.addEventListener("click", testarImpressora);
  2266.       $("topGuiche").textContent = formatarGuiche(myGuiche);
  2267.  
  2268.       $("guicheSelect").addEventListener("change", e => {
  2269.         myGuiche = Number(e.target.value);
  2270.         localStorage.setItem("filafacil_guiche", String(myGuiche));
  2271.         $("topGuiche").textContent = formatarGuiche(myGuiche);
  2272.         syncCurrent();
  2273.         render();
  2274.         toast(`Você está no ${formatarGuiche(myGuiche)}.`);
  2275.       });
  2276.  
  2277.       $("ticketForm").addEventListener("submit", e => {
  2278.         e.preventDefault();
  2279.         criarTicket($("priority").value, e.target);
  2280.       });
  2281.  
  2282.       $("ticketFormNovo").addEventListener("submit", e => {
  2283.         e.preventDefault();
  2284.         const ok = criarTicket($("priorityNovo").value, e.target);
  2285.         if (ok) setTimeout(() => switchView("inicio"), 600);
  2286.       });
  2287.  
  2288.       $("callNext").addEventListener("click", () => call());
  2289.       $("resolve").addEventListener("click", () => resolveTicket());
  2290.       $("cancelCurrent").addEventListener("click", () => {
  2291.         if (current) cancelTicket(current);
  2292.       });
  2293.  
  2294.       $("soundToggle").addEventListener("click", () => {
  2295.         somAtivo = !somAtivo;
  2296.         localStorage.setItem("filafacil_som", somAtivo ? "on" : "off");
  2297.         atualizarBotaoSom();
  2298.         if (somAtivo) tocarSomChamada(null, true);
  2299.         else if ("speechSynthesis" in window) window.speechSynthesis.cancel();
  2300.         toast(somAtivo ? "Som de chamada ativado." : "Som de chamada desativado.");
  2301.       });
  2302.  
  2303.       $("testSound").addEventListener("click", () => {
  2304.         if (!somAtivo) {
  2305.           somAtivo = true;
  2306.           localStorage.setItem("filafacil_som", "on");
  2307.           atualizarBotaoSom();
  2308.         }
  2309.         tocarSomChamada(null, true);
  2310.         toast("Teste de áudio iniciado.");
  2311.       });
  2312.  
  2313.       $("repeatCall").addEventListener("click", () => {
  2314.         const ticket = tickets.find(t => t.id === current && t.status === "atendimento");
  2315.         if (!ticket) {
  2316.           toast("Não há senha atual para repetir.");
  2317.           return;
  2318.         }
  2319.         tocarSomChamada(ticket);
  2320.         destacarTelãoMini();
  2321.         toast(`Repetindo chamada da senha ${formatarSenha(ticket)}.`);
  2322.       });
  2323.  
  2324.       $("soundVolume").addEventListener("input", e => {
  2325.         volumeSom = Number(e.target.value) / 100;
  2326.         localStorage.setItem("filafacil_volume", String(Math.round(volumeSom * 100)));
  2327.         $("volumeValue").textContent = `${Math.round(volumeSom * 100)}%`;
  2328.       });
  2329.  
  2330.       $("voiceToggle").addEventListener("change", e => {
  2331.         vozAtiva = e.target.checked;
  2332.         localStorage.setItem("filafacil_voz", vozAtiva ? "on" : "off");
  2333.         if (!vozAtiva && "speechSynthesis" in window) window.speechSynthesis.cancel();
  2334.         toast(vozAtiva ? "Anúncio por voz ativado." : "Anúncio por voz desativado.");
  2335.       });
  2336.  
  2337.       $("clearResolved").addEventListener("click", () => {
  2338.         const qtd = tickets.filter(t => t.status === "resolvido" || t.status === "cancelado").length;
  2339.         if (qtd === 0) {
  2340.           toast("Não há senhas resolvidas ou canceladas para limpar.");
  2341.           return;
  2342.         }
  2343.         if (!confirm(`Remover ${qtd} senha(s) resolvidas/canceladas?`)) return;
  2344.         tickets = tickets.filter(t => t.status === "fila" || t.status === "atendimento");
  2345.         render();
  2346.         toast(`${qtd} senha(s) removidas.`);
  2347.       });
  2348.  
  2349.       $("rows").addEventListener("click", e => {
  2350.         const callBtn = e.target.closest("[data-call]");
  2351.         const cancelBtn = e.target.closest("[data-cancel]");
  2352.         if (callBtn) call(callBtn.dataset.call);
  2353.         if (cancelBtn) cancelTicket(cancelBtn.dataset.cancel);
  2354.       });
  2355.  
  2356.       $("rowsAll").addEventListener("click", e => {
  2357.         const callBtn = e.target.closest("[data-call]");
  2358.         const cancelBtn = e.target.closest("[data-cancel]");
  2359.         const resolveBtn = e.target.closest("[data-resolve]");
  2360.         const requeueBtn = e.target.closest("[data-requeue]");
  2361.         if (callBtn) call(callBtn.dataset.call);
  2362.         if (cancelBtn) cancelTicket(cancelBtn.dataset.cancel);
  2363.         if (resolveBtn) resolveTicket(resolveBtn.dataset.resolve);
  2364.         if (requeueBtn) requeueTicket(requeueBtn.dataset.requeue);
  2365.         const printBtn = e.target.closest("[data-print]");
  2366.         if (printBtn) {
  2367.           const ticket = tickets.find(t => t.id === Number(printBtn.dataset.print));
  2368.           imprimirSenha(ticket);
  2369.         }
  2370.       });
  2371.  
  2372.       $("search").addEventListener("input", render);
  2373.       $("filter").addEventListener("change", render);
  2374.       $("searchAll").addEventListener("input", renderAllTable);
  2375.       $("filterStatus").addEventListener("change", renderAllTable);
  2376.       $("filterPriorityAll").addEventListener("change", renderAllTable);
  2377.  
  2378.       $("newTop").addEventListener("click", () => switchView("novo"));
  2379.       $("mobileMenu").addEventListener("click", () => $("sidebar").classList.toggle("open"));
  2380.  
  2381.       const navTitles = {
  2382.         inicio: ["Visão geral", "Fila de Atendimento", "Acompanhe as senhas e organize os atendimentos por prioridade."],
  2383.         novo: ["Nova senha", "Gerar nova senha", "Escolha a prioridade e gere uma senha de atendimento."],
  2384.         fila: ["Fila de atendimento", "Fila de Atendimento", "Senhas aguardando atendimento, ordenadas por prioridade e chegada."],
  2385.         todos: ["Todas as senhas", "Todas as Senhas", "Resumo de senhas na fila, em atendimento, resolvidas e canceladas."]
  2386.       };
  2387.  
  2388.       function switchView(view) {
  2389.         currentView = view;
  2390.         document.querySelectorAll(".nav button").forEach(btn => {
  2391.           btn.classList.toggle("active", btn.dataset.view === view);
  2392.         });
  2393.         $("sidebar").classList.remove("open");
  2394.  
  2395.         const [crumb, title, subtitle] = navTitles[view];
  2396.         $("breadcrumb").textContent = crumb;
  2397.         $("pageTitle").textContent = title;
  2398.         $("subtitle").textContent = subtitle;
  2399.  
  2400.         const viewInicio = $("viewInicio");
  2401.         const viewTodos = $("viewTodos");
  2402.         const viewNovo = $("viewNovo");
  2403.         const screenPanel = $("screenPanel");
  2404.  
  2405.         viewInicio.classList.remove("active");
  2406.         viewTodos.classList.remove("active");
  2407.         viewNovo.classList.remove("active");
  2408.  
  2409.         if (view === "inicio" || view === "fila") {
  2410.           viewInicio.classList.add("active");
  2411.           screenPanel.style.display = "";
  2412.         } else if (view === "todos") {
  2413.           viewTodos.classList.add("active");
  2414.           screenPanel.style.display = "none";
  2415.           renderAllTable();
  2416.         } else if (view === "novo") {
  2417.           viewNovo.classList.add("active");
  2418.           screenPanel.style.display = "none";
  2419.         }
  2420.  
  2421.         window.scrollTo({ top: 0, behavior: "smooth" });
  2422.       }
  2423.  
  2424.       document.querySelectorAll(".nav button").forEach(button => {
  2425.         button.addEventListener("click", () => switchView(button.dataset.view));
  2426.       });
  2427.  
  2428.       $("dateText").textContent = new Intl.DateTimeFormat("pt-BR", {
  2429.         weekday: "short", day: "2-digit", month: "short", year: "numeric"
  2430.       }).format(new Date());
  2431.  
  2432.       atualizarBotaoSom();
  2433.       render();
  2434.  
  2435.       window.addEventListener("storage", event => {
  2436.         if (event.key === KEY) {
  2437.           tickets = carregarTickets();
  2438.           syncCurrent();
  2439.           render();
  2440.         }
  2441.       });
  2442.     }
  2443.  
  2444.     // ========== BOOT ==========
  2445.     if (viewMode === "telao") {
  2446.       initTelao();
  2447.     } else if (viewMode === "kiosk") {
  2448.       initKiosk();
  2449.     } else {
  2450.       initPainel();
  2451.     }
  2452.   </script>
  2453. </body>
  2454. </html>

Raw Paste

Comments 0
Login to post a comment.
  • No comments yet. Be the first.
Login to post a comment. Login or Register
We use cookies. To comply with GDPR in the EU and the UK we have to show you these.

We use cookies and similar technologies to keep this website functional (including spam protection via Google reCAPTCHA or Cloudflare Turnstile), and — with your consent — to measure usage and show ads. See Privacy.