Files
fechamento-caixa/src/index.html
T
root d185f82bd5
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
Revert "v1.2.3: btn voltar no preview; relatorio corrigido (fechamento-esperado); cartoes com detalhes p/ Supabase"
This reverts commit 2253d45d00.
2026-06-24 01:18:57 +00:00

1634 lines
80 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>
<!-- DIFERENÇA — dentro do Saldo Esperado, logo abaixo -->
<div class="dif-box" style="margin-top:8px">
<span class="dif-label">Diferença</span>
<span class="dif-value zero" id="diferenca">R$ 0,00</span>
<span id="dif-badge" style="font-size:11px;font-weight:700;padding:2px 8px;border-radius:10px;background:#e8f5e9;color:#2e7d32;display:none">OK</span>
</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>
<!-- 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';
// For numeric cols that hold money, format with 2 decimals
// For text cols (hora, desc, obs, nome, gerente, motivo, numero) keep as-is
const numericCols = ['valor', 'retirada'];
inp.value = rowData[col] !== undefined
? (numericCols.includes(col) ? (rowData[col] === 0 ? '' : fmtNum(rowData[col])) : (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;
// datalist only for fields that should offer suggestions from config
if (col === 'gerente') inp.setAttribute('list', 'dl-gerente');
if (col === 'desc') inp.setAttribute('list', 'dl-despesa');
if (col === 'nome' && tblId === 'tbl-vales') inp.setAttribute('list', 'dl-vales');
// tbl-pix nome = free text (no datalist)
// tbl-receber nome = free text (no datalist)
// tbl-cancelamentos numero + motivo = free text (no datalist)
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 {
// text fields: nome, desc, obs, gerente, motivo, numero — keep as string
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();
}
// For text cols (hora, nome, desc, obs, gerente, motivo, numero) — keep as-is
}
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 numericCols = ['valor', 'retirada'];
const empty = {};
cfg.cols.forEach(c => { empty[c] = numericCols.includes(c) ? 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');
const badgeEl = document.getElementById('dif-badge');
if (difEl) {
difEl.textContent = fmt(diff);
if (diff === 0) {
difEl.className = 'dif-value zero';
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
} else if (diff < 0 && diff >= -5) {
difEl.className = 'dif-value neg'; // OK até 5 de tolerância
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
} else if (diff > 0 && diff <= 3) {
difEl.className = 'dif-value zero'; // OK + até 3
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
} else {
// Sobra mais de R$3 ou falta mais de R$5
difEl.className = diff > 0 ? 'dif-value pos' : 'dif-value neg';
if (badgeEl) {
if (diff > 0) {
badgeEl.textContent = '⚠ SOBRANDO';
badgeEl.style.background='#fff3e0';
badgeEl.style.color='#e65100';
} else {
badgeEl.textContent = '⚠ FALTANDO';
badgeEl.style.background='#ffebee';
badgeEl.style.color='#c0272d';
}
badgeEl.style.display='inline';
}
}
}
}
// ─── 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 — debug completo
console.log('=== Enviando para Supabase ===');
console.log('URL:', 'https://supabase.ofrangao.com.br/rest/v1/fechamentos');
console.log('Payload:', JSON.stringify(data, null, 2));
try {
const result = await window.__TAURI__.core.invoke('sb_salvar_fechamento', { estado: data });
console.log('Supabase resposta:', JSON.stringify(result));
return result;
} catch(e) {
console.error('Erro sb_salvar_fechamento:', JSON.stringify(e));
// Tenta debug: qual era o payload exato?
console.error('Payload que falhou:', JSON.stringify(data));
throw e;
}
}
// ─── Print Preview Window ──────────────────────────────────────────────────
function buildPrintHTML(data) {
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
const diff = (data.saldo_troco - data.saldo_esperado);
const diffStr = diff >= 0 ? `+${fmt(diff)}` : fmt(diff);
const diffColor = diff === 0 ? '#2e7d32' : (diff > 0 ? '#e65100' : '#c0272d');
let rows = '';
// Sangrias
if (data.sangrias && data.sangrias.length > 0) {
data.sangrias.forEach(s => { if (s.hora || s.retirada) rows += `<tr><td>${s.hora||''}</td><td>${fmt(s.retirada)}</td><td>${s.gerente||''}</td></tr>`; });
}
// Despesas
if (data.despesas && data.despesas.length > 0) {
data.despesas.forEach(d => { if (d.desc || d.valor) rows += `<tr><td>${d.desc||''}</td><td>${fmt(d.valor)}</td><td>${d.obs||''}</td></tr>`; });
}
// Vales
if (data.vales && data.vales.length > 0) {
data.vales.forEach(v => { if (v.nome || v.valor) rows += `<tr><td>${v.nome||''}</td><td>${fmt(v.valor)}</td><td>${v.obs||''}</td></tr>`; });
}
// Receber
if (data.receber && data.receber.length > 0) {
data.receber.forEach(r => { if (r.nome || r.valor) rows += `<tr><td>${r.nome||''}</td><td>${fmt(r.valor)}</td><td></td></tr>`; });
}
// PIX CNPJ
if (data.pixcnpj && data.pixcnpj.length > 0) {
data.pixcnpj.forEach(p => { if (p.nome || p.valor) rows += `<tr><td>PIX: ${p.nome||''}</td><td>${fmt(p.valor)}</td><td></td></tr>`; });
}
// Cancelamentos
if (data.cancelamentos && data.cancelamentos.length > 0) {
data.cancelamentos.forEach(c => { if (c.numero || c.valor) rows += `<tr><td>${c.numero||''}</td><td>${fmt(c.valor)}</td><td>${c.motivo||''}</td></tr>`; });
}
return `<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
<title>Relatório de Fechamento</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Courier New', monospace; font-size: 13px; width: 80mm; margin: 0 auto; padding: 8px; }
.header { text-align: center; margin-bottom: 8px; }
.header h1 { font-size: 18px; }
.header h2 { font-size: 14px; font-weight: normal; }
hr { border: none; border-top: 1px dashed #000; margin: 6px 0; }
.section { margin: 6px 0; }
.section-title { font-weight: bold; font-size: 14px; border-top: 1px dashed #000; padding-top: 4px; margin-top: 6px; }
.row { display: flex; justify-content: space-between; padding: 2px 0; }
.row.space-between { justify-content: space-between; }
.row.below { margin-top: 4px; }
table { width: 100%; border-collapse: collapse; font-size: 12px; }
td { padding: 2px 4px; vertical-align: top; }
td:nth-child(2) { text-align: right; }
td:nth-child(3) { text-align: right; color: #555; font-size: 11px; }
.total-row { font-weight: bold; border-top: 1px dashed #000; }
.grand-total { font-size: 16px; font-weight: bold; text-align: center; padding: 6px; border: 2px solid #000; margin: 8px 0; }
.obs { font-size: 11px; color: #555; margin-top: 6px; white-space: pre-wrap; }
.footer { text-align: center; font-size: 11px; color: #555; margin-top: 8px; }
@media print { body { margin: 0; } }
</style>
</head><body>
<div class="header">
<h1>${data.loja}</h1>
<h2>Fechamento de Caixa</h2>
</div>
<hr>
<div class="row"><span>Data:</span><span>${data.data}</span></div>
<div class="row"><span>Operador:</span><span>${data.operador}</span></div>
<div class="row"><span>Turno:</span><span>${data.turno}</span></div>
<div class="section-title">ENTRADAS</div>
<div class="row"><span>Saldo Troco:</span><span>${fmt(data.saldo_troco)}</span></div>
<div class="row"><span>Fechamento:</span><span>${fmt(data.fechamento)}</span></div>
<div class="row"><span>Saldo Esperado:</span><span>${fmt(data.saldo_esperado)}</span></div>
${rows ? `<div class="section-title">DETALHES</div><table><tbody>${rows}</tbody></table>` : ''}
${data.observacoes ? `<div class="obs">Obs: ${data.observacoes}</div>` : ''}
<hr>
<div class="section-title">CARTÕES</div>
<div class="row"><span>Crédito:</span><span>${fmt(data.credito)}</span></div>
<div class="row"><span>Débito:</span><span>${fmt(data.debito)}</span></div>
<div class="row"><span>Alimentação:</span><span>${fmt(data.alimentacao)}</span></div>
<div class="row"><span>PIX Cartão:</span><span>${fmt(data.cartoes?.pix||0)}</span></div>
<div class="row"><span>Cancelamentos:</span><span>${fmt(data.cancelamentos)}</span></div>
<hr>
<div class="grand-total">DIFERENÇA: ${diffStr}</div>
<div class="footer">Gerado em ${new Date().toLocaleString('pt-BR')}</div>
</body></html>`;
}
function openPrintPreview(data) {
// Gera HTML e injeta no body de uma div temporária para window.print()
const html = buildPrintHTML(data);
const style = document.createElement('style');
style.textContent = `
@media print {
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 13px; }
.no-print { display: none !important; }
}
.print-actions { position: fixed; top: 10px; right: 10px; z-index: 9999; }
.print-actions button { padding: 8px 16px; cursor: pointer; background: #2563eb; color: #fff; border: none; border-radius: 4px; font-size: 14px; }
.print-actions button:hover { background: #1d4ed8; }
`;
const actions = document.createElement('div');
actions.className = 'print-actions no-print';
actions.innerHTML = '<button onclick="window.print()">🖨️ Imprimir (Ctrl+P)</button>';
// Guarda conteúdo original
const originalBody = document.body.innerHTML;
document.body.innerHTML = html;
document.head.appendChild(style);
document.body.insertBefore(actions, document.body.firstChild);
// Quando fechar print dialog, restaura
const onAfterPrint = () => {
document.body.innerHTML = originalBody;
window.removeEventListener('afterprint', onAfterPrint);
// Re-carrega scripts essenciais
location.reload();
};
window.addEventListener('afterprint', onAfterPrint);
}
function triggerWindowPrint() {
// Chama window.print() na janela pai (o próprio app)
window.print();
}
// ─── Recibo 80mm via Tauri (ESC/POS USB) ─────────────────────────────────
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,
};
// Tenta ESC/POS via Tauri primeiro; se falhar, abre preview
try {
return await window.__TAURI__.core.invoke('imprimir_recibo', { estado: JSON.stringify(data) });
} catch(e) {
console.warn('ESC/POS falhou, abrindo preview:', e);
openPrintPreview(data);
throw e;
}
}
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 pixcnpj = estado.pixcnpj.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'));
const diferenca = (parseNum(val('sys-troco')) || 0) - saldoEsp;
const diffClass = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
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>🔵 PIX CNPJ: <strong>${fmt(pixcnpj)}</strong></div>
<div>💳 Crédito: <strong>${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">📐 Diferença: <strong style="color:${diffClass}">${fmt(diferenca)}</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 — abre preview no navegador
document.getElementById('confirm-print').addEventListener('click', async () => {
closeConfirmModal();
// Reconstrói os mesmos dados do tauriRecibo e abre preview
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 data = {
loja: estado.loja, data: estado.data, operador: estado.operador, turno: estado.turno,
saldo_troco: parseNum(val('sys-troco')) || 0,
saldo_esperado: parseNum(val('f-saldo-esperado')) || 0,
fechamento: 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: 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||'' })),
credito: cartCred, debito: cartDeb, alimentacao: cartAli,
cartoes: { pix: cartPix },
observacoes: estado.observacoes,
};
openPrintPreview(data);
});
// 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!\n\n📋 Tabela: fechamentos_web\n🌐 Servidor: supabase.ofrangao.com.br\n\nAbra o Supabase Studio para verificar.');
} catch(e) {
await tauriSaveLocal();
const msg = e?.message || e?.toString?.() || String(e);
alert(
'⚠️ Erro ao enviar para o Supabase.\n\n' +
'📋 Tabela: fechamentos_web\n' +
'🌐 Servidor: supabase.ofrangao.com.br\n\n' +
'O servidor pode estar offline. Dados salvos localmente.\n\n' +
'Erro: ' + msg
);
} 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 {
// Tenta ESC/POS primeiro; se falhar, abre preview no navegador
await tauriRecibo();
// Se chegou aqui sem erro, ESC/POS funcionou
} catch(e) {
// ESC/POS falhou — openPrintPreview já foi chamado dentro de tauriRecibo
// Só mostra alerta se nem o preview abriu
console.warn('Recibo:', 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 loja = val('f-loja') || 'União';
// Carrega lista de datas recentes do Supabase
let datas = [];
try {
const lista = await window.__TAURI__.core.invoke('sb_listar_recentes', { loja, limite: 20 });
if (lista && lista.length > 0) {
// Extrai datas únicas e já ordenadas
datas = [...new Set(lista.map(l => l.data).filter(Boolean))].sort().reverse();
}
} catch(e) {
console.warn('sb_listar_recentes falhou:', e);
}
let selectedDate = null;
if (datas.length > 0) {
// Mostra seletor com as datas disponíveis
const msg = datas.map((d, i) => `${i + 1}) ${d}`).join('\n');
const escolha = prompt(`Selecione o número do dia anterior:\n${msg}\n\n(Deixe em branco e clique Cancelar para ver só o dia anterior automático)`);
if (escolha !== null && escolha.trim() !== '') {
const idx = parseInt(escolha) - 1;
if (idx >= 0 && idx < datas.length) selectedDate = datas[idx];
}
}
// Se não escolheu data, usa dia anterior automático
if (!selectedDate) {
const d = new Date(estado.data); d.setDate(d.getDate() - 1);
selectedDate = d.toISOString().split('T')[0];
}
await tauriSaveLocal();
setVal('f-data', selectedDate);
estado.data = selectedDate;
estado.uuid = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
estado.id_local = null;
syncHeader();
// Tenta Supabase primeiro, senão local
let rascunho = null;
try {
rascunho = await window.__TAURI__.core.invoke('sb_carregar_rascunho', { loja, data: selectedDate });
} catch(e) { console.warn('sb_carregar_rascunho falhou:', e); }
if (!rascunho) {
try {
rascunho = await window.__TAURI__.core.invoke('carregar_rascunho', { loja, data: selectedDate });
} catch(e) { console.warn('carregar_rascunho local falhou:', e); }
}
if (rascunho) {
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; }
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 {
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', '');
}
// Marcar que este é um rascunho carregado (não novo)
estado.sync_status = rascunho ? 'loaded' : 'local';
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>