2093 lines
102 KiB
HTML
2093 lines
102 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="pt-BR">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Fechamento de Caixa — O Frangão</title>
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
:root {
|
||
--orange: #e65100;
|
||
--orange-dark: #bf360c;
|
||
--orange-light: #ff8a50;
|
||
--orange-pale: #fff3e0;
|
||
--orange-row: #fff0e0;
|
||
--red: #c0272d;
|
||
--bg: #ffffff;
|
||
--surface: #f7f7f7;
|
||
--surface2: #f0f0f0;
|
||
--border: #e0e0e0;
|
||
--text: #1a1a1a;
|
||
--muted: #777;
|
||
--green: #2e7d32;
|
||
--radius: 6px;
|
||
--font: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||
}
|
||
html, body { height: 100%; overflow: visible; }
|
||
body { font-family: var(--font); background: var(--bg); color: var(--text); display: flex; flex-direction: column; }
|
||
|
||
/* ── HEADER ─────────────────────────────────────────────────────────── */
|
||
.header {
|
||
background: linear-gradient(135deg, #bf360c 0%, #e65100 50%, #ff8a50 100%);
|
||
color: #fff; padding: 8px 18px;
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
flex-shrink: 0; gap: 14px;
|
||
position: sticky; top: 0; z-index: 100;
|
||
}
|
||
.header-left { display: flex; align-items: center; gap: 14px; flex: 1; }
|
||
.header-title { font-size: 15px; font-weight: 700; white-space: nowrap; }
|
||
.header-logo { height: 38px; width: auto; max-width: 140px; object-fit: contain; border-radius: 6px; }
|
||
.header-id { font-size: 11px; font-family: monospace; opacity: 0.9; white-space: nowrap; }
|
||
.header .gear-btn {
|
||
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
|
||
color: #fff; border-radius: var(--radius); padding: 5px 11px;
|
||
cursor: pointer; font-size: 13px; display: flex; align-items: center; gap: 5px;
|
||
font-weight: 600; font-family: var(--font); transition: background 0.2s;
|
||
}
|
||
.header .gear-btn:hover { background: rgba(255,255,255,0.25); }
|
||
|
||
/* ── META BAR ────────────────────────────────────────────────────────── */
|
||
.meta-bar {
|
||
background: var(--surface); border-bottom: 1px solid var(--border);
|
||
padding: 8px 18px; display: flex; gap: 14px; align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
.meta-field { display: flex; flex-direction: column; gap: 2px; }
|
||
.meta-label { font-size: 10px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.meta-input, .meta-select {
|
||
border: 1px solid var(--border); border-radius: var(--radius);
|
||
padding: 5px 9px; font-size: 13px; background: #fff; color: var(--text);
|
||
min-width: 115px; outline: none; font-family: var(--font);
|
||
}
|
||
.meta-input:focus, .meta-select:focus { border-color: var(--orange); box-shadow: 0 0 0 2px var(--orange-pale); }
|
||
.meta-select { cursor: pointer; }
|
||
|
||
/* ── MAIN 2 COLUMNS — single scroll, no internal scrollbars ──────────── */
|
||
.main { flex: 1; display: grid; grid-template-columns: 1fr 1fr; overflow: visible; }
|
||
.col { padding: 12px 16px; overflow: visible; }
|
||
.col-left { border-right: 1px solid var(--border); }
|
||
.col-right { background: var(--surface); display: flex; flex-direction: column; }
|
||
.col-right .section { flex-shrink: 0; }
|
||
.col-right .section:last-child { flex: 1; }
|
||
|
||
/* ── SECTION ─────────────────────────────────────────────────────────── */
|
||
.section { margin-bottom: 14px; }
|
||
.section-header {
|
||
font-size: 11px; font-weight: 700; color: var(--orange);
|
||
text-transform: uppercase; letter-spacing: 0.7px; padding: 4px 0;
|
||
margin-bottom: 5px; border-bottom: 2px solid var(--orange);
|
||
display: flex; align-items: center; gap: 5px;
|
||
}
|
||
|
||
/* ── TABLE ─────────────────────────────────────────────────────────── */
|
||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
th {
|
||
background: var(--surface2); font-weight: 700; color: var(--muted);
|
||
text-align: left; font-size: 10px; text-transform: uppercase;
|
||
letter-spacing: 0.4px; padding: 5px 8px; border: 1px solid var(--border); white-space: nowrap;
|
||
}
|
||
td { padding: 4px 6px; border: 1px solid var(--border); vertical-align: middle; }
|
||
td input, td select {
|
||
width: 100%; border: none; background: transparent;
|
||
font-size: 13px; padding: 4px 2px; outline: none; font-family: var(--font); color: var(--text);
|
||
}
|
||
td input:focus, td select:focus { background: #fffde7; border-radius: 3px; }
|
||
td.num { text-align: right; }
|
||
td.num input { text-align: right; }
|
||
tr.total-row td { background: var(--orange-row); font-weight: 700; color: var(--orange); font-size: 13px; }
|
||
tfoot td { background: var(--surface2); font-weight: 700; font-size: 13px; }
|
||
tfoot td:last-child { text-align: right; }
|
||
|
||
.row-del {
|
||
width: 26px; height: 26px; border: none; background: transparent;
|
||
color: #ccc; cursor: pointer; font-size: 14px; border-radius: 4px;
|
||
display: flex; align-items: center; justify-content: center; padding: 0;
|
||
}
|
||
.row-del:hover { background: #fee2e2; color: var(--red); }
|
||
|
||
.add-btn {
|
||
display: inline-flex; align-items: center; gap: 4px;
|
||
background: var(--orange); color: #fff; border: none; border-radius: var(--radius);
|
||
padding: 5px 12px; font-size: 11px; cursor: pointer; font-weight: 600; margin-top: 5px;
|
||
}
|
||
.add-btn:hover { background: var(--orange-dark); }
|
||
|
||
/* ── SYS BOX ─────────────────────────────────────────────────────────── */
|
||
.sys-grid { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
||
.sys-row {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
padding: 7px 12px; border-bottom: 1px solid var(--border);
|
||
font-size: 13px; background: #fff;
|
||
}
|
||
.sys-row:nth-child(even) { background: var(--surface); }
|
||
.sys-row:last-child { border-bottom: none; }
|
||
.sys-row.total { background: var(--orange-row); font-weight: 700; color: var(--orange); }
|
||
.sys-row .lbl { color: var(--muted); font-size: 12px; }
|
||
.sys-row.total .lbl { color: var(--orange); }
|
||
.sys-row .val { font-variant-numeric: tabular-nums; font-weight: 700; font-size: 13px; }
|
||
.sys-row.total .val { color: var(--orange); font-size: 14px; }
|
||
.sys-row input {
|
||
border: none; background: #e8f5e9; text-align: right; font-size: 13px;
|
||
font-family: var(--font); font-weight: 700; color: #2e7d32;
|
||
width: 90px; padding: 2px 4px; outline: none; border-radius: 3px;
|
||
}
|
||
.sys-row input:focus { background: #c8e6c9; }
|
||
.sys-row input.editable {
|
||
background: #fffde7; color: var(--text); cursor: text;
|
||
}
|
||
.sys-row input.editable:focus { background: #fff9c4; }
|
||
|
||
/* ── FECHAMENTO COUNTED ──────────────────────────────────────────────── */
|
||
.fech-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
|
||
.fech-item { border: 1px solid var(--border); border-radius: var(--radius); padding: 8px 12px; background: #fff; }
|
||
.fech-item-label { font-size: 10px; text-transform: uppercase; color: var(--muted); font-weight: 700; letter-spacing: 0.4px; margin-bottom: 4px; }
|
||
.fech-item input {
|
||
border: none; font-size: 17px; font-weight: 700; color: var(--text);
|
||
width: 100%; font-family: var(--font); outline: none; background: transparent; text-align: right;
|
||
}
|
||
.fech-item input:focus { background: #fffde7; border-radius: 3px; }
|
||
.fech-total {
|
||
border: 1px solid var(--border); border-radius: var(--radius);
|
||
padding: 9px 14px; background: var(--orange-row);
|
||
display: flex; justify-content: space-between; align-items: center; margin-top: 8px;
|
||
}
|
||
.fech-total-label { font-size: 11px; font-weight: 700; color: var(--orange); text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.fech-total-value { font-size: 16px; font-weight: 700; color: var(--orange); font-variant-numeric: tabular-nums; }
|
||
|
||
/* ── DIFERENÇA ─────────────────────────────────────────────────────────── */
|
||
.dif-box {
|
||
border: 2px solid var(--border); border-radius: var(--radius);
|
||
padding: 10px 16px; display: flex; justify-content: space-between; align-items: center;
|
||
}
|
||
.dif-label { font-size: 12px; font-weight: 700; color: var(--text); text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.dif-value { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
||
.dif-value.pos, .dif-value.zero { color: var(--green); }
|
||
.dif-value.neg { color: var(--red); }
|
||
|
||
/* ── SALDO ESPERADO ─────────────────────────────────────────────────── */
|
||
.saldo-esperado-wrap { margin-top: 6px; }
|
||
.saldo-esperado-wrap label { font-size: 10px; text-transform: uppercase; color: var(--muted); font-weight: 700; letter-spacing: 0.4px; display: block; margin-bottom: 3px; }
|
||
.saldo-esperado-wrap input {
|
||
border: 1px solid var(--border); border-radius: var(--radius);
|
||
padding: 8px 10px; font-size: 16px; font-weight: 700; width: 100%;
|
||
font-family: var(--font); outline: none; text-align: right; color: var(--text);
|
||
}
|
||
.saldo-esperado-wrap input:focus { border-color: var(--orange); box-shadow: 0 0 0 2px var(--orange-pale); }
|
||
|
||
/* ── CARDS (4 separate blocks) ─────────────────────────────────────── */
|
||
.cards-section {
|
||
background: var(--surface); border-top: 1px solid var(--border);
|
||
padding: 10px 18px; flex-shrink: 0;
|
||
}
|
||
.cards-header {
|
||
font-size: 11px; font-weight: 700; color: var(--orange);
|
||
text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 8px;
|
||
padding-bottom: 4px; border-bottom: 2px solid var(--orange);
|
||
}
|
||
.cards-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
|
||
.card-block { border: 1px solid var(--border); border-radius: var(--radius); overflow: visible; }
|
||
.card-block-header {
|
||
background: var(--orange); color: #fff; text-align: center;
|
||
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
||
letter-spacing: 0.6px; padding: 6px 4px; border-radius: var(--radius) var(--radius) 0 0;
|
||
}
|
||
.card-block table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
.card-block td { padding: 3px 6px; border: 1px solid var(--border); vertical-align: middle; }
|
||
.card-block td input {
|
||
width: 100%; border: none; background: transparent;
|
||
font-size: 13px; padding: 4px 2px; outline: none; font-family: var(--font); color: var(--text);
|
||
}
|
||
.card-block td input:focus { background: #fffde7; border-radius: 3px; }
|
||
.card-block td.num { text-align: right; }
|
||
.card-block td.num input { text-align: right; }
|
||
.card-block tfoot td { background: var(--orange-pale); font-weight: 700; color: var(--orange); font-size: 13px; text-align: right; }
|
||
.card-block .add-btn { width: calc(100% - 12px); justify-content: center; margin: 5px 6px 6px; }
|
||
.soma-row {
|
||
display: flex; justify-content: flex-end; align-items: center;
|
||
gap: 12px; padding: 7px 10px; background: var(--orange-pale);
|
||
border-radius: var(--radius); font-size: 13px; font-weight: 700; color: var(--orange); margin-top: 6px;
|
||
}
|
||
.soma-row span:last-child { font-size: 16px; }
|
||
|
||
/* ── FOOTER ─────────────────────────────────────────────────────────── */
|
||
.footer {
|
||
background: var(--surface); border-top: 1px solid var(--border);
|
||
padding: 8px 18px; display: flex; align-items: center; gap: 10px;
|
||
flex-shrink: 0; flex-wrap: wrap;
|
||
}
|
||
.auto-save { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--muted); margin-right: auto; }
|
||
.dot { width: 8px; height: 8px; border-radius: 50%; background: #4caf50; }
|
||
.btn {
|
||
border: 1px solid var(--border); background: #fff; border-radius: var(--radius);
|
||
padding: 7px 14px; font-size: 12px; cursor: pointer; display: flex;
|
||
align-items: center; gap: 6px; font-weight: 600; font-family: var(--font); color: var(--text);
|
||
}
|
||
.btn:hover { background: var(--surface2); }
|
||
.btn-danger { background: var(--orange); color: #fff; border-color: var(--orange); font-size: 13px; padding: 8px 16px; }
|
||
.btn-danger:hover { background: var(--orange-dark); }
|
||
|
||
/* ── MODAL ──────────────────────────────────────────────────────────── */
|
||
.modal-overlay {
|
||
display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5);
|
||
z-index: 9999; justify-content: center; align-items: center;
|
||
}
|
||
.modal-overlay.open { display: flex; }
|
||
.modal {
|
||
background: #fff; border-radius: 10px; width: 560px; max-width: 95vw;
|
||
max-height: 85vh; display: flex; flex-direction: column;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.3); overflow: hidden;
|
||
}
|
||
.modal-header {
|
||
background: linear-gradient(135deg, #bf360c, #e65100);
|
||
color: #fff; padding: 13px 20px;
|
||
display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
|
||
}
|
||
.modal-header h2 { font-size: 15px; font-weight: 700; }
|
||
.modal-close {
|
||
background: rgba(255,255,255,0.2); border: none; color: #fff;
|
||
border-radius: 4px; width: 28px; height: 28px; cursor: pointer;
|
||
font-size: 16px; display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.modal-close:hover { background: rgba(255,255,255,0.35); }
|
||
.modal-body { flex: 1; overflow-y: auto; padding: 16px 20px; }
|
||
.cfg-section { margin-bottom: 18px; }
|
||
.cfg-section-title {
|
||
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
||
letter-spacing: 0.6px; color: var(--orange);
|
||
padding-bottom: 4px; margin-bottom: 8px; border-bottom: 1px solid var(--border);
|
||
}
|
||
.cfg-item { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
|
||
.cfg-item input[type="text"] {
|
||
flex: 1; border: 1px solid var(--border); border-radius: var(--radius);
|
||
padding: 6px 10px; font-size: 13px; font-family: var(--font); outline: none;
|
||
}
|
||
.cfg-item input[type="text"]:focus { border-color: var(--orange); }
|
||
.cfg-item .btn-rm {
|
||
width: 26px; height: 26px; border: none; background: #fee2e2;
|
||
color: var(--red); border-radius: 4px; cursor: pointer; font-size: 13px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.btn-add-config {
|
||
display: inline-flex; align-items: center; gap: 4px;
|
||
background: var(--orange); color: #fff; border: none; border-radius: var(--radius);
|
||
padding: 5px 12px; font-size: 11px; cursor: pointer; font-weight: 600; margin-top: 4px;
|
||
}
|
||
.btn-add-config:hover { background: var(--orange-dark); }
|
||
.modal-footer {
|
||
padding: 12px 20px; border-top: 1px solid var(--border);
|
||
display: flex; justify-content: flex-end; gap: 10px; flex-shrink: 0;
|
||
}
|
||
.modal-footer .btn-primary {
|
||
background: var(--orange); color: #fff; border: none; border-radius: var(--radius);
|
||
padding: 8px 20px; font-size: 13px; cursor: pointer; font-weight: 600; font-family: var(--font);
|
||
}
|
||
.modal-footer .btn-primary:hover { background: var(--orange-dark); }
|
||
|
||
/* ── Scrollbar ────────────────────────────────────────────────────── */
|
||
::-webkit-scrollbar { width: 7px; }
|
||
::-webkit-scrollbar-track { background: var(--surface); }
|
||
::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ═══ HEADER ════════════════════════════════════════════════════════════ -->
|
||
<div class="header">
|
||
<div class="header-left">
|
||
<span class="header-title">Fechamento de Caixa</span>
|
||
<img class="header-logo" id="store-logo" src="./logo.png" alt="Logo" />
|
||
<span class="header-id" id="fechamento-id">—</span>
|
||
</div>
|
||
<button class="gear-btn" id="btn-config" title="Configurar">⚙️ Configurar</button>
|
||
</div>
|
||
|
||
<!-- ═══ META BAR ════════════════════════════════════════════════════════ -->
|
||
<div class="meta-bar">
|
||
<div class="meta-field">
|
||
<span class="meta-label">Data</span>
|
||
<input type="date" class="meta-input" id="f-data" />
|
||
</div>
|
||
<div class="meta-field">
|
||
<span class="meta-label">Operador</span>
|
||
<input type="text" class="meta-input" id="f-operador" list="dl-operadores" placeholder="Ex: Isabelle" />
|
||
</div>
|
||
<div class="meta-field">
|
||
<span class="meta-label">Turno</span>
|
||
<select class="meta-select" id="f-turno">
|
||
<option value="">—</option>
|
||
<option value="integral">Integral</option>
|
||
<option value="manhã">Manhã</option>
|
||
<option value="tarde">Tarde</option>
|
||
</select>
|
||
</div>
|
||
<div class="meta-field">
|
||
<span class="meta-label">Loja</span>
|
||
<select class="meta-select" id="f-loja">
|
||
<option value="União">União</option>
|
||
<option value="Aliança">Aliança</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ MAIN 2 COLUMNS ════════════════════════════════════════════════ -->
|
||
<div class="main">
|
||
|
||
<!-- ── LEFT: entradas, sangrias, despesas, pix, vales, receber, cancelamentos ── -->
|
||
<div class="col col-left">
|
||
|
||
<!-- SANGRIAS -->
|
||
<div class="section">
|
||
<div class="section-header">💸 Sangrias</div>
|
||
<table id="tbl-sangrias"><thead><tr><th>Hora</th><th>Retirada (R$)</th><th>Gerente</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td colspan="2">TOTAL</td><td class="num" id="total-sangrias">R$ 0,00</td><td></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-sangrias">+ Adicionar sangria</button>
|
||
</div>
|
||
|
||
<!-- DESPESAS -->
|
||
<div class="section">
|
||
<div class="section-header">📝 Despesas</div>
|
||
<table id="tbl-despesas"><thead><tr><th>Descrição</th><th>Obs</th><th>Valor (R$)</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td colspan="2">TOTAL</td><td class="num" id="total-despesas">R$ 0,00</td><td></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-despesas">+ Adicionar despesa</button>
|
||
</div>
|
||
|
||
<!-- PIX CNPJ -->
|
||
<div class="section">
|
||
<div class="section-header">💳 PIX CNPJ</div>
|
||
<table id="tbl-pix"><thead><tr><th>Nome</th><th>Valor (R$)</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td>TOTAL</td><td class="num" id="total-pix">R$ 0,00</td><td></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-pix">+ Adicionar PIX</button>
|
||
</div>
|
||
|
||
<!-- VALES -->
|
||
<div class="section">
|
||
<div class="section-header">💰 Vales (Funcionários)</div>
|
||
<table id="tbl-vales"><thead><tr><th>Nome</th><th>Valor (R$)</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td>TOTAL</td><td class="num" id="total-vales">R$ 0,00</td><td></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-vales">+ Adicionar vale</button>
|
||
</div>
|
||
|
||
<!-- À RECEBER -->
|
||
<div class="section">
|
||
<div class="section-header">👥 À Receber (Clientes)</div>
|
||
<table id="tbl-receber"><thead><tr><th>Nome</th><th>Valor (R$)</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td>TOTAL</td><td class="num" id="total-receber">R$ 0,00</td><td></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-receber">+ Adicionar cliente</button>
|
||
</div>
|
||
|
||
<!-- CANCELAMENTOS -->
|
||
<div class="section">
|
||
<div class="section-header">❌ Cancelamentos</div>
|
||
<table id="tbl-cancelamentos"><thead><tr><th>Número</th><th>Valor (R$)</th><th>Motivo</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td colspan="1">TOTAL</td><td class="num" id="total-cancelamentos">R$ 0,00</td><td colspan="2"></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-cancelamentos">+ Adicionar cancelamento</button>
|
||
</div>
|
||
|
||
</div><!-- /col-left -->
|
||
|
||
<!-- ── RIGHT: saldo caixa, fechamento contado, diferença, obs ── -->
|
||
<div class="col col-right">
|
||
|
||
<!-- SALDO DE CAIXA -->
|
||
<div class="section">
|
||
<div class="section-header">🖥️ Fechamento de Caixa</div>
|
||
<div class="sys-grid">
|
||
<div class="sys-row"><span class="lbl">Troco (+)</span><input type="text" id="sys-troco" class="editable" placeholder="0,00" /></div>
|
||
<div class="sys-row"><span class="lbl">Crédito (+)</span><input type="text" id="sys-credito" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">Débito (+)</span><input type="text" id="sys-debito" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">Alimentação (+)</span><input type="text" id="sys-alimentacao" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">Vales (+)</span><input type="text" id="sys-vales" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">À Receber (+)</span><input type="text" id="sys-receber" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">PIX Cartão (+)</span><input type="text" id="sys-pix" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">PIX CNPJ (+)</span><input type="text" id="sys-pixcnpj" readonly tabindex="-1" /></div>
|
||
<div class="sys-row total"><span class="lbl">TOTAL</span><span class="val" id="sys-total">R$ 0,00</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SALDO ESPERADO -->
|
||
<div class="section">
|
||
<div class="section-header">🎯 Saldo Esperado</div>
|
||
<div class="saldo-esperado-wrap">
|
||
<label>Informe o saldo esperado (contado em caixa)</label>
|
||
<input type="text" id="f-saldo-esperado" placeholder="0,00" />
|
||
</div>
|
||
<!-- DIFERENÇA — dentro do Saldo Esperado, logo abaixo -->
|
||
<div class="dif-box" style="margin-top:8px">
|
||
<span class="dif-label">Diferença</span>
|
||
<span class="dif-value zero" id="diferenca">R$ 0,00</span>
|
||
<span id="dif-badge" style="font-size:11px;font-weight:700;padding:2px 8px;border-radius:10px;background:#e8f5e9;color:#2e7d32;display:none">OK</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- FECHAMENTO (VALORES CONTADOS) -->
|
||
<div class="section">
|
||
<div class="section-header">🛒 Saldo de Caixa</div>
|
||
<div class="fech-grid">
|
||
<div class="fech-item"><div class="fech-item-label">Dinheiro</div><input type="text" id="f-fech-dinheiro" placeholder="0,00" /></div>
|
||
<div class="fech-item"><div class="fech-item-label">Cartões</div><input type="text" id="f-fech-cartoes" placeholder="0,00" /></div>
|
||
<div class="fech-item"><div class="fech-item-label">À Receber</div><input type="text" id="f-fech-receber" placeholder="0,00" /></div>
|
||
</div>
|
||
<div class="fech-total">
|
||
<span class="fech-total-label">Total Programa</span>
|
||
<span class="fech-total-value" id="fech-total">R$ 0,00</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- OBS -->
|
||
<div class="section" style="flex:1;display:flex;flex-direction:column">
|
||
<div class="section-header">📝 Observações</div>
|
||
<textarea id="f-obs" rows="4" style="flex:1;width:100%;min-height:80px;border:1px solid var(--border);border-radius:var(--radius);padding:10px;font-size:15px;font-family:var(--font);resize:none" placeholder="Observações..."></textarea>
|
||
</div>
|
||
|
||
</div><!-- /col-right -->
|
||
</div><!-- /main -->
|
||
|
||
<!-- ═══ CARTÕES ════════════════════════════════════════════════════════ -->
|
||
<div class="cards-section">
|
||
<div class="cards-header">💳 Cartões</div>
|
||
<div class="cards-grid">
|
||
|
||
<div class="card-block">
|
||
<div class="card-block-header">Crédito</div>
|
||
<table id="tbl-cartao-credito"><thead><tr><th>Valor (R$)</th></tr></thead><tbody></tbody><tfoot><tr><td id="total-cartao-credito">R$ 0,00</td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-cartao-credito">+</button>
|
||
</div>
|
||
|
||
<div class="card-block">
|
||
<div class="card-block-header">Débito</div>
|
||
<table id="tbl-cartao-debito"><thead><tr><th>Valor (R$)</th></tr></thead><tbody></tbody><tfoot><tr><td id="total-cartao-debito">R$ 0,00</td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-cartao-debito">+</button>
|
||
</div>
|
||
|
||
<div class="card-block">
|
||
<div class="card-block-header">Alimentação</div>
|
||
<table id="tbl-cartao-alimentacao"><thead><tr><th>Valor (R$)</th></tr></thead><tbody></tbody><tfoot><tr><td id="total-cartao-alimentacao">R$ 0,00</td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-cartao-alimentacao">+</button>
|
||
</div>
|
||
|
||
<div class="card-block">
|
||
<div class="card-block-header">PIX</div>
|
||
<table id="tbl-cartao-pix"><thead><tr><th>Valor (R$)</th></tr></thead><tbody></tbody><tfoot><tr><td id="total-cartao-pix">R$ 0,00</td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-cartao-pix">+</button>
|
||
</div>
|
||
|
||
</div>
|
||
<div class="soma-row">
|
||
<span>Soma Cartões (todas as colunas)</span>
|
||
<span id="soma-cartoes">R$ 0,00</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ FOOTER ════════════════════════════════════════════════════════ -->
|
||
<div class="footer">
|
||
<div class="auto-save"><span class="dot"></span> Salvamento automático activo</div>
|
||
<button class="btn" id="btn-anterior">✏️ Dia anterior</button>
|
||
<button class="btn" id="btn-recibo">🖨️ Recibo 80mm</button>
|
||
<button class="btn" id="btn-limpar">🗑️ Limpar rascunho</button>
|
||
<button class="btn btn-danger" id="btn-enviar">💾 Enviar Fechamento</button>
|
||
</div>
|
||
|
||
<!-- ═══ CONFIG MODAL ══════════════════════════════════════════════════ -->
|
||
<div class="modal-overlay" id="modal-config">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h2>⚙️ Configurar Preenchimento Automático</h2>
|
||
<button class="modal-close" id="modal-close">✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="cfg-section">
|
||
<div class="cfg-section-title">👤 Operadores</div>
|
||
<div id="cfg-operadores-list"></div>
|
||
<button class="btn-add-config" id="cfg-add-operador">+ Adicionar operador</button>
|
||
</div>
|
||
<div class="cfg-section">
|
||
<div class="cfg-section-title">👔 Gerentes</div>
|
||
<div id="cfg-gerentes-list"></div>
|
||
<button class="btn-add-config" id="cfg-add-gerente">+ Adicionar gerente</button>
|
||
</div>
|
||
<div class="cfg-section">
|
||
<div class="cfg-section-title">📝 Despesas (sugestões)</div>
|
||
<div id="cfg-despesas-list"></div>
|
||
<button class="btn-add-config" id="cfg-add-despesa">+ Adicionar despesa</button>
|
||
</div>
|
||
<div class="cfg-section">
|
||
<div class="cfg-section-title">👥 Clientes (À Receber)</div>
|
||
<div id="cfg-clientes-list"></div>
|
||
<button class="btn-add-config" id="cfg-add-cliente">+ Adicionar cliente</button>
|
||
</div>
|
||
<div class="cfg-section">
|
||
<div class="cfg-section-title">💰 Vales (nomes para autofill)</div>
|
||
<div id="cfg-vales-list"></div>
|
||
<button class="btn-add-config" id="cfg-add-vales">+ Adicionar nome</button>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn-primary" id="cfg-salvar">Salvar Configuração</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ CONFIRM MODAL ══════════════════════════════════════════════════ -->
|
||
<div class="modal-overlay" id="modal-confirm">
|
||
<div class="modal" style="width:520px">
|
||
<div class="modal-header">
|
||
<h2>⚠️ Confirmar envio do fechamento</h2>
|
||
<button class="modal-close" id="confirm-close">✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p style="font-size:13px;color:#444;margin-bottom:14px">
|
||
Tem certeza que deseja <strong>enviar este fechamento e encerrar o caixa?</strong>
|
||
</p>
|
||
<div id="confirm-summary" style="border:1px solid #b3d9f7;border-radius:8px;padding:12px;background:#f0f7ff;font-size:12px;font-family:monospace">
|
||
</div>
|
||
<p style="font-size:11px;color:#888;margin-top:10px">
|
||
Após enviar, <strong>não dá pra editar este fechamento</strong> sem a senha do Filipe.
|
||
</p>
|
||
</div>
|
||
<div class="modal-footer" style="justify-content:space-between">
|
||
<button class="btn" id="confirm-print" style="color:#333">
|
||
🖨️ Imprimir Relatório
|
||
</button>
|
||
<div style="display:flex;gap:10px">
|
||
<button class="btn" id="confirm-cancel" style="border-color:var(--red);color:var(--red)">✕ Cancelar</button>
|
||
<button class="btn btn-danger" id="confirm-send">✓ Sim, enviar e encerrar</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ JAVASCRIPT ══════════════════════════════════════════════════ -->
|
||
<script>
|
||
'use strict';
|
||
|
||
// ─── Config ────────────────────────────────────────────────────────────────
|
||
let config = {
|
||
operadores: ['Isabelle', 'Silvanea', 'Rafael'],
|
||
gerentes: ['Carlos', 'Ana', 'João'],
|
||
despesas: ['Energia', 'Água', 'Manutenção', 'Limpeza', 'Insumos'],
|
||
clientes: [], // preenchimento livre — não é datalist fixo
|
||
vales_nomes: [], // nomes usados em vales ( histórico para autofill)
|
||
};
|
||
|
||
// ─── App State ────────────────────────────────────────────────────────────
|
||
let estado = {
|
||
uuid: crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(),
|
||
loja: 'União', data: '', operador: '', turno: '',
|
||
saldo_troco: 0, fechamento: 0, saldo_esperado: 0, diferenca: 0,
|
||
despesas: [], sangrias: [], pixcnpj: [], vales: [], receber: [], cancelamentos: [],
|
||
cartoes: { credito: [], debito: [], alimentacao: [], pix: [] },
|
||
fechamento_contado: { dinheiro: 0, cartoes: 0, receber: 0 },
|
||
observacoes: '', id_local: null, sync_status: 'local'
|
||
};
|
||
|
||
// ─── Helpers ──────────────────────────────────────────────────────────────
|
||
const fmt = v => 'R$ ' + (parseFloat(v) || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
||
const parseNum = v => {
|
||
// Already a number? Return directly (don't treat '.' as thousand separator)
|
||
if (typeof v === 'number') return isNaN(v) ? 0 : v;
|
||
const s = String(v || '0').trim();
|
||
if (!s) return 0;
|
||
// Brazilian format: 1.234,56 → 1234.56
|
||
// Strategy: if string has comma, treat as BRL (decimal comma)
|
||
// If no comma but has dot, treat as US (decimal dot)
|
||
const lastComma = s.lastIndexOf(',');
|
||
const lastDot = s.lastIndexOf('.');
|
||
const hasCommaDecimal = lastComma > lastDot;
|
||
if (hasCommaDecimal) {
|
||
const intPart = s.slice(0, lastComma).replace(/\./g, '').replace(/,/g, '');
|
||
const decPart = s.slice(lastComma + 1);
|
||
return parseFloat(intPart + '.' + decPart) || 0;
|
||
}
|
||
// No comma: US-style number, just strip any dots (thousand sep)
|
||
return parseFloat(s.replace(/,/g, '').replace(/\./g, '')) || 0;
|
||
};
|
||
const setText = (id, t) => { const e = document.getElementById(id); if (e) e.textContent = t; };
|
||
const setVal = (id, v) => { const e = document.getElementById(id); if (e) e.value = v; };
|
||
const val = id => { const e = document.getElementById(id); return e ? e.value : ''; };
|
||
const fmtNum = v => {
|
||
const n = typeof v === 'number' ? v : parseNum(v);
|
||
if (isNaN(n)) return '';
|
||
// Always 2 decimal places for money
|
||
return n.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
};
|
||
|
||
// Format time: 4 digits → HH:MM
|
||
function fmtTime(v) {
|
||
const digits = (v || '').replace(/\D/g, '').slice(0, 4);
|
||
if (digits.length <= 2) return digits;
|
||
return digits.slice(0, 2) + ':' + digits.slice(2);
|
||
}
|
||
|
||
// ─── Tables ───────────────────────────────────────────────────────────────
|
||
const TABLES = {
|
||
'tbl-sangrias': { field: 'sangrias', cols: ['hora','retirada','gerente'], enterNav: { hora:'retirada', retirada:'gerente', gerente:'_nextRow' } },
|
||
'tbl-despesas': { field: 'despesas', cols: ['desc','obs','valor'], enterNav: { desc:'obs', obs:'valor', valor:'_nextRow' } },
|
||
'tbl-pix': { field: 'pixcnpj', cols: ['nome','valor'], enterNav: { nome:'valor', valor:'_nextRow' } },
|
||
'tbl-vales': { field: 'vales', cols: ['nome','valor'], enterNav: { nome:'valor', valor:'_nextRow' } },
|
||
'tbl-receber': { field: 'receber', cols: ['nome','valor'], enterNav: { nome:'valor', valor:'_nextRow' } },
|
||
'tbl-cancelamentos': { field: 'cancelamentos', cols: ['numero','valor','motivo'], enterNav: { numero:'valor', valor:'motivo', motivo:'_nextRow' } },
|
||
'tbl-cartao-credito': { field: 'cartoes.credito', cols: ['valor'], enterNav: { valor:'_nextRow' } },
|
||
'tbl-cartao-debito': { field: 'cartoes.debito', cols: ['valor'], enterNav: { valor:'_nextRow' } },
|
||
'tbl-cartao-alimentacao': { field: 'cartoes.alimentacao', cols: ['valor'], enterNav: { valor:'_nextRow' } },
|
||
'tbl-cartao-pix': { field: 'cartoes.pix', cols: ['valor'], enterNav: { valor:'_nextRow' } },
|
||
};
|
||
|
||
function getField(tblId) {
|
||
const parts = TABLES[tblId].field.split('.');
|
||
let cur = estado;
|
||
for (const p of parts) cur = cur[p];
|
||
return cur;
|
||
}
|
||
|
||
function makeRow(tblId, rowIndex, rowData) {
|
||
const cfg = TABLES[tblId];
|
||
const tr = document.createElement('tr');
|
||
cfg.cols.forEach(col => {
|
||
const td = document.createElement('td');
|
||
if (col === 'valor' || col === 'retirada') td.className = 'num';
|
||
const inp = document.createElement('input');
|
||
inp.type = 'text';
|
||
// For numeric cols that hold money, format with 2 decimals
|
||
// For text cols (hora, desc, obs, nome, gerente, motivo, numero) keep as-is
|
||
const numericCols = ['valor', 'retirada'];
|
||
inp.value = rowData[col] !== undefined
|
||
? (numericCols.includes(col) ? (rowData[col] === 0 ? '' : fmtNum(rowData[col])) : (rowData[col] || ''))
|
||
: '';
|
||
inp.placeholder = col === 'valor' || col === 'retirada' ? '0,00' : (col === 'hora' ? 'HH:MM' : '');
|
||
inp.dataset.tbl = tblId;
|
||
inp.dataset.row = rowIndex;
|
||
inp.dataset.col = col;
|
||
// datalist only for fields that should offer suggestions from config
|
||
if (col === 'gerente') inp.setAttribute('list', 'dl-gerente');
|
||
if (col === 'desc') inp.setAttribute('list', 'dl-despesa');
|
||
if (col === 'nome' && tblId === 'tbl-vales') inp.setAttribute('list', 'dl-vales');
|
||
// tbl-pix nome = free text (no datalist)
|
||
// tbl-receber nome = free text (no datalist)
|
||
// tbl-cancelamentos numero + motivo = free text (no datalist)
|
||
inp.addEventListener('input', onTableInput);
|
||
inp.addEventListener('blur', onCellBlur);
|
||
inp.addEventListener('keydown', onCellKeydown);
|
||
td.appendChild(inp);
|
||
tr.appendChild(td);
|
||
});
|
||
const delTd = document.createElement('td');
|
||
const delBtn = document.createElement('button');
|
||
delBtn.className = 'row-del';
|
||
delBtn.innerHTML = '✕';
|
||
delBtn.dataset.tbl = tblId;
|
||
delBtn.dataset.row = rowIndex;
|
||
delBtn.addEventListener('click', onDeleteRow);
|
||
delTd.appendChild(delBtn);
|
||
tr.appendChild(delTd);
|
||
return tr;
|
||
}
|
||
|
||
function renderTable(tblId) {
|
||
const cfg = TABLES[tblId];
|
||
const tbody = document.querySelector(`#${tblId} tbody`);
|
||
if (!tbody) return;
|
||
tbody.innerHTML = '';
|
||
getField(tblId).forEach((r, i) => { tbody.appendChild(makeRow(tblId, i, r)); });
|
||
}
|
||
|
||
function onTableInput(e) {
|
||
const { tbl, row, col } = e.target.dataset;
|
||
const field = getField(tbl);
|
||
if (!field || !field[row]) return;
|
||
if (col === 'valor' || col === 'retirada') {
|
||
field[row][col] = parseNum(e.target.value);
|
||
} else if (col === 'hora') {
|
||
const formatted = fmtTime(e.target.value);
|
||
if (formatted !== e.target.value) e.target.value = formatted;
|
||
field[row][col] = formatted;
|
||
} else {
|
||
// text fields: nome, desc, obs, gerente, motivo, numero — keep as string
|
||
field[row][col] = e.target.value;
|
||
}
|
||
updateTotals();
|
||
scheduleSave();
|
||
}
|
||
|
||
function onCellBlur(e) {
|
||
const { col } = e.target.dataset;
|
||
if (col === 'valor' || col === 'retirada') {
|
||
const v = parseNum(e.target.value);
|
||
e.target.value = isNaN(v) || v === 0 ? '' : fmtNum(v);
|
||
updateTotals();
|
||
}
|
||
// For text cols (hora, nome, desc, obs, gerente, motivo, numero) — keep as-is
|
||
}
|
||
|
||
function onCellKeydown(e) {
|
||
if (e.key !== 'Enter') return;
|
||
// 50ms delay so the browser finishes filling the datalist value
|
||
// before we read e.target.value and move focus
|
||
setTimeout(() => {
|
||
const { tbl, row, col } = e.target.dataset;
|
||
const cfg = TABLES[tbl];
|
||
if (!cfg) return;
|
||
const next = cfg.enterNav[col];
|
||
if (next === '_nextRow') {
|
||
const nextRowIndex = parseInt(row) + 1;
|
||
if (nextRowIndex < getField(tbl).length) {
|
||
focusInput(tbl, nextRowIndex, cfg.cols[0]);
|
||
} else {
|
||
addRowAndFocus(tbl, cfg.cols[0]);
|
||
}
|
||
} else if (next) {
|
||
focusInput(tbl, parseInt(row), next);
|
||
}
|
||
}, 50);
|
||
}
|
||
|
||
function focusInput(tblId, rowIndex, col) {
|
||
const sel = `#${tblId} tbody tr:nth-child(${rowIndex + 1}) input[data-col="${col}"]`;
|
||
const inp = document.querySelector(sel);
|
||
if (inp) { inp.focus(); if (col === 'valor' || col === 'retirada' || col === 'hora') inp.select(); }
|
||
}
|
||
|
||
function addRowAndFocus(tblId, focusCol) {
|
||
const cfg = TABLES[tblId];
|
||
const field = getField(tblId);
|
||
const numericCols = ['valor', 'retirada'];
|
||
const empty = {};
|
||
cfg.cols.forEach(c => { empty[c] = numericCols.includes(c) ? 0 : ''; });
|
||
field.push(empty);
|
||
renderTable(tblId);
|
||
focusInput(tblId, field.length - 1, focusCol);
|
||
}
|
||
|
||
function onDeleteRow(e) {
|
||
const { tbl, row } = e.target.dataset;
|
||
const field = getField(tbl);
|
||
if (field && field.length > 1) {
|
||
field.splice(parseInt(row), 1);
|
||
renderTable(tbl);
|
||
updateTotals();
|
||
scheduleSave();
|
||
} else {
|
||
const cfg = TABLES[tbl];
|
||
cfg.cols.forEach(c => { field[0][c] = c === 'valor' || c === 'retirada' ? 0 : ''; });
|
||
renderTable(tbl);
|
||
updateTotals();
|
||
scheduleSave();
|
||
}
|
||
}
|
||
|
||
// Add buttons
|
||
document.querySelectorAll('.add-btn[data-tbl]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const tblId = btn.dataset.tbl;
|
||
const cfg = TABLES[tblId];
|
||
if (!cfg) return;
|
||
const field = getField(tblId);
|
||
const empty = {};
|
||
cfg.cols.forEach(c => { empty[c] = c === 'valor' ? 0 : ''; });
|
||
field.push(empty);
|
||
renderTable(tblId);
|
||
focusInput(tblId, field.length - 1, cfg.cols[0]);
|
||
});
|
||
});
|
||
|
||
// ─── Totals ────────────────────────────────────────────────────────────────
|
||
function updateTotals() {
|
||
// ── Sangrias ──
|
||
const sangriasTotal = estado.sangrias.reduce((s, r) => s + (parseNum(r['retirada'] || 0) || 0), 0);
|
||
setText('total-sangrias', fmt(sangriasTotal));
|
||
|
||
// ── Tabelas de valor ──
|
||
const map = {
|
||
'tbl-despesas':'despesas', 'tbl-pix':'pixcnpj', 'tbl-vales':'vales',
|
||
'tbl-receber':'receber', 'tbl-cancelamentos':'cancelamentos',
|
||
};
|
||
Object.entries(map).forEach(([tblId, fieldName]) => {
|
||
const total = estado[fieldName].reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
setText(tblId.replace('tbl-','total-'), fmt(total));
|
||
});
|
||
|
||
// ── Cartões ──
|
||
const cartCred = estado.cartoes.credito.reduce((s, r) => s + (r.valor || 0), 0);
|
||
const cartDeb = estado.cartoes.debito.reduce((s, r) => s + (r.valor || 0), 0);
|
||
const cartAli = estado.cartoes.alimentacao.reduce((s, r) => s + (r.valor || 0), 0);
|
||
const cartPix = estado.cartoes.pix.reduce((s, r) => s + (r.valor || 0), 0);
|
||
|
||
['credito','debito','alimentacao','pix'].forEach(c => {
|
||
const total = estado.cartoes[c].reduce((s, r) => s + (r.valor || 0), 0);
|
||
setText(`total-cartao-${c}`, fmt(total));
|
||
});
|
||
|
||
const somaCartoes = cartCred + cartDeb + cartAli + cartPix;
|
||
setText('soma-cartoes', fmt(somaCartoes));
|
||
|
||
// ── Saldo de Caixa — auto-populate readonly fields ──
|
||
// Crédito = total cartão crédito
|
||
document.getElementById('sys-credito').value = cartCred > 0 ? fmtNum(cartCred) : '';
|
||
// Débito = total cartão débito
|
||
document.getElementById('sys-debito').value = cartDeb > 0 ? fmtNum(cartDeb) : '';
|
||
// Alimentação = total cartão alimentação
|
||
document.getElementById('sys-alimentacao').value = cartAli > 0 ? fmtNum(cartAli) : '';
|
||
// PIX Cartão = total cartão PIX
|
||
document.getElementById('sys-pix').value = cartPix > 0 ? fmtNum(cartPix) : '';
|
||
// PIX CNPJ = total PIX CNPJ
|
||
const pixCnpjTotal = estado.pixcnpj.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
document.getElementById('sys-pixcnpj').value = pixCnpjTotal > 0 ? fmtNum(pixCnpjTotal) : '';
|
||
// Vales = total vales
|
||
const valesTotal = estado.vales.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
document.getElementById('sys-vales').value = valesTotal > 0 ? fmtNum(valesTotal) : '';
|
||
// À Receber = total receber
|
||
const receberTotal = estado.receber.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
document.getElementById('sys-receber').value = receberTotal > 0 ? fmtNum(receberTotal) : '';
|
||
|
||
// SYS total = troco + crédito + débito + alimentação + vales + receber + pixCartão + pixCnpj
|
||
const sysTotal = parseNum(val('sys-troco')) + cartCred + cartDeb + cartAli + valesTotal + receberTotal + cartPix + pixCnpjTotal;
|
||
setText('sys-total', fmt(sysTotal));
|
||
|
||
// ── Fechamento Contado — auto-populate (readonly, filled from tables) ──
|
||
// Dinheiro = total sangrias
|
||
document.getElementById('f-fech-dinheiro').value = sangriasTotal > 0 ? fmtNum(sangriasTotal) : '';
|
||
// Cartões = total cartões (crédito+débito+alimentação+pix)
|
||
document.getElementById('f-fech-cartoes').value = somaCartoes > 0 ? fmtNum(somaCartoes) : '';
|
||
// À Receber = total receber
|
||
document.getElementById('f-fech-receber').value = receberTotal > 0 ? fmtNum(receberTotal) : '';
|
||
|
||
// Fechamento total
|
||
const fd = sangriasTotal;
|
||
const fc = somaCartoes;
|
||
const fr = receberTotal;
|
||
setText('fech-total', fmt(fd + fc + fr));
|
||
|
||
// Diferença = Saldo Caixa − Saldo Esperado
|
||
const saldoCaixa = sysTotal;
|
||
const diff = saldoCaixa - parseNum(val('f-saldo-esperado'));
|
||
const difEl = document.getElementById('diferenca');
|
||
const badgeEl = document.getElementById('dif-badge');
|
||
if (difEl) {
|
||
difEl.textContent = fmt(diff);
|
||
if (diff === 0) {
|
||
difEl.className = 'dif-value zero';
|
||
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
|
||
} else if (diff < 0 && diff >= -5) {
|
||
difEl.className = 'dif-value neg'; // OK − até −5 de tolerância
|
||
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
|
||
} else if (diff > 0 && diff <= 3) {
|
||
difEl.className = 'dif-value zero'; // OK + até 3
|
||
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
|
||
} else {
|
||
// Sobra mais de R$3 ou falta mais de R$5
|
||
difEl.className = diff > 0 ? 'dif-value pos' : 'dif-value neg';
|
||
if (badgeEl) {
|
||
if (diff > 0) {
|
||
badgeEl.textContent = '⚠ SOBRANDO';
|
||
badgeEl.style.background='#fff3e0';
|
||
badgeEl.style.color='#e65100';
|
||
} else {
|
||
badgeEl.textContent = '⚠ FALTANDO';
|
||
badgeEl.style.background='#ffebee';
|
||
badgeEl.style.color='#c0272d';
|
||
}
|
||
badgeEl.style.display='inline';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── Numeric input: auto ,00 on digit+blur pattern ───────────────────────
|
||
// For fields that use datalist (text input), we need to avoid triggering
|
||
// auto-formatter when the browser is about to fill from datalist.
|
||
// Strategy: detect "comma-only" input and convert to ",00" immediately.
|
||
// For normal digit input: add ,00 on blur (not on every digit).
|
||
|
||
// ─── Numeric input: auto ,00 on digit+blur pattern ───────────────────────
|
||
// These fields are plain text inputs (no datalist). Format on blur only.
|
||
// For sys-troco which is user-editable: also format on Enter keydown.
|
||
|
||
['troco','credito','debito','alimentacao','vales','receber','pix'].forEach(k => {
|
||
const el = document.getElementById(`sys-${k}`);
|
||
if (!el) return;
|
||
|
||
// On input: just track for autosave
|
||
el.addEventListener('input', () => {
|
||
updateTotals();
|
||
scheduleSave();
|
||
});
|
||
|
||
// On keydown Enter: format value directly (Enter does not trigger blur for tabindex=-1)
|
||
el.addEventListener('keydown', e => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
const raw = el.value.trim();
|
||
if (!raw) { el.value = ''; return; }
|
||
const num = parseNum(raw);
|
||
if (isNaN(num) || num === 0) { el.value = ''; }
|
||
else { el.value = fmtNum(num); }
|
||
updateTotals();
|
||
scheduleSave();
|
||
}
|
||
});
|
||
|
||
// On blur: format the value
|
||
el.addEventListener('blur', () => {
|
||
const raw = el.value.trim();
|
||
if (!raw) { el.value = ''; return; }
|
||
const num = parseNum(raw);
|
||
if (isNaN(num) || num === 0) { el.value = ''; }
|
||
else { el.value = fmtNum(num); }
|
||
updateTotals();
|
||
scheduleSave();
|
||
});
|
||
});
|
||
|
||
['f-fech-dinheiro','f-fech-cartoes','f-fech-receber','f-saldo-esperado'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
|
||
el.addEventListener('input', () => {
|
||
const v = el.value;
|
||
if (v.endsWith(',') && !v.includes(',') && v !== ',') {
|
||
el.value = v + '00';
|
||
const pos = v.length;
|
||
setTimeout(() => { el.selectionStart = el.selectionEnd = pos; }, 0);
|
||
}
|
||
updateTotals();
|
||
scheduleSave();
|
||
});
|
||
|
||
el.addEventListener('blur', () => {
|
||
const raw = el.value.trim();
|
||
if (!raw) { el.value = ''; return; }
|
||
const num = parseNum(raw);
|
||
if (isNaN(num) || num === 0) { el.value = ''; }
|
||
else { el.value = fmtNum(num); }
|
||
updateTotals();
|
||
scheduleSave();
|
||
});
|
||
});
|
||
|
||
// ─── Metadata ──────────────────────────────────────────────────────────────
|
||
['f-data','f-operador','f-turno','f-loja'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
el.addEventListener('change', () => {
|
||
estado.data = val('f-data');
|
||
estado.operador = val('f-operador');
|
||
estado.turno = val('f-turno');
|
||
estado.loja = val('f-loja');
|
||
syncHeader();
|
||
scheduleSave();
|
||
});
|
||
});
|
||
|
||
function syncHeader() {
|
||
setText('fechamento-id', `${estado.loja}_${estado.data}_op`);
|
||
}
|
||
|
||
// ─── Datalists ─────────────────────────────────────────────────────────────
|
||
function buildDatalists() {
|
||
// Auto-build vales_nomes from current entries (autofill dinâmico)
|
||
const valesUsed = [...new Set(estado.vales.map(v => v.nome).filter(Boolean))];
|
||
config.vales_nomes = [...new Set([...config.vales_nomes, ...valesUsed])]
|
||
.sort((a, b) => a.localeCompare(b, 'pt-BR'));
|
||
|
||
// Gerentes e Operadores também acumulam dos lançamentos
|
||
const gerentesUsed = [...new Set(estado.sangrias.map(r => r.gerente).filter(Boolean))];
|
||
config.gerentes = [...new Set([...config.gerentes, ...gerentesUsed])]
|
||
.sort((a, b) => a.localeCompare(b, 'pt-BR'));
|
||
|
||
const lists = {
|
||
'dl-operadores': [...new Set([...config.operadores])].sort((a,b) => a.localeCompare(b, 'pt-BR')),
|
||
'dl-gerente': [...new Set([...config.gerentes])].sort((a,b) => a.localeCompare(b, 'pt-BR')),
|
||
'dl-despesa': [...new Set([...config.despesas])].sort((a,b) => a.localeCompare(b, 'pt-BR')),
|
||
// Clientes: escrita livre — sem datalist fixo
|
||
'dl-vales': [...new Set([...config.vales_nomes])].sort((a,b) => a.localeCompare(b, 'pt-BR')),
|
||
};
|
||
Object.entries(lists).forEach(([id, items]) => {
|
||
let dl = document.getElementById(id);
|
||
if (!dl) { dl = document.createElement('datalist'); dl.id = id; document.body.appendChild(dl); }
|
||
dl.innerHTML = items.map(v => `<option value="${v}">`).join('');
|
||
});
|
||
// Clientes: sem datalist (escrita livre)
|
||
const clienteDl = document.getElementById('dl-cliente');
|
||
if (clienteDl) clienteDl.remove();
|
||
}
|
||
|
||
// ─── Init ─────────────────────────────────────────────────────────────────
|
||
const PRE_FILL_TABLES = Object.keys(TABLES);
|
||
|
||
function ensureOneRow(tblId) {
|
||
const cfg = TABLES[tblId];
|
||
if (!cfg) return;
|
||
const field = getField(tblId);
|
||
if (field && field.length === 0) {
|
||
const empty = {};
|
||
cfg.cols.forEach(c => { empty[c] = c === 'valor' || c === 'retirada' ? 0 : ''; });
|
||
field.push(empty);
|
||
}
|
||
}
|
||
|
||
function init() {
|
||
const today = new Date().toISOString().split('T')[0];
|
||
setVal('f-data', today);
|
||
estado.data = today;
|
||
setVal('f-loja', estado.loja || 'União');
|
||
setVal('f-operador', estado.operador || '');
|
||
setVal('f-turno', estado.turno || '');
|
||
syncHeader();
|
||
PRE_FILL_TABLES.forEach(ensureOneRow);
|
||
Object.keys(TABLES).forEach(renderTable);
|
||
updateTotals();
|
||
}
|
||
|
||
// ─── LocalStorage ──────────────────────────────────────────────────────────
|
||
const LS_KEY = 'fc_v7';
|
||
const CFG_KEY = 'fc_cfg';
|
||
|
||
function buildEstadoForBackend() {
|
||
// Monta o objeto exactly as o backend espera (storage.rs → salvar_fechamento)
|
||
return {
|
||
uuid: estado.uuid,
|
||
id_local: estado.id_local,
|
||
loja: estado.loja,
|
||
data: estado.data,
|
||
operador: estado.operador,
|
||
turno: estado.turno,
|
||
saldo_troco: parseNum(val('sys-troco')), // troco do sistema
|
||
saldo_esperado: parseNum(val('f-saldo-esperado')),
|
||
fechamento: parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')),
|
||
despesas: estado.despesas.map(r => ({ desc: r.desc || '', obs: r.obs || '', valor: r.valor || 0 })),
|
||
sangrias: estado.sangrias.map(r => ({ hora: r.hora || '', retirada: r.retirada || 0, gerente: r.gerente || '' })),
|
||
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome || '', valor: r.valor || 0 })),
|
||
vales: estado.vales.map(r => ({ nome: r.nome || '', obs: r.obs || '', valor: r.valor || 0 })),
|
||
receber: estado.receber.map(r => ({ nome: r.nome || '', valor: r.valor || 0 })),
|
||
cancelamentos: estado.cancelamentos.map(r => ({ numero: r.numero || '', valor: r.valor || 0, motivo: r.motivo || '' })),
|
||
cartoes: {
|
||
credito: estado.cartoes.credito.map(r => r.valor || 0),
|
||
debito: estado.cartoes.debito.map(r => r.valor || 0),
|
||
alimentacao: estado.cartoes.alimentacao.map(r => r.valor || 0),
|
||
pix: estado.cartoes.pix.map(r => r.valor || 0),
|
||
},
|
||
observacoes: estado.observacoes,
|
||
sync_status: estado.sync_status,
|
||
};
|
||
}
|
||
|
||
function saveState() {
|
||
// Sync reactive fields from DOM before saving
|
||
estado.operador = val('f-operador');
|
||
estado.turno = val('f-turno');
|
||
estado.loja = val('f-loja');
|
||
estado.saldo_troco = parseNum(val('sys-troco'));
|
||
estado.saldo_esperado = parseNum(val('f-saldo-esperado'));
|
||
estado.fechamento_contado = {
|
||
dinheiro: parseNum(val('f-fech-dinheiro')),
|
||
cartoes: parseNum(val('f-fech-cartoes')),
|
||
receber: parseNum(val('f-fech-receber')),
|
||
};
|
||
estado.updatedAt = new Date().toISOString();
|
||
try { localStorage.setItem(LS_KEY, JSON.stringify(estado)); } catch(e) {}
|
||
}
|
||
function loadState() {
|
||
try {
|
||
const raw = localStorage.getItem(LS_KEY);
|
||
if (!raw) return false;
|
||
const saved = JSON.parse(raw);
|
||
// Restore simple fields
|
||
estado.uuid = saved.uuid || estado.uuid;
|
||
estado.id_local = saved.id_local !== undefined ? saved.id_local : null;
|
||
estado.loja = saved.loja || estado.loja;
|
||
estado.data = saved.data || estado.data;
|
||
estado.operador = saved.operador || estado.operador;
|
||
estado.turno = saved.turno || estado.turno;
|
||
estado.saldo_troco = saved.saldo_troco || 0;
|
||
estado.saldo_esperado = saved.saldo_esperado || 0;
|
||
estado.fechamento = saved.fechamento || 0;
|
||
estado.observacoes = saved.observacoes || '';
|
||
estado.sync_status = saved.sync_status || 'local';
|
||
// Restore tables (always as arrays of objects)
|
||
estado.despesas = Array.isArray(saved.despesas) ? saved.despesas : [];
|
||
estado.sangrias = Array.isArray(saved.sangrias) ? saved.sangrias : [];
|
||
estado.pixcnpj = Array.isArray(saved.pixcnpj) ? saved.pixcnpj : [];
|
||
estado.vales = Array.isArray(saved.vales) ? saved.vales : [];
|
||
estado.receber = Array.isArray(saved.receber) ? saved.receber : [];
|
||
estado.cancelamentos = Array.isArray(saved.cancelamentos) ? saved.cancelamentos : [];
|
||
// Restore cartoes — each entry must be { valor: number }
|
||
if (saved.cartoes) {
|
||
estado.cartoes.credito = Array.isArray(saved.cartoes.credito)
|
||
? saved.cartoes.credito.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
|
||
: [];
|
||
estado.cartoes.debito = Array.isArray(saved.cartoes.debito)
|
||
? saved.cartoes.debito.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
|
||
: [];
|
||
estado.cartoes.alimentacao = Array.isArray(saved.cartoes.alimentacao)
|
||
? saved.cartoes.alimentacao.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
|
||
: [];
|
||
estado.cartoes.pix = Array.isArray(saved.cartoes.pix)
|
||
? saved.cartoes.pix.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
|
||
: [];
|
||
}
|
||
// Restore fechamento_contado
|
||
if (saved.fechamento_contado) {
|
||
estado.fechamento_contado = saved.fechamento_contado;
|
||
}
|
||
setVal('f-data', estado.data || '');
|
||
setVal('f-operador', estado.operador || '');
|
||
setVal('f-turno', estado.turno || '');
|
||
setVal('f-loja', estado.loja || 'União');
|
||
setVal('f-obs', estado.observacoes || '');
|
||
setVal('f-saldo-esperado', estado.saldo_esperado === 0 ? '' : fmtNum(estado.saldo_esperado));
|
||
setVal('sys-troco', estado.saldo_troco === 0 ? '' : fmtNum(estado.saldo_troco));
|
||
const fc = estado.fechamento_contado || {};
|
||
setVal('f-fech-dinheiro', fc.dinheiro ? fmtNum(fc.dinheiro) : '');
|
||
setVal('f-fech-cartoes', fc.cartoes ? fmtNum(fc.cartoes) : '');
|
||
setVal('f-fech-receber', fc.receber ? fmtNum(fc.receber) : '');
|
||
syncHeader();
|
||
PRE_FILL_TABLES.forEach(ensureOneRow);
|
||
Object.keys(TABLES).forEach(renderTable);
|
||
updateTotals();
|
||
return true;
|
||
} catch(e) { return false; }
|
||
}
|
||
function loadConfig() {
|
||
try {
|
||
const raw = localStorage.getItem(CFG_KEY);
|
||
if (raw) config = { ...config, ...JSON.parse(raw) };
|
||
} catch(e) {}
|
||
}
|
||
function saveConfig() {
|
||
try { localStorage.setItem(CFG_KEY, JSON.stringify(config)); } catch(e) {}
|
||
buildDatalists();
|
||
}
|
||
|
||
// ─── Config Modal ──────────────────────────────────────────────────────────
|
||
function openConfig() {
|
||
document.getElementById('modal-config').classList.add('open');
|
||
renderCfgList('operadores', 'cfg-operadores-list');
|
||
renderCfgList('gerentes', 'cfg-gerentes-list');
|
||
renderCfgList('despesas', 'cfg-despesas-list');
|
||
renderCfgList('clientes', 'cfg-clientes-list');
|
||
renderCfgList('vales_nomes', 'cfg-vales-list');
|
||
}
|
||
function closeConfig() { document.getElementById('modal-config').classList.remove('open'); }
|
||
function renderCfgList(key, containerId) {
|
||
const container = document.getElementById(containerId);
|
||
container.innerHTML = '';
|
||
config[key].forEach((item, i) => {
|
||
const div = document.createElement('div');
|
||
div.className = 'cfg-item';
|
||
div.innerHTML = `<input type="text" value="${item}" data-key="${key}" data-index="${i}" /><button class="btn-rm" data-key="${key}" data-index="${i}">✕</button>`;
|
||
container.appendChild(div);
|
||
});
|
||
container.querySelectorAll('.btn-rm').forEach(btn => {
|
||
btn.onclick = () => { const { key: k, index: idx } = btn.dataset; config[k].splice(parseInt(idx), 1); renderCfgList(k, containerId); };
|
||
});
|
||
container.querySelectorAll('input').forEach(inp => {
|
||
inp.addEventListener('change', () => { const { key: k, index: idx } = inp.dataset; config[k][parseInt(idx)] = inp.value; });
|
||
});
|
||
}
|
||
|
||
document.getElementById('btn-config').addEventListener('click', openConfig);
|
||
document.getElementById('modal-close').addEventListener('click', closeConfig);
|
||
document.getElementById('modal-config').addEventListener('click', e => { if (e.target.id === 'modal-config') closeConfig(); });
|
||
|
||
['operador','gerente','despesa','cliente','vales'].forEach((_, i) => {
|
||
const btnMap = ['cfg-add-operador','cfg-add-gerente','cfg-add-despesa','cfg-add-cliente','cfg-add-vales'];
|
||
const keyMap = ['operadores','gerentes','despesas','clientes','vales_nomes'];
|
||
const listMap = ['cfg-operadores-list','cfg-gerentes-list','cfg-despesas-list','cfg-clientes-list','cfg-vales-list'];
|
||
document.getElementById(btnMap[i]).addEventListener('click', () => {
|
||
const k = keyMap[i]; config[k].push(''); renderCfgList(k, listMap[i]);
|
||
const inputs = document.getElementById(listMap[i]).querySelectorAll('input');
|
||
inputs[inputs.length - 1].focus();
|
||
});
|
||
});
|
||
|
||
document.getElementById('cfg-salvar').addEventListener('click', () => { saveConfig(); closeConfig(); scheduleSave(); });
|
||
|
||
// ─── Tauri Commands ───────────────────────────────────────────────────────
|
||
let _saveTimer = null;
|
||
function scheduleSave() {
|
||
clearTimeout(_saveTimer);
|
||
_saveTimer = setTimeout(() => { saveState(); if (window.__TAURI__) tauriSaveLocal(); }, 1000);
|
||
}
|
||
|
||
async function tauriSaveLocal() {
|
||
try {
|
||
const data = buildEstadoForBackend();
|
||
await window.__TAURI__.core.invoke('salvar_fechamento', { estado: data });
|
||
} catch(e) { console.warn('salvar_fechamento:', e); }
|
||
}
|
||
|
||
async function tauriEnviar() {
|
||
const data = buildEstadoForBackend();
|
||
// 1. Salva local
|
||
await window.__TAURI__.core.invoke('salvar_fechamento', { estado: data }).catch(e => console.warn('local save err:', e));
|
||
// 2. Envia Supabase — debug completo
|
||
console.log('=== Enviando para Supabase ===');
|
||
console.log('URL:', 'https://supabase.ofrangao.com.br/rest/v1/fechamentos');
|
||
console.log('Payload:', JSON.stringify(data, null, 2));
|
||
try {
|
||
const result = await window.__TAURI__.core.invoke('sb_salvar_fechamento', { estado: data });
|
||
console.log('Supabase resposta:', JSON.stringify(result));
|
||
return result;
|
||
} catch(e) {
|
||
console.error('Erro sb_salvar_fechamento:', JSON.stringify(e));
|
||
// Tenta debug: qual era o payload exato?
|
||
console.error('Payload que falhou:', JSON.stringify(data));
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
function buildConfirmHTML(data) {
|
||
// 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('.', ',');
|
||
const diferenca = data.diferenca !== undefined ? data.diferenca
|
||
: ((data.sys_total || 0) - (data.saldo_esperado || 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>`;
|
||
|
||
return `<!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: 13px; width: 80mm; margin: 0 auto; padding: 8px; }
|
||
.header { text-align: center; margin-bottom: 8px; }
|
||
.header h1 { font-size: 18px; }
|
||
.header h2 { font-size: 14px; font-weight: normal; }
|
||
hr { border: none; border-top: 1px dashed #000; margin: 5px 0; }
|
||
.section-title { font-weight: bold; font-size: 13px; border-top: 1px dashed #000; padding-top: 3px; margin-top: 6px; }
|
||
.row { display: flex; justify-content: space-between; padding: 1px 0; font-size: 12px; }
|
||
.row.total-row { font-weight: bold; }
|
||
.cart-table { width: 100%; border-collapse: collapse; font-size: 11px; margin-top: 4px; }
|
||
.cart-table th { font-weight: bold; border-bottom: 1px solid #000; padding: 2px; }
|
||
.cart-table td { padding: 2px 4px; text-align: right; }
|
||
.cart-table td:first-child { text-align: left; }
|
||
.cart-table tr.total-row { font-weight: bold; border-top: 1px dashed #000; }
|
||
.subtotal { font-size: 12px; margin-top: 4px; }
|
||
.diff-ok { color: #2e7d32; }
|
||
.diff-bad { color: #c0272d; }
|
||
.obs { font-size: 11px; color: #555; margin-top: 6px; white-space: pre-wrap; }
|
||
.footer { text-align: center; font-size: 11px; color: #555; margin-top: 8px; }
|
||
@media print { body { margin: 0; } }
|
||
</style>
|
||
</head><body>
|
||
<div class="header">
|
||
<h1>${data.loja}</h1>
|
||
<h2>Fechamento de Caixa</h2>
|
||
</div>
|
||
<hr>
|
||
<div class="row"><span>Data:</span><span>${data.data}</span></div>
|
||
<div class="row"><span>Operador:</span><span>${data.operador}</span></div>
|
||
<div class="row"><span>Turno:</span><span>${data.turno}</span></div>
|
||
|
||
${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 ? `Nº ${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>
|
||
|
||
${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('.', ',');
|
||
const diferenca = data.diferenca !== undefined ? data.diferenca
|
||
: ((data.sys_total || 0) - (data.saldo_esperado || 0));
|
||
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 ? `Nº ${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: 13px; width: 80mm; margin: 0 auto; padding: 8px; }
|
||
.header { text-align: center; margin-bottom: 8px; }
|
||
.header h1 { font-size: 18px; }
|
||
.header h2 { font-size: 14px; font-weight: normal; }
|
||
hr { border: none; border-top: 1px dashed #000; margin: 5px 0; }
|
||
.section-title { font-weight: bold; font-size: 13px; border-top: 1px dashed #000; padding-top: 3px; margin-top: 6px; }
|
||
.row { display: flex; justify-content: space-between; padding: 1px 0; font-size: 12px; }
|
||
.row.total-row { font-weight: bold; }
|
||
.cart-table { width: 100%; border-collapse: collapse; font-size: 11px; margin-top: 4px; }
|
||
.cart-table th { font-weight: bold; border-bottom: 1px solid #000; padding: 2px; }
|
||
.cart-table td { padding: 2px 4px; text-align: right; }
|
||
.cart-table td:first-child { text-align: left; }
|
||
.cart-table tr.total-row { font-weight: bold; border-top: 1px dashed #000; }
|
||
.subtotal { font-size: 12px; margin-top: 4px; }
|
||
.subtotal .row { padding: 1px 0; }
|
||
.diff-ok { color: #2e7d32; }
|
||
.diff-bad { color: #c0272d; }
|
||
.obs { font-size: 11px; color: #555; margin-top: 6px; white-space: pre-wrap; }
|
||
.footer { text-align: center; font-size: 11px; color: #555; margin-top: 8px; }
|
||
@media print { body { margin: 0; } }
|
||
</style>
|
||
</head><body>
|
||
<div class="header">
|
||
<h1>${data.loja}</h1>
|
||
<h2>Fechamento de Caixa</h2>
|
||
</div>
|
||
<hr>
|
||
<div class="row"><span>Data:</span><span>${data.data}</span></div>
|
||
<div class="row"><span>Operador:</span><span>${data.operador}</span></div>
|
||
<div class="row"><span>Turno:</span><span>${data.turno}</span></div>
|
||
|
||
${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>
|
||
|
||
${data.observacoes ? `<div class="obs">${data.observacoes}</div>` : ''}
|
||
|
||
<div class="footer">Gerado em ${new Date().toLocaleString('pt-BR')}</div>
|
||
</body></html>`;
|
||
|
||
return html;
|
||
}
|
||
|
||
function openPrintPreview(data, mode) {
|
||
const html = mode === 'confirm' ? buildConfirmHTML(data) : buildPrintHTML(data);
|
||
// Remove preview anterior se existir
|
||
const existing = document.getElementById('print-preview-overlay');
|
||
if (existing) document.body.removeChild(existing);
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.id = 'print-preview-overlay';
|
||
overlay.style.cssText = `
|
||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||
background: rgba(0,0,0,0.85); z-index: 99999;
|
||
display: flex; flex-direction: column; align-items: center;
|
||
font-family: 'Courier New', monospace;
|
||
`;
|
||
|
||
const previewStyle = `
|
||
<style>
|
||
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 13px; background: #fff; padding: 10px; }
|
||
.header { text-align: center; margin-bottom: 8px; }
|
||
.header h1 { font-size: 18px; }
|
||
.header h2 { font-size: 14px; font-weight: normal; }
|
||
hr { border: none; border-top: 1px dashed #000; margin: 6px 0; }
|
||
.section-title { font-weight: bold; font-size: 14px; border-top: 1px dashed #000; padding-top: 4px; margin-top: 6px; }
|
||
.row { display: flex; justify-content: space-between; padding: 2px 0; }
|
||
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||
td { padding: 2px 4px; vertical-align: top; }
|
||
td:nth-child(2) { text-align: right; }
|
||
td:nth-child(3) { text-align: right; color: #555; font-size: 11px; }
|
||
.grand-total { font-size: 16px; font-weight: bold; text-align: center; padding: 6px; border: 2px solid #000; margin: 8px 0; }
|
||
.obs { font-size: 11px; color: #555; margin-top: 6px; white-space: pre-wrap; }
|
||
.footer { text-align: center; font-size: 11px; color: #555; margin-top: 8px; }
|
||
@media print { body { background: #fff; } }
|
||
</style>
|
||
`;
|
||
|
||
const fullHtml = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Recibo</title>${previewStyle}</head><body>${html}</body></html>`;
|
||
|
||
const iframe = document.createElement('iframe');
|
||
iframe.style.cssText = 'width:360px; height:600px; border:2px solid #555; margin-top:16px; background:#fff;';
|
||
iframe.id = 'print-iframe';
|
||
|
||
const btnFechar = document.createElement('button');
|
||
btnFechar.textContent = '✕ Fechar';
|
||
btnFechar.style.cssText = `
|
||
margin-top: 12px; padding: 10px 32px;
|
||
background: #444; color: #fff; border: none; border-radius: 6px;
|
||
font-size: 15px; cursor: pointer;
|
||
`;
|
||
btnFechar.onclick = () => {
|
||
document.body.removeChild(overlay);
|
||
};
|
||
|
||
const btnImprimir = document.createElement('button');
|
||
btnImprimir.textContent = '🖨️ Imprimir';
|
||
btnImprimir.style.cssText = `
|
||
margin-top: 8px; padding: 10px 32px;
|
||
background: #e65100; color: #fff; border: none; border-radius: 6px;
|
||
font-size: 15px; cursor: pointer;
|
||
`;
|
||
btnImprimir.onclick = () => {
|
||
iframe.contentWindow.focus();
|
||
iframe.contentWindow.print();
|
||
};
|
||
|
||
overlay.appendChild(iframe);
|
||
overlay.appendChild(btnImprimir);
|
||
overlay.appendChild(btnFechar);
|
||
document.body.appendChild(overlay);
|
||
|
||
const iframeDoc = iframe.contentWindow.document;
|
||
iframeDoc.open();
|
||
iframeDoc.write(fullHtml);
|
||
iframeDoc.close();
|
||
}
|
||
|
||
function triggerWindowPrint() {
|
||
// Chama window.print() na janela pai (o próprio app)
|
||
window.print();
|
||
}
|
||
|
||
// ─── Recibo 80mm via Tauri (ESC/POS USB) ─────────────────────────────────
|
||
// ─── Recibo 80mm via Tauri (ESC/POS USB) ─────────────────────────────────
|
||
// Build data object shared by both ESC/POS and preview
|
||
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'));
|
||
// SYS total = troco + crédito + débito + alimentação + vales + receber + pixCartão + pixCnpj
|
||
const sysTotal = parseNum(val('sys-troco')) + cartCred + cartDeb + cartAli + valesTotal + receberTotal + cartPix + pixCnpjTotal;
|
||
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: saldoEsp,
|
||
fechamento: fechamentoCounted,
|
||
diferenca: sysTotal - saldoEsp,
|
||
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
|
||
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),
|
||
observacoes: estado.observacoes,
|
||
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')) || 0) + cartCred + cartDeb + cartAli + cartPix + vales + receber + pixcnpj;
|
||
const diferenca = sysTotal - saldoEsp;
|
||
const diffClass = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
|
||
document.getElementById('confirm-summary').innerHTML =
|
||
`<div style="margin-bottom:6px"><strong>📅 Data:</strong> ${estado.data} <strong>👤 Operador:</strong> ${operador} <strong>⏰ Turno:</strong> ${turno} <strong>🏪 Loja:</strong> ${loja}</div>
|
||
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
|
||
<div>💸 Sangrias: <strong>${fmt(sangrias)}</strong></div>
|
||
<div>📝 Despesas: <strong>${fmt(despesas)}</strong></div>
|
||
<div>💰 Vales: <strong>${fmt(vales)}</strong></div>
|
||
<div>👥 À Receber: <strong>${fmt(receber)}</strong></div>
|
||
<div>🔵 PIX CNPJ: <strong>${fmt(pixcnpj)}</strong></div>
|
||
<div>💳 Crédito: <strong>${fmt(cartCred)}</strong></div>
|
||
<div>💳 Débito: <strong>${fmt(cartDeb)}</strong></div>
|
||
<div>💳 Alimentação: <strong>${fmt(cartAli)}</strong></div>
|
||
<div>❌ Cancelamentos: <strong>${fmt(cancelamentos)}</strong></div>
|
||
<div>📋 Fechamento (contado): <strong>${fmt(fechamento)}</strong></div>
|
||
<div>🖥️ Total Sistema: <strong>${fmt(sysTotal)}</strong></div>
|
||
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
|
||
<div>🎯 Saldo Esperado (contado): <strong>${fmt(saldoEsp)}</strong></div>
|
||
<div style="margin-top:4px">📐 Diferença: <strong style="color:${diffClass}">${fmt(diferenca)}</strong></div>
|
||
<div style="margin-top:4px;font-size:10px;color:#888">ID: ${estado.loja}_${estado.data}_${estado.operador}</div>`;
|
||
console.log('[DEBUG] openConfirmModal: tentando abrir modal');
|
||
document.getElementById('modal-confirm').classList.add('open');
|
||
}
|
||
|
||
function closeConfirmModal() {
|
||
document.getElementById('modal-confirm').classList.remove('open');
|
||
}
|
||
|
||
document.getElementById('confirm-close').addEventListener('click', closeConfirmModal);
|
||
document.getElementById('confirm-cancel').addEventListener('click', closeConfirmModal);
|
||
document.getElementById('modal-confirm').addEventListener('click', e => { if (e.target.id === 'modal-confirm') closeConfirmModal(); });
|
||
|
||
// Print from confirm modal — abre preview idêntico ao modal de confirmação
|
||
document.getElementById('confirm-print').addEventListener('click', async () => {
|
||
// 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, 'confirm');
|
||
});
|
||
|
||
// Confirm send
|
||
document.getElementById('confirm-send').addEventListener('click', async () => {
|
||
closeConfirmModal();
|
||
const btn = document.getElementById('btn-enviar');
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Enviando...';
|
||
try {
|
||
const r = await tauriEnviar();
|
||
estado.sync_status = 'synced';
|
||
saveState();
|
||
alert('✅ Enviado com sucesso!\n\n📋 Tabela: fechamentos_web\n🌐 Servidor: supabase.ofrangao.com.br\n\nAbra o Supabase Studio para verificar.');
|
||
} catch(e) {
|
||
await tauriSaveLocal();
|
||
const msg = e?.message || e?.toString?.() || String(e);
|
||
alert(
|
||
'⚠️ Erro ao enviar para o Supabase.\n\n' +
|
||
'📋 Tabela: fechamentos_web\n' +
|
||
'🌐 Servidor: supabase.ofrangao.com.br\n\n' +
|
||
'O servidor pode estar offline. Dados salvos localmente.\n\n' +
|
||
'Erro: ' + msg
|
||
);
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.textContent = '💾 Enviar Fechamento';
|
||
}
|
||
});
|
||
|
||
// ─── Buttons ───────────────────────────────────────────────────────────────
|
||
document.getElementById('btn-enviar').addEventListener('click', () => {
|
||
console.log('[DEBUG] btn-enviar clicado');
|
||
const operador = document.getElementById('f-operador')?.value;
|
||
const turno = document.getElementById('f-turno')?.value;
|
||
const loja = document.getElementById('f-loja')?.value;
|
||
console.log('[DEBUG] operador:', operador, 'turno:', turno, 'loja:', loja);
|
||
openConfirmModal();
|
||
});
|
||
|
||
document.getElementById('btn-recibo').addEventListener('click', async () => {
|
||
const btn = document.getElementById('btn-recibo');
|
||
btn.disabled = true;
|
||
try {
|
||
// Tenta ESC/POS primeiro; se falhar, nada acontece visualmente
|
||
// — siempre abre preview depois para o usuário ver algo
|
||
await tauriRecibo();
|
||
} catch(e) {
|
||
console.warn('ESC/POS falhou:', e);
|
||
}
|
||
// Sempre abre preview depois da tentativa ESC/POS
|
||
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);
|
||
// Reseta estado e re-renderiza sem reload
|
||
estado = {
|
||
uuid: crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(),
|
||
loja: estado.loja, data: estado.data, operador: '', turno: '',
|
||
saldo_troco: 0, fechamento: 0, saldo_esperado: 0, diferenca: 0,
|
||
despesas: [], sangrias: [], pixcnpj: [], vales: [], receber: [], cancelamentos: [],
|
||
cartoes: { credito: [], debito: [], alimentacao: [], pix: [] },
|
||
fechamento_contado: { dinheiro: 0, cartoes: 0, receber: 0 },
|
||
observacoes: '', id_local: null, sync_status: 'local'
|
||
};
|
||
setVal('f-operador', ''); setVal('f-turno', '');
|
||
setVal('f-fech-dinheiro', ''); setVal('f-fech-cartoes', ''); setVal('f-fech-receber', '');
|
||
setVal('f-saldo-esperado', ''); setVal('f-obs', '');
|
||
setVal('sys-troco', '');
|
||
calcular();
|
||
renderAllTables();
|
||
syncUI();
|
||
showToast('Rascunho limpo.', 'info');
|
||
});
|
||
|
||
const btnAnterior = document.getElementById('btn-anterior');
|
||
if (btnAnterior) {
|
||
btnAnterior.addEventListener('click', async () => {
|
||
console.log('[DEBUG] btn-anterior clicado');
|
||
const loja = val('f-loja') || 'União';
|
||
console.log('[DEBUG] btn-anterior loja:', loja);
|
||
const hasTauri = !!(window.__TAURI__ && window.__TAURI__.core);
|
||
|
||
// Salva rascunho atual antes de tudo
|
||
if (hasTauri) {
|
||
try { await window.__TAURI__.core.invoke('salvar_rascunho_local', {}); } catch(e) { console.warn('salvar_rascunho_local:', e); }
|
||
}
|
||
|
||
// Calcula dia anterior
|
||
const prevDate = new Date(estado.data);
|
||
prevDate.setDate(prevDate.getDate() - 1);
|
||
const prevDateStr = prevDate.toISOString().split('T')[0];
|
||
|
||
// Confirmação explícita antes de trocar de dia
|
||
const confirmMsg = `ATENÇÃO: vai carregar o fechamento de ${prevDateStr}.\n\nO formulário atual será LIMPO caso não tenha enviado.\n\nContinuar?`;
|
||
if (!confirm(confirmMsg)) {
|
||
console.log('[DEBUG] btn-anterior cancelado pelo usuário');
|
||
return;
|
||
}
|
||
|
||
// Carrega lista de fechamentos recentes do Supabase com uuid
|
||
let recentList = [];
|
||
if (hasTauri) {
|
||
try {
|
||
recentList = await window.__TAURI__.core.invoke('sb_listar_recentes', { loja, limite: 20 });
|
||
console.log('[DEBUG] sb_listar_recentes retornou:', recentList?.length, 'itens');
|
||
} catch(e) {
|
||
console.warn('sb_listar_recentes falhou:', e);
|
||
}
|
||
} else {
|
||
console.warn('Tauri não disponível, pulando sb_listar_recentes');
|
||
}
|
||
|
||
// Extrai datas únicas ordenadas + mapeamento data→id
|
||
const dateIdMap = {};
|
||
const dateList = [];
|
||
if (recentList && recentList.length > 0) {
|
||
recentList.forEach(item => {
|
||
if (item.data && !dateIdMap[item.data]) {
|
||
dateIdMap[item.data] = item.id;
|
||
dateList.push(item.data);
|
||
}
|
||
});
|
||
dateList.sort().reverse();
|
||
}
|
||
console.log('[DEBUG] dateList:', dateList);
|
||
|
||
let selectedDate = null;
|
||
let selectedId = null;
|
||
if (dateList.length > 0) {
|
||
const msg = dateList.map((d, i) => `${i + 1}) ${d}`).join('\n');
|
||
const escolha = prompt(`Selecione o número do dia anterior:\n${msg}\n\n(Deixe em branco e clique Cancelar para ver só o dia anterior automático)`);
|
||
if (escolha !== null && escolha.trim() !== '') {
|
||
const idx = parseInt(escolha) - 1;
|
||
if (idx >= 0 && idx < dateList.length) {
|
||
selectedDate = dateList[idx];
|
||
selectedId = dateIdMap[selectedDate];
|
||
}
|
||
}
|
||
}
|
||
|
||
// Se não escolheu data, usa dia anterior automático (sem id — carrega por data)
|
||
if (!selectedDate) {
|
||
selectedDate = prevDateStr;
|
||
}
|
||
console.log('[DEBUG] selectedDate:', selectedDate, 'selectedId:', selectedId);
|
||
|
||
setVal('f-data', selectedDate);
|
||
estado.data = selectedDate;
|
||
estado.uuid = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
|
||
estado.id_local = null;
|
||
syncHeader();
|
||
|
||
// Tenta Supabase primeiro (com id se disponível), senão local
|
||
let rascunho = null;
|
||
if (hasTauri) {
|
||
if (selectedId) {
|
||
try {
|
||
rascunho = await window.__TAURI__.core.invoke('sb_buscar_por_id', { id: selectedId });
|
||
console.log('[DEBUG] sb_buscar_por_id retornou:', !!rascunho);
|
||
} catch(e) { console.warn('sb_buscar_por_id falhou:', e); }
|
||
}
|
||
if (!rascunho) {
|
||
try {
|
||
rascunho = await window.__TAURI__.core.invoke('sb_carregar_rascunho', { loja, data: selectedDate });
|
||
console.log('[DEBUG] sb_carregar_rascunho retornou:', !!rascunho);
|
||
} catch(e) { console.warn('sb_carregar_rascunho falhou:', e); }
|
||
}
|
||
if (!rascunho) {
|
||
try {
|
||
rascunho = await window.__TAURI__.core.invoke('carregar_rascunho', { loja, data: selectedDate });
|
||
console.log('[DEBUG] carregar_rascunho local retornou:', !!rascunho);
|
||
} catch(e) { console.warn('carregar_rascunho local falhou:', e); }
|
||
}
|
||
} else {
|
||
console.warn('Tauri não disponível, não foi possível carregar rascunho');
|
||
}
|
||
|
||
if (rascunho) {
|
||
console.log('[DEBUG] rascunho carregado com sucesso');
|
||
if (rascunho.operador) { setVal('f-operador', rascunho.operador); estado.operador = rascunho.operador; }
|
||
if (rascunho.turno) { setVal('f-turno', rascunho.turno); estado.turno = rascunho.turno; }
|
||
if (rascunho.saldo_troco) setVal('sys-troco', fmtNum(rascunho.saldo_troco));
|
||
if (rascunho.saldo_esperado) setVal('f-saldo-esperado', fmtNum(rascunho.saldo_esperado));
|
||
if (rascunho.observacoes) { setVal('f-obs', rascunho.observacoes); estado.observacoes = rascunho.observacoes; }
|
||
if (rascunho.despesas && Array.isArray(rascunho.despesas)) {
|
||
estado.despesas = rascunho.despesas.map(d => ({ desc: d.desc || d.descricao || '', obs: d.obs || '', valor: parseNum(d.valor) || 0 }));
|
||
}
|
||
if (rascunho.sangrias && Array.isArray(rascunho.sangrias)) {
|
||
estado.sangrias = rascunho.sangrias.map(s => ({ hora: s.hora || '', retirada: parseNum(s.retirada) || 0, gerente: s.gerente || '' }));
|
||
}
|
||
if (rascunho.pixcnpj && Array.isArray(rascunho.pixcnpj)) {
|
||
estado.pixcnpj = rascunho.pixcnpj.map(p => ({ nome: p.nome || '', valor: parseNum(p.valor) || 0 }));
|
||
}
|
||
if (rascunho.vales && Array.isArray(rascunho.vales)) {
|
||
estado.vales = rascunho.vales.map(v => ({ nome: v.nome || '', obs: v.obs || '', valor: parseNum(v.valor) || 0 }));
|
||
}
|
||
if (rascunho.receber && Array.isArray(rascunho.receber)) {
|
||
estado.receber = rascunho.receber.map(r => ({ nome: r.nome || '', valor: parseNum(r.valor) || 0 }));
|
||
}
|
||
if (rascunho.cancelamentos && Array.isArray(rascunho.cancelamentos)) {
|
||
estado.cancelamentos = rascunho.cancelamentos.map(c => ({ numero: c.numero || '', valor: parseNum(c.valor) || 0, motivo: c.motivo || '' }));
|
||
}
|
||
if (rascunho.cartoes) {
|
||
const norm = (arr) => Array.isArray(arr) ? arr.map(v => ({ valor: (typeof v === 'object' && v !== null) ? (parseNum(v.valor) || 0) : (parseNum(v) || 0) })) : [];
|
||
estado.cartoes.credito = norm(rascunho.cartoes.credito);
|
||
estado.cartoes.debito = norm(rascunho.cartoes.debito);
|
||
estado.cartoes.alimentacao = norm(rascunho.cartoes.alimentacao);
|
||
estado.cartoes.pix = norm(rascunho.cartoes.pix);
|
||
}
|
||
} else {
|
||
console.log('[DEBUG] nenhum rascunho encontrado para esta data');
|
||
estado.despesas = []; estado.sangrias = []; estado.pixcnpj = [];
|
||
estado.vales = []; estado.receber = []; estado.cancelamentos = [];
|
||
estado.cartoes = { credito: [], debito: [], alimentacao: [], pix: [] };
|
||
setVal('sys-troco', ''); setVal('f-saldo-esperado', ''); setVal('f-obs', '');
|
||
}
|
||
|
||
estado.sync_status = rascunho ? 'loaded' : 'local';
|
||
|
||
PRE_FILL_TABLES.forEach(ensureOneRow);
|
||
Object.keys(TABLES).forEach(renderTable);
|
||
updateTotals();
|
||
scheduleSave();
|
||
console.log('[DEBUG] btn-anterior concluído');
|
||
});
|
||
} else {
|
||
console.error('[DEBUG] btn-anterior NÃO encontrado no DOM!');
|
||
}
|
||
|
||
document.getElementById('f-obs').addEventListener('input', e => { estado.observacoes = e.target.value; scheduleSave(); });
|
||
|
||
// ─── Boot ─────────────────────────────────────────────────────────────────
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadConfig();
|
||
buildDatalists();
|
||
if (!loadState()) init();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|