Files
fechamento-caixa/src/index.html
T
root 42aadcca2c
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled
fix: dados JSONB no PATCH, total_cancelamentos, corrige duplicados total_*
2026-07-01 14:50:35 +00:00

3375 lines
172 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>
<!-- CONTADORES -->
<div class="section">
<div class="section-header">📦 Vendas</div>
<div style="display:flex;gap:12px">
<div style="flex:1">
<label style="font-size:12px;color:#666">Vendas (R$)</label>
<input type="text" id="f-vendas" placeholder="0,00" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:var(--radius);font-size:15px;text-align:right" onblur="fmtMoneyInput(this)" onfocus="unfmtMoneyInput(this)" />
</div>
</div>
</div>
<!-- CONTADORES -->
<div class="section">
<div class="section-header">👥 Clientes & Frango Assado</div>
<div style="display:flex;gap:12px">
<div style="flex:1">
<label style="font-size:12px;color:#666">Clientes</label>
<input type="number" id="f-clientes" min="0" placeholder="0" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:var(--radius);font-size:15px" />
</div>
<div style="flex:1">
<label style="font-size:12px;color:#666">Frango Assado</label>
<input type="number" id="f-frango" min="0" placeholder="0" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:var(--radius);font-size:15px" />
</div>
</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 class="cfg-section">
<div class="cfg-section-title">🔐 Senha Admin (editar dias anteriores)</div>
<div style="display:flex;gap:6px;margin-top:4px">
<input type="password" id="cfg-admin-password" style="flex:1;padding:6px;border:1px solid #ddd;border-radius:4px;font-size:13px" placeholder="Deixe em branco se não quiser senha">
</div>
</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:14px;background:#f0f7ff;font-size:14px;font-family:Arial,sans-serif;line-height:1.6">
</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)
admin_password: '', // senha para editar dias anteriores no Supabase
};
// ─── 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 },
clientes: 0, frango: 0, vendas: 0, // contadores
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().replace(/^R\$\s*/, ''); // strip currency prefix first
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 });
};
const fmtMoneyInput = el => {
const raw = parseNum(el.value);
if (raw > 0) el.value = fmtNum(raw);
};
const unfmtMoneyInput = el => {
const raw = parseNum(el.value);
if (raw > 0) el.value = raw.toString();
};
// 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 && rowData[col] !== null
? (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) : '';
// Saldo de Caixa (Sistema) = 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 = total_fechamento (sysTotal: troco+credito+debito+alimentacao+vales+receber+pixCartao+pixCnpj) - saldo_esperado - cancelamentos
const cancelTotal = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
const diff = sysTotal - parseNum(val('f-saldo-esperado')) - cancelTotal;
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() {
// Calcula totais das tabelas (mesma lógica do updateTotals)
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 pixcnpjTotal = estado.pixcnpj.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 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);
// 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')),
saldo_esperado: parseNum(val('f-saldo-esperado')),
fechamento: parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')),
// Saldos de cartões (do Sys / Saldo de Caixa)
credito: parseNum(val('sys-credito')) || cartCred,
debito: parseNum(val('sys-debito')) || cartDeb,
alimentacao: parseNum(val('sys-alimentacao')) || cartAli,
pixcnpj: parseNum(val('sys-pixcnpj')) || pixcnpjTotal,
vales: parseNum(val('sys-vales')) || valesTotal,
receber: parseNum(val('sys-receber')) || receberTotal,
// Totais para colunas SQL
total_sangrias: sangriasTotal,
total_despesas: despesasTotal,
total_pixcnpj: pixcnpjTotal,
total_vales: valesTotal,
total_areceber: receberTotal,
total_cancelamentos: cancelTotal,
total_cartao_credito: cartCred,
total_cartao_debito: cartDeb,
total_cartao_alimentacao: cartAli,
total_cartao_pix: cartPix,
// Fechamento contado
fd: parseNum(val('f-fech-dinheiro')),
fc: parseNum(val('f-fech-cartoes')),
fr: 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,
clientes: estado.clientes,
frango: estado.frango,
vendas: estado.vendas,
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.clientes = parseInt(document.getElementById('f-clientes').value) || 0;
estado.frango = parseInt(document.getElementById('f-frango').value) || 0;
estado.vendas = parseNum(val('f-vendas'));
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.clientes = saved.clientes || 0;
estado.frango = saved.frango || 0;
estado.vendas = saved.vendas || 0;
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() {
config.admin_password = document.getElementById('cfg-admin-password').value.trim();
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');
document.getElementById('cfg-admin-password').value = config.admin_password || '';
}
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) {
// Relatório completo — mesmo layout do PDF
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
const fmtNeg = v => (v < 0 ? '-' : '') + 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
// Diferença = fechamento - saldo_esperado - cancelamentos
const diferenca = data.diferenca !== undefined ? data.diferenca
: ((data.fechamento || 0) - (data.saldo_esperado || 0) - (data.cancelamentos || 0));
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmtNeg(diferenca);
const diffOk = diferenca === 0;
const sangrias_arr = Array.isArray(data.sangrias_arr) ? data.sangrias_arr : [];
const despesas_arr = Array.isArray(data.despesas_arr) ? data.despesas_arr : [];
const pixcnpj_arr = Array.isArray(data.pixcnpj_arr) ? data.pixcnpj_arr : [];
const vales_arr = Array.isArray(data.vales_arr) ? data.vales_arr : [];
const receber_arr = Array.isArray(data.receber_arr) ? data.receber_arr : [];
const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
const cartCred_arr = Array.isArray(data.cartoes_credito) ? data.cartoes_credito : [];
const cartDeb_arr = Array.isArray(data.cartoes_debito) ? data.cartoes_debito : [];
const cartAli_arr = Array.isArray(data.cartoes_alimentacao) ? data.cartoes_alimentacao : [];
const cartPix_arr = Array.isArray(data.cartoes_pix) ? data.cartoes_pix : [];
const totalSangrias = sangrias_arr.reduce((s,r) => s+(r.retirada||0)+(r.valor||0), 0);
const totalDespesas = despesas_arr.reduce((s,r) => s+(r.valor||0), 0);
const totalPixCnpj = pixcnpj_arr.reduce((s,r) => s+(r.valor||0), 0);
const totalVales = vales_arr.reduce((s,r) => s+(r.valor||0), 0);
const totalReceber = receber_arr.reduce((s,r) => s+(r.valor||0), 0);
const totalCancel = cancelamentos_arr.reduce((s,r) => s+(r.valor||0), 0);
const cartCred = cartCred_arr.reduce((s,v) => s+(v||0), 0);
const cartDeb = cartDeb_arr.reduce((s,v) => s+(v||0), 0);
const cartAli = cartAli_arr.reduce((s,v) => s+(v||0), 0);
const cartPix = cartPix_arr.reduce((s,v) => s+(v||0), 0);
const totalCartoes = cartCred + cartDeb + cartAli + cartPix;
const fdin = data.fechamento_dinheiro || 0;
const fcarts = data.fechamento_cartoes || 0;
const frec = data.fechamento_receber || 0;
const ftotal = data.fechamento || (fdin + fcarts + frec);
const sysTotal = data.sys_total || 0;
const saldoEsperado = data.saldo_esperado || 0;
// Cartoes table
const maxRows = Math.max(cartCred_arr.length, cartDeb_arr.length, cartAli_arr.length, cartPix_arr.length);
let cartTableRows = '';
for (let i = 0; i < maxRows; i++) {
const cr = cartCred_arr[i] || 0;
const db = cartDeb_arr[i] || 0;
const al = cartAli_arr[i] || 0;
const px = cartPix_arr[i] || 0;
cartTableRows += `<tr>
<td>${cr > 0 ? fmt(cr) : '-'}</td>
<td>${db > 0 ? fmt(db) : '-'}</td>
<td>${al > 0 ? fmt(al) : '-'}</td>
<td>${px > 0 ? fmt(px) : '-'}</td>
</tr>`;
}
cartTableRows += `<tr class="total-row">
<td>${cartCred > 0 ? fmt(cartCred) : '-'}</td>
<td>${cartDeb > 0 ? fmt(cartDeb) : '-'}</td>
<td>${cartAli > 0 ? fmt(cartAli) : '-'}</td>
<td>${cartPix > 0 ? fmt(cartPix) : '-'}</td>
</tr>`;
let html = `<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
<title>Fechamento de Caixa</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Courier New', monospace; font-size: 15px; width: 80mm; margin: 0 auto; padding: 8px; font-weight: bold !important; print-color-adjust: exact; -webkit-print-color-adjust: exact; }
.header { text-align: center; margin-bottom: 8px; }
.header h1 { font-size: 20px; font-weight: bold !important; }
.header h2 { font-size: 16px; font-weight: bold !important; }
hr { border: none; border-top: 2px solid #000; margin: 6px 0; }
.section-title { font-weight: bold !important; font-size: 14px; border-top: 2px solid #000; padding-top: 4px; margin-top: 8px; }
.row { display: flex; justify-content: space-between; padding: 2px 0; font-size: 14px; font-weight: bold !important; }
.row.total-row { font-weight: bold !important; }
.cart-table { table-layout: fixed; width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 4px; font-weight: bold !important; }
.cart-table th { font-weight: bold !important; border-bottom: 2px solid #000; padding: 3px; font-size: 13px; width: 25%; }
.cart-table td { padding: 3px 5px; text-align: right; font-weight: bold !important; font-size: 13px; }
.cart-table td:first-child { text-align: left; }
.cart-table tr.total-row { font-weight: bold !important; border-top: 2px solid #000; }
.subtotal { font-size: 13px; margin-top: 4px; }
.subtotal .row { padding: 1px 0; font-weight: bold !important; }
.diff-ok { color: #2e7d32; font-weight: bold !important; }
.diff-bad { color: #c0272d; font-weight: bold !important; }
.obs { font-size: 12px; color: #555; margin-top: 6px; white-space: pre-wrap; font-weight: bold !important; }
.footer { text-align: center; font-size: 12px; color: #555; margin-top: 8px; font-weight: bold !important; }
@media print { body { background: #fff; font-weight: bold !important; font-size: 15px !important; } }
</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>
${sangrias_arr.length > 0 ? `<div class="section-title">SANGRIAS</div>
${sangrias_arr.map(s => `<div class="row"><span>${(s.hora||'')}: ${s.gerente||''}</span><span>${fmt((s.retirada||0)+(s.valor||0))}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL SANGRIAS:</span><span>${fmt(totalSangrias)}</span></div>` : ''}
${despesas_arr.length > 0 ? `<div class="section-title">DESPESAS</div>
${despesas_arr.map(d => `<div class="row"><span>${d.desc||''}${d.obs ? ` (${d.obs})` : ''}</span><span>${fmt(d.valor||0)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL DESPESAS:</span><span>${fmt(totalDespesas)}</span></div>` : ''}
${pixcnpj_arr.length > 0 ? `<div class="section-title">PIX CNPJ</div>
${pixcnpj_arr.map(p => `<div class="row"><span>${p.nome||''}</span><span>${fmt(p.valor||0)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL PIX CNPJ:</span><span>${fmt(totalPixCnpj)}</span></div>` : ''}
${vales_arr.length > 0 ? `<div class="section-title">VALES</div>
${vales_arr.map(v => `<div class="row"><span>${v.nome||''}${v.obs ? ` (${v.obs})` : ''}</span><span>${fmt(v.valor||0)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL VALES:</span><span>${fmt(totalVales)}</span></div>` : ''}
${receber_arr.length > 0 ? `<div class="section-title">A RECEBER</div>
${receber_arr.map(r => `<div class="row"><span>${r.nome||''}</span><span>${fmt(r.valor||0)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL A RECEBER:</span><span>${fmt(totalReceber)}</span></div>` : ''}
${cancelamentos_arr.length > 0 ? `<div class="section-title">CANCELAMENTOS</div>
${cancelamentos_arr.map(c => `<div class="row"><span>${c.numero ? `${c.numero}` : ''}${c.motivo ? ` (${c.motivo})` : ''}</span><span>${fmt(c.valor||0)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL:</span><span>${fmt(totalCancel)}</span></div>` : ''}
<div class="section-title">DETALHAMENTO DE CARTÕES</div>
<table class="cart-table">
<thead>
<tr>
<th>CRÉDITO</th><th>DÉBITO</th><th>ALIMENT.</th><th>PIX</th>
</tr>
</thead>
<tbody>
${cartTableRows}
</tbody>
</table>
<div class="subtotal"><div class="row total-row"><span>Soma Cartões:</span><span>${fmt(totalCartoes)}</span></div></div>
<div class="section-title">SALDO DE CAIXA (SISTEMA) — RESUMO</div>
<div class="row"><span>Troco</span><span>${fmt(data.saldo_troco||0)}</span></div>
<div class="row"><span>Crédito</span><span>${fmt(cartCred)}</span></div>
<div class="row"><span>Débito</span><span>${fmt(cartDeb)}</span></div>
<div class="row"><span>Alimentação</span><span>${fmt(cartAli)}</span></div>
<div class="row"><span>Vales</span><span>${fmt(totalVales)}</span></div>
<div class="row"><span>À Receber</span><span>${fmt(totalReceber)}</span></div>
<div class="row"><span>PIX Cartão</span><span>${fmt(cartPix)}</span></div>
<div class="row"><span>PIX CNPJ</span><span>${fmt(totalPixCnpj)}</span></div>
<div class="row total-row"><span>TOTAL SISTEMA</span><span>${fmt(sysTotal)}</span></div>
<div class="row"><span>Saldo Esperado</span><span>${fmt(saldoEsperado)}</span></div>
<div class="row"><span>DIFERENÇA</span><span class="${diffOk ? 'diff-ok' : 'diff-bad'}">${diffStr}${diffOk ? ' ✓ OK' : ''}</span></div>
<div class="section-title">FECHAMENTO (CONTADO)</div>
<div class="row"><span>Dinheiro:</span><span>${fmt(fdin)}</span></div>
<div class="row"><span>Cartões:</span><span>${fmt(fcarts)}</span></div>
<div class="row"><span>À Receber:</span><span>${fmt(frec)}</span></div>
<div class="row total-row"><span>TOTAL:</span><span>${fmt(ftotal)}</span></div>
<div class="section-title">CONTADORES</div>
<div class="row"><span>Clientes:</span><span>${data.clientes || 0}</span></div>
<div class="row"><span>Frango Assado:</span><span>${data.frango || 0}</span></div>
${data.observacoes ? `<div class="obs">${data.observacoes}</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('.', ',');
const fmtNeg = v => (v < 0 ? '-' : '') + 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
// Diferença = Total Sistema - Saldo Esperado - Cancelamentos
const sysTotalCalc = (data.sysTotal || 0);
const saldoEsperadoCalc = (data.saldo_esperado || 0);
const cancelamentosCalc = (data.cancelamentos || 0);
const diferenca = sysTotalCalc - saldoEsperadoCalc - cancelamentosCalc;
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmtNeg(diferenca);
const diffColor = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
// Arrays — support both array-of-objects and flat totals
const sangrias_arr = Array.isArray(data.sangrias_arr) ? data.sangrias_arr : [];
const despesas_arr = Array.isArray(data.despesas_arr) ? data.despesas_arr : [];
const pixcnpj_arr = Array.isArray(data.pixcnpj_arr) ? data.pixcnpj_arr : [];
const vales_arr = Array.isArray(data.vales_arr) ? data.vales_arr : [];
const receber_arr = Array.isArray(data.receber_arr) ? data.receber_arr : [];
const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
// Cartoes arrays from estado
const cartCred_arr = Array.isArray(data.cartoes_credito) ? data.cartoes_credito : [];
const cartDeb_arr = Array.isArray(data.cartoes_debito) ? data.cartoes_debito : [];
const cartAli_arr = Array.isArray(data.cartoes_alimentacao) ? data.cartoes_alimentacao : [];
const cartPix_arr = Array.isArray(data.cartoes_pix) ? data.cartoes_pix : [];
// Flat totals
const totalSangrias = sangrias_arr.reduce((s,r) => s+(r.retirada||0)+(r.valor||0), 0);
const totalDespesas = despesas_arr.reduce((s,r) => s+(r.valor||0), 0);
const totalPixCnpj = pixcnpj_arr.reduce((s,r) => s+(r.valor||0), 0);
const totalVales = vales_arr.reduce((s,r) => s+(r.valor||0), 0);
const totalReceber = receber_arr.reduce((s,r) => s+(r.valor||0), 0);
const totalCancel = cancelamentos_arr.reduce((s,r) => s+(r.valor||0), 0);
const cartCred = cartCred_arr.reduce((s,v) => s+(v||0), 0);
const cartDeb = cartDeb_arr.reduce((s,v) => s+(v||0), 0);
const cartAli = cartAli_arr.reduce((s,v) => s+(v||0), 0);
const cartPix = cartPix_arr.reduce((s,v) => s+(v||0), 0);
const totalCartoes = cartCred + cartDeb + cartAli + cartPix;
// Closed-form fields
const fdin = data.fechamento_dinheiro || 0;
const fcarts = data.fechamento_cartoes || 0;
const frec = data.fechamento_receber || 0;
const ftotal = data.fechamento || (fdin + fcarts + frec);
// Build section rows for sangrias, despesas, etc.
function section(title, lines, total, fmtFn) {
if (!lines.length && total <= 0) return '';
let html = `<div class="section-title">${title}</div>`;
lines.forEach(l => { if (l.valor > 0 || l.retirada > 0) { html += `<div class="row"><span>${l.label||''}</span><span>${fmtFn(l.valor||l.retirada||0)}</span></div>`; } });
if (total > 0) html += `<div class="row total-row"><span>TOTAL ${title}:</span><span>${fmtFn(total)}</span></div>`;
return html;
}
// Sangrias rows
const sangriaLines = sangrias_arr.map(s => ({
label: (s.hora ? s.hora + ': ' : '') + (s.gerente ? `(${s.gerente})` : ''),
valor: (s.retirada||0)+(s.valor||0)
})).filter(l => l.valor > 0);
// Despesas rows
const despLines = despesas_arr.map(d => ({
label: (d.desc||'') + (d.obs ? ` (${d.obs})` : ''),
valor: d.valor||0
})).filter(l => l.valor > 0);
// PIX CNPJ rows
const pixLines = pixcnpj_arr.map(p => ({
label: p.nome||'PIX CNPJ',
valor: p.valor||0
})).filter(l => l.valor > 0);
// Vales rows
const valesLines = vales_arr.map(v => ({
label: (v.nome||'') + (v.obs ? ` (${v.obs})` : ''),
valor: v.valor||0
})).filter(l => l.valor > 0);
// A Receber rows
const recLines = receber_arr.map(r => ({
label: r.nome||'',
valor: r.valor||0
})).filter(l => l.valor > 0);
// Cancelamentos rows
const cancLines = cancelamentos_arr.map(c => ({
label: (c.numero ? `${c.numero}` : '') + (c.motivo ? ` (${c.motivo})` : ''),
valor: c.valor||0
})).filter(l => l.valor > 0);
// Cartoes table (4 columns)
const maxRows = Math.max(cartCred_arr.length, cartDeb_arr.length, cartAli_arr.length, cartPix_arr.length);
let cartTableRows = '';
for (let i = 0; i < maxRows; i++) {
const cr = cartCred_arr[i] || 0;
const db = cartDeb_arr[i] || 0;
const al = cartAli_arr[i] || 0;
const px = cartPix_arr[i] || 0;
cartTableRows += `<tr>
<td>${cr > 0 ? fmt(cr) : '-'}</td>
<td>${db > 0 ? fmt(db) : '-'}</td>
<td>${al > 0 ? fmt(al) : '-'}</td>
<td>${px > 0 ? fmt(px) : '-'}</td>
</tr>`;
}
// Totals row
cartTableRows += `<tr class="total-row">
<td>${cartCred > 0 ? fmt(cartCred) : '-'}</td>
<td>${cartDeb > 0 ? fmt(cartDeb) : '-'}</td>
<td>${cartAli > 0 ? fmt(cartAli) : '-'}</td>
<td>${cartPix > 0 ? fmt(cartPix) : '-'}</td>
</tr>`;
const sysTotal = data.sys_total || 0;
const saldoEsperado = data.saldo_esperado || 0;
const diffOk = diferenca === 0;
let html = `<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
<title>Fechamento de Caixa</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Courier New', monospace; font-size: 15px; width: 80mm; margin: 0 auto; padding: 8px; font-weight: bold !important; print-color-adjust: exact; -webkit-print-color-adjust: exact; }
.header { text-align: center; margin-bottom: 8px; }
.header h1 { font-size: 20px; font-weight: bold !important; }
.header h2 { font-size: 16px; font-weight: bold !important; }
hr { border: none; border-top: 2px solid #000; margin: 6px 0; }
.section-title { font-weight: bold !important; font-size: 14px; border-top: 2px solid #000; padding-top: 4px; margin-top: 8px; }
.row { display: flex; justify-content: space-between; padding: 2px 0; font-size: 14px; font-weight: bold !important; }
.row.total-row { font-weight: bold !important; }
.cart-table { table-layout: fixed; width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 4px; font-weight: bold !important; }
.cart-table th { font-weight: bold !important; border-bottom: 2px solid #000; padding: 3px; font-size: 13px; width: 25%; }
.cart-table td { padding: 3px 5px; text-align: right; font-weight: bold !important; font-size: 13px; }
.cart-table td:first-child { text-align: left; }
.cart-table tr.total-row { font-weight: bold !important; border-top: 2px solid #000; }
.subtotal { font-size: 13px; margin-top: 4px; }
.subtotal .row { padding: 1px 0; font-weight: bold !important; }
.diff-ok { color: #2e7d32; font-weight: bold !important; }
.diff-bad { color: #c0272d; font-weight: bold !important; }
.obs { font-size: 12px; color: #555; margin-top: 6px; white-space: pre-wrap; font-weight: bold !important; }
.footer { text-align: center; font-size: 12px; color: #555; margin-top: 8px; font-weight: bold !important; }
@media print { body { background: #fff; font-weight: bold !important; font-size: 15px !important; } }
</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>
${sangriaLines.length > 0 ? `<div class="section-title">SANGRIAS</div>
${sangriaLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL SANGRIAS:</span><span>${fmt(totalSangrias)}</span></div>` : ''}
${despLines.length > 0 ? `<div class="section-title">DESPESAS</div>
${despLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL DESPESAS:</span><span>${fmt(totalDespesas)}</span></div>` : ''}
${pixLines.length > 0 ? `<div class="section-title">PIX CNPJ</div>
${pixLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL PIX CNPJ:</span><span>${fmt(totalPixCnpj)}</span></div>` : ''}
${valesLines.length > 0 ? `<div class="section-title">VALES</div>
${valesLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL VALES:</span><span>${fmt(totalVales)}</span></div>` : ''}
${recLines.length > 0 ? `<div class="section-title">A RECEBER</div>
${recLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL A RECEBER:</span><span>${fmt(totalReceber)}</span></div>` : ''}
${cancLines.length > 0 ? `<div class="section-title">CANCELAMENTOS</div>
${cancLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
<div class="row total-row"><span>TOTAL:</span><span>${fmt(totalCancel)}</span></div>` : ''}
<div class="section-title">DETALHAMENTO DE CARTÕES</div>
<table class="cart-table">
<thead>
<tr>
<th>CRÉDITO</th><th>DÉBITO</th><th>ALIMENT.</th><th>PIX</th>
</tr>
</thead>
<tbody>
${cartTableRows}
</tbody>
</table>
<div class="subtotal"><div class="row total-row"><span>Soma Cartões:</span><span>${fmt(totalCartoes)}</span></div></div>
<div class="section-title">SALDO DE CAIXA (SISTEMA) — RESUMO</div>
<div class="row"><span>Troco</span><span>${fmt(data.saldo_troco||0)}</span></div>
<div class="row"><span>Crédito</span><span>${fmt(cartCred)}</span></div>
<div class="row"><span>Débito</span><span>${fmt(cartDeb)}</span></div>
<div class="row"><span>Alimentação</span><span>${fmt(cartAli)}</span></div>
<div class="row"><span>Vales</span><span>${fmt(totalVales)}</span></div>
<div class="row"><span>À Receber</span><span>${fmt(totalReceber)}</span></div>
<div class="row"><span>PIX Cartão</span><span>${fmt(cartPix)}</span></div>
<div class="row"><span>PIX CNPJ</span><span>${fmt(totalPixCnpj)}</span></div>
<div class="row total-row"><span>TOTAL SISTEMA</span><span>${fmt(sysTotal)}</span></div>
<div class="row"><span>Saldo Esperado</span><span>${fmt(saldoEsperado)}</span></div>
<div class="row"><span>DIFERENÇA</span><span class="${diffOk ? 'diff-ok' : 'diff-bad'}">${diffStr}${diffOk ? ' ✓ OK' : ''}</span></div>
<div class="section-title">FECHAMENTO (CONTADO)</div>
<div class="row"><span>Dinheiro:</span><span>${fmt(fdin)}</span></div>
<div class="row"><span>Cartões:</span><span>${fmt(fcarts)}</span></div>
<div class="row"><span>À Receber:</span><span>${fmt(frec)}</span></div>
<div class="row total-row"><span>TOTAL:</span><span>${fmt(ftotal)}</span></div>
<div class="section-title">CONTADORES</div>
<div class="row"><span>Clientes:</span><span>${data.clientes || 0}</span></div>
<div class="row"><span>Frango Assado:</span><span>${data.frango || 0}</span></div>
<div class="row"><span>Vendas:</span><span>${fmt(data.vendas || 0)}</span></div>
${data.observacoes ? `<div class="obs">${data.observacoes}</div>` : ''}
<div class="footer">Gerado em ${new Date().toLocaleString('pt-BR')}</div>
</body></html>`;
return html;
}
// ─── Relatório de Impressão (layout exato do usuário) ──────────────────────
function buildReportHTML(data) {
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
const fmtNeg = v => (v < 0 ? '-' : '') + 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
const cartCred = data.credito || 0;
const cartDeb = data.debito || 0;
const cartAli = data.alimentacao || 0;
const cartPix = data.voucher || 0;
const sangrias = data.sangrias || 0;
const despesas = data.despesas || 0;
const vales = data.vales || 0;
const receber = data.areceber || 0;
const pixcnpj = data.pixcnpj || 0;
const cancelamentos = data.cancelamentos || 0;
const fechamento = data.fechamento || 0;
const sysTotal = data.sys_total || 0;
const saldoEsp = data.saldo_esperado || 0;
const diferenca = data.diferenca !== undefined ? data.diferenca
: (fechamento - saldoEsp - cancelamentos);
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmtNeg(diferenca);
const diffColor = diferenca === 0 ? '#2e7d32' : '#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; }
hr { border: none; border-top: 1px dashed #000; margin: 6px 0; }
.row { display: flex; justify-content: space-between; padding: 2px 0; font-size: 12px; }
.row .val { font-weight: bold; }
.footer { text-align: center; font-size: 11px; color: #555; margin-top: 8px; }
@media print { body { margin: 0; } }
</style>
</head><body>
<div class="header">
<div>📅 Data: ${data.data} &nbsp; 👤 Operador: ${data.operador} &nbsp; ⏰ Turno: ${data.turno} &nbsp; 🏪 Loja: ${data.loja}</div>
</div>
<hr>
<div class="row"><span>💸 Sangrias:</span><span class="val">${fmt(sangrias)}</span></div>
<div class="row"><span>📝 Despesas:</span><span class="val">${fmt(despesas)}</span></div>
<div class="row"><span>💰 Vales:</span><span class="val">${fmt(vales)}</span></div>
<div class="row"><span>👥 À Receber:</span><span class="val">${fmt(receber)}</span></div>
<div class="row"><span>🔵 PIX CNPJ:</span><span class="val">${fmt(pixcnpj)}</span></div>
<div class="row"><span>💳 Crédito:</span><span class="val">${fmt(cartCred)}</span></div>
<div class="row"><span>💳 Débito:</span><span class="val">${fmt(cartDeb)}</span></div>
<div class="row"><span>💳 Alimentação:</span><span class="val">${fmt(cartAli)}</span></div>
<div class="row"><span>❌ Cancelamentos:</span><span class="val">${fmt(cancelamentos)}</span></div>
<hr>
<div class="row"><span>📋 Fechamento (contado):</span><span class="val">${fmt(fechamento)}</span></div>
<div class="row"><span>🖥️ Total Sistema:</span><span class="val">${fmt(sysTotal)}</span></div>
<hr>
<div class="row"><span>🎯 Saldo Esperado (contado):</span><span class="val">${fmt(saldoEsp)}</span></div>
<div class="row"><span>📐 Diferença:</span><span class="val" style="color:${diffColor}">${diffStr}</span></div>
<hr>
<div class="row"><span>👥 Clientes:</span><span class="val">${data.clientes || 0}</span></div>
<div class="row"><span>🍗 Frango Assado:</span><span class="val">${data.frango || 0}</span></div>
<div class="row"><span>💰 Vendas:</span><span class="val">${fmt(data.vendas || 0)}</span></div>
<hr>
<div style="text-align:center;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>`;
}
function openPrintPreview(data, mode) {
const html = mode === 'confirm' ? buildConfirmHTML(data)
: mode === 'report' ? buildReportHTML(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();
}
// ─── buildReciboData — lê do FORMULÁRIO principal (não do estado carregado do BD) ───
// Importante: quando editing, o estado estápopulado pelo abrirFechamentoEdicao, não do BD
// ─── buildReciboData — lê do FORMULÁRIO principal ───────────────────────────
// usado tanto para novo fechamento quanto para edição
function buildReciboData() {
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'));
// Saldo de Caixa (Sistema) = troco + crédito + débito + alimentação + voucher pix + pixcnpj + vales + receber
const sysTotal = parseNum(val('sys-troco')) + cartCred + cartDeb + cartAli + cartPix + pixCnpjTotal + valesTotal + receberTotal;
// Diferença = saldo de caixa (sistema) - saldo_esperado - cancelamentos
const diferenca = sysTotal - saldoEsp - cancelTotal;
return {
uuid: estado.uuid,
id_local: estado.id_local,
id: estado.id,
loja: estado.loja,
data: estado.data,
operador: estado.operador,
turno: estado.turno,
saldo_troco: parseNum(val('sys-troco')),
saldo_esperado: saldoEsp,
fechamento: fechamentoCounted,
diferenca: diferenca,
sys_total: sysTotal,
fechamento_dinheiro: parseNum(val('f-fech-dinheiro')),
fechamento_cartoes: parseNum(val('f-fech-cartoes')),
fechamento_receber: parseNum(val('f-fech-receber')),
// 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 preview / Supabase
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: estado.cartoes.credito.map(r => r.valor || 0),
cartoes_debito: estado.cartoes.debito.map(r => r.valor || 0),
cartoes_alimentacao: estado.cartoes.alimentacao.map(r => r.valor || 0),
cartoes_pix: estado.cartoes.pix.map(r => r.valor || 0),
total_cartao_credito: cartCred,
total_cartao_debito: cartDeb,
total_cartao_alimentacao: cartAli,
total_cartao_pix: cartPix,
total_pixcnpj: pixCnpjTotal,
total_sangrias: sangriasTotal,
total_despesas: despesasTotal,
total_vales: valesTotal,
total_areceber: receberTotal,
total_cancelamentos: cancelTotal,
observacoes: estado.observacoes,
clientes: estado.clientes,
frango: estado.frango,
vendas: estado.vendas || 0,
edited_at: new Date().toISOString(),
edited_by: config.admin_user || 'admin',
sync_status: estado.sync_status,
};
}
async function tauriRecibo() {
const data = buildReciboData();
// Tenta ESC/POS via Tauri primeiro; se falhar, so lanca erro (preview tratado pelo caller)
try {
return await window.__TAURI__.core.invoke('imprimir_recibo', { data: JSON.stringify(data) });
} catch(e) {
console.warn('ESC/POS falhou:', e);
throw e; // re-lanca para o caller tratar o preview
}
}
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')) + cartCred + cartDeb + cartAli + cartPix + vales + receber + pixcnpj;
const diferenca = sysTotal - saldoEsp - cancelamentos;
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>👥 Clientes: <strong>${estado.clientes || 0}</strong></div>
<div>🍗 Frango Assado: <strong>${estado.frango || 0}</strong></div>
<div>💰 Vendas: <strong>${fmt(estado.vendas || 0)}</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 () => {
// Usa buildReciboData para não duplicar lógica e ter todos os campos certos
let data;
try {
data = buildReciboData();
} catch(e) {
console.error('Erro ao montar dados do recibo:', e);
alert('Erro ao montar dados do recibo.');
return;
}
// Fecha modal APÓS gerar preview (não antes)
closeConfirmModal();
openPrintPreview(data, 'report');
});
// 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('f-obs').addEventListener('input', e => { estado.observacoes = e.target.value; scheduleSave(); });
document.getElementById('f-clientes').addEventListener('input', e => { estado.clientes = parseInt(e.target.value) || 0; scheduleSave(); });
document.getElementById('f-frango').addEventListener('input', e => { estado.frango = parseInt(e.target.value) || 0; scheduleSave(); });
document.getElementById('btn-recibo').addEventListener('click', async () => {
const btn = document.getElementById('btn-recibo');
btn.disabled = true;
try {
await tauriRecibo();
} catch(e) {
console.warn('ESC/POS falhou:', e);
}
try {
const data = buildReciboData();
openPrintPreview(data, 'recibo');
} catch(e2) {
console.error('Preview falhou:', e2);
alert('Erro ao gerar preview do recibo.');
} 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);
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', '');
updateTotals();
renderAllTables();
syncUI();
showToast('Rascunho limpo.', 'info');
});
// ─── Dia Anterior — seleção + consulta + edição de registros do Supabase ───
const btnAnterior = document.getElementById('btn-anterior');
if (btnAnterior) {
btnAnterior.addEventListener('click', async () => {
const hasTauri = !!(window.__TAURI__ && window.__TAURI__.core);
let recentList = [];
if (hasTauri) {
try {
const loja = val('f-loja') || 'União';
recentList = await window.__TAURI__.core.invoke('sb_listar_recentes', { loja, limite: 50 });
} catch(e) {
console.warn('sb_listar_recentes falhou:', e);
}
}
if (!recentList || recentList.length === 0) {
alert('Nenhum fechamento encontrado no servidor para esta loja.');
return;
}
buildConsultaModal(recentList);
});
}
// Monta o modal de consulta — lista de fechamentos do Supabase
function buildConsultaModal(recentList) {
const old = document.getElementById('modal-consulta');
if (old) document.body.removeChild(old);
const overlay = document.createElement('div');
overlay.id = 'modal-consulta';
overlay.style.cssText = `
position:fixed;top:0;left:0;right:0;bottom:0;
background:rgba(0,0,0,0.6);z-index:99998;
display:flex;align-items:center;justify-content:center;
`;
const fmtDate = d => {
if (!d) return '';
const [y,m,day] = d.split('-');
return `${day}/${m}/${y}`;
};
const fmtMoney = v => v != null ? `R$ ${parseFloat(v).toFixed(2).replace('.', ',')}` : 'R$ 0,00';
const syncBadge = s => {
const colors = { synced: '#2e7d32', local: '#e65100', syncing: '#f57f17', error: '#c0272d' };
const labels = { synced: '✓ Sync', local: '⚠ Local', syncing: '⏳ Sync', error: '✗ Erro' };
return `<span style="background:${colors[s]||'#888'};color:#fff;padding:1px 5px;border-radius:3px;font-size:10px">${labels[s]||s||'?'}</span>`;
};
const statusBadge = s => {
if (s === 'ok') return '<span style="color:#2e7d32;font-weight:bold">✓ OK</span>';
if (s === 'sobrando') return '<span style="color:#e65100;font-weight:bold">⚠ SOBRANDO</span>';
if (s === 'faltando') return '<span style="color:#c0272d;font-weight:bold">⚠ FALTANDO</span>';
return s || '?';
};
const rows = recentList.map((item, i) => {
const total = (parseFloat(item.fechamento_dinheiro)||0) + (parseFloat(item.fechamento_cartoes)||0) + (parseFloat(item.fechamento_areceber)||0);
return `<tr>
<td style="padding:6px 8px;border-bottom:1px solid #eee;font-size:12px;cursor:pointer" onclick="openConsultaRegistro(${i})">${fmtDate(item.data)}</td>
<td style="padding:6px 8px;border-bottom:1px solid #eee;font-size:12px;cursor:pointer" onclick="openConsultaRegistro(${i})">${item.operador||'?'}</td>
<td style="padding:6px 8px;border-bottom:1px solid #eee;font-size:12px;cursor:pointer" onclick="openConsultaRegistro(${i})">${item.turno||'?'}</td>
<td style="padding:6px 8px;border-bottom:1px solid #eee;font-size:12px;text-align:right;cursor:pointer" onclick="openConsultaRegistro(${i})">${fmtMoney(total)}</td>
<td style="padding:6px 8px;border-bottom:1px solid #eee;font-size:12px;text-align:right;cursor:pointer" onclick="openConsultaRegistro(${i})">${fmtMoney(item.saldo_esperado)}</td>
<td style="padding:6px 8px;border-bottom:1px solid #eee;font-size:12px;cursor:pointer" onclick="openConsultaRegistro(${i})">${statusBadge(item.status_diferenca)}</td>
<td style="padding:6px 8px;border-bottom:1px solid #eee;font-size:12px;cursor:pointer" onclick="openConsultaRegistro(${i})">${syncBadge(item.sync_status)}</td>
<td style="padding:4px 6px;border-bottom:1px solid #eee;text-align:center">
<button onclick="event.stopPropagation();window.abrirEditaFechamentoIndex(${i})" style="background:#e65100;color:#fff;border:none;border-radius:4px;padding:4px 10px;cursor:pointer;font-size:11px;white-space:nowrap">✏️ Editar</button>
</td>
</tr>`;
}).join('');
overlay.innerHTML = `
<div style="background:#fff;border-radius:10px;padding:20px;max-width:780px;width:95%;max-height:85vh;overflow-y:auto">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<h2 style="margin:0;font-size:16px">📅 Consultar Fechamentos Anteriores</h2>
<button onclick="closeConsultaModal()" style="background:none;border:none;font-size:18px;cursor:pointer">✕</button>
</div>
<p style="font-size:12px;color:#666;margin:0 0 10px">Clique em um registro para ver o relatório ou clique em <strong>Editar</strong> para abrir a janela de edição.</p>
<table style="width:100%;border-collapse:collapse">
<thead>
<tr style="background:#f5f5f5">
<th style="padding:6px 8px;text-align:left;font-size:11px;color:#888">DATA</th>
<th style="padding:6px 8px;text-align:left;font-size:11px;color:#888">OPERADOR</th>
<th style="padding:6px 8px;text-align:left;font-size:11px;color:#888">TURNO</th>
<th style="padding:6px 8px;text-align:right;font-size:11px;color:#888">FECHAMENTO</th>
<th style="padding:6px 8px;text-align:right;font-size:11px;color:#888">ESPERADO</th>
<th style="padding:6px 8px;text-align:center;font-size:11px;color:#888">STATUS</th>
<th style="padding:6px 8px;text-align:center;font-size:11px;color:#888">SYNC</th>
<th style="padding:6px 8px;text-align:center;font-size:11px;color:#888">AÇÃO</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
`;
document.body.appendChild(overlay);
window._recentList = recentList;
}
// Abre a janela de edição completa de um registro pelo índice na lista
window.abrirEditaFechamentoIndex = async function(idx) {
const item = window._recentList ? window._recentList[idx] : null;
if (!item) return;
const hasTauri = !!(window.__TAURI__ && window.__TAURI__.core);
if (!hasTauri) { alert('Tauri não disponível.'); return; }
let full = null;
try {
if (item.id) {
try { full = await window.__TAURI__.core.invoke('sb_buscar_por_id', { id: item.id }); } catch(e) {}
}
if (!full && item.uuid) {
try { full = await window.__TAURI__.core.invoke('sb_buscar_por_uuid', { uuid: item.uuid }); } catch(e) {}
}
if (!full && item.id_fechamento) {
try { full = await window.__TAURI__.core.invoke('sb_buscar_por_id_fechamento', { idFechamento: item.id_fechamento }); } catch(e) {}
}
} catch(e) {
console.warn('Erro buscando registro:', e);
}
if (!full) { alert('Registro não encontrado.'); return; }
// Adiciona id_fechamento se não vier do banco
if (!full.id_fechamento && full.loja && full.data && full.operador) {
full.id_fechamento = (full.loja + '_' + full.data + '_' + full.operador).toLowerCase().replace(/ /g, '_');
}
closeConsultaModal();
window.abrirEditaFechamento(full);
};
function closeConsultaModal() {
const m = document.getElementById('modal-consulta');
if (m) document.body.removeChild(m);
window._recentList = null;
window._currentConsulta = null;
}
// Abre o relatório de um registro selecionado
window.openConsultaRegistro = async function(idx) {
const item = window._recentList[idx];
if (!item) return;
closeConsultaModal();
const hasTauri = !!(window.__TAURI__ && window.__TAURI__.core);
if (!hasTauri) { alert('Tauri não disponível.'); return; }
let full = null;
try {
// 1. Tentar por id numérico (campo "id" da tabela, pk)
if (item.id) {
try { full = await window.__TAURI__.core.invoke('sb_buscar_por_id', { id: item.id }); } catch(e) {}
}
// 2. Tentar por uuid externo
if (!full && item.uuid) {
try { full = await window.__TAURI__.core.invoke('sb_buscar_por_uuid', { uuid: item.uuid }); } catch(e) {}
}
// 3. Tentar por id_fechamento (ex: uniao_2026-06-11_filipe)
if (!full && item.id_fechamento) {
try { full = await window.__TAURI__.core.invoke('sb_buscar_por_id_fechamento', { idFechamento: item.id_fechamento }); } catch(e) {}
}
} catch(e) {
console.warn('Erro buscando registro:', e);
}
if (!full) { alert('Registro não encontrado.'); return; }
// Preserva o id numerico (pk da tabela) antes de guardar o registro completo
window._currentConsulta = full;
window._currentConsultaId = item.id;
buildRelatorioModal(full);
};
// Monta o modal de relatório (readonly) com opção de editar
function buildRelatorioModal(full) {
const old = document.getElementById('modal-relatorio');
if (old) document.body.removeChild(old);
const overlay = document.createElement('div');
overlay.id = 'modal-relatorio';
overlay.style.cssText = `
position:fixed;top:0;left:0;right:0;bottom:0;
background:rgba(0,0,0,0.6);z-index:99998;
display:flex;align-items:center;justify-content:center;
`;
const fmt = v => v != null ? parseFloat(v).toFixed(2).replace('.', ',') : '0,00';
const fmtMoney = v => `R$ ${fmt(v)}`;
const dados = full.dados || {};
// Campos do sistema
const troco = parseFloat(full.saldo_troco || 0);
const credito = parseFloat(full.saldo_credito || 0);
const debito = parseFloat(full.saldo_debito || 0);
const alimentacao = parseFloat(full.saldo_alimentacao || 0);
const vales = parseFloat(full.saldo_vales || 0);
const pixCartao = parseFloat(full.total_cartao_pix || 0);
const pixCnpj = parseFloat(full.total_pixcnpj || 0);
const sysTotal = troco + credito + debito + alimentacao + vales + (parseFloat(full.saldo_areceber)||0) + pixCartao + pixCnpj;
// Campos contados
const fd = parseFloat(full.fechamento_dinheiro || 0);
const fc = parseFloat(full.fechamento_cartoes || 0);
const fr = parseFloat(full.fechamento_areceber || 0);
const fechamentoContado = fd + fc + fr;
// Despesas e sangrias
const totalDespesas = parseFloat(full.total_despesas || 0);
const totalSangrias = parseFloat(full.total_sangrias || 0);
const totalCancelamentos = parseFloat(full.total_cancelamentos || 0);
// Esperado e diferença — calculada: Total Sistema - Saldo Esperado - Cancelamentos
const saldoEsperado = parseFloat(full.saldo_esperado || 0);
const diferenca = sysTotal - saldoEsperado - totalCancelamentos;
const statusDif = diferenca === 0 ? 'ok' : (diferenca > 0 ? 'sobrando' : 'faltando');
const diffClass = statusDif === 'ok' ? '#2e7d32' : (statusDif === 'sobrando' ? '#e65100' : '#c0272d');
const diffLabel = statusDif === 'ok' ? '✓ OK' : (statusDif === 'sobrando' ? '⚠ SOBRANDO' : (statusDif === 'faltando' ? '⚠ FALTANDO' : statusDif));
const operador = full.operador || '?';
const turno = full.turno || '?';
const data = full.data || '?';
const loja = full.loja || '?';
const obs = full.observacoes || '';
const clientes = full.clientes || 0;
const frango = full.frango || 0;
const atualizado = full.atualizado_em ? new Date(full.atualizado_em).toLocaleString('pt-BR') : '?';
const totalGeral = fd + fc + fr;
overlay.innerHTML = `
<div id="relatorio-inner" style="background:#fff;border-radius:10px;padding:20px;max-width:520px;width:95%;max-height:90vh;overflow-y:auto;font-family:monospace;font-size:13px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<h2 style="margin:0;font-size:15px">🧾 Relatório — ${loja.toUpperCase()}</h2>
<button onclick="closeRelatorioModal()" style="background:none;border:none;font-size:18px;cursor:pointer">✕</button>
</div>
<div style="background:#f8f8f8;border-radius:6px;padding:10px;margin-bottom:10px;font-size:12px">
<div><strong>📅 Data:</strong> ${data} &nbsp; <strong>Turno:</strong> ${turno} &nbsp; <strong>Operador:</strong> ${operador}</div>
<div><strong>Atualizado:</strong> ${atualizado}</div>
</div>
<div style="border:1px solid #ddd;border-radius:6px;overflow:hidden;margin-bottom:10px">
<div style="background:#e65100;color:#fff;padding:4px 8px;font-weight:bold;font-size:12px">FECHAMENTO DE CAIXA</div>
<div style="padding:8px">
<div style="display:flex;justify-content:space-between"><span>Troco (+)</span><span>${fmtMoney(troco)}</span></div>
<div style="display:flex;justify-content:space-between"><span>Credito (+)</span><span>${fmtMoney(credito)}</span></div>
<div style="display:flex;justify-content:space-between"><span>Debito (+)</span><span>${fmtMoney(debito)}</span></div>
<div style="display:flex;justify-content:space-between"><span>Alimentacao (+)</span><span>${fmtMoney(alimentacao)}</span></div>
<div style="display:flex;justify-content:space-between"><span>Vales (+)</span><span>${fmtMoney(vales)}</span></div>
<div style="display:flex;justify-content:space-between"><span>A Receber (+)</span><span>${fmtMoney(parseFloat(full.saldo_areceber||0))}</span></div>
<div style="display:flex;justify-content:space-between"><span>PIX Cartao (+)</span><span>${fmtMoney(pixCartao)}</span></div>
<div style="display:flex;justify-content:space-between"><span>PIX CNPJ (+)</span><span>${fmtMoney(pixCnpj)}</span></div>
<div style="border-top:1px solid #ddd;margin-top:4px;padding-top:4px;display:flex;justify-content:space-between;font-weight:bold"><span>TOTAL SISTEMA</span><span style="color:#e65100">${fmtMoney(sysTotal)}</span></div>
</div>
</div>
<div style="border:1px solid #ddd;border-radius:6px;overflow:hidden;margin-bottom:10px">
<div style="background:#1565c0;color:#fff;padding:4px 8px;font-weight:bold;font-size:12px">SALDO DE CAIXA (CONTADO)</div>
<div style="padding:8px">
<div style="display:flex;justify-content:space-between"><span>Dinheiro</span><span>${fmtMoney(fd)}</span></div>
<div style="display:flex;justify-content:space-between"><span>Cartoes</span><span>${fmtMoney(fc)}</span></div>
<div style="display:flex;justify-content:space-between"><span>A Receber</span><span>${fmtMoney(fr)}</span></div>
<div style="border-top:1px solid #ddd;margin-top:4px;padding-top:4px;display:flex;justify-content:space-between;font-weight:bold"><span>TOTAL CONTADO</span><span>${fmtMoney(totalGeral)}</span></div>
</div>
</div>
<div style="border:1px solid #ddd;border-radius:6px;overflow:hidden;margin-bottom:10px">
<div style="background:#333;color:#fff;padding:4px 8px;font-weight:bold;font-size:12px">RESULTADO</div>
<div style="padding:8px">
<div style="display:flex;justify-content:space-between"><span>Saldo Esperado</span><span>${fmtMoney(saldoEsperado)}</span></div>
<div style="display:flex;justify-content:space-between"><span>Cancelamentos</span><span>- ${fmtMoney(totalCancelamentos)}</span></div>
<div style="display:flex;justify-content:space-between;border-top:1px solid #ddd;margin-top:4px;padding-top:4px">
<span>Diferenca</span>
<span style="color:${diffClass};font-weight:bold">${fmtMoney(Math.abs(diferenca))} ${diferenca >= 0 ? '(+)' : '(-)'}</span>
</div>
<div style="text-align:center;margin-top:4px;font-weight:bold;color:${diffClass}">${diffLabel}</div>
</div>
</div>
<div style="border:1px solid #ddd;border-radius:6px;overflow:hidden;margin-bottom:10px">
<div style="background:#555;color:#fff;padding:4px 8px;font-weight:bold;font-size:12px">DEMAIS TOTAIS</div>
<div style="padding:8px">
<div style="display:flex;justify-content:space-between"><span>Total Sangrias</span><span>${fmtMoney(totalSangrias)}</span></div>
<div style="display:flex;justify-content:space-between"><span>Total Despesas</span><span>${fmtMoney(totalDespesas)}</span></div>
<div style="display:flex;justify-content:space-between"><span>Clientes</span><span>${clientes}</span></div>
<div style="display:flex;justify-content:space-between"><span>Frango Assado</span><span>${frango}</span></div>
<div style="display:flex;justify-content:space-between"><span>Vendas</span><span>${fmtMoney(full.vendas || 0)}</span></div>
</div>
</div>
${obs ? `<div style="background:#fff8e1;border-radius:6px;padding:8px;margin-bottom:10px;font-size:12px"><strong>Obs:</strong> ${obs}</div>` : ''}
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button onclick="window.printRelatorio()" style="flex:1;padding:8px;background:#1565c0;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px">🖨️ Imprimir</button>
<button onclick="window.toggleEditConsulta()" style="flex:1;padding:8px;background:#e65100;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px" id="btn-edit-consulta">✏️ Editar (senha)</button>
</div>
</div>
`;
document.body.appendChild(overlay);
window._editMode = false;
}
// Fecha o modal de relatório
function closeRelatorioModal() {
const m = document.getElementById('modal-relatorio');
if (m) document.body.removeChild(m);
window._currentConsulta = null;
window._editMode = false;
}
// Restaura o estado completo do JSONB 'dados' e volta ao formulário em modo de edição
window.abrirFechamentoEdicao = function(full) {
const dados = full.dados || full;
if (!dados) { alert('Dados detalhados não encontrados neste registro.'); return; }
// IDs do registro — CRÍTICO para o salvamento via Supabase
estado.uuid = full.uuid || null;
estado.id = full.id || full.uuid || null;
// Preenche o estado global com todos os campos do JSONB
estado.loja = dados.loja || full.loja || '';
estado.data = dados.data || full.data || '';
estado.operador = dados.operador || full.operador || '';
estado.turno = dados.turno || full.turno || '';
estado.clientes = dados.clientes ?? full.clientes ?? 0;
estado.frango = dados.frango ?? full.frango ?? 0;
estado.vendas = dados.vendas ?? full.vendas ?? 0;
estado.observacoes = dados.observacoes || full.observacoes || '';
estado.saldo_esperado = dados.saldo_esperado ?? full.saldo_esperado ?? 0;
// Campos de sistema — vem dos campos flat do Supabase
estado.saldo_troco = parseFloat(full.saldo_troco || dados.saldo_troco || 0);
estado.saldo_credito = parseFloat(full.saldo_credito || dados.saldo_credito || 0);
estado.saldo_debito = parseFloat(full.saldo_debito || dados.saldo_debito || 0);
estado.saldo_alimentacao = parseFloat(full.saldo_alimentacao || dados.saldo_alimentacao || 0);
estado.saldo_vales = parseFloat(full.saldo_vales || dados.saldo_vales || 0);
estado.saldo_areceber = parseFloat(full.saldo_areceber || dados.saldo_areceber || 0);
estado.total_cartao_pix = parseFloat(full.total_cartao_pix || dados.total_cartao_pix || 0);
estado.total_pixcnpj = parseFloat(full.total_pixcnpj || dados.total_pixcnpj || 0);
// Listas detalhadas
estado.sangrias = (dados.sangrias || []).map(r => ({ hora: r.hora||r.Hora||'', retirada: r.retirada||r.Retirada||r.valor||0, gerente: r.gerente||r.Gerente||'' }));
estado.despesas = (dados.despesas || []).map(r => ({ desc: r.desc||r.Desc||'', obs: r.obs||r.Obs||'', valor: r.valor||r.Valor||0 }));
estado.vales = (dados.vales || []).map(r => ({ nome: r.nome||r.Nome||'', valor: r.valor||r.Valor||0 }));
estado.pixcnpj = (dados.pixcnpj || []).map(r => ({ desc: r.desc||'', valor: r.valor||r.Valor||0 }));
estado.receber = (dados.receber || []).map(r => ({ nome: r.nome||r.Nome||'', valor: r.valor||r.Valor||0 }));
estado.cancelamentos = (dados.cancelamentos || []).map(r => ({ numero: r.numero||r.Numero||'', valor: r.valor||r.Valor||0 }));
estado.cartoes = {
credito: (dados.cartoes?.credito || dados.cartoes_credito || []).map(r => ({ valor: typeof r === 'object' ? (r.valor ?? r.Valor ?? 0) : (parseFloat(r) || 0) })),
debito: (dados.cartoes?.debito || dados.cartoes_debito || []).map(r => ({ valor: typeof r === 'object' ? (r.valor ?? r.Valor ?? 0) : (parseFloat(r) || 0) })),
alimentacao:(dados.cartoes?.alimentacao|| dados.cartoes_alimentacao || []).map(r => ({ valor: typeof r === 'object' ? (r.valor ?? r.Valor ?? 0) : (parseFloat(r) || 0) })),
pix: (dados.cartoes?.pix || dados.cartoes_pix || []).map(r => ({ valor: typeof r === 'object' ? (r.valor ?? r.Valor ?? 0) : (parseFloat(r) || 0) }))
};
estado.pixcnpj = (dados.pixcnpj || []).map(r => ({ nome: r.nome || r.Nome || r.desc || r.Desc || '', valor: typeof r === 'object' ? (r.valor ?? r.Valor ?? 0) : (parseFloat(r) || 0) }));
estado.fechamento_contado = dados.fechamento_contado || { dinheiro: 0, cartoes: 0, receber: 0 };
// Fecha o modal de consulta
closeRelatorioModal();
closeConsultaModal();
// Campos de sistema (saldo de caixa)
setVal('sys-troco', fmtNum(estado.saldo_troco));
setVal('sys-credito', fmtNum(estado.saldo_credito));
setVal('sys-debito', fmtNum(estado.saldo_debito));
setVal('sys-alimentacao',fmtNum(estado.saldo_alimentacao));
setVal('sys-pixcnpj', fmtNum(estado.total_pixcnpj));
// Fechamento contado
setVal('f-fech-dinheiro', estado.fechamento_contado?.dinheiro || '');
setVal('f-fech-cartoes', estado.fechamento_contado?.cartoes || '');
setVal('f-fech-receber', estado.fechamento_contado?.receber || '');
setVal('f-saldo-esperado', estado.saldo_esperado || '');
// Preenche os campos do formulário
setVal('f-loja', estado.loja);
setVal('f-data', estado.data);
setVal('f-operador', estado.operador);
setVal('f-turno', estado.turno);
setVal('f-clientes', estado.clientes);
setVal('f-frango', estado.frango);
setVal('f-vendas', estado.vendas);
// Renderiza todas as tabelas com os dados restaurados
renderTable('tbl-sangrias', estado.sangrias);
renderTable('tbl-despesas', estado.despesas);
renderTable('tbl-vales', estado.vales);
renderTable('tbl-pix', estado.pixcnpj);
renderTable('tbl-receber', estado.receber);
renderTable('tbl-cancelamentos',estado.cancelamentos);
renderTable('tbl-cartao-credito', estado.cartoes.credito);
renderTable('tbl-cartao-debito', estado.cartoes.debito);
renderTable('tbl-cartao-alimentacao',estado.cartoes.alimentacao);
renderTable('tbl-cartao-pix', estado.cartoes.pix);
// Recalcula totais
updateTotals();
// Rola para o topo do formulário
window.scrollTo(0, 0);
alert('✅ Caixa carregado para edição. Verifique todos os valores antes de enviar.');
};
// Abre a tela de edição (modal separado) — requer senha admin
window.toggleEditConsulta = async function() {
const full = window._currentConsulta;
if (!full) return;
// Se já está editando, apenas fecha
if (window._editModalOpen) { closeEditModal(); return; }
// Pede senha admin primeiro — sempre, para proteger edições
const pw = config.admin_password;
if (pw) {
const ok = await askPassword();
if (!ok) return;
} else {
// Sem senha configurada — avisa e abre mesmo assim (primeiro uso)
const ok = await askPassword(); // mostra diálogo vazio para o admin configurar mentalmente
if (!ok) return;
}
openEditModal(full);
closeRelatorioModal();
};
// Pede senha admin — retorna Promise<boolean>
function askPassword() {
return new Promise(resolve => {
const pw = config.admin_password;
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:999999;display:flex;align-items:center;justify-content:center;';
overlay.id = 'pw-modal';
overlay.innerHTML = `
<div style="background:#fff;border-radius:10px;padding:24px;width:300px;text-align:center">
<div style="font-size:18px;margin-bottom:16px">🔐 Senha Admin</div>
<input type="password" id="pw-input" placeholder="Digite a senha..." style="width:100%;padding:10px;font-size:14px;border:1px solid #ccc;border-radius:6px;box-sizing:border-box;margin-bottom:12px" />
<div id="pw-error" style="color:#c0272d;font-size:12px;margin-bottom:8px;display:none">Senha incorreta</div>
<div style="display:flex;gap:8px;justify-content:center">
<button id="pw-cancel-btn" style="padding:8px 16px;background:#888;color:#fff;border:none;border-radius:6px;cursor:pointer">Cancelar</button>
<button id="pw-ok-btn" style="padding:8px 16px;background:#e65100;color:#fff;border:none;border-radius:6px;cursor:pointer">OK</button>
</div>
</div>`;
document.body.appendChild(overlay);
const inp = document.getElementById('pw-input');
inp.focus();
document.getElementById('pw-cancel-btn').addEventListener('click', () => {
document.getElementById('pw-modal').remove();
resolve(false);
});
inp.addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('pw-ok-btn').click(); });
document.getElementById('pw-ok-btn').addEventListener('click', () => {
const val = document.getElementById('pw-input').value;
if (val === pw) {
document.getElementById('pw-modal').remove();
resolve(true);
} else {
document.getElementById('pw-error').style.display = 'block';
inp.value = '';
inp.focus();
}
});
});
}
// Monta HTML de uma grade editável dentro do modal
function editTableHTML(id, headers, rows, numericCols) {
const fmtNum = v => {
const n = parseFloat(v);
return isNaN(n) ? '' : 'R$ ' + n.toLocaleString('pt-BR', { minimumFractionDigits: 2 });
};
const hdr = headers.map((h,i) => `<th style="padding:2px 4px;text-align:${numericCols.includes(i)?'right':'left'};font-size:10px;white-space:nowrap">${h}</th>`).join('');
const body = rows.map((row, ri) => {
const cells = row.map((val, ci) => {
const isNum = numericCols.includes(ci);
const displayVal = (val !== null && val !== undefined && val !== '')
? (isNum ? fmtNum(val) : val)
: '';
const extra = isNum
? `oninput="window._recalcEditTotals()" onblur="this.value=fmtBR(this)" `
: '';
return `<td style="padding:1px 2px"><input data-tbl="${id}" data-row="${ri}" data-col="${ci}" value="${displayVal}" ${extra}${isNum ? `style="width:100%;padding:2px;font-size:11px;text-align:right;border:1px solid #ccc;border-radius:2px"` : `style="width:100%;padding:2px;font-size:11px;border:1px solid #ccc;border-radius:2px"`} /></td>`;
}).join('');
return `<tr>${cells}</tr>`;
}).join('');
return `<table style="width:100%;border-collapse:collapse;font-size:11px;margin-bottom:4px"><thead><tr>${hdr}</tr></thead><tbody>${body}</tbody></table>`;
}
// Monta uma grade vazia com N linhas editáveis
function editTableEmpty(id, headers, nRows, numericCols) {
const emptyRows = Array.from({length: nRows}, () => headers.map(() => ''));
return editTableHTML(id, headers, emptyRows, numericCols);
}
// Adiciona linha a uma grade editável no modal
window._editAddRow = function(tblId) {
const container = document.getElementById('tbl-edit-' + tblId);
if (!container) return;
const tbl = container.querySelector('table');
if (!tbl) return;
const numericCols = container.dataset.numeric ? JSON.parse(container.dataset.numeric) : [];
const cols = tbl.querySelector('tr').cells.length;
const newRowIdx = tbl.rows.length;
const row = tbl.insertRow(-1);
for (let c = 0; c < cols; c++) {
const td = row.insertCell(-1);
const isNum = numericCols.includes(c);
td.innerHTML = `<input data-tbl="${tblId}" data-row="${newRowIdx}" data-col="${c}" value="" ${isNum ? `oninput="window._recalcEditTotals()" onblur="this.value=fmtBR(this)" style="width:100%;padding:2px;font-size:11px;text-align:right;border:1px solid #ccc;border-radius:2px"` : `style="width:100%;padding:2px;font-size:11px;border:1px solid #ccc;border-radius:2px"`} />`;
}
// Recalcula após adicionar
window._recalcEditTotals();
};
// Lê todos os dados das grades editáveis do modal
function readEditTables() {
const inputs = document.querySelectorAll('#modal-edit input[data-tbl]');
const tables = {};
inputs.forEach(inp => {
const tbl = inp.dataset.tbl;
const row = parseInt(inp.dataset.row);
const col = parseInt(inp.dataset.col);
if (!tables[tbl]) tables[tbl] = [];
while (tables[tbl].length <= row) tables[tbl].push([]);
tables[tbl][row][col] = inp.value;
});
// Converte valores numéricos
const numericCols = {
'credito': [0], 'debito': [0], 'alimentacao': [0], 'pix': [0],
'vales': [1], 'despesas': [2], 'sangrias': [1],
'cancelamentos': [1], 'pixcnpj': [1], 'receber': [1]
};
Object.keys(tables).forEach(tbl => {
const ncols = numericCols[tbl] || [];
tables[tbl] = tables[tbl].filter(row => row.some(c => c !== '' && c != null));
tables[tbl] = tables[tbl].map(row => row.map((val, ci) => {
if (ncols.includes(ci)) return parseNum(val);
return (val || '').trim();
}));
});
return tables;
}
// Recalcula todos os totais do modal de edição e atualiza o display
window._recalcEditTotals = function() {
const tables = readEditTables();
const cartCred_tot = (tables.credito || []).reduce((s, r) => s + (r[0] || 0), 0);
const cartDeb_tot = (tables.debito || []).reduce((s, r) => s + (r[0] || 0), 0);
const cartAli_tot = (tables.alimentacao || []).reduce((s, r) => s + (r[0] || 0), 0);
const cartPix_tot = (tables.pix || []).reduce((s, r) => s + (r[0] || 0), 0);
const vales_tot = (tables.vales || []).reduce((s, r) => s + (r[1] || 0), 0);
const desp_tot = (tables.despesas || []).reduce((s, r) => s + (r[2] || 0), 0);
const sang_tot = (tables.sangrias || []).reduce((s, r) => s + (r[1] || 0), 0);
const pixcnpj_tot = (tables.pixcnpj || []).reduce((s, r) => s + (r[1] || 0), 0);
const recv_tot = (tables.receber || []).reduce((s, r) => s + (r[1] || 0), 0);
const canc_tot = (tables.cancelamentos || []).reduce((s, r) => s + (r[1] || 0), 0);
const el = id => document.getElementById(id);
const fmtTot = v => v > 0 ? 'R$ ' + v.toLocaleString('pt-BR', { minimumFractionDigits: 2 }) : '—';
// Atualiza spans do total-box (prefixo "edit-total-") E do resumo rápido (prefixo "tot-")
if (el('edit-total-credito')) el('edit-total-credito').textContent = fmtTot(cartCred_tot);
if (el('edit-total-debito')) el('edit-total-debito').textContent = fmtTot(cartDeb_tot);
if (el('edit-total-alimentacao')) el('edit-total-alimentacao').textContent = fmtTot(cartAli_tot);
if (el('edit-total-pix')) el('edit-total-pix').textContent = fmtTot(cartPix_tot);
if (el('edit-total-vales')) el('edit-total-vales').textContent = fmtTot(vales_tot);
if (el('edit-total-despesas')) el('edit-total-despesas').textContent = fmtTot(desp_tot);
if (el('edit-total-sangrias')) el('edit-total-sangrias').textContent = fmtTot(sang_tot);
if (el('edit-total-pixcnpj')) el('edit-total-pixcnpj').textContent = fmtTot(pixcnpj_tot);
if (el('edit-total-receber')) el('edit-total-receber').textContent = fmtTot(recv_tot);
if (el('edit-total-cancel')) el('edit-total-cancel').textContent = fmtTot(canc_tot);
// Atualiza também os spans do total-box da popup (prefixo "tot-")
if (el('tot-credito')) el('tot-credito').textContent = fmtTot(cartCred_tot);
if (el('tot-debito')) el('tot-debito').textContent = fmtTot(cartDeb_tot);
if (el('tot-ali')) el('tot-ali').textContent = fmtTot(cartAli_tot);
if (el('tot-pix')) el('tot-pix').textContent = fmtTot(cartPix_tot);
if (el('tot-pixcnpj')) el('tot-pixcnpj').textContent = fmtTot(pixcnpj_tot);
if (el('tot-vales')) el('tot-vales').textContent = fmtTot(vales_tot);
if (el('tot-areceber')) el('tot-areceber').textContent = fmtTot(recv_tot);
if (el('tot-sangrias')) el('tot-sangrias').textContent = fmtTot(sang_tot);
if (el('tot-despesas')) el('tot-despesas').textContent = fmtTot(desp_tot);
if (el('tot-cancel')) el('tot-cancel').textContent = fmtTot(canc_tot);
};
// Modal de edição completo com todas as grades
function openEditModal(full) {
window._editModalOpen = true;
// Salva o registro ANTES de fechar o modal de relatório — usa variável separada para não conflitar
window._editingRecord = full;
const dados = full.dados || {};
// helper para fmt dinheiro
const fmtMR = v => {
const n = parseFloat(v||0);
if (isNaN(n)) return '';
if (n === 0) return 'R$ 0,00';
return 'R$ ' + n.toLocaleString('pt-BR', { minimumFractionDigits: 2 });
};
// helper inline para fmt table cell (já em editTableHTML)
const editedInfo = full.edited_at
? `<div style="background:#fff8e1;border-radius:4px;padding:6px 10px;font-size:11px;color:#666;margin-bottom:8px">
<strong>⚠️ Última edição:</strong> ${full.edited_at.replace('T',' ').substring(0,16)} por <strong>${full.edited_by||'?'}</strong>
</div>`
: '';
// --- Cartões ---
const cartCred_rows = (dados.cartoes?.credito || full.cartoes_credito || []).map(r => [typeof r === 'object' ? r.valor : r]);
const cartDeb_rows = (dados.cartoes?.debito || full.cartoes_debito || []).map(r => [typeof r === 'object' ? r.valor : r]);
const cartAli_rows = (dados.cartoes?.alimentacao || full.cartoes_alimentacao || []).map(r => [typeof r === 'object' ? r.valor : r]);
const cartPix_rows = (dados.cartoes?.pix || full.cartoes_pix || []).map(r => [typeof r === 'object' ? r.valor : r]);
const cartCredHTML = editTableHTML('credito', ['Valor (R$)'], cartCred_rows.length ? cartCred_rows : [['']], [0]);
const cartDebHTML = editTableHTML('debito', ['Valor (R$)'], cartDeb_rows.length ? cartDeb_rows : [['']], [0]);
const cartAliHTML = editTableHTML('alimentacao', ['Valor (R$)'], cartAli_rows.length ? cartAli_rows : [['']], [0]);
const cartPixHTML = editTableHTML('pix', ['Valor (R$)'], cartPix_rows.length ? cartPix_rows : [['']], [0]);
// --- Vales ---
const vales_rows = (dados.vales || []).map(r => [r.nome||'', r.valor||0, r.obs||'']);
const valesHTML = editTableHTML('vales', ['Nome', 'Valor (R$)', 'Obs'], vales_rows.length ? vales_rows : [['','','']], [1]);
// --- Despesas ---
const despesas_rows = (dados.despesas || []).map(r => [r.desc||'', r.obs||'', r.valor||0]);
const despesasHTML = editTableHTML('despesas', ['Descrição', 'Obs', 'Valor (R$)'], despesas_rows.length ? despesas_rows : [['','','']], [2]);
// --- Sangrias ---
const sangrias_rows = (dados.sangrias || []).map(r => [r.hora||'', r.retirada||r.valor||0, r.gerente||'']);
const sangriasHTML = editTableHTML('sangrias', ['Hora', 'Valor (R$)', 'Gerente'], sangrias_rows.length ? sangrias_rows : [['','','']], [1]);
// --- Cancelamentos ---
const cancel_rows = (dados.cancelamentos || []).map(r => [r.numero||'', r.valor||0, r.motivo||'']);
const cancelHTML = editTableHTML('cancelamentos', ['Número', 'Valor (R$)', 'Motivo'], cancel_rows.length ? cancel_rows : [['','','']], [1]);
// --- PIX CNPJ ---
const pixcnpj_rows = (dados.pixcnpj || []).map(r => [r.nome||r.desc||'', r.valor||0]);
const pixcnpjHTML = editTableHTML('pixcnpj', ['Nome', 'Valor (R$)'], pixcnpj_rows.length ? pixcnpj_rows : [['','']], [1]);
// --- A Receber ---
const receber_rows = (dados.receber || []).map(r => [r.nome||'', r.valor||0]);
const receberHTML = editTableHTML('receber', ['Nome', 'Valor (R$)'], receber_rows.length ? receber_rows : [['','']], [1]);
// --- Totais (só leitura formatado) ---
const totaisInfo = `
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:4px;background:#f5f5f5;border-radius:6px;padding:8px;margin-bottom:8px;font-size:11px">
<div><strong>Total Crédito:</strong> <span id="edit-total-credito">${fmtMR(full.total_cartao_credito||0)}</span></div>
<div><strong>Total Débito:</strong> <span id="edit-total-debito">${fmtMR(full.total_cartao_debito||0)}</span></div>
<div><strong>Total Alimentação:</strong> <span id="edit-total-alimentacao">${fmtMR(full.total_cartao_alimentacao||0)}</span></div>
<div><strong>Total PIX:</strong> <span id="edit-total-pix">${fmtMR(full.total_cartao_pix||0)}</span></div>
<div><strong>Total Sangrias:</strong> <span id="edit-total-sangrias">${fmtMR(full.total_sangrias||0)}</span></div>
<div><strong>Total Despesas:</strong> <span id="edit-total-despesas">${fmtMR(full.total_despesas||0)}</span></div>
<div><strong>Total Vales:</strong> <span id="edit-total-vales">${fmtMR(full.total_vales||0)}</span></div>
<div><strong>Total PIX CNPJ:</strong> <span id="edit-total-pixcnpj">${fmtMR(full.total_pixcnpj||0)}</span></div>
<div><strong>Total Receber:</strong> <span id="edit-total-receber">${fmtMR(full.total_areceber||0)}</span></div>
<div><strong>Total Cancel:</strong> <span id="edit-total-cancel">${fmtMR(full.total_cancelamentos||0)}</span></div>
</div>`;
const overlay = document.createElement('div');
overlay.id = 'modal-edit';
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:99998;overflow-y:auto';
overlay.innerHTML = `
<div style="background:#fff;margin:16px auto;border-radius:12px;padding:16px;max-width:800px;width:95%;font-size:13px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;position:sticky;top:0;background:#fff;z-index:1">
<h2 style="margin:0;font-size:15px;color:#e65100">✏️ Editar Fechamento Completo</h2>
<button onclick="closeEditModal()" style="background:none;border:none;font-size:18px;cursor:pointer;padding:4px 8px">✕</button>
</div>
${editedInfo}
<!-- Identificação -->
<div style="background:#f5f5f5;border-radius:6px;padding:8px;margin-bottom:10px">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:8px">
<div><label style="font-size:10px;color:#555">Data:</label><input type="date" id="edit-data" value="${full.data||''}" style="width:100%;padding:5px;border:1px solid #ccc;border-radius:4px" /></div>
<div><label style="font-size:10px;color:#555">Loja:</label>
<select id="edit-loja" style="width:100%;padding:5px;border:1px solid #ccc;border-radius:4px">
<option value="alianca" ${(full.loja||'').toLowerCase()==='alianca'?'selected':''}>Aliança</option>
<option value="uniao" ${(full.loja||'').toLowerCase()==='uniao'?'selected':''}>União</option>
</select>
</div>
<div><label style="font-size:10px;color:#555">Turno:</label>
<select id="edit-turno" style="width:100%;padding:5px;border:1px solid #ccc;border-radius:4px">
<option value="manha" ${(full.turno||'')==='manha'?'selected':''}>Manhã</option>
<option value="tarde" ${(full.turno||'')==='tarde'?'selected':''}>Tarde</option>
<option value="integral" ${(full.turno||'')==='integral'?'selected':''}>Integral</option>
</select>
</div>
<div><label style="font-size:10px;color:#555">Operador:</label><input type="text" id="edit-operador" value="${full.operador||''}" style="width:100%;padding:5px;border:1px solid #ccc;border-radius:4px" /></div>
</div>
</div>
<!-- Totais (leitura) -->
${totaisInfo}
<!-- Cartões de Crédito -->
<div style="margin-bottom:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
<strong style="font-size:12px;color:#1565c0">💳 Cartão de Crédito</strong>
<button onclick="_editAddRow('credito')" style="background:#1565c0;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
</div>
<div id="tbl-edit-credito" data-numeric="[0]">${cartCredHTML}</div>
</div>
<!-- Cartões de Débito -->
<div style="margin-bottom:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
<strong style="font-size:12px;color:#2e7d32">💳 Cartão de Débito</strong>
<button onclick="_editAddRow('debito')" style="background:#2e7d32;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
</div>
<div id="tbl-edit-debito" data-numeric="[0]">${cartDebHTML}</div>
</div>
<!-- Alimentação -->
<div style="margin-bottom:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
<strong style="font-size:12px;color:#e65100">🍽️ Alimentação</strong>
<button onclick="_editAddRow('alimentacao')" style="background:#e65100;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
</div>
<div id="tbl-edit-alimentacao" data-numeric="[0]">${cartAliHTML}</div>
</div>
<!-- PIX -->
<div style="margin-bottom:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
<strong style="font-size:12px;color:#7b1fa2">📱 PIX</strong>
<button onclick="_editAddRow('pix')" style="background:#7b1fa2;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
</div>
<div id="tbl-edit-pix" data-numeric="[0]">${cartPixHTML}</div>
</div>
<!-- Vales -->
<div style="margin-bottom:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
<strong style="font-size:12px;color:#c62828">📋 Vales</strong>
<button onclick="_editAddRow('vales')" style="background:#c62828;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
</div>
<div id="tbl-edit-vales" data-numeric="[1]">${valesHTML}</div>
</div>
<!-- Despesas -->
<div style="margin-bottom:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
<strong style="font-size:12px;color:#ef6c00">💸 Despesas</strong>
<button onclick="_editAddRow('despesas')" style="background:#ef6c00;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
</div>
<div id="tbl-edit-despesas" data-numeric="[2]">${despesasHTML}</div>
</div>
<!-- Sangrias -->
<div style="margin-bottom:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
<strong style="font-size:12px;color:#00695c">🏧 Sangrias</strong>
<button onclick="_editAddRow('sangrias')" style="background:#00695c;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
</div>
<div id="tbl-edit-sangrias" data-numeric="[1]">${sangriasHTML}</div>
</div>
<!-- PIX CNPJ -->
<div style="margin-bottom:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
<strong style="font-size:12px;color:#6a1b9a">PIX CNPJ</strong>
<button onclick="_editAddRow('pixcnpj')" style="background:#6a1b9a;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
</div>
<div id="tbl-edit-pixcnpj" data-numeric="[1]">${pixcnpjHTML}</div>
</div>
<!-- A Receber -->
<div style="margin-bottom:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
<strong style="font-size:12px;color:#283593">📬 A Receber</strong>
<button onclick="_editAddRow('receber')" style="background:#283593;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
</div>
<div id="tbl-edit-receber" data-numeric="[1]">${receberHTML}</div>
</div>
<!-- Cancelamentos -->
<div style="margin-bottom:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
<strong style="font-size:12px;color:#777">❌ Cancelamentos</strong>
<button onclick="_editAddRow('cancelamentos')" style="background:#777;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
</div>
<div id="tbl-edit-cancelamentos" data-numeric="[1]">${cancelHTML}</div>
</div>
<!-- Saldo de Caixa (SÓ TROCO é editável — restante é soma automática) -->
<div style="margin-bottom:12px;background:#f5f5f5;border-radius:6px;padding:8px">
<strong style="font-size:12px;color:#333;margin-bottom:4px;display:block">💰 Saldo de Caixa <span style="font-size:10px;color:#888">(apenas Troco é editável)</span></strong>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:6px">
<div><label style="font-size:10px;color:#555">Troco:</label><input type="text" id="edit-troco" value="${fmtMR(full.saldo_troco)}" style="width:100%;padding:5px;border:1px solid #2e7d32;border-radius:4px;text-align:right" onblur="this.value=fmtBR(this)" /></div>
<div><label style="font-size:10px;color:#888">Crédito:</label><input type="text" id="edit-sld-credito" value="${fmtMR(full.saldo_credito)}" readonly tabindex="-1" style="width:100%;padding:5px;border:1px solid #ddd;border-radius:4px;text-align:right;background:#eee;color:#666" /></div>
<div><label style="font-size:10px;color:#888">Débito:</label><input type="text" id="edit-sld-debito" value="${fmtMR(full.saldo_debito)}" readonly tabindex="-1" style="width:100%;padding:5px;border:1px solid #ddd;border-radius:4px;text-align:right;background:#eee;color:#666" /></div>
<div><label style="font-size:10px;color:#888">Alimentação:</label><input type="text" id="edit-sld-alimentacao" value="${fmtMR(full.saldo_alimentacao)}" readonly tabindex="-1" style="width:100%;padding:5px;border:1px solid #ddd;border-radius:4px;text-align:right;background:#eee;color:#666" /></div>
<div><label style="font-size:10px;color:#888">Vales:</label><input type="text" id="edit-sld-vales" value="${fmtMR(full.saldo_vales)}" readonly tabindex="-1" style="width:100%;padding:5px;border:1px solid #ddd;border-radius:4px;text-align:right;background:#eee;color:#666" /></div>
<div><label style="font-size:10px;color:#888">PIX CNPJ:</label><input type="text" id="edit-sld-pixcnpj" value="${fmtMR(full.total_pixcnpj)}" readonly tabindex="-1" style="width:100%;padding:5px;border:1px solid #ddd;border-radius:4px;text-align:right;background:#eee;color:#666" /></div>
<div><label style="font-size:10px;color:#888">À Receber:</label><input type="text" id="edit-sld-areceber" value="${fmtMR(full.saldo_areceber)}" readonly tabindex="-1" style="width:100%;padding:5px;border:1px solid #ddd;border-radius:4px;text-align:right;background:#eee;color:#666" /></div>
<div><label style="font-size:10px;color:#888">PIX Cartão:</label><input type="text" id="edit-sld-pixcartao" value="${fmtMR(full.total_cartao_pix)}" readonly tabindex="-1" style="width:100%;padding:5px;border:1px solid #ddd;border-radius:4px;text-align:right;background:#eee;color:#666" /></div>
</div>
</div>
<!-- Contadores -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:6px;margin-bottom:12px">
<div><label style="font-size:10px;color:#555">Clientes:</label><input type="number" id="edit-clientes" value="${dados.clientes||full.clientes||0}" min="0" style="width:100%;padding:5px;border:1px solid #ccc;border-radius:4px" /></div>
<div><label style="font-size:10px;color:#555">Frango Assado:</label><input type="number" id="edit-frango" value="${dados.frango||full.frango||0}" min="0" style="width:100%;padding:5px;border:1px solid #ccc;border-radius:4px" /></div>
<div><label style="font-size:10px;color:#555">Vendas (R$):</label><input type="text" id="edit-vendas" value="${fmtMR(dados.vendas||full.vendas||0)}" style="width:100%;padding:5px;border:1px solid #e65100;border-radius:4px;text-align:right;font-weight:bold" onblur="this.value=fmtBR(this)" /></div>
<div><label style="font-size:10px;color:#555">Saldo Esperado:</label><input type="text" id="edit-esperado" value="${fmtMR(full.saldo_esperado)}" style="width:100%;padding:5px;border:1px solid #e65100;border-radius:4px;text-align:right;font-weight:bold" onblur="this.value=fmtBR(this)" /></div>
</div>
<!-- Observações -->
<div style="margin-bottom:12px">
<label style="font-size:10px;color:#555">Observações:</label>
<textarea id="edit-obs" style="width:100%;padding:5px;border:1px solid #ccc;border-radius:4px;font-size:13px;height:60px">${full.observacoes||''}</textarea>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button onclick="window.salvarEdicaoModal()" style="flex:1;padding:10px;background:#2e7d32;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;font-weight:bold">💾 Salvar Alterações</button>
<button onclick="closeEditModal()" style="padding:10px 16px;background:#888;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px">Cancelar</button>
</div>
</div>`;
document.body.appendChild(overlay);
}
// Formata campo de dinheiro brasileiro
function fmtBR(inp) {
const v = parseNum(inp.value);
if (isNaN(v)) return '';
if (v === 0) return 'R$ 0,00';
return 'R$ ' + v.toLocaleString('pt-BR', { minimumFractionDigits: 2 });
}
// Fecha modal de edição
function closeEditModal() {
const m = document.getElementById('modal-edit');
if (m) document.body.removeChild(m);
window._editModalOpen = false;
}
// ─── Janela de edição completa (usa modal interno, não popup) ─────────────────
// Reaproveita openEditModal que já tem todas as tabelas editáveis,
// grades de cartões/vales/despesas/sangrias/pixcnjp/receber/cancelamentos,
// e conexão Tauri funcionando. Apenas pré-preenche o registro e abre.
window.abrirEditaFechamento = async function(full) {
if (!full) { alert('Registro não fornecido.'); return; }
// Garante id_fechamento
if (!full.id_fechamento && full.loja && full.data && full.operador) {
full.id_fechamento = (full.loja + '_' + full.data + '_' + full.operador).toLowerCase().replace(/ /g, '_');
}
// Usa o mesmo modal de edição completo que já funciona — só abre com o registro
openEditModal(full);
};
// Salva edições do modal de edição completo
window.salvarEdicaoModal = async function() {
console.log('[DEBUG] salvarEdicaoModal called');
const full = window._editingRecord;
if (!full) { console.log('[DEBUG] _editingRecord is null'); alert('Nenhum registro selecionado.'); return; }
console.log('[DEBUG] full.uuid:', full.uuid, 'full.id:', full.id);
const p = v => parseNum(v);
const inp = id => { const el = document.getElementById(id); return el ? el.value : ''; };
// Lê todas as grades editáveis
const tables = readEditTables();
// Calcula totais dos cartões
const cartCred_tot = (tables.credito || []).reduce((s, r) => s + (r[0] || 0), 0);
const cartDeb_tot = (tables.debito || []).reduce((s, r) => s + (r[0] || 0), 0);
const cartAli_tot = (tables.alimentacao || []).reduce((s, r) => s + (r[0] || 0), 0);
const cartPix_tot = (tables.pix || []).reduce((s, r) => s + (r[0] || 0), 0);
const vales_tot = (tables.vales || []).reduce((s, r) => s + (r[1] || 0), 0);
const desp_tot = (tables.despesas || []).reduce((s, r) => s + (r[2] || 0), 0);
const sang_tot = (tables.sangrias || []).reduce((s, r) => s + (r[1] || 0), 0);
const pixcnpj_tot = (tables.pixcnpj || []).reduce((s, r) => s + (r[1] || 0), 0);
const recv_tot = (tables.receber || []).reduce((s, r) => s + (r[1] || 0), 0);
const canc_tot = (tables.cancelamentos || []).reduce((s, r) => s + (r[1] || 0), 0);
// Monta estruturas de volta
const cartoes = {
credito: (tables.credito || []).map(r => r[0]),
debito: (tables.debito || []).map(r => r[0]),
alimentacao: (tables.alimentacao || []).map(r => r[0]),
pix: (tables.pix || []).map(r => r[0]),
};
const vales = (tables.vales || []).map(r => ({ nome: r[0], valor: r[1], obs: r[2]||'' }));
const despesas = (tables.despesas || []).map(r => ({ desc: r[0], obs: r[1]||'', valor: r[2] }));
const sangrias = (tables.sangrias || []).map(r => ({ hora: r[0], retirada: r[1], gerente: r[2]||'' }));
const cancelamentos = (tables.cancelamentos || []).map(r => ({ numero: r[0], valor: r[1], motivo: r[2]||'' }));
const pixcnpj = (tables.pixcnpj || []).map(r => ({ nome: r[0], valor: r[1] }));
const receber = (tables.receber || []).map(r => ({ nome: r[0], valor: r[1] }));
const now = new Date().toISOString();
const operador = estado.operador || full.operador || '';
const updates = {
// Chave natural — essencial para o Supabase encontrar o registro
id_fechamento: inp('edit-loja') + '_' + inp('edit-data') + '_' + inp('edit-operador'),
// Identificação
loja: inp('edit-loja'),
data: inp('edit-data'),
turno: inp('edit-turno'),
operador: inp('edit-operador'),
// Cartões (como arrays de números crus)
cartoes_credito: cartoes.credito,
cartoes_debito: cartoes.debito,
cartoes_alimentacao: cartoes.alimentacao,
cartoes_pix: cartoes.pix,
// Totais de cartões
total_cartao_credito: cartCred_tot,
total_cartao_debito: cartDeb_tot,
total_cartao_alimentacao: cartAli_tot,
total_cartao_pix: cartPix_tot,
// Saldo de caixa — soma automática dos itens editados (não dos campos readonly)
saldo_troco: p(inp('edit-troco')),
saldo_credito: cartCred_tot,
saldo_debito: cartDeb_tot,
saldo_alimentacao: cartAli_tot,
saldo_vales: vales_tot,
saldo_areceber: recv_tot,
// Totais
total_vales: vales_tot,
total_despesas: desp_tot,
total_sangrias: sang_tot,
total_pixcnpj: pixcnpj_tot,
total_areceber: recv_tot,
total_cancelamentos: canc_tot,
// Lançamentos detalhados (JSONB — gravados no campo `dados`)
dados: JSON.stringify({
cartoes,
vales,
despesas,
sangrias,
cancelamentos,
pixcnpj,
receber,
clientes: parseInt(inp('edit-clientes') || '0'),
frango: parseInt(inp('edit-frango') || '0'),
vendas: p(inp('edit-vendas')),
}),
// Contadores
clientes: parseInt(inp('edit-clientes') || '0'),
frango: parseInt(inp('edit-frango') || '0'),
vendas: p(inp('edit-vendas')),
// Meta
saldo_esperado: p(inp('edit-esperado')),
observacoes: inp('edit-obs'),
// Edit log
edited_at: now,
edited_by: operador,
};
const hasTauri = !!(window.__TAURI__ && window.__TAURI__.core);
if (!hasTauri) { alert('Tauri não disponível.'); return; }
try {
// Usa id_fechamento que é a chave natural do registro
const id_fechamento = full.id_fechamento || (full.loja && full.data && full.operador
? `${full.loja}_${full.data}_${full.operador}`.toLowerCase().replace(/ /g, '_')
: null);
if (!id_fechamento) { alert('id_fechamento não encontrado.'); return; }
console.log('[DEBUG] salvarEdicaoModal id_fechamento:', id_fechamento, 'updates:', JSON.stringify(updates));
console.log('[DEBUG] invoking sb_atualizar_fechamento...');
const result = await window.__TAURI__.core.invoke('sb_atualizar_fechamento', { id_fechamento, updates });
console.log('[DEBUG] invoke result:', result);
closeEditModal();
closeRelatorioModal();
alert('✅ Atualizado com sucesso!');
} catch(e) {
console.error('Erro ao salvar:', e);
alert('Erro ao salvar: ' + (e?.message || e));
}
};
// Salva edições de volta ao Supabase
window.salvarEdicaoConsulta = async function() {
const full = window._currentConsulta;
if (!full) return;
const p = v => parseFloat((v || '0').replace(',', '.') || '0');
const inp = id => document.getElementById(id).value;
const updates = {
saldo_troco: p(inp('edit-troco')),
saldo_credito: p(inp('edit-credito')),
saldo_debito: p(inp('edit-debito')),
saldo_alimentacao: p(inp('edit-alimentacao')),
saldo_vales: p(inp('edit-vales')),
total_cartao_pix: p(inp('edit-pixcartao')),
total_pixcnpj: p(inp('edit-pixcnpj')),
saldo_areceber: p(inp('edit-areceber')),
fechamento_dinheiro: p(inp('edit-fd')),
fechamento_cartoes: p(inp('edit-fc')),
fechamento_areceber: p(inp('edit-fr')),
total_sangrias: p(inp('edit-sangrias')),
total_despesas: p(inp('edit-despesas')),
clientes: parseInt(inp('edit-clientes') || '0'),
frango: parseInt(inp('edit-frango') || '0'),
vendas: p(inp('edit-vendas')),
saldo_esperado: p(inp('edit-esperado')),
observacoes: inp('edit-obs'),
edited_at: new Date().toISOString(),
edited_by: full.operador || '',
};
const hasTauri = !!(window.__TAURI__ && window.__TAURI__.core);
if (!hasTauri) { alert('Tauri não disponível.'); return; }
try {
const id = document.getElementById('edit-id').value || window._currentConsultaId || full.id;
if (!id) { alert('ID do registro não encontrado. Não é possível atualizar.'); return; }
await window.__TAURI__.core.invoke('sb_atualizar_fechamento', { id, updates });
alert('✅ Atualizado com sucesso!');
closeRelatorioModal();
} catch(e) {
console.error('Erro ao salvar:', e);
alert('Erro ao salvar: ' + (e?.message || e));
}
};
// Converte o registro "full" do Supabase para o formato usado pelo buildPrintHTML
function consultaToPrintData(full) {
const fmtDiff = v => 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
const fmtDiffNeg = v => (v < 0 ? '-' : '') + 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
const cartCred = parseFloat(full.saldo_credito || 0);
const cartDeb = parseFloat(full.saldo_debito || 0);
const cartAli = parseFloat(full.saldo_alimentacao || 0);
const cartPix = parseFloat(full.total_cartao_pix || 0);
const pixCnpj = parseFloat(full.total_pixcnpj || 0);
const vales = parseFloat(full.saldo_vales || 0);
const receber = parseFloat(full.saldo_areceber || 0);
const troco = parseFloat(full.saldo_troco || 0);
const sysTotal = troco + cartCred + cartDeb + cartAli + cartPix + pixCnpj + vales + receber;
const fd = parseFloat(full.fechamento_dinheiro || 0);
const fc = parseFloat(full.fechamento_cartoes || 0);
const fr = parseFloat(full.fechamento_areceber || 0);
const ftotal = fd + fc + fr;
const saldoEsp = parseFloat(full.saldo_esperado || 0);
const diferenca = parseFloat(full.diferenca || 0);
const diffOk = diferenca === 0;
const diffStr = diferenca >= 0 ? `+${fmtDiff(diferenca)}` : fmtDiffNeg(diferenca);
const dados = full.dados || {};
const despesas_arr = (dados.despesas || []).map(d => ({ desc: d.desc || '', obs: d.obs || '', valor: parseFloat(d.valor || 0) }));
const sangrias_arr = (dados.sangrias || []).map(s => ({ hora: s.hora || '', retirada: parseFloat(s.retirada || 0), gerente: s.gerente || '', valor: parseFloat(s.valor || 0) }));
const pixcnpj_arr = (dados.pixcnpj || []).map(p => ({ nome: p.nome || '', valor: parseFloat(p.valor || 0) }));
const vales_arr = (dados.vales || []).map(v => ({ nome: v.nome || '', obs: v.obs || '', valor: parseFloat(v.valor || 0) }));
const receber_arr = (dados.receber || []).map(r => ({ nome: r.nome || '', valor: parseFloat(r.valor || 0) }));
const cancelamentos_arr = (dados.cancelamentos || []).map(c => ({ numero: c.numero || '', valor: parseFloat(c.valor || 0), motivo: c.motivo || '' }));
const cartoes_credito = (dados.cartoes?.credito || full.cartoes_credito || []).map(v => parseFloat(typeof v === 'object' ? (v.valor ?? 0) : (v || 0)));
const cartoes_debito = (dados.cartoes?.debito || full.cartoes_debito || []).map(v => parseFloat(typeof v === 'object' ? (v.valor ?? 0) : (v || 0)));
const cartoes_alimentacao = (dados.cartoes?.alimentacao || full.cartoes_alimentacao || []).map(v => parseFloat(typeof v === 'object' ? (v.valor ?? 0) : (v || 0)));
const cartoes_pix = (dados.cartoes?.pix || full.cartoes_pix || []).map(v => parseFloat(typeof v === 'object' ? (v.valor ?? 0) : (v || 0)));
const totalSangrias = sangrias_arr.reduce((s, r) => s + r.retirada + r.valor, 0);
const totalDespesas = despesas_arr.reduce((s, r) => s + r.valor, 0);
const totalPixCnpj = pixcnpj_arr.reduce((s, r) => s + r.valor, 0);
const totalVales = vales_arr.reduce((s, r) => s + r.valor, 0);
const totalReceber = receber_arr.reduce((s, r) => s + r.valor, 0);
const totalCancel = cancelamentos_arr.reduce((s, r) => s + r.valor, 0);
const totalCartoes = cartCred + cartDeb + cartAli + cartPix;
return {
loja: full.loja || '?',
data: full.data || '?',
operador: full.operador || '?',
turno: full.turno || '?',
saldo_troco: troco,
saldo_esperado: saldoEsp,
fechamento: ftotal,
diferenca: diferenca,
sys_total: sysTotal,
fechamento_dinheiro: fd,
fechamento_cartoes: fc,
fechamento_areceber: fr,
credito: cartCred,
debito: cartDeb,
alimentacao: cartAli,
voucher: cartPix,
pixcnpj: pixCnpj,
vales: vales,
areceber: receber,
cancelamentos: totalCancel,
despesas_arr,
sangrias_arr,
pixcnpj_arr,
vales_arr,
receber_arr,
cancelamentos_arr,
cartoes_credito,
cartoes_debito,
cartoes_alimentacao,
cartoes_pix,
totalSangrias,
totalDespesas,
totalPixCnpj,
totalVales,
totalReceber,
totalCancel,
totalCartoes,
sysTotal,
saldoEsperado: saldoEsp,
diffStr,
diffOk,
fdin: fd,
fcarts: fc,
frec: fr,
ftotal,
observacoes: full.observacoes || '',
clientes: full.clientes || 0,
frango: full.frango || 0,
vendas: dados.vendas ?? full.vendas ?? 0,
};
}
// Imprime o relatório — usa iframe como openPrintPreview (Tauri não bloqueia)
window.printRelatorio = function() {
const full = window._currentConsulta;
if (!full) return;
const data = consultaToPrintData(full);
let html = `<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
<title>Fechamento de Caixa</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Courier New', monospace; font-size: 14px; width: 80mm; margin: 0 auto; padding: 8px; font-weight: bold; print-color-adjust: exact; -webkit-print-color-adjust: exact; }
.header { text-align: center; margin-bottom: 8px; }
.header h1 { font-size: 20px; font-weight: bold; }
.header h2 { font-size: 15px; font-weight: normal; }
hr { border: none; border-top: 2px solid #000; margin: 6px 0; }
.section-title { font-weight: bold; font-size: 14px; border-top: 2px solid #000; padding-top: 4px; margin-top: 8px; }
.row { display: flex; justify-content: space-between; padding: 2px 0; font-size: 13px; font-weight: bold; }
.row.total-row { font-weight: bold; }
.cart-table { table-layout: fixed; width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 4px; font-weight: bold; }
.cart-table th { font-weight: bold; border-bottom: 2px solid #000; padding: 2px; width: 25%; }
.cart-table td { padding: 2px 4px; text-align: right; font-weight: bold; }
.cart-table td:first-child { text-align: left; }
.cart-table tr.total-row { font-weight: bold; border-top: 2px solid #000; }
.subtotal { font-size: 13px; margin-top: 4px; }
.subtotal .row { padding: 2px 0; }
.diff-ok { color: #2e7d32; font-weight: bold; }
.diff-bad { color: #c0272d; font-weight: bold; }
.obs { font-size: 11px; color: #555; margin-top: 6px; white-space: pre-wrap; font-weight: normal; }
.footer { text-align: center; font-size: 11px; color: #555; margin-top: 8px; }
@media print { body { background: #fff; font-weight: bold !important; } }
</style>
</head><body>
${buildPrintHTML(data)}
</body></html>`;
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 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(btnFechar);
overlay.appendChild(btnImprimir);
document.body.appendChild(overlay);
iframe.srcdoc = html;
};
// Intercepta o envio — se sync_status=editing, faz PATCH
const _originalBtnEnviarHandler = () => {
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;
}
openConfirmModal();
};
// Modifica btn-enviar para detectar modo editing
document.getElementById('btn-enviar').replaceWith(
Object.assign(document.createElement('button'), {
id: 'btn-enviar', className: 'btn', innerHTML: '💾 Enviar Fechamento',
onclick() {
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;
}
if (estado.sync_status === 'editing') {
// Confirmação antes de atualizar
if (!confirm(`⚠️ ATENÇÃO: vai ATUALIZAR o registro existente no Supabase!\n\nUUID: ${estado.uuid}\n\nContinuar?`)) return;
salvarEdicaoSupabase();
} else {
openConfirmModal();
}
}
})
);
async function salvarEdicaoSupabase() {
const hasTauri = !!(window.__TAURI__ && window.__TAURI__.core);
if (!hasTauri) { alert('Tauri não disponível.'); return; }
const btn = document.getElementById('btn-enviar');
if (btn) { btn.disabled = true; btn.textContent = '⏳ Salvando...'; }
try {
const data = buildReciboData();
// Remove campos que não devem ir no PATCH
delete data.uuid;
delete data.id_local;
// Usa o id numérico se disponível, senão usa uuid
const id = estado.id || estado.uuid;
console.log('[DEBUG] salvando id:', id, 'data:', JSON.stringify(data));
const result = await window.__TAURI__.core.invoke('sb_atualizar_fechamento', {
id: id,
updates: JSON.stringify(data)
});
console.log('[DEBUG] sb_atualizar_fechamento result:', result);
estado.sync_status = 'synced';
saveState();
alert('✅ Registro atualizado com sucesso no Supabase!');
} catch(e) {
console.error('Erro ao atualizar:', e);
alert('❌ Erro ao atualizar no Supabase:\n' + (e?.message || e));
} finally {
if (btn) { btn.disabled = false; btn.textContent = '💾 Enviar Fechamento'; }
}
}
// ─── Boot ─────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
loadConfig();
buildDatalists();
if (!loadState()) init();
});
</script>
</body>
</html>