Files
fechamento-caixa/src/index.html
T
root d623d11e23
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
fix: 5 problemas recorrentes
1. PIX CNPJ: linha separada no Saldo de Caixa + incluído na soma sys-total
2. Casas decimais: fmtNum sempre 2 casas (padrão dinheiro)
3. btn-anterior: agora busca do Supabase via sb_carregar_rascunho e repovoa todas as tabelas
4. Impressão: reimplementado usb_windows (WriteFile no Windows); adicionado modo teste (grava .bin)
5. Supabase: feedback de erro com console.error; storage salvar_agora persiste TODAS as tabelas (sangrias, pixcnpj, vales, cancelamentos)
2026-06-23 14:22:06 +00:00

1431 lines
68 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fechamento de Caixa — O Frangão</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--orange: #e65100;
--orange-dark: #bf360c;
--orange-light: #ff8a50;
--orange-pale: #fff3e0;
--orange-row: #fff0e0;
--red: #c0272d;
--bg: #ffffff;
--surface: #f7f7f7;
--surface2: #f0f0f0;
--border: #e0e0e0;
--text: #1a1a1a;
--muted: #777;
--green: #2e7d32;
--radius: 6px;
--font: 'Segoe UI', system-ui, -apple-system, sans-serif;
}
html, body { height: 100%; overflow: visible; }
body { font-family: var(--font); background: var(--bg); color: var(--text); display: flex; flex-direction: column; }
/* ── HEADER ─────────────────────────────────────────────────────────── */
.header {
background: linear-gradient(135deg, #bf360c 0%, #e65100 50%, #ff8a50 100%);
color: #fff; padding: 8px 18px;
display: flex; align-items: center; justify-content: space-between;
flex-shrink: 0; gap: 14px;
position: sticky; top: 0; z-index: 100;
}
.header-left { display: flex; align-items: center; gap: 14px; flex: 1; }
.header-title { font-size: 15px; font-weight: 700; white-space: nowrap; }
.header-logo { height: 38px; width: auto; max-width: 140px; object-fit: contain; border-radius: 6px; }
.header-id { font-size: 11px; font-family: monospace; opacity: 0.9; white-space: nowrap; }
.header .gear-btn {
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
color: #fff; border-radius: var(--radius); padding: 5px 11px;
cursor: pointer; font-size: 13px; display: flex; align-items: center; gap: 5px;
font-weight: 600; font-family: var(--font); transition: background 0.2s;
}
.header .gear-btn:hover { background: rgba(255,255,255,0.25); }
/* ── META BAR ────────────────────────────────────────────────────────── */
.meta-bar {
background: var(--surface); border-bottom: 1px solid var(--border);
padding: 8px 18px; display: flex; gap: 14px; align-items: center;
flex-shrink: 0;
}
.meta-field { display: flex; flex-direction: column; gap: 2px; }
.meta-label { font-size: 10px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
.meta-input, .meta-select {
border: 1px solid var(--border); border-radius: var(--radius);
padding: 5px 9px; font-size: 13px; background: #fff; color: var(--text);
min-width: 115px; outline: none; font-family: var(--font);
}
.meta-input:focus, .meta-select:focus { border-color: var(--orange); box-shadow: 0 0 0 2px var(--orange-pale); }
.meta-select { cursor: pointer; }
/* ── MAIN 2 COLUMNS — single scroll, no internal scrollbars ──────────── */
.main { flex: 1; display: grid; grid-template-columns: 1fr 1fr; overflow: visible; }
.col { padding: 12px 16px; overflow: visible; }
.col-left { border-right: 1px solid var(--border); }
.col-right { background: var(--surface); display: flex; flex-direction: column; }
.col-right .section { flex-shrink: 0; }
.col-right .section:last-child { flex: 1; }
/* ── SECTION ─────────────────────────────────────────────────────────── */
.section { margin-bottom: 14px; }
.section-header {
font-size: 11px; font-weight: 700; color: var(--orange);
text-transform: uppercase; letter-spacing: 0.7px; padding: 4px 0;
margin-bottom: 5px; border-bottom: 2px solid var(--orange);
display: flex; align-items: center; gap: 5px;
}
/* ── TABLE ─────────────────────────────────────────────────────────── */
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th {
background: var(--surface2); font-weight: 700; color: var(--muted);
text-align: left; font-size: 10px; text-transform: uppercase;
letter-spacing: 0.4px; padding: 5px 8px; border: 1px solid var(--border); white-space: nowrap;
}
td { padding: 4px 6px; border: 1px solid var(--border); vertical-align: middle; }
td input, td select {
width: 100%; border: none; background: transparent;
font-size: 13px; padding: 4px 2px; outline: none; font-family: var(--font); color: var(--text);
}
td input:focus, td select:focus { background: #fffde7; border-radius: 3px; }
td.num { text-align: right; }
td.num input { text-align: right; }
tr.total-row td { background: var(--orange-row); font-weight: 700; color: var(--orange); font-size: 13px; }
tfoot td { background: var(--surface2); font-weight: 700; font-size: 13px; }
tfoot td:last-child { text-align: right; }
.row-del {
width: 26px; height: 26px; border: none; background: transparent;
color: #ccc; cursor: pointer; font-size: 14px; border-radius: 4px;
display: flex; align-items: center; justify-content: center; padding: 0;
}
.row-del:hover { background: #fee2e2; color: var(--red); }
.add-btn {
display: inline-flex; align-items: center; gap: 4px;
background: var(--orange); color: #fff; border: none; border-radius: var(--radius);
padding: 5px 12px; font-size: 11px; cursor: pointer; font-weight: 600; margin-top: 5px;
}
.add-btn:hover { background: var(--orange-dark); }
/* ── SYS BOX ─────────────────────────────────────────────────────────── */
.sys-grid { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.sys-row {
display: flex; justify-content: space-between; align-items: center;
padding: 7px 12px; border-bottom: 1px solid var(--border);
font-size: 13px; background: #fff;
}
.sys-row:nth-child(even) { background: var(--surface); }
.sys-row:last-child { border-bottom: none; }
.sys-row.total { background: var(--orange-row); font-weight: 700; color: var(--orange); }
.sys-row .lbl { color: var(--muted); font-size: 12px; }
.sys-row.total .lbl { color: var(--orange); }
.sys-row .val { font-variant-numeric: tabular-nums; font-weight: 700; font-size: 13px; }
.sys-row.total .val { color: var(--orange); font-size: 14px; }
.sys-row input {
border: none; background: #e8f5e9; text-align: right; font-size: 13px;
font-family: var(--font); font-weight: 700; color: #2e7d32;
width: 90px; padding: 2px 4px; outline: none; border-radius: 3px;
}
.sys-row input:focus { background: #c8e6c9; }
.sys-row input.editable {
background: #fffde7; color: var(--text); cursor: text;
}
.sys-row input.editable:focus { background: #fff9c4; }
/* ── FECHAMENTO COUNTED ──────────────────────────────────────────────── */
.fech-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
.fech-item { border: 1px solid var(--border); border-radius: var(--radius); padding: 8px 12px; background: #fff; }
.fech-item-label { font-size: 10px; text-transform: uppercase; color: var(--muted); font-weight: 700; letter-spacing: 0.4px; margin-bottom: 4px; }
.fech-item input {
border: none; font-size: 17px; font-weight: 700; color: var(--text);
width: 100%; font-family: var(--font); outline: none; background: transparent; text-align: right;
}
.fech-item input:focus { background: #fffde7; border-radius: 3px; }
.fech-total {
border: 1px solid var(--border); border-radius: var(--radius);
padding: 9px 14px; background: var(--orange-row);
display: flex; justify-content: space-between; align-items: center; margin-top: 8px;
}
.fech-total-label { font-size: 11px; font-weight: 700; color: var(--orange); text-transform: uppercase; letter-spacing: 0.5px; }
.fech-total-value { font-size: 16px; font-weight: 700; color: var(--orange); font-variant-numeric: tabular-nums; }
/* ── DIFERENÇA ─────────────────────────────────────────────────────────── */
.dif-box {
border: 2px solid var(--border); border-radius: var(--radius);
padding: 10px 16px; display: flex; justify-content: space-between; align-items: center;
}
.dif-label { font-size: 12px; font-weight: 700; color: var(--text); text-transform: uppercase; letter-spacing: 0.5px; }
.dif-value { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; }
.dif-value.pos, .dif-value.zero { color: var(--green); }
.dif-value.neg { color: var(--red); }
/* ── SALDO ESPERADO ─────────────────────────────────────────────────── */
.saldo-esperado-wrap { margin-top: 6px; }
.saldo-esperado-wrap label { font-size: 10px; text-transform: uppercase; color: var(--muted); font-weight: 700; letter-spacing: 0.4px; display: block; margin-bottom: 3px; }
.saldo-esperado-wrap input {
border: 1px solid var(--border); border-radius: var(--radius);
padding: 8px 10px; font-size: 16px; font-weight: 700; width: 100%;
font-family: var(--font); outline: none; text-align: right; color: var(--text);
}
.saldo-esperado-wrap input:focus { border-color: var(--orange); box-shadow: 0 0 0 2px var(--orange-pale); }
/* ── CARDS (4 separate blocks) ─────────────────────────────────────── */
.cards-section {
background: var(--surface); border-top: 1px solid var(--border);
padding: 10px 18px; flex-shrink: 0;
}
.cards-header {
font-size: 11px; font-weight: 700; color: var(--orange);
text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 8px;
padding-bottom: 4px; border-bottom: 2px solid var(--orange);
}
.cards-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
.card-block { border: 1px solid var(--border); border-radius: var(--radius); overflow: visible; }
.card-block-header {
background: var(--orange); color: #fff; text-align: center;
font-size: 11px; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.6px; padding: 6px 4px; border-radius: var(--radius) var(--radius) 0 0;
}
.card-block table { width: 100%; border-collapse: collapse; font-size: 13px; }
.card-block td { padding: 3px 6px; border: 1px solid var(--border); vertical-align: middle; }
.card-block td input {
width: 100%; border: none; background: transparent;
font-size: 13px; padding: 4px 2px; outline: none; font-family: var(--font); color: var(--text);
}
.card-block td input:focus { background: #fffde7; border-radius: 3px; }
.card-block td.num { text-align: right; }
.card-block td.num input { text-align: right; }
.card-block tfoot td { background: var(--orange-pale); font-weight: 700; color: var(--orange); font-size: 13px; text-align: right; }
.card-block .add-btn { width: calc(100% - 12px); justify-content: center; margin: 5px 6px 6px; }
.soma-row {
display: flex; justify-content: flex-end; align-items: center;
gap: 12px; padding: 7px 10px; background: var(--orange-pale);
border-radius: var(--radius); font-size: 13px; font-weight: 700; color: var(--orange); margin-top: 6px;
}
.soma-row span:last-child { font-size: 16px; }
/* ── FOOTER ─────────────────────────────────────────────────────────── */
.footer {
background: var(--surface); border-top: 1px solid var(--border);
padding: 8px 18px; display: flex; align-items: center; gap: 10px;
flex-shrink: 0; flex-wrap: wrap;
}
.auto-save { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--muted); margin-right: auto; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: #4caf50; }
.btn {
border: 1px solid var(--border); background: #fff; border-radius: var(--radius);
padding: 7px 14px; font-size: 12px; cursor: pointer; display: flex;
align-items: center; gap: 6px; font-weight: 600; font-family: var(--font); color: var(--text);
}
.btn:hover { background: var(--surface2); }
.btn-danger { background: var(--orange); color: #fff; border-color: var(--orange); font-size: 13px; padding: 8px 16px; }
.btn-danger:hover { background: var(--orange-dark); }
/* ── MODAL ──────────────────────────────────────────────────────────── */
.modal-overlay {
display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5);
z-index: 9999; justify-content: center; align-items: center;
}
.modal-overlay.open { display: flex; }
.modal {
background: #fff; border-radius: 10px; width: 560px; max-width: 95vw;
max-height: 85vh; display: flex; flex-direction: column;
box-shadow: 0 20px 60px rgba(0,0,0,0.3); overflow: hidden;
}
.modal-header {
background: linear-gradient(135deg, #bf360c, #e65100);
color: #fff; padding: 13px 20px;
display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
}
.modal-header h2 { font-size: 15px; font-weight: 700; }
.modal-close {
background: rgba(255,255,255,0.2); border: none; color: #fff;
border-radius: 4px; width: 28px; height: 28px; cursor: pointer;
font-size: 16px; display: flex; align-items: center; justify-content: center;
}
.modal-close:hover { background: rgba(255,255,255,0.35); }
.modal-body { flex: 1; overflow-y: auto; padding: 16px 20px; }
.cfg-section { margin-bottom: 18px; }
.cfg-section-title {
font-size: 11px; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.6px; color: var(--orange);
padding-bottom: 4px; margin-bottom: 8px; border-bottom: 1px solid var(--border);
}
.cfg-item { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
.cfg-item input[type="text"] {
flex: 1; border: 1px solid var(--border); border-radius: var(--radius);
padding: 6px 10px; font-size: 13px; font-family: var(--font); outline: none;
}
.cfg-item input[type="text"]:focus { border-color: var(--orange); }
.cfg-item .btn-rm {
width: 26px; height: 26px; border: none; background: #fee2e2;
color: var(--red); border-radius: 4px; cursor: pointer; font-size: 13px;
display: flex; align-items: center; justify-content: center;
}
.btn-add-config {
display: inline-flex; align-items: center; gap: 4px;
background: var(--orange); color: #fff; border: none; border-radius: var(--radius);
padding: 5px 12px; font-size: 11px; cursor: pointer; font-weight: 600; margin-top: 4px;
}
.btn-add-config:hover { background: var(--orange-dark); }
.modal-footer {
padding: 12px 20px; border-top: 1px solid var(--border);
display: flex; justify-content: flex-end; gap: 10px; flex-shrink: 0;
}
.modal-footer .btn-primary {
background: var(--orange); color: #fff; border: none; border-radius: var(--radius);
padding: 8px 20px; font-size: 13px; cursor: pointer; font-weight: 600; font-family: var(--font);
}
.modal-footer .btn-primary:hover { background: var(--orange-dark); }
/* ── Scrollbar ────────────────────────────────────────────────────── */
::-webkit-scrollbar { width: 7px; }
::-webkit-scrollbar-track { background: var(--surface); }
::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
</style>
</head>
<body>
<!-- ═══ HEADER ════════════════════════════════════════════════════════════ -->
<div class="header">
<div class="header-left">
<span class="header-title">Fechamento de Caixa</span>
<img class="header-logo" id="store-logo" src="./logo.png" alt="Logo" />
<span class="header-id" id="fechamento-id"></span>
</div>
<button class="gear-btn" id="btn-config" title="Configurar">⚙️ Configurar</button>
</div>
<!-- ═══ META BAR ════════════════════════════════════════════════════════ -->
<div class="meta-bar">
<div class="meta-field">
<span class="meta-label">Data</span>
<input type="date" class="meta-input" id="f-data" />
</div>
<div class="meta-field">
<span class="meta-label">Operador</span>
<input type="text" class="meta-input" id="f-operador" list="dl-operadores" placeholder="Ex: Isabelle" />
</div>
<div class="meta-field">
<span class="meta-label">Turno</span>
<select class="meta-select" id="f-turno">
<option value=""></option>
<option value="integral">Integral</option>
<option value="manhã">Manhã</option>
<option value="tarde">Tarde</option>
</select>
</div>
<div class="meta-field">
<span class="meta-label">Loja</span>
<select class="meta-select" id="f-loja">
<option value="União">União</option>
<option value="Aliança">Aliança</option>
</select>
</div>
</div>
<!-- ═══ MAIN 2 COLUMNS ════════════════════════════════════════════════ -->
<div class="main">
<!-- ── LEFT: entradas, sangrias, despesas, pix, vales, receber, cancelamentos ── -->
<div class="col col-left">
<!-- SANGRIAS -->
<div class="section">
<div class="section-header">💸 Sangrias</div>
<table id="tbl-sangrias"><thead><tr><th>Hora</th><th>Retirada (R$)</th><th>Gerente</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td colspan="2">TOTAL</td><td class="num" id="total-sangrias">R$ 0,00</td><td></td></tr></tfoot></table>
<button class="add-btn" data-tbl="tbl-sangrias">+ Adicionar sangria</button>
</div>
<!-- DESPESAS -->
<div class="section">
<div class="section-header">📝 Despesas</div>
<table id="tbl-despesas"><thead><tr><th>Descrição</th><th>Obs</th><th>Valor (R$)</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td colspan="2">TOTAL</td><td class="num" id="total-despesas">R$ 0,00</td><td></td></tr></tfoot></table>
<button class="add-btn" data-tbl="tbl-despesas">+ Adicionar despesa</button>
</div>
<!-- PIX CNPJ -->
<div class="section">
<div class="section-header">💳 PIX CNPJ</div>
<table id="tbl-pix"><thead><tr><th>Nome</th><th>Valor (R$)</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td>TOTAL</td><td class="num" id="total-pix">R$ 0,00</td><td></td></tr></tfoot></table>
<button class="add-btn" data-tbl="tbl-pix">+ Adicionar PIX</button>
</div>
<!-- VALES -->
<div class="section">
<div class="section-header">💰 Vales (Funcionários)</div>
<table id="tbl-vales"><thead><tr><th>Nome</th><th>Valor (R$)</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td>TOTAL</td><td class="num" id="total-vales">R$ 0,00</td><td></td></tr></tfoot></table>
<button class="add-btn" data-tbl="tbl-vales">+ Adicionar vale</button>
</div>
<!-- À RECEBER -->
<div class="section">
<div class="section-header">👥 À Receber (Clientes)</div>
<table id="tbl-receber"><thead><tr><th>Nome</th><th>Valor (R$)</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td>TOTAL</td><td class="num" id="total-receber">R$ 0,00</td><td></td></tr></tfoot></table>
<button class="add-btn" data-tbl="tbl-receber">+ Adicionar cliente</button>
</div>
<!-- CANCELAMENTOS -->
<div class="section">
<div class="section-header">❌ Cancelamentos</div>
<table id="tbl-cancelamentos"><thead><tr><th>Número</th><th>Valor (R$)</th><th>Motivo</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td colspan="1">TOTAL</td><td class="num" id="total-cancelamentos">R$ 0,00</td><td colspan="2"></td></tr></tfoot></table>
<button class="add-btn" data-tbl="tbl-cancelamentos">+ Adicionar cancelamento</button>
</div>
</div><!-- /col-left -->
<!-- ── RIGHT: saldo caixa, fechamento contado, diferença, obs ── -->
<div class="col col-right">
<!-- SALDO DE CAIXA -->
<div class="section">
<div class="section-header">🖥️ Saldo de Caixa (Valores do Sistema)</div>
<div class="sys-grid">
<div class="sys-row"><span class="lbl">Troco (+)</span><input type="text" id="sys-troco" class="editable" placeholder="0,00" /></div>
<div class="sys-row"><span class="lbl">Crédito (+)</span><input type="text" id="sys-credito" readonly tabindex="-1" /></div>
<div class="sys-row"><span class="lbl">Débito (+)</span><input type="text" id="sys-debito" readonly tabindex="-1" /></div>
<div class="sys-row"><span class="lbl">Alimentação (+)</span><input type="text" id="sys-alimentacao" readonly tabindex="-1" /></div>
<div class="sys-row"><span class="lbl">Vales (+)</span><input type="text" id="sys-vales" readonly tabindex="-1" /></div>
<div class="sys-row"><span class="lbl">À Receber (+)</span><input type="text" id="sys-receber" readonly tabindex="-1" /></div>
<div class="sys-row"><span class="lbl">PIX Cartão (+)</span><input type="text" id="sys-pix" readonly tabindex="-1" /></div>
<div class="sys-row"><span class="lbl">PIX CNPJ (+)</span><input type="text" id="sys-pixcnpj" readonly tabindex="-1" /></div>
<div class="sys-row total"><span class="lbl">TOTAL</span><span class="val" id="sys-total">R$ 0,00</span></div>
</div>
</div>
<!-- SALDO ESPERADO -->
<div class="section">
<div class="section-header">🎯 Saldo Esperado</div>
<div class="saldo-esperado-wrap">
<label>Informe o saldo esperado (contado em caixa)</label>
<input type="text" id="f-saldo-esperado" placeholder="0,00" />
</div>
</div>
<!-- FECHAMENTO (VALORES CONTADOS) -->
<div class="section">
<div class="section-header">🛒 Fechamento (Valores Contados)</div>
<div class="fech-grid">
<div class="fech-item"><div class="fech-item-label">Dinheiro</div><input type="text" id="f-fech-dinheiro" placeholder="0,00" /></div>
<div class="fech-item"><div class="fech-item-label">Cartões</div><input type="text" id="f-fech-cartoes" placeholder="0,00" /></div>
<div class="fech-item"><div class="fech-item-label">À Receber</div><input type="text" id="f-fech-receber" placeholder="0,00" /></div>
</div>
<div class="fech-total">
<span class="fech-total-label">Total Programa</span>
<span class="fech-total-value" id="fech-total">R$ 0,00</span>
</div>
</div>
<!-- DIFERENÇA -->
<div class="section">
<div class="dif-box">
<span class="dif-label">Diferença</span>
<span class="dif-value zero" id="diferenca">R$ 0,00</span>
</div>
</div>
<!-- OBS -->
<div class="section" style="flex:1;display:flex;flex-direction:column">
<div class="section-header">📝 Observações</div>
<textarea id="f-obs" rows="4" style="flex:1;width:100%;min-height:80px;border:1px solid var(--border);border-radius:var(--radius);padding:10px;font-size:15px;font-family:var(--font);resize:none" placeholder="Observações..."></textarea>
</div>
</div><!-- /col-right -->
</div><!-- /main -->
<!-- ═══ CARTÕES ════════════════════════════════════════════════════════ -->
<div class="cards-section">
<div class="cards-header">💳 Cartões</div>
<div class="cards-grid">
<div class="card-block">
<div class="card-block-header">Crédito</div>
<table id="tbl-cartao-credito"><thead><tr><th>Valor (R$)</th></tr></thead><tbody></tbody><tfoot><tr><td id="total-cartao-credito">R$ 0,00</td></tr></tfoot></table>
<button class="add-btn" data-tbl="tbl-cartao-credito">+</button>
</div>
<div class="card-block">
<div class="card-block-header">Débito</div>
<table id="tbl-cartao-debito"><thead><tr><th>Valor (R$)</th></tr></thead><tbody></tbody><tfoot><tr><td id="total-cartao-debito">R$ 0,00</td></tr></tfoot></table>
<button class="add-btn" data-tbl="tbl-cartao-debito">+</button>
</div>
<div class="card-block">
<div class="card-block-header">Alimentação</div>
<table id="tbl-cartao-alimentacao"><thead><tr><th>Valor (R$)</th></tr></thead><tbody></tbody><tfoot><tr><td id="total-cartao-alimentacao">R$ 0,00</td></tr></tfoot></table>
<button class="add-btn" data-tbl="tbl-cartao-alimentacao">+</button>
</div>
<div class="card-block">
<div class="card-block-header">PIX</div>
<table id="tbl-cartao-pix"><thead><tr><th>Valor (R$)</th></tr></thead><tbody></tbody><tfoot><tr><td id="total-cartao-pix">R$ 0,00</td></tr></tfoot></table>
<button class="add-btn" data-tbl="tbl-cartao-pix">+</button>
</div>
</div>
<div class="soma-row">
<span>Soma Cartões (todas as colunas)</span>
<span id="soma-cartoes">R$ 0,00</span>
</div>
</div>
<!-- ═══ FOOTER ════════════════════════════════════════════════════════ -->
<div class="footer">
<div class="auto-save"><span class="dot"></span> Salvamento automático activo</div>
<button class="btn" id="btn-anterior">✏️ Dia anterior</button>
<button class="btn" id="btn-recibo">🖨️ Recibo 80mm</button>
<button class="btn" id="btn-limpar">🗑️ Limpar rascunho</button>
<button class="btn btn-danger" id="btn-enviar">💾 Enviar Fechamento</button>
</div>
<!-- ═══ CONFIG MODAL ══════════════════════════════════════════════════ -->
<div class="modal-overlay" id="modal-config">
<div class="modal">
<div class="modal-header">
<h2>⚙️ Configurar Preenchimento Automático</h2>
<button class="modal-close" id="modal-close"></button>
</div>
<div class="modal-body">
<div class="cfg-section">
<div class="cfg-section-title">👤 Operadores</div>
<div id="cfg-operadores-list"></div>
<button class="btn-add-config" id="cfg-add-operador">+ Adicionar operador</button>
</div>
<div class="cfg-section">
<div class="cfg-section-title">👔 Gerentes</div>
<div id="cfg-gerentes-list"></div>
<button class="btn-add-config" id="cfg-add-gerente">+ Adicionar gerente</button>
</div>
<div class="cfg-section">
<div class="cfg-section-title">📝 Despesas (sugestões)</div>
<div id="cfg-despesas-list"></div>
<button class="btn-add-config" id="cfg-add-despesa">+ Adicionar despesa</button>
</div>
<div class="cfg-section">
<div class="cfg-section-title">👥 Clientes (À Receber)</div>
<div id="cfg-clientes-list"></div>
<button class="btn-add-config" id="cfg-add-cliente">+ Adicionar cliente</button>
</div>
<div class="cfg-section">
<div class="cfg-section-title">💰 Vales (nomes para autofill)</div>
<div id="cfg-vales-list"></div>
<button class="btn-add-config" id="cfg-add-vales">+ Adicionar nome</button>
</div>
</div>
<div class="modal-footer">
<button class="btn-primary" id="cfg-salvar">Salvar Configuração</button>
</div>
</div>
</div>
<!-- ═══ CONFIRM MODAL ══════════════════════════════════════════════════ -->
<div class="modal-overlay" id="modal-confirm">
<div class="modal" style="width:520px">
<div class="modal-header">
<h2>⚠️ Confirmar envio do fechamento</h2>
<button class="modal-close" id="confirm-close"></button>
</div>
<div class="modal-body">
<p style="font-size:13px;color:#444;margin-bottom:14px">
Tem certeza que deseja <strong>enviar este fechamento e encerrar o caixa?</strong>
</p>
<div id="confirm-summary" style="border:1px solid #b3d9f7;border-radius:8px;padding:12px;background:#f0f7ff;font-size:12px;font-family:monospace">
</div>
<p style="font-size:11px;color:#888;margin-top:10px">
Após enviar, <strong>não dá pra editar este fechamento</strong> sem a senha do Filipe.
</p>
</div>
<div class="modal-footer" style="justify-content:space-between">
<button class="btn" id="confirm-print" style="color:#333">
🖨️ Imprimir Relatório
</button>
<div style="display:flex;gap:10px">
<button class="btn" id="confirm-cancel" style="border-color:var(--red);color:var(--red)">✕ Cancelar</button>
<button class="btn btn-danger" id="confirm-send">✓ Sim, enviar e encerrar</button>
</div>
</div>
</div>
</div>
<!-- ═══ JAVASCRIPT ══════════════════════════════════════════════════ -->
<script>
'use strict';
// ─── Config ────────────────────────────────────────────────────────────────
let config = {
operadores: ['Isabelle', 'Silvanea', 'Rafael'],
gerentes: ['Carlos', 'Ana', 'João'],
despesas: ['Energia', 'Água', 'Manutenção', 'Limpeza', 'Insumos'],
clientes: [], // preenchimento livre — não é datalist fixo
vales_nomes: [], // nomes usados em vales ( histórico para autofill)
};
// ─── App State ────────────────────────────────────────────────────────────
let estado = {
uuid: crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(),
loja: 'União', data: '', operador: '', turno: '',
saldo_troco: 0, fechamento: 0, saldo_esperado: 0, diferenca: 0,
despesas: [], sangrias: [], pixcnpj: [], vales: [], receber: [], cancelamentos: [],
cartoes: { credito: [], debito: [], alimentacao: [], pix: [] },
fechamento_contado: { dinheiro: 0, cartoes: 0, receber: 0 },
observacoes: '', id_local: null, sync_status: 'local'
};
// ─── Helpers ──────────────────────────────────────────────────────────────
const fmt = v => 'R$ ' + (parseFloat(v) || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 });
const parseNum = v => {
// Already a number? Return directly (don't treat '.' as thousand separator)
if (typeof v === 'number') return isNaN(v) ? 0 : v;
const s = String(v || '0').trim();
if (!s) return 0;
// Brazilian format: 1.234,56 → 1234.56
// Strategy: if string has comma, treat as BRL (decimal comma)
// If no comma but has dot, treat as US (decimal dot)
const lastComma = s.lastIndexOf(',');
const lastDot = s.lastIndexOf('.');
const hasCommaDecimal = lastComma > lastDot;
if (hasCommaDecimal) {
const intPart = s.slice(0, lastComma).replace(/\./g, '').replace(/,/g, '');
const decPart = s.slice(lastComma + 1);
return parseFloat(intPart + '.' + decPart) || 0;
}
// No comma: US-style number, just strip any dots (thousand sep)
return parseFloat(s.replace(/,/g, '').replace(/\./g, '')) || 0;
};
const setText = (id, t) => { const e = document.getElementById(id); if (e) e.textContent = t; };
const setVal = (id, v) => { const e = document.getElementById(id); if (e) e.value = v; };
const val = id => { const e = document.getElementById(id); return e ? e.value : ''; };
const fmtNum = v => {
const n = typeof v === 'number' ? v : parseNum(v);
if (isNaN(n)) return '';
// Always 2 decimal places for money
return n.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
// Format time: 4 digits → HH:MM
function fmtTime(v) {
const digits = (v || '').replace(/\D/g, '').slice(0, 4);
if (digits.length <= 2) return digits;
return digits.slice(0, 2) + ':' + digits.slice(2);
}
// ─── Tables ───────────────────────────────────────────────────────────────
const TABLES = {
'tbl-sangrias': { field: 'sangrias', cols: ['hora','retirada','gerente'], enterNav: { hora:'retirada', retirada:'gerente', gerente:'_nextRow' } },
'tbl-despesas': { field: 'despesas', cols: ['desc','obs','valor'], enterNav: { desc:'obs', obs:'valor', valor:'_nextRow' } },
'tbl-pix': { field: 'pixcnpj', cols: ['nome','valor'], enterNav: { nome:'valor', valor:'_nextRow' } },
'tbl-vales': { field: 'vales', cols: ['nome','valor'], enterNav: { nome:'valor', valor:'_nextRow' } },
'tbl-receber': { field: 'receber', cols: ['nome','valor'], enterNav: { nome:'valor', valor:'_nextRow' } },
'tbl-cancelamentos': { field: 'cancelamentos', cols: ['numero','valor','motivo'], enterNav: { numero:'valor', valor:'motivo', motivo:'_nextRow' } },
'tbl-cartao-credito': { field: 'cartoes.credito', cols: ['valor'], enterNav: { valor:'_nextRow' } },
'tbl-cartao-debito': { field: 'cartoes.debito', cols: ['valor'], enterNav: { valor:'_nextRow' } },
'tbl-cartao-alimentacao': { field: 'cartoes.alimentacao', cols: ['valor'], enterNav: { valor:'_nextRow' } },
'tbl-cartao-pix': { field: 'cartoes.pix', cols: ['valor'], enterNav: { valor:'_nextRow' } },
};
function getField(tblId) {
const parts = TABLES[tblId].field.split('.');
let cur = estado;
for (const p of parts) cur = cur[p];
return cur;
}
function makeRow(tblId, rowIndex, rowData) {
const cfg = TABLES[tblId];
const tr = document.createElement('tr');
cfg.cols.forEach(col => {
const td = document.createElement('td');
if (col === 'valor' || col === 'retirada') td.className = 'num';
const inp = document.createElement('input');
inp.type = 'text';
inp.value = rowData[col] !== undefined
? (rowData[col] === 0 ? '' : fmtNum(rowData[col]))
: '';
inp.placeholder = col === 'valor' || col === 'retirada' ? '0,00' : (col === 'hora' ? 'HH:MM' : '');
inp.dataset.tbl = tblId;
inp.dataset.row = rowIndex;
inp.dataset.col = col;
if (col === 'gerente') inp.setAttribute('list', 'dl-gerente');
if (col === 'desc') inp.setAttribute('list', 'dl-despesa');
// tbl-receber: clientes free-text (sem datalist)
// tbl-vales: usa dl-vales
if (col === 'nome' && tblId === 'tbl-vales') inp.setAttribute('list', 'dl-vales');
if (col === 'nome' && tblId === 'tbl-pix') inp.setAttribute('list', 'dl-generico');
inp.addEventListener('input', onTableInput);
inp.addEventListener('blur', onCellBlur);
inp.addEventListener('keydown', onCellKeydown);
td.appendChild(inp);
tr.appendChild(td);
});
const delTd = document.createElement('td');
const delBtn = document.createElement('button');
delBtn.className = 'row-del';
delBtn.innerHTML = '✕';
delBtn.dataset.tbl = tblId;
delBtn.dataset.row = rowIndex;
delBtn.addEventListener('click', onDeleteRow);
delTd.appendChild(delBtn);
tr.appendChild(delTd);
return tr;
}
function renderTable(tblId) {
const cfg = TABLES[tblId];
const tbody = document.querySelector(`#${tblId} tbody`);
if (!tbody) return;
tbody.innerHTML = '';
getField(tblId).forEach((r, i) => { tbody.appendChild(makeRow(tblId, i, r)); });
}
function onTableInput(e) {
const { tbl, row, col } = e.target.dataset;
const field = getField(tbl);
if (!field || !field[row]) return;
if (col === 'valor' || col === 'retirada') {
field[row][col] = parseNum(e.target.value);
} else if (col === 'hora') {
const formatted = fmtTime(e.target.value);
if (formatted !== e.target.value) e.target.value = formatted;
field[row][col] = formatted;
} else {
field[row][col] = e.target.value;
}
updateTotals();
scheduleSave();
}
function onCellBlur(e) {
const { col } = e.target.dataset;
if (col === 'valor' || col === 'retirada') {
const v = parseNum(e.target.value);
e.target.value = isNaN(v) || v === 0 ? '' : fmtNum(v);
updateTotals();
}
}
function onCellKeydown(e) {
if (e.key !== 'Enter') return;
// 50ms delay so the browser finishes filling the datalist value
// before we read e.target.value and move focus
setTimeout(() => {
const { tbl, row, col } = e.target.dataset;
const cfg = TABLES[tbl];
if (!cfg) return;
const next = cfg.enterNav[col];
if (next === '_nextRow') {
const nextRowIndex = parseInt(row) + 1;
if (nextRowIndex < getField(tbl).length) {
focusInput(tbl, nextRowIndex, cfg.cols[0]);
} else {
addRowAndFocus(tbl, cfg.cols[0]);
}
} else if (next) {
focusInput(tbl, parseInt(row), next);
}
}, 50);
}
function focusInput(tblId, rowIndex, col) {
const sel = `#${tblId} tbody tr:nth-child(${rowIndex + 1}) input[data-col="${col}"]`;
const inp = document.querySelector(sel);
if (inp) { inp.focus(); if (col === 'valor' || col === 'retirada' || col === 'hora') inp.select(); }
}
function addRowAndFocus(tblId, focusCol) {
const cfg = TABLES[tblId];
const field = getField(tblId);
const empty = {};
cfg.cols.forEach(c => { empty[c] = c === 'valor' || c === 'retirada' ? 0 : ''; });
field.push(empty);
renderTable(tblId);
focusInput(tblId, field.length - 1, focusCol);
}
function onDeleteRow(e) {
const { tbl, row } = e.target.dataset;
const field = getField(tbl);
if (field && field.length > 1) {
field.splice(parseInt(row), 1);
renderTable(tbl);
updateTotals();
scheduleSave();
} else {
const cfg = TABLES[tbl];
cfg.cols.forEach(c => { field[0][c] = c === 'valor' || c === 'retirada' ? 0 : ''; });
renderTable(tbl);
updateTotals();
scheduleSave();
}
}
// Add buttons
document.querySelectorAll('.add-btn[data-tbl]').forEach(btn => {
btn.addEventListener('click', () => {
const tblId = btn.dataset.tbl;
const cfg = TABLES[tblId];
if (!cfg) return;
const field = getField(tblId);
const empty = {};
cfg.cols.forEach(c => { empty[c] = c === 'valor' ? 0 : ''; });
field.push(empty);
renderTable(tblId);
focusInput(tblId, field.length - 1, cfg.cols[0]);
});
});
// ─── Totals ────────────────────────────────────────────────────────────────
function updateTotals() {
// ── Sangrias ──
const sangriasTotal = estado.sangrias.reduce((s, r) => s + (parseNum(r['retirada'] || 0) || 0), 0);
setText('total-sangrias', fmt(sangriasTotal));
// ── Tabelas de valor ──
const map = {
'tbl-despesas':'despesas', 'tbl-pix':'pixcnpj', 'tbl-vales':'vales',
'tbl-receber':'receber', 'tbl-cancelamentos':'cancelamentos',
};
Object.entries(map).forEach(([tblId, fieldName]) => {
const total = estado[fieldName].reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
setText(tblId.replace('tbl-','total-'), fmt(total));
});
// ── Cartões ──
const cartCred = estado.cartoes.credito.reduce((s, r) => s + (r.valor || 0), 0);
const cartDeb = estado.cartoes.debito.reduce((s, r) => s + (r.valor || 0), 0);
const cartAli = estado.cartoes.alimentacao.reduce((s, r) => s + (r.valor || 0), 0);
const cartPix = estado.cartoes.pix.reduce((s, r) => s + (r.valor || 0), 0);
['credito','debito','alimentacao','pix'].forEach(c => {
const total = estado.cartoes[c].reduce((s, r) => s + (r.valor || 0), 0);
setText(`total-cartao-${c}`, fmt(total));
});
const somaCartoes = cartCred + cartDeb + cartAli + cartPix;
setText('soma-cartoes', fmt(somaCartoes));
// ── Saldo de Caixa — auto-populate readonly fields ──
// Crédito = total cartão crédito
document.getElementById('sys-credito').value = cartCred > 0 ? fmtNum(cartCred) : '';
// Débito = total cartão débito
document.getElementById('sys-debito').value = cartDeb > 0 ? fmtNum(cartDeb) : '';
// Alimentação = total cartão alimentação
document.getElementById('sys-alimentacao').value = cartAli > 0 ? fmtNum(cartAli) : '';
// PIX Cartão = total cartão PIX
document.getElementById('sys-pix').value = cartPix > 0 ? fmtNum(cartPix) : '';
// PIX CNPJ = total PIX CNPJ
const pixCnpjTotal = estado.pixcnpj.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
document.getElementById('sys-pixcnpj').value = pixCnpjTotal > 0 ? fmtNum(pixCnpjTotal) : '';
// Vales = total vales
const valesTotal = estado.vales.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
document.getElementById('sys-vales').value = valesTotal > 0 ? fmtNum(valesTotal) : '';
// À Receber = total receber
const receberTotal = estado.receber.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
document.getElementById('sys-receber').value = receberTotal > 0 ? fmtNum(receberTotal) : '';
// SYS total = troco + crédito + débito + alimentação + vales + receber + pixCartão + pixCnpj
const sysTotal = parseNum(val('sys-troco')) + cartCred + cartDeb + cartAli + valesTotal + receberTotal + cartPix + pixCnpjTotal;
setText('sys-total', fmt(sysTotal));
// ── Fechamento Contado — auto-populate (readonly, filled from tables) ──
// Dinheiro = total sangrias
document.getElementById('f-fech-dinheiro').value = sangriasTotal > 0 ? fmtNum(sangriasTotal) : '';
// Cartões = total cartões (crédito+débito+alimentação+pix)
document.getElementById('f-fech-cartoes').value = somaCartoes > 0 ? fmtNum(somaCartoes) : '';
// À Receber = total receber
document.getElementById('f-fech-receber').value = receberTotal > 0 ? fmtNum(receberTotal) : '';
// Fechamento total
const fd = sangriasTotal;
const fc = somaCartoes;
const fr = receberTotal;
setText('fech-total', fmt(fd + fc + fr));
// Diferença = Saldo Caixa Saldo Esperado
const saldoCaixa = sysTotal;
const diff = saldoCaixa - parseNum(val('f-saldo-esperado'));
const difEl = document.getElementById('diferenca');
if (difEl) {
difEl.textContent = fmt(diff);
if (diff === 0) {
difEl.className = 'dif-value zero';
} else if (diff < 0 && diff >= -5) {
difEl.className = 'dif-value neg'; // OK até 5
} else if (diff > 0 && diff <= 3) {
difEl.className = 'dif-value zero'; // OK + até 3
} else {
difEl.className = 'dif-value pos'; // Sobra > +3
}
}
}
// ─── Numeric input: auto ,00 on digit+blur pattern ───────────────────────
// For fields that use datalist (text input), we need to avoid triggering
// auto-formatter when the browser is about to fill from datalist.
// Strategy: detect "comma-only" input and convert to ",00" immediately.
// For normal digit input: add ,00 on blur (not on every digit).
// ─── Numeric input: auto ,00 on digit+blur pattern ───────────────────────
// These fields are plain text inputs (no datalist). Format on blur only.
// For sys-troco which is user-editable: also format on Enter keydown.
['troco','credito','debito','alimentacao','vales','receber','pix'].forEach(k => {
const el = document.getElementById(`sys-${k}`);
if (!el) return;
// On input: just track for autosave
el.addEventListener('input', () => {
updateTotals();
scheduleSave();
});
// On keydown Enter: format value directly (Enter does not trigger blur for tabindex=-1)
el.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
const raw = el.value.trim();
if (!raw) { el.value = ''; return; }
const num = parseNum(raw);
if (isNaN(num) || num === 0) { el.value = ''; }
else { el.value = fmtNum(num); }
updateTotals();
scheduleSave();
}
});
// On blur: format the value
el.addEventListener('blur', () => {
const raw = el.value.trim();
if (!raw) { el.value = ''; return; }
const num = parseNum(raw);
if (isNaN(num) || num === 0) { el.value = ''; }
else { el.value = fmtNum(num); }
updateTotals();
scheduleSave();
});
});
['f-fech-dinheiro','f-fech-cartoes','f-fech-receber','f-saldo-esperado'].forEach(id => {
const el = document.getElementById(id);
if (!el) return;
el.addEventListener('input', () => {
const v = el.value;
if (v.endsWith(',') && !v.includes(',') && v !== ',') {
el.value = v + '00';
const pos = v.length;
setTimeout(() => { el.selectionStart = el.selectionEnd = pos; }, 0);
}
updateTotals();
scheduleSave();
});
el.addEventListener('blur', () => {
const raw = el.value.trim();
if (!raw) { el.value = ''; return; }
const num = parseNum(raw);
if (isNaN(num) || num === 0) { el.value = ''; }
else { el.value = fmtNum(num); }
updateTotals();
scheduleSave();
});
});
// ─── Metadata ──────────────────────────────────────────────────────────────
['f-data','f-operador','f-turno','f-loja'].forEach(id => {
const el = document.getElementById(id);
if (!el) return;
el.addEventListener('change', () => {
estado.data = val('f-data');
estado.operador = val('f-operador');
estado.turno = val('f-turno');
estado.loja = val('f-loja');
syncHeader();
scheduleSave();
});
});
function syncHeader() {
setText('fechamento-id', `${estado.loja}_${estado.data}_op`);
}
// ─── Datalists ─────────────────────────────────────────────────────────────
function buildDatalists() {
// Auto-build vales_nomes from current entries (autofill dinâmico)
const valesUsed = [...new Set(estado.vales.map(v => v.nome).filter(Boolean))];
config.vales_nomes = [...new Set([...config.vales_nomes, ...valesUsed])]
.sort((a, b) => a.localeCompare(b, 'pt-BR'));
// Gerentes e Operadores também acumulam dos lançamentos
const gerentesUsed = [...new Set(estado.sangrias.map(r => r.gerente).filter(Boolean))];
config.gerentes = [...new Set([...config.gerentes, ...gerentesUsed])]
.sort((a, b) => a.localeCompare(b, 'pt-BR'));
const lists = {
'dl-operadores': [...new Set([...config.operadores])].sort((a,b) => a.localeCompare(b, 'pt-BR')),
'dl-gerente': [...new Set([...config.gerentes])].sort((a,b) => a.localeCompare(b, 'pt-BR')),
'dl-despesa': [...new Set([...config.despesas])].sort((a,b) => a.localeCompare(b, 'pt-BR')),
// Clientes: escrita livre — sem datalist fixo
'dl-vales': [...new Set([...config.vales_nomes])].sort((a,b) => a.localeCompare(b, 'pt-BR')),
};
Object.entries(lists).forEach(([id, items]) => {
let dl = document.getElementById(id);
if (!dl) { dl = document.createElement('datalist'); dl.id = id; document.body.appendChild(dl); }
dl.innerHTML = items.map(v => `<option value="${v}">`).join('');
});
// Clientes: sem datalist (escrita livre)
const clienteDl = document.getElementById('dl-cliente');
if (clienteDl) clienteDl.remove();
}
// ─── Init ─────────────────────────────────────────────────────────────────
const PRE_FILL_TABLES = Object.keys(TABLES);
function ensureOneRow(tblId) {
const cfg = TABLES[tblId];
if (!cfg) return;
const field = getField(tblId);
if (field && field.length === 0) {
const empty = {};
cfg.cols.forEach(c => { empty[c] = c === 'valor' || c === 'retirada' ? 0 : ''; });
field.push(empty);
}
}
function init() {
const today = new Date().toISOString().split('T')[0];
setVal('f-data', today);
estado.data = today;
setVal('f-loja', estado.loja);
syncHeader();
PRE_FILL_TABLES.forEach(ensureOneRow);
Object.keys(TABLES).forEach(renderTable);
updateTotals();
}
// ─── LocalStorage ──────────────────────────────────────────────────────────
const LS_KEY = 'fc_v7';
const CFG_KEY = 'fc_cfg';
function buildEstadoForBackend() {
// Monta o objeto exactly as o backend espera (storage.rs → salvar_fechamento)
return {
uuid: estado.uuid,
id_local: estado.id_local,
loja: estado.loja,
data: estado.data,
operador: estado.operador,
turno: estado.turno,
saldo_troco: parseNum(val('sys-troco')), // troco do sistema
saldo_esperado: parseNum(val('f-saldo-esperado')),
fechamento: parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')),
diferenca: parseNum(val('f-saldo-esperado')) - (parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'))),
despesas: estado.despesas.map(r => ({ desc: r.desc || '', obs: r.obs || '', valor: r.valor || 0 })),
sangrias: estado.sangrias.map(r => ({ hora: r.hora || '', retirada: r.retirada || 0, gerente: r.gerente || '' })),
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome || '', valor: r.valor || 0 })),
vales: estado.vales.map(r => ({ nome: r.nome || '', obs: r.obs || '', valor: r.valor || 0 })),
receber: estado.receber.map(r => ({ nome: r.nome || '', valor: r.valor || 0 })),
cancelamentos: estado.cancelamentos.map(r => ({ numero: r.numero || '', valor: r.valor || 0, motivo: r.motivo || '' })),
cartoes: {
credito: estado.cartoes.credito.map(r => r.valor || 0),
debito: estado.cartoes.debito.map(r => r.valor || 0),
alimentacao: estado.cartoes.alimentacao.map(r => r.valor || 0),
pix: estado.cartoes.pix.map(r => r.valor || 0),
},
observacoes: estado.observacoes,
sync_status: estado.sync_status,
};
}
function saveState() {
// Sync reactive fields from DOM before saving
estado.operador = val('f-operador');
estado.turno = val('f-turno');
estado.loja = val('f-loja');
estado.saldo_troco = parseNum(val('sys-troco'));
estado.saldo_esperado = parseNum(val('f-saldo-esperado'));
estado.fechamento_contado = {
dinheiro: parseNum(val('f-fech-dinheiro')),
cartoes: parseNum(val('f-fech-cartoes')),
receber: parseNum(val('f-fech-receber')),
};
estado.updatedAt = new Date().toISOString();
try { localStorage.setItem(LS_KEY, JSON.stringify(estado)); } catch(e) {}
}
function loadState() {
try {
const raw = localStorage.getItem(LS_KEY);
if (!raw) return false;
const saved = JSON.parse(raw);
// Merge carefully (cartoes may be in old format)
Object.assign(estado, saved);
setVal('f-data', estado.data || '');
setVal('f-operador', estado.operador || '');
setVal('f-turno', estado.turno || '');
setVal('f-loja', estado.loja || 'União');
setVal('f-obs', estado.observacoes || '');
setVal('f-saldo-esperado', estado.saldo_esperado === 0 ? '' : fmtNum(estado.saldo_esperado));
setVal('sys-troco', estado.saldo_troco === 0 ? '' : fmtNum(estado.saldo_troco));
setVal('f-fech-dinheiro', estado.fechamento_contado && estado.fechamento_contado.dinheiro ? fmtNum(estado.fechamento_contado.dinheiro) : '');
setVal('f-fech-cartoes', estado.fechamento_contado && estado.fechamento_contado.cartoes ? fmtNum(estado.fechamento_contado.cartoes) : '');
setVal('f-fech-receber', estado.fechamento_contado && estado.fechamento_contado.receber ? fmtNum(estado.fechamento_contado.receber) : '');
syncHeader();
PRE_FILL_TABLES.forEach(ensureOneRow);
Object.keys(TABLES).forEach(renderTable);
updateTotals();
return true;
} catch(e) { return false; }
}
function loadConfig() {
try {
const raw = localStorage.getItem(CFG_KEY);
if (raw) config = { ...config, ...JSON.parse(raw) };
} catch(e) {}
}
function saveConfig() {
try { localStorage.setItem(CFG_KEY, JSON.stringify(config)); } catch(e) {}
buildDatalists();
}
// ─── Config Modal ──────────────────────────────────────────────────────────
function openConfig() {
document.getElementById('modal-config').classList.add('open');
renderCfgList('operadores', 'cfg-operadores-list');
renderCfgList('gerentes', 'cfg-gerentes-list');
renderCfgList('despesas', 'cfg-despesas-list');
renderCfgList('clientes', 'cfg-clientes-list');
renderCfgList('vales_nomes', 'cfg-vales-list');
}
function closeConfig() { document.getElementById('modal-config').classList.remove('open'); }
function renderCfgList(key, containerId) {
const container = document.getElementById(containerId);
container.innerHTML = '';
config[key].forEach((item, i) => {
const div = document.createElement('div');
div.className = 'cfg-item';
div.innerHTML = `<input type="text" value="${item}" data-key="${key}" data-index="${i}" /><button class="btn-rm" data-key="${key}" data-index="${i}">✕</button>`;
container.appendChild(div);
});
container.querySelectorAll('.btn-rm').forEach(btn => {
btn.onclick = () => { const { key: k, index: idx } = btn.dataset; config[k].splice(parseInt(idx), 1); renderCfgList(k, containerId); };
});
container.querySelectorAll('input').forEach(inp => {
inp.addEventListener('change', () => { const { key: k, index: idx } = inp.dataset; config[k][parseInt(idx)] = inp.value; });
});
}
document.getElementById('btn-config').addEventListener('click', openConfig);
document.getElementById('modal-close').addEventListener('click', closeConfig);
document.getElementById('modal-config').addEventListener('click', e => { if (e.target.id === 'modal-config') closeConfig(); });
['operador','gerente','despesa','cliente','vales'].forEach((_, i) => {
const btnMap = ['cfg-add-operador','cfg-add-gerente','cfg-add-despesa','cfg-add-cliente','cfg-add-vales'];
const keyMap = ['operadores','gerentes','despesas','clientes','vales_nomes'];
const listMap = ['cfg-operadores-list','cfg-gerentes-list','cfg-despesas-list','cfg-clientes-list','cfg-vales-list'];
document.getElementById(btnMap[i]).addEventListener('click', () => {
const k = keyMap[i]; config[k].push(''); renderCfgList(k, listMap[i]);
const inputs = document.getElementById(listMap[i]).querySelectorAll('input');
inputs[inputs.length - 1].focus();
});
});
document.getElementById('cfg-salvar').addEventListener('click', () => { saveConfig(); closeConfig(); scheduleSave(); });
// ─── Tauri Commands ───────────────────────────────────────────────────────
let _saveTimer = null;
function scheduleSave() {
clearTimeout(_saveTimer);
_saveTimer = setTimeout(() => { saveState(); if (window.__TAURI__) tauriSaveLocal(); }, 1000);
}
async function tauriSaveLocal() {
try {
const data = buildEstadoForBackend();
await window.__TAURI__.core.invoke('salvar_fechamento', { estado: data });
} catch(e) { console.warn('salvar_fechamento:', e); }
}
async function tauriEnviar() {
const data = buildEstadoForBackend();
// 1. Salva local
await window.__TAURI__.core.invoke('salvar_fechamento', { estado: data }).catch(e => console.warn('local save err:', e));
// 2. Envia Supabase
try {
const result = await window.__TAURI__.core.invoke('sb_salvar_fechamento', { estado: data });
console.log('Supabase salvo:', result);
return result;
} catch(e) {
console.error('Erro sb_salvar_fechamento:', e);
throw e;
}
}
async function tauriRecibo() {
// Build flat totals for the Rust ReciboData struct
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
const cartDeb = estado.cartoes.debito.reduce((s,r) => s+(r.valor||0), 0);
const cartAli = estado.cartoes.alimentacao.reduce((s,r) => s+(r.valor||0), 0);
const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
const pixCnpjTotal = estado.pixcnpj.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const sangriasTotal = estado.sangrias.reduce((s,r) => s+(parseNum(r.retirada||0)||0), 0);
const despesasTotal = estado.despesas.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const valesTotal = estado.vales.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const receberTotal = estado.receber.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const cancelTotal = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const fechamentoCounted = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
const saldoEsp = parseNum(val('f-saldo-esperado'));
const sysTroco = parseNum(val('sys-troco'));
const data = {
uuid: estado.uuid,
id_local: estado.id_local,
loja: estado.loja,
data: estado.data,
operador: estado.operador,
turno: estado.turno,
saldo_troco: sysTroco,
saldo_esperado: saldoEsp,
fechamento: fechamentoCounted,
diferenca: sysTroco - saldoEsp,
despesas: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
sangrias: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
vales: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })),
receber: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
cancelamentos: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
cartoes: { credito: cartCred, debito: cartDeb, alimentacao: cartAli, pix: cartPix },
// Flat totals for ReciboData struct (Rust)
credito: cartCred,
debito: cartDeb,
alimentacao: cartAli,
voucher: cartPix,
pixcnpj: pixCnpjTotal,
sangrias: sangriasTotal,
despesas: despesasTotal,
vales: valesTotal,
areceber: receberTotal,
cancelamentos: cancelTotal,
observacoes: estado.observacoes,
sync_status: estado.sync_status,
};
return await window.__TAURI__.core.invoke('imprimir_recibo', { estado: JSON.stringify(data) });
}
async function tauriCarregarRascunho() {
const loja = val('f-loja') || 'União';
const data = val('f-data');
if (!data) return null;
try {
return await window.__TAURI__.core.invoke('carregar_rascunho', { loja, data });
} catch(e) { console.warn('carregar_rascunho:', e); return null; }
}
// ─── Confirm Modal ────────────────────────────────────────────────────────
function openConfirmModal() {
const operador = val('f-operador').trim();
const turno = val('f-turno').trim();
const loja = val('f-loja').trim();
if (!operador || !turno || !loja) {
alert('⚠️ Preencha Operador, Turno e Loja antes de enviar.');
return;
}
// Build summary
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
const cartDeb = estado.cartoes.debito.reduce((s,r) => s+(r.valor||0), 0);
const cartAli = estado.cartoes.alimentacao.reduce((s,r) => s+(r.valor||0), 0);
const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
const sangrias = estado.sangrias.reduce((s,r) => s+(parseNum(r.retirada||0)||0), 0);
const despesas = estado.despesas.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const vales = estado.vales.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const receber = estado.receber.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const cancelamentos = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const fechamento = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
const saldoEsp = parseNum(val('f-saldo-esperado'));
document.getElementById('confirm-summary').innerHTML =
`<div style="margin-bottom:6px"><strong>📅 Data:</strong> ${estado.data} &nbsp; <strong>👤 Operador:</strong> ${operador} &nbsp; <strong>⏰ Turno:</strong> ${turno} &nbsp; <strong>🏪 Loja:</strong> ${loja}</div>
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
<div>💸 Sangrias: <strong>${fmt(sangrias)}</strong></div>
<div>📝 Despesas: <strong>${fmt(despesas)}</strong></div>
<div>💰 Vales: <strong>${fmt(vales)}</strong></div>
<div>👥 À Receber: <strong>${fmt(receber)}</strong></div>
<div>💳 Crédito: <strong style="color:#c0272d">${fmt(cartCred)}</strong></div>
<div>💳 Débito: <strong>${fmt(cartDeb)}</strong></div>
<div>💳 Alimentação: <strong>${fmt(cartAli)}</strong></div>
<div>❌ Cancelamentos: <strong>${fmt(cancelamentos)}</strong></div>
<div>📋 Fechamento: <strong>${fmt(fechamento)}</strong></div>
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
<div>🎯 Saldo Esperado: <strong>${fmt(saldoEsp)}</strong></div>
<div style="margin-top:4px;font-size:10px;color:#888">ID: ${estado.loja}_${estado.data}_${estado.operador}</div>`;
document.getElementById('modal-confirm').classList.add('open');
}
function closeConfirmModal() {
document.getElementById('modal-confirm').classList.remove('open');
}
document.getElementById('confirm-close').addEventListener('click', closeConfirmModal);
document.getElementById('confirm-cancel').addEventListener('click', closeConfirmModal);
document.getElementById('modal-confirm').addEventListener('click', e => { if (e.target.id === 'modal-confirm') closeConfirmModal(); });
// Print from confirm modal
document.getElementById('confirm-print').addEventListener('click', async () => {
closeConfirmModal();
const btn = document.getElementById('btn-recibo');
btn.disabled = true;
try {
await tauriRecibo();
alert('✅ Impressão enviada!');
} catch(e) {
alert('Erro ao imprimir: ' + (e.message || e));
} finally {
btn.disabled = false;
}
});
// Confirm send
document.getElementById('confirm-send').addEventListener('click', async () => {
closeConfirmModal();
const btn = document.getElementById('btn-enviar');
btn.disabled = true;
btn.textContent = '⏳ Enviando...';
try {
const r = await tauriEnviar();
estado.sync_status = 'synced';
saveState();
alert('✅ Enviado com sucesso!');
} catch(e) {
await tauriSaveLocal();
alert('Erro ao enviar: ' + (e.message || e) + '\n(Dados salvos localmente)');
} finally {
btn.disabled = false;
btn.textContent = '💾 Enviar Fechamento';
}
});
// ─── Buttons ───────────────────────────────────────────────────────────────
document.getElementById('btn-enviar').addEventListener('click', () => {
openConfirmModal();
});
document.getElementById('btn-recibo').addEventListener('click', async () => {
const btn = document.getElementById('btn-recibo');
btn.disabled = true;
try {
await tauriRecibo();
alert('✅ Impressão enviada!');
} catch(e) {
alert('Erro ao imprimir: ' + (e.message || e));
} finally {
btn.disabled = false;
}
});
document.getElementById('btn-limpar').addEventListener('click', () => {
if (!confirm('Limpar rascunho? Esta acção não pode ser revertida.')) return;
localStorage.removeItem(LS_KEY);
location.reload();
});
document.getElementById('btn-anterior').addEventListener('click', async () => {
const d = new Date(estado.data); d.setDate(d.getDate() - 1);
const prevDate = d.toISOString().split('T')[0];
await tauriSaveLocal();
setVal('f-data', prevDate);
estado.data = prevDate;
estado.uuid = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
estado.id_local = null;
syncHeader();
// Try Supabase first (source of truth for submitted closings)
let rascunho = null;
try {
rascunho = await window.__TAURI__.core.invoke('sb_carregar_rascunho', {
loja: val('f-loja'),
data: prevDate
});
} catch(e) {
console.warn('sb_carregar_rascunho falhou, tentando local:', e);
}
// Fallback: local SQLite
if (!rascunho) {
try {
rascunho = await window.__TAURI__.core.invoke('carregar_rascunho', {
loja: val('f-loja'),
data: prevDate
});
} catch(e) {
console.warn('carregar_rascunho local falhou:', e);
}
}
if (rascunho) {
// Meta fields
if (rascunho.operador) { setVal('f-operador', rascunho.operador); estado.operador = rascunho.operador; }
if (rascunho.turno) { setVal('f-turno', rascunho.turno); estado.turno = rascunho.turno; }
if (rascunho.saldo_troco) setVal('sys-troco', fmtNum(rascunho.saldo_troco));
if (rascunho.saldo_esperado) setVal('f-saldo-esperado', fmtNum(rascunho.saldo_esperado));
if (rascunho.observacoes) { setVal('f-obs', rascunho.observacoes); estado.observacoes = rascunho.observacoes; }
// All tables from Supabase/local
if (rascunho.despesas && Array.isArray(rascunho.despesas)) {
estado.despesas = rascunho.despesas.map(d => ({ desc: d.desc || d.descricao || '', obs: d.obs || '', valor: parseNum(d.valor) || 0 }));
}
if (rascunho.sangrias && Array.isArray(rascunho.sangrias)) {
estado.sangrias = rascunho.sangrias.map(s => ({ hora: s.hora || '', retirada: parseNum(s.retirada) || 0, gerente: s.gerente || '' }));
}
if (rascunho.pixcnpj && Array.isArray(rascunho.pixcnpj)) {
estado.pixcnpj = rascunho.pixcnpj.map(p => ({ nome: p.nome || '', valor: parseNum(p.valor) || 0 }));
}
if (rascunho.vales && Array.isArray(rascunho.vales)) {
estado.vales = rascunho.vales.map(v => ({ nome: v.nome || '', obs: v.obs || '', valor: parseNum(v.valor) || 0 }));
}
if (rascunho.receber && Array.isArray(rascunho.receber)) {
estado.receber = rascunho.receber.map(r => ({ nome: r.nome || '', valor: parseNum(r.valor) || 0 }));
}
if (rascunho.cancelamentos && Array.isArray(rascunho.cancelamentos)) {
estado.cancelamentos = rascunho.cancelamentos.map(c => ({ numero: c.numero || '', valor: parseNum(c.valor) || 0, motivo: c.motivo || '' }));
}
if (rascunho.cartoes) {
estado.cartoes.credito = (rascunho.cartoes.credito || []).map(v => ({ valor: parseNum(v) || 0 }));
estado.cartoes.debito = (rascunho.cartoes.debito || []).map(v => ({ valor: parseNum(v) || 0 }));
estado.cartoes.alimentacao = (rascunho.cartoes.alimentacao || []).map(v => ({ valor: parseNum(v) || 0 }));
estado.cartoes.pix = (rascunho.cartoes.pix || []).map(v => ({ valor: parseNum(v) || 0 }));
}
} else {
// No data for previous day: clear everything
estado.despesas = [];
estado.sangrias = [];
estado.pixcnpj = [];
estado.vales = [];
estado.receber = [];
estado.cancelamentos = [];
estado.cartoes = { credito: [], debito: [], alimentacao: [], pix: [] };
setVal('sys-troco', '');
setVal('f-saldo-esperado', '');
setVal('f-obs', '');
}
PRE_FILL_TABLES.forEach(ensureOneRow);
Object.keys(TABLES).forEach(renderTable);
updateTotals();
scheduleSave();
});
document.getElementById('f-obs').addEventListener('input', e => { estado.observacoes = e.target.value; scheduleSave(); });
// ─── Boot ─────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
loadConfig();
buildDatalists();
if (!loadState()) init();
});
</script>
</body>
</html>