Files
fechamento-caixa/src/index.html
T
root 5e4340adca
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
v1.3.7: só nomenclatura — Fechamento de Caixa / Saldo de Caixa
2026-06-26 02:57:34 +00:00

1862 lines
91 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">🖥️ Fechamento de Caixa</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">🛒 Saldo de Caixa</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 || 'União');
setVal('f-operador', estado.operador || '');
setVal('f-turno', estado.turno || '');
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')),
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);
// Restore simple fields
estado.uuid = saved.uuid || estado.uuid;
estado.id_local = saved.id_local !== undefined ? saved.id_local : null;
estado.loja = saved.loja || estado.loja;
estado.data = saved.data || estado.data;
estado.operador = saved.operador || estado.operador;
estado.turno = saved.turno || estado.turno;
estado.saldo_troco = saved.saldo_troco || 0;
estado.saldo_esperado = saved.saldo_esperado || 0;
estado.fechamento = saved.fechamento || 0;
estado.observacoes = saved.observacoes || '';
estado.sync_status = saved.sync_status || 'local';
// Restore tables (always as arrays of objects)
estado.despesas = Array.isArray(saved.despesas) ? saved.despesas : [];
estado.sangrias = Array.isArray(saved.sangrias) ? saved.sangrias : [];
estado.pixcnpj = Array.isArray(saved.pixcnpj) ? saved.pixcnpj : [];
estado.vales = Array.isArray(saved.vales) ? saved.vales : [];
estado.receber = Array.isArray(saved.receber) ? saved.receber : [];
estado.cancelamentos = Array.isArray(saved.cancelamentos) ? saved.cancelamentos : [];
// Restore cartoes — each entry must be { valor: number }
if (saved.cartoes) {
estado.cartoes.credito = Array.isArray(saved.cartoes.credito)
? saved.cartoes.credito.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
: [];
estado.cartoes.debito = Array.isArray(saved.cartoes.debito)
? saved.cartoes.debito.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
: [];
estado.cartoes.alimentacao = Array.isArray(saved.cartoes.alimentacao)
? saved.cartoes.alimentacao.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
: [];
estado.cartoes.pix = Array.isArray(saved.cartoes.pix)
? saved.cartoes.pix.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
: [];
}
// Restore fechamento_contado
if (saved.fechamento_contado) {
estado.fechamento_contado = saved.fechamento_contado;
}
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));
const fc = estado.fechamento_contado || {};
setVal('f-fech-dinheiro', fc.dinheiro ? fmtNum(fc.dinheiro) : '');
setVal('f-fech-cartoes', fc.cartoes ? fmtNum(fc.cartoes) : '');
setVal('f-fech-receber', fc.receber ? fmtNum(fc.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;
}
}
function buildConfirmHTML(data) {
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
const sangrias = (data.sangrias_arr || data.sangrias || []).reduce((s,r) => s+(parseNum(r.retirada||r.valor||0)||0), 0);
const despesas = (data.despesas_arr || data.despesas || []).reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const vales = (data.vales_arr || data.vales || []).reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const receber = (data.receber_arr || data.receber || []).reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const pixcnpj = (data.pixcnpj_arr || data.pixcnpj || []).reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const cancelamentos = (data.cancelamentos_arr || data.cancelamentos || []).reduce((s,c) => s+(parseNum(c.valor||0)||0), 0);
const cartCred = data.credito || 0;
const cartDeb = data.debito || 0;
const cartAli = data.alimentacao || 0;
const cartPix = data.voucher || data.pix || 0;
const fechamento = data.fechamento || 0;
const saldoEsp = data.saldo_esperado || 0;
const diferenca = fechamento - saldoEsp;
const diffClass = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
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 #b3d9f7; margin: 6px 0; }
.row { display: flex; justify-content: space-between; padding: 2px 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>
<div style="margin-bottom:6px"><strong>📅 Data:</strong> ${data.data} &nbsp; <strong>👤 Operador:</strong> ${data.operador} &nbsp; <strong>⏰ Turno:</strong> ${data.turno}</div>
<hr>
<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>
<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: ${data.loja}_${data.data}_${data.operador}</div>
<div class="footer">Gerado em ${new Date().toLocaleString('pt-BR')}</div>
</body></html>`;
}
// ─── Print Preview Window ──────────────────────────────────────────────────
function buildPrintHTML(data) {
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
// Usa a diferenca ja calculada pelo tauriRecibo (sys_total - saldo_esperado),
// igual ao modal de confirmacao — nao recalcula de fechamento.
const diferenca = data.diferenca !== undefined ? data.diferenca
: ((data.sys_total || 0) - (data.saldo_esperado || 0));
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmt(diferenca);
const diffColor = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
let rows = '';
// Cancelamentos (sum the array first — single cancelamentos is a float total, array is from confirm-print)
const cancelTotal = Array.isArray(data.cancelamentos)
? data.cancelamentos.reduce((s, c) => s + (parseNum(c.valor || 0) || 0), 0)
: (data.cancelamentos || 0);
// PIX total (flat number from tauriRecibo; pixcnpj is also flat total)
const pixTotal = data.pix || data.voucher || 0;
const pixCnpjTotal = typeof data.pixcnpj === 'number' ? data.pixcnpj : 0;
// 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 (flat total, not array)
if (pixCnpjTotal > 0) {
rows += `<tr><td>PIX CNPJ</td><td>${fmt(pixCnpjTotal)}</td><td></td></tr>`;
}
// Cancelamentos
if (cancelTotal > 0) {
rows += `<tr><td>Cancelamentos</td><td>${fmt(cancelTotal)}</td><td></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||0)}</span></div>
<div class="row"><span>Débito:</span><span>${fmt(data.debito||0)}</span></div>
<div class="row"><span>Alimentação:</span><span>${fmt(data.alimentacao||0)}</span></div>
<div class="row"><span>PIX Cartão:</span><span>${fmt(pixTotal)}</span></div>
<div class="row"><span>Cancelamentos:</span><span>${fmt(cancelTotal)}</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, mode) {
const html = mode === 'confirm' ? buildConfirmHTML(data) : buildPrintHTML(data);
// Remove preview anterior se existir
const existing = document.getElementById('print-preview-overlay');
if (existing) document.body.removeChild(existing);
const overlay = document.createElement('div');
overlay.id = 'print-preview-overlay';
overlay.style.cssText = `
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.85); z-index: 99999;
display: flex; flex-direction: column; align-items: center;
font-family: 'Courier New', monospace;
`;
const previewStyle = `
<style>
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 13px; background: #fff; padding: 10px; }
.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-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; }
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; }
.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 { background: #fff; } }
</style>
`;
const fullHtml = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Recibo</title>${previewStyle}</head><body>${html}</body></html>`;
const iframe = document.createElement('iframe');
iframe.style.cssText = 'width:360px; height:600px; border:2px solid #555; margin-top:16px; background:#fff;';
iframe.id = 'print-iframe';
const btnFechar = document.createElement('button');
btnFechar.textContent = '✕ Fechar';
btnFechar.style.cssText = `
margin-top: 12px; padding: 10px 32px;
background: #444; color: #fff; border: none; border-radius: 6px;
font-size: 15px; cursor: pointer;
`;
btnFechar.onclick = () => {
document.body.removeChild(overlay);
};
const btnImprimir = document.createElement('button');
btnImprimir.textContent = '🖨️ Imprimir';
btnImprimir.style.cssText = `
margin-top: 8px; padding: 10px 32px;
background: #e65100; color: #fff; border: none; border-radius: 6px;
font-size: 15px; cursor: pointer;
`;
btnImprimir.onclick = () => {
iframe.contentWindow.focus();
iframe.contentWindow.print();
};
overlay.appendChild(iframe);
overlay.appendChild(btnImprimir);
overlay.appendChild(btnFechar);
document.body.appendChild(overlay);
const iframeDoc = iframe.contentWindow.document;
iframeDoc.open();
iframeDoc.write(fullHtml);
iframeDoc.close();
}
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'));
// 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;
const data = {
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')),
saldo_esperado: saldoEsp,
fechamento: fechamentoCounted,
diferenca: sysTotal - saldoEsp,
sys_total: sysTotal,
// Flat totals for ReciboData struct (Rust) — must be numbers
credito: cartCred,
debito: cartDeb,
alimentacao: cartAli,
voucher: cartPix,
pixcnpj: pixCnpjTotal,
sangrias: sangriasTotal,
despesas: despesasTotal,
vales: valesTotal,
areceber: receberTotal,
cancelamentos: cancelTotal,
// Full arrays for backend/storage
despesas_arr: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
sangrias_arr: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
pixcnpj_arr: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
vales_arr: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })),
receber_arr: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
cancelamentos_arr: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
cartoes: { credito: cartCred, debito: cartDeb, alimentacao: cartAli, pix: cartPix },
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', { data: 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() {
console.log('[DEBUG] openConfirmModal chamado');
const operador = val('f-operador').trim();
const turno = val('f-turno').trim();
const loja = val('f-loja').trim();
console.log('[DEBUG] openConfirmModal campos:', {operador, turno, loja});
if (!operador || !turno || !loja) {
console.log('[DEBUG] openConfirmModal: campos faltando, alert');
alert('⚠️ Preencha Operador, Turno e Loja antes de enviar.');
return;
}
console.log('[DEBUG] openConfirmModal: campos OK, montando modal');
// 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 sysTotal = (parseNum(val('sys-troco')) || 0) + cartCred + cartDeb + cartAli + cartPix + vales + receber + pixcnpj;
const diferenca = sysTotal - 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 (contado): <strong>${fmt(fechamento)}</strong></div>
<div>🖥️ Total Sistema: <strong>${fmt(sysTotal)}</strong></div>
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
<div>🎯 Saldo Esperado (contado): <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>`;
console.log('[DEBUG] openConfirmModal: tentando abrir modal');
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 idêntico ao modal de confirmação
document.getElementById('confirm-print').addEventListener('click', async () => {
closeConfirmModal();
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 fechamentoCounted = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
const saldoEsp = parseNum(val('f-saldo-esperado'));
const data = {
loja: estado.loja, data: estado.data, operador: estado.operador, turno: estado.turno,
saldo_troco: parseNum(val('sys-troco')) || 0,
saldo_esperado: saldoEsp,
fechamento: fechamentoCounted,
diferenca: fechamentoCounted - saldoEsp,
despesas_arr: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
sangrias_arr: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
pixcnpj_arr: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
vales_arr: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })),
receber_arr: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
cancelamentos_arr: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
credito: cartCred, debito: cartDeb, alimentacao: cartAli,
voucher: cartPix,
observacoes: estado.observacoes,
};
openPrintPreview(data, 'confirm');
});
// 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', () => {
console.log('[DEBUG] btn-enviar clicado');
const operador = document.getElementById('f-operador')?.value;
const turno = document.getElementById('f-turno')?.value;
const loja = document.getElementById('f-loja')?.value;
console.log('[DEBUG] operador:', operador, 'turno:', turno, 'loja:', loja);
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);
// Reseta estado e re-renderiza sem reload
estado = {
uuid: crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(),
loja: estado.loja, data: estado.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'
};
setVal('f-operador', ''); setVal('f-turno', '');
setVal('f-fech-dinheiro', ''); setVal('f-fech-cartoes', ''); setVal('f-fech-receber', '');
setVal('f-saldo-esperado', ''); setVal('f-obs', '');
setVal('sys-troco', '');
calcular();
renderAllTables();
syncUI();
showToast('Rascunho limpo.', 'info');
});
const btnAnterior = document.getElementById('btn-anterior');
if (btnAnterior) {
btnAnterior.addEventListener('click', async () => {
console.log('[DEBUG] btn-anterior clicado');
const loja = val('f-loja') || 'União';
console.log('[DEBUG] btn-anterior loja:', loja);
// Salva rascunho atual antes de tudo
await tauriSaveLocal();
// Calcula dia anterior
const prevDate = new Date(estado.data);
prevDate.setDate(prevDate.getDate() - 1);
const prevDateStr = prevDate.toISOString().split('T')[0];
// Confirmação explícita antes de trocar de dia
const confirmMsg = `ATENÇÃO: vai carregar o fechamento de ${prevDateStr}.\n\nO formulário atual será LIMPO caso não tenha enviado.\n\nContinuar?`;
if (!confirm(confirmMsg)) {
console.log('[DEBUG] btn-anterior cancelado pelo usuário');
return;
}
// Carrega lista de fechamentos recentes do Supabase com uuid
let recentList = [];
try {
recentList = await window.__TAURI__.core.invoke('sb_listar_recentes', { loja, limite: 20 });
console.log('[DEBUG] sb_listar_recentes retornou:', recentList?.length, 'itens');
} catch(e) {
console.warn('sb_listar_recentes falhou:', e);
}
// Extrai datas únicas ordenadas + mapeamento data→id
const dateIdMap = {};
const dateList = [];
if (recentList && recentList.length > 0) {
recentList.forEach(item => {
if (item.data && !dateIdMap[item.data]) {
dateIdMap[item.data] = item.id;
dateList.push(item.data);
}
});
dateList.sort().reverse();
}
console.log('[DEBUG] dateList:', dateList);
let selectedDate = null;
let selectedId = null;
if (dateList.length > 0) {
const msg = dateList.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 < dateList.length) {
selectedDate = dateList[idx];
selectedId = dateIdMap[selectedDate];
}
}
}
// Se não escolheu data, usa dia anterior automático (sem id — carrega por data)
if (!selectedDate) {
selectedDate = prevDateStr;
}
console.log('[DEBUG] selectedDate:', selectedDate, 'selectedId:', selectedId);
setVal('f-data', selectedDate);
estado.data = selectedDate;
estado.uuid = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
estado.id_local = null;
syncHeader();
// Tenta Supabase primeiro (com id se disponível), senão local
let rascunho = null;
if (selectedId) {
try {
rascunho = await window.__TAURI__.core.invoke('sb_buscar_por_id', { id: selectedId });
console.log('[DEBUG] sb_buscar_por_id retornou:', !!rascunho);
} catch(e) { console.warn('sb_buscar_por_id falhou:', e); }
}
if (!rascunho) {
try {
rascunho = await window.__TAURI__.core.invoke('sb_carregar_rascunho', { loja, data: selectedDate });
console.log('[DEBUG] sb_carregar_rascunho retornou:', !!rascunho);
} catch(e) { console.warn('sb_carregar_rascunho falhou:', e); }
}
if (!rascunho) {
try {
rascunho = await window.__TAURI__.core.invoke('carregar_rascunho', { loja, data: selectedDate });
console.log('[DEBUG] carregar_rascunho local retornou:', !!rascunho);
} catch(e) { console.warn('carregar_rascunho local falhou:', e); }
}
if (rascunho) {
console.log('[DEBUG] rascunho carregado com sucesso');
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) {
const norm = (arr) => Array.isArray(arr) ? arr.map(v => ({ valor: (typeof v === 'object' && v !== null) ? (parseNum(v.valor) || 0) : (parseNum(v) || 0) })) : [];
estado.cartoes.credito = norm(rascunho.cartoes.credito);
estado.cartoes.debito = norm(rascunho.cartoes.debito);
estado.cartoes.alimentacao = norm(rascunho.cartoes.alimentacao);
estado.cartoes.pix = norm(rascunho.cartoes.pix);
}
} else {
console.log('[DEBUG] nenhum rascunho encontrado para esta data');
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', '');
}
estado.sync_status = rascunho ? 'loaded' : 'local';
PRE_FILL_TABLES.forEach(ensureOneRow);
Object.keys(TABLES).forEach(renderTable);
updateTotals();
scheduleSave();
console.log('[DEBUG] btn-anterior concluído');
});
} else {
console.error('[DEBUG] btn-anterior NÃO encontrado no DOM!');
}
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>