3678 lines
190 KiB
HTML
3678 lines
190 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: 18px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
||
.dif-value.pos, .dif-value.zero { color: var(--green); }
|
||
.dif-value.neg { color: var(--red); }
|
||
|
||
/* ── SALDO ESPERADO ─────────────────────────────────────────────────── */
|
||
.saldo-esperado-wrap { margin-top: 6px; }
|
||
.saldo-esperado-wrap label { font-size: 10px; text-transform: uppercase; color: var(--muted); font-weight: 700; letter-spacing: 0.4px; display: block; margin-bottom: 3px; }
|
||
.saldo-esperado-wrap input {
|
||
border: 1px solid var(--border); border-radius: var(--radius);
|
||
padding: 8px 10px; font-size: 16px; font-weight: 700; width: 100%;
|
||
font-family: var(--font); outline: none; text-align: right; color: var(--text);
|
||
}
|
||
.saldo-esperado-wrap input:focus { border-color: var(--orange); box-shadow: 0 0 0 2px var(--orange-pale); }
|
||
|
||
/* ── CARDS (4 separate blocks) ─────────────────────────────────────── */
|
||
.cards-section {
|
||
background: var(--surface); border-top: 1px solid var(--border);
|
||
padding: 10px 18px; flex-shrink: 0;
|
||
}
|
||
.cards-header {
|
||
font-size: 11px; font-weight: 700; color: var(--orange);
|
||
text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 8px;
|
||
padding-bottom: 4px; border-bottom: 2px solid var(--orange);
|
||
}
|
||
.cards-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
|
||
.card-block { border: 1px solid var(--border); border-radius: var(--radius); overflow: visible; }
|
||
.card-block-header {
|
||
background: var(--orange); color: #fff; text-align: center;
|
||
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
||
letter-spacing: 0.6px; padding: 6px 4px; border-radius: var(--radius) var(--radius) 0 0;
|
||
}
|
||
.card-block table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
.card-block td { padding: 3px 6px; border: 1px solid var(--border); vertical-align: middle; }
|
||
.card-block td input {
|
||
width: 100%; border: none; background: transparent;
|
||
font-size: 13px; padding: 4px 2px; outline: none; font-family: var(--font); color: var(--text);
|
||
}
|
||
.card-block td input:focus { background: #fffde7; border-radius: 3px; }
|
||
.card-block td.num { text-align: right; }
|
||
.card-block td.num input { text-align: right; }
|
||
.card-block tfoot td { background: var(--orange-pale); font-weight: 700; color: var(--orange); font-size: 13px; text-align: right; }
|
||
.card-block .add-btn { width: calc(100% - 12px); justify-content: center; margin: 5px 6px 6px; }
|
||
.soma-row {
|
||
display: flex; justify-content: flex-end; align-items: center;
|
||
gap: 12px; padding: 7px 10px; background: var(--orange-pale);
|
||
border-radius: var(--radius); font-size: 13px; font-weight: 700; color: var(--orange); margin-top: 6px;
|
||
}
|
||
.soma-row span:last-child { font-size: 16px; }
|
||
|
||
/* ── FOOTER ─────────────────────────────────────────────────────────── */
|
||
.footer {
|
||
background: var(--surface); border-top: 1px solid var(--border);
|
||
padding: 8px 18px; display: flex; align-items: center; gap: 10px;
|
||
flex-shrink: 0; flex-wrap: wrap;
|
||
}
|
||
.auto-save { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--muted); margin-right: auto; }
|
||
.dot { width: 8px; height: 8px; border-radius: 50%; background: #4caf50; }
|
||
.btn {
|
||
border: 1px solid var(--border); background: #fff; border-radius: var(--radius);
|
||
padding: 7px 14px; font-size: 12px; cursor: pointer; display: flex;
|
||
align-items: center; gap: 6px; font-weight: 600; font-family: var(--font); color: var(--text);
|
||
}
|
||
.btn:hover { background: var(--surface2); }
|
||
.btn-danger { background: var(--orange); color: #fff; border-color: var(--orange); font-size: 13px; padding: 8px 16px; }
|
||
.btn-danger:hover { background: var(--orange-dark); }
|
||
|
||
/* ── MODAL ──────────────────────────────────────────────────────────── */
|
||
.modal-overlay {
|
||
display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5);
|
||
z-index: 9999; justify-content: center; align-items: center;
|
||
}
|
||
.modal-overlay.open { display: flex; }
|
||
.modal {
|
||
background: #fff; border-radius: 10px; width: 560px; max-width: 95vw;
|
||
max-height: 85vh; display: flex; flex-direction: column;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.3); overflow: hidden;
|
||
}
|
||
.modal-header {
|
||
background: linear-gradient(135deg, #bf360c, #e65100);
|
||
color: #fff; padding: 13px 20px;
|
||
display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
|
||
}
|
||
.modal-header h2 { font-size: 15px; font-weight: 700; }
|
||
.modal-close {
|
||
background: rgba(255,255,255,0.2); border: none; color: #fff;
|
||
border-radius: 4px; width: 28px; height: 28px; cursor: pointer;
|
||
font-size: 16px; display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.modal-close:hover { background: rgba(255,255,255,0.35); }
|
||
.modal-body { flex: 1; overflow-y: auto; padding: 16px 20px; }
|
||
.cfg-section { margin-bottom: 18px; }
|
||
.cfg-section-title {
|
||
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
||
letter-spacing: 0.6px; color: var(--orange);
|
||
padding-bottom: 4px; margin-bottom: 8px; border-bottom: 1px solid var(--border);
|
||
}
|
||
.cfg-item { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
|
||
.cfg-item input[type="text"] {
|
||
flex: 1; border: 1px solid var(--border); border-radius: var(--radius);
|
||
padding: 6px 10px; font-size: 13px; font-family: var(--font); outline: none;
|
||
}
|
||
.cfg-item input[type="text"]:focus { border-color: var(--orange); }
|
||
.cfg-item .btn-rm {
|
||
width: 26px; height: 26px; border: none; background: #fee2e2;
|
||
color: var(--red); border-radius: 4px; cursor: pointer; font-size: 13px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.btn-add-config {
|
||
display: inline-flex; align-items: center; gap: 4px;
|
||
background: var(--orange); color: #fff; border: none; border-radius: var(--radius);
|
||
padding: 5px 12px; font-size: 11px; cursor: pointer; font-weight: 600; margin-top: 4px;
|
||
}
|
||
.btn-add-config:hover { background: var(--orange-dark); }
|
||
.modal-footer {
|
||
padding: 12px 20px; border-top: 1px solid var(--border);
|
||
display: flex; justify-content: flex-end; gap: 10px; flex-shrink: 0;
|
||
}
|
||
.modal-footer .btn-primary {
|
||
background: var(--orange); color: #fff; border: none; border-radius: var(--radius);
|
||
padding: 8px 20px; font-size: 13px; cursor: pointer; font-weight: 600; font-family: var(--font);
|
||
}
|
||
.modal-footer .btn-primary:hover { background: var(--orange-dark); }
|
||
|
||
/* ── Scrollbar ────────────────────────────────────────────────────── */
|
||
::-webkit-scrollbar { width: 7px; }
|
||
::-webkit-scrollbar-track { background: var(--surface); }
|
||
::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ═══ HEADER ════════════════════════════════════════════════════════════ -->
|
||
<div class="header">
|
||
<div class="header-left">
|
||
<span class="header-title">Fechamento de Caixa</span>
|
||
<img class="header-logo" id="store-logo" src="./logo.png" alt="Logo" />
|
||
<span class="header-id" id="fechamento-id">—</span>
|
||
</div>
|
||
<button class="gear-btn" id="btn-config" title="Configurar">⚙️ Configurar</button>
|
||
</div>
|
||
|
||
<!-- ═══ META BAR ════════════════════════════════════════════════════════ -->
|
||
<div class="meta-bar">
|
||
<div class="meta-field">
|
||
<span class="meta-label">Data</span>
|
||
<input type="date" class="meta-input" id="f-data" />
|
||
</div>
|
||
<div class="meta-field">
|
||
<span class="meta-label">Operador</span>
|
||
<input type="text" class="meta-input" id="f-operador" list="dl-operadores" placeholder="Ex: Isabelle" />
|
||
</div>
|
||
<div class="meta-field">
|
||
<span class="meta-label">Turno</span>
|
||
<select class="meta-select" id="f-turno">
|
||
<option value="">—</option>
|
||
<option value="integral">Integral</option>
|
||
<option value="manhã">Manhã</option>
|
||
<option value="tarde">Tarde</option>
|
||
</select>
|
||
</div>
|
||
<div class="meta-field">
|
||
<span class="meta-label">Loja</span>
|
||
<select class="meta-select" id="f-loja">
|
||
<option value="União">União</option>
|
||
<option value="Aliança">Aliança</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ MAIN 2 COLUMNS ════════════════════════════════════════════════ -->
|
||
<div class="main">
|
||
|
||
<!-- ── LEFT: entradas, sangrias, despesas, pix, vales, receber, cancelamentos ── -->
|
||
<div class="col col-left">
|
||
|
||
<!-- SANGRIAS -->
|
||
<div class="section">
|
||
<div class="section-header">💸 Sangrias</div>
|
||
<table id="tbl-sangrias"><thead><tr><th>Hora</th><th>Retirada (R$)</th><th>Gerente</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td colspan="2">TOTAL</td><td class="num" id="total-sangrias">R$ 0,00</td><td></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-sangrias">+ Adicionar sangria</button>
|
||
</div>
|
||
|
||
<!-- DESPESAS -->
|
||
<div class="section">
|
||
<div class="section-header">📝 Despesas</div>
|
||
<table id="tbl-despesas"><thead><tr><th>Descrição</th><th>Obs</th><th>Valor (R$)</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td colspan="2">TOTAL</td><td class="num" id="total-despesas">R$ 0,00</td><td></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-despesas">+ Adicionar despesa</button>
|
||
</div>
|
||
|
||
<!-- PIX CNPJ -->
|
||
<div class="section">
|
||
<div class="section-header">💳 PIX CNPJ</div>
|
||
<table id="tbl-pix"><thead><tr><th>Nome</th><th>Valor (R$)</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td>TOTAL</td><td class="num" id="total-pix">R$ 0,00</td><td></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-pix">+ Adicionar PIX</button>
|
||
</div>
|
||
|
||
<!-- VALES -->
|
||
<div class="section">
|
||
<div class="section-header">💰 Vales (Funcionários)</div>
|
||
<table id="tbl-vales"><thead><tr><th>Nome</th><th>Valor (R$)</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td>TOTAL</td><td class="num" id="total-vales">R$ 0,00</td><td></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-vales">+ Adicionar vale</button>
|
||
</div>
|
||
|
||
<!-- À RECEBER -->
|
||
<div class="section">
|
||
<div class="section-header">👥 À Receber (Clientes)</div>
|
||
<table id="tbl-receber"><thead><tr><th>Nome</th><th>Valor (R$)</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td>TOTAL</td><td class="num" id="total-receber">R$ 0,00</td><td></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-receber">+ Adicionar cliente</button>
|
||
</div>
|
||
|
||
<!-- CANCELAMENTOS -->
|
||
<div class="section">
|
||
<div class="section-header">❌ Cancelamentos</div>
|
||
<table id="tbl-cancelamentos"><thead><tr><th>Número</th><th>Valor (R$)</th><th>Motivo</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td colspan="1">TOTAL</td><td class="num" id="total-cancelamentos">R$ 0,00</td><td colspan="2"></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-cancelamentos">+ Adicionar cancelamento</button>
|
||
</div>
|
||
|
||
</div><!-- /col-left -->
|
||
|
||
<!-- ── RIGHT: saldo caixa, fechamento contado, diferença, obs ── -->
|
||
<div class="col col-right">
|
||
|
||
<!-- SALDO DE CAIXA -->
|
||
<div class="section">
|
||
<div class="section-header">🖥️ Fechamento de Caixa</div>
|
||
<div class="sys-grid">
|
||
<div class="sys-row"><span class="lbl">Troco (+)</span><input type="text" id="sys-troco" class="editable" placeholder="0,00" /></div>
|
||
<div class="sys-row"><span class="lbl">Crédito (+)</span><input type="text" id="sys-credito" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">Débito (+)</span><input type="text" id="sys-debito" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">Alimentação (+)</span><input type="text" id="sys-alimentacao" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">Vales (+)</span><input type="text" id="sys-vales" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">À Receber (+)</span><input type="text" id="sys-receber" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">PIX Cartão (+)</span><input type="text" id="sys-pix" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">PIX CNPJ (+)</span><input type="text" id="sys-pixcnpj" readonly tabindex="-1" /></div>
|
||
<div class="sys-row total"><span class="lbl">TOTAL</span><span class="val" id="sys-total">R$ 0,00</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SALDO ESPERADO -->
|
||
<div class="section">
|
||
<div class="section-header">🎯 Saldo Esperado</div>
|
||
<div class="saldo-esperado-wrap">
|
||
<label>Informe o saldo esperado (contado em caixa)</label>
|
||
<input type="text" id="f-saldo-esperado" placeholder="0,00" />
|
||
</div>
|
||
<!-- DIFERENÇA — dentro do Saldo Esperado, logo abaixo -->
|
||
<div class="dif-box" style="margin-top:8px">
|
||
<span class="dif-label">Diferença</span>
|
||
<span class="dif-value zero" id="diferenca">R$ 0,00</span>
|
||
<span id="dif-badge" style="font-size:11px;font-weight:700;padding:2px 8px;border-radius:10px;background:#e8f5e9;color:#2e7d32;display:none">OK</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- FECHAMENTO (VALORES CONTADOS) -->
|
||
<div class="section">
|
||
<div class="section-header">🛒 Saldo de Caixa</div>
|
||
<div class="fech-grid">
|
||
<div class="fech-item"><div class="fech-item-label">Dinheiro</div><input type="text" id="f-fech-dinheiro" placeholder="0,00" /></div>
|
||
<div class="fech-item"><div class="fech-item-label">Cartões</div><input type="text" id="f-fech-cartoes" placeholder="0,00" /></div>
|
||
<div class="fech-item"><div class="fech-item-label">À Receber</div><input type="text" id="f-fech-receber" placeholder="0,00" /></div>
|
||
</div>
|
||
<div class="fech-total">
|
||
<span class="fech-total-label">Total Programa</span>
|
||
<span class="fech-total-value" id="fech-total">R$ 0,00</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CONTADORES -->
|
||
<div class="section">
|
||
<div class="section-header">📦 Vendas</div>
|
||
<div style="display:flex;gap:12px">
|
||
<div style="flex:1">
|
||
<label style="font-size:12px;color:#666">Vendas (R$)</label>
|
||
<input type="text" id="f-vendas" placeholder="0,00" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:var(--radius);font-size:15px;text-align:right" onblur="fmtMoneyInput(this)" onfocus="unfmtMoneyInput(this)" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CONTADORES -->
|
||
<div class="section">
|
||
<div class="section-header">👥 Clientes & Frango Assado</div>
|
||
<div style="display:flex;gap:12px">
|
||
<div style="flex:1">
|
||
<label style="font-size:12px;color:#666">Clientes</label>
|
||
<input type="number" id="f-clientes" min="0" placeholder="0" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:var(--radius);font-size:15px" />
|
||
</div>
|
||
<div style="flex:1">
|
||
<label style="font-size:12px;color:#666">Frango Assado</label>
|
||
<input type="number" id="f-frango" min="0" placeholder="0" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:var(--radius);font-size:15px" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- OBS -->
|
||
<div class="section" style="flex:1;display:flex;flex-direction:column">
|
||
<div class="section-header">📝 Observações</div>
|
||
<textarea id="f-obs" rows="4" style="flex:1;width:100%;min-height:80px;border:1px solid var(--border);border-radius:var(--radius);padding:10px;font-size:15px;font-family:var(--font);resize:none" placeholder="Observações..."></textarea>
|
||
</div>
|
||
|
||
</div><!-- /col-right -->
|
||
</div><!-- /main -->
|
||
|
||
<!-- ═══ CARTÕES ════════════════════════════════════════════════════════ -->
|
||
<div class="cards-section">
|
||
<div class="cards-header">💳 Cartões</div>
|
||
<div class="cards-grid">
|
||
|
||
<div class="card-block">
|
||
<div class="card-block-header">Crédito</div>
|
||
<table id="tbl-cartao-credito"><thead><tr><th>Valor (R$)</th></tr></thead><tbody></tbody><tfoot><tr><td id="total-cartao-credito">R$ 0,00</td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-cartao-credito">+</button>
|
||
</div>
|
||
|
||
<div class="card-block">
|
||
<div class="card-block-header">Débito</div>
|
||
<table id="tbl-cartao-debito"><thead><tr><th>Valor (R$)</th></tr></thead><tbody></tbody><tfoot><tr><td id="total-cartao-debito">R$ 0,00</td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-cartao-debito">+</button>
|
||
</div>
|
||
|
||
<div class="card-block">
|
||
<div class="card-block-header">Alimentação</div>
|
||
<table id="tbl-cartao-alimentacao"><thead><tr><th>Valor (R$)</th></tr></thead><tbody></tbody><tfoot><tr><td id="total-cartao-alimentacao">R$ 0,00</td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-cartao-alimentacao">+</button>
|
||
</div>
|
||
|
||
<div class="card-block">
|
||
<div class="card-block-header">PIX</div>
|
||
<table id="tbl-cartao-pix"><thead><tr><th>Valor (R$)</th></tr></thead><tbody></tbody><tfoot><tr><td id="total-cartao-pix">R$ 0,00</td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-cartao-pix">+</button>
|
||
</div>
|
||
|
||
</div>
|
||
<div class="soma-row">
|
||
<span>Soma Cartões (todas as colunas)</span>
|
||
<span id="soma-cartoes">R$ 0,00</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ FOOTER ════════════════════════════════════════════════════════ -->
|
||
<div class="footer">
|
||
<div class="auto-save"><span class="dot"></span> Salvamento automático activo</div>
|
||
<button class="btn" id="btn-anterior">✏️ Dia anterior</button>
|
||
<button class="btn" id="btn-recibo">🖨️ Recibo 80mm</button>
|
||
<button class="btn" id="btn-limpar">🗑️ Limpar rascunho</button>
|
||
<button class="btn btn-danger" id="btn-enviar">💾 Enviar Fechamento</button>
|
||
</div>
|
||
|
||
<!-- ═══ CONFIG MODAL ══════════════════════════════════════════════════ -->
|
||
<div class="modal-overlay" id="modal-config">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h2>⚙️ Configurar Preenchimento Automático</h2>
|
||
<button class="modal-close" id="modal-close">✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="cfg-section">
|
||
<div class="cfg-section-title">👤 Operadores</div>
|
||
<div id="cfg-operadores-list"></div>
|
||
<button class="btn-add-config" id="cfg-add-operador">+ Adicionar operador</button>
|
||
</div>
|
||
<div class="cfg-section">
|
||
<div class="cfg-section-title">👔 Gerentes</div>
|
||
<div id="cfg-gerentes-list"></div>
|
||
<button class="btn-add-config" id="cfg-add-gerente">+ Adicionar gerente</button>
|
||
</div>
|
||
<div class="cfg-section">
|
||
<div class="cfg-section-title">📝 Despesas (sugestões)</div>
|
||
<div id="cfg-despesas-list"></div>
|
||
<button class="btn-add-config" id="cfg-add-despesa">+ Adicionar despesa</button>
|
||
</div>
|
||
<div class="cfg-section">
|
||
<div class="cfg-section-title">👥 Clientes (À Receber)</div>
|
||
<div id="cfg-clientes-list"></div>
|
||
<button class="btn-add-config" id="cfg-add-cliente">+ Adicionar cliente</button>
|
||
</div>
|
||
<div class="cfg-section">
|
||
<div class="cfg-section-title">💰 Vales (nomes para autofill)</div>
|
||
<div id="cfg-vales-list"></div>
|
||
<button class="btn-add-config" id="cfg-add-vales">+ Adicionar nome</button>
|
||
</div>
|
||
<div class="cfg-section">
|
||
<div class="cfg-section-title">🔐 Senha Admin (editar dias anteriores)</div>
|
||
<div style="display:flex;gap:6px;margin-top:4px">
|
||
<input type="password" id="cfg-admin-password" style="flex:1;padding:6px;border:1px solid #ddd;border-radius:4px;font-size:13px" placeholder="Senha fixa (ou deixe em branco)">
|
||
<input type="password" id="cfg-dynamic-password" style="flex:1;padding:6px;border:1px solid #ddd;border-radius:4px;font-size:13px" placeholder="Dinâmica: 10+dia+mes (ex: 1217)">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn-primary" id="cfg-salvar">Salvar Configuração</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ CONFIRM MODAL ══════════════════════════════════════════════════ -->
|
||
<div class="modal-overlay" id="modal-confirm">
|
||
<div class="modal" style="width:520px">
|
||
<div class="modal-header">
|
||
<h2>⚠️ Confirmar envio do fechamento</h2>
|
||
<button class="modal-close" id="confirm-close">✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p style="font-size:13px;color:#444;margin-bottom:14px">
|
||
Tem certeza que deseja <strong>enviar este fechamento e encerrar o caixa?</strong>
|
||
</p>
|
||
<div id="confirm-summary" style="border:1px solid #b3d9f7;border-radius:8px;padding:18px;background:#f0f7ff;font-size:16px;font-family:Arial,sans-serif;line-height:1.8">
|
||
</div>
|
||
<p style="font-size:11px;color:#888;margin-top:10px">
|
||
Após enviar, <strong>não dá pra editar este fechamento</strong> sem a senha do Filipe.
|
||
</p>
|
||
</div>
|
||
<div class="modal-footer" style="justify-content:space-between">
|
||
<button class="btn" id="confirm-print" style="color:#333">
|
||
🖨️ Imprimir Relatório
|
||
</button>
|
||
<div style="display:flex;gap:10px">
|
||
<button class="btn" id="confirm-cancel" style="border-color:var(--red);color:var(--red)">✕ Cancelar</button>
|
||
<button class="btn btn-danger" id="confirm-send">✓ Sim, enviar e encerrar</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ JAVASCRIPT ══════════════════════════════════════════════════ -->
|
||
<script>
|
||
'use strict';
|
||
|
||
// ─── Config ────────────────────────────────────────────────────────────────
|
||
let config = {
|
||
operadores: ['Isabelle', 'Silvanea', 'Rafael'],
|
||
gerentes: ['Carlos', 'Ana', 'João'],
|
||
despesas: ['Energia', 'Água', 'Manutenção', 'Limpeza', 'Insumos'],
|
||
clientes: [], // preenchimento livre — não é datalist fixo
|
||
vales_nomes: [], // nomes usados em vales ( histórico para autofill)
|
||
admin_password: '', // senha para editar dias anteriores no Supabase
|
||
};
|
||
|
||
// ─── App State ────────────────────────────────────────────────────────────
|
||
let estado = {
|
||
uuid: crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(),
|
||
loja: 'União', data: '', operador: '', turno: '',
|
||
saldo_troco: 0, fechamento: 0, saldo_esperado: 0, diferenca: 0,
|
||
despesas: [], sangrias: [], pixcnpj: [], vales: [], receber: [], cancelamentos: [],
|
||
cartoes: { credito: [], debito: [], alimentacao: [], pix: [] },
|
||
fechamento_contado: { dinheiro: 0, cartoes: 0, receber: 0 },
|
||
clientes: 0, frango: 0, vendas: 0, // contadores
|
||
observacoes: '', id_local: null, sync_status: 'local'
|
||
};
|
||
|
||
// ─── Helpers ──────────────────────────────────────────────────────────────
|
||
const fmt = v => 'R$ ' + (parseFloat(v) || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
||
const parseNum = v => {
|
||
// Already a number? Return directly (don't treat '.' as thousand separator)
|
||
if (typeof v === 'number') return isNaN(v) ? 0 : v;
|
||
const s = String(v || '0').trim().replace(/^R\$\s*/, ''); // strip currency prefix first
|
||
if (!s) return 0;
|
||
// Brazilian format: 1.234,56 → 1234.56
|
||
// Strategy: if string has comma, treat as BRL (decimal comma)
|
||
// If no comma but has dot, treat as US (decimal dot)
|
||
const lastComma = s.lastIndexOf(',');
|
||
const lastDot = s.lastIndexOf('.');
|
||
const hasCommaDecimal = lastComma > lastDot;
|
||
if (hasCommaDecimal) {
|
||
const intPart = s.slice(0, lastComma).replace(/\./g, '').replace(/,/g, '');
|
||
const decPart = s.slice(lastComma + 1);
|
||
return parseFloat(intPart + '.' + decPart) || 0;
|
||
}
|
||
// No comma: US-style number, just strip any dots (thousand sep)
|
||
return parseFloat(s.replace(/,/g, '').replace(/\./g, '')) || 0;
|
||
};
|
||
const setText = (id, t) => { const e = document.getElementById(id); if (e) e.textContent = t; };
|
||
const setVal = (id, v) => { const e = document.getElementById(id); if (e) e.value = v; };
|
||
const val = id => { const e = document.getElementById(id); return e ? e.value : ''; };
|
||
const fmtNum = v => {
|
||
const n = typeof v === 'number' ? v : parseNum(v);
|
||
if (isNaN(n)) return '';
|
||
// Always 2 decimal places for money
|
||
return n.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
};
|
||
const fmtMoneyInput = el => {
|
||
const raw = parseNum(el.value);
|
||
if (raw > 0) el.value = fmtNum(raw);
|
||
};
|
||
const unfmtMoneyInput = el => {
|
||
const raw = parseNum(el.value);
|
||
if (raw > 0) el.value = raw.toString();
|
||
};
|
||
|
||
// Format time: 4 digits → HH:MM
|
||
function fmtTime(v) {
|
||
const digits = (v || '').replace(/\D/g, '').slice(0, 4);
|
||
if (digits.length <= 2) return digits;
|
||
return digits.slice(0, 2) + ':' + digits.slice(2);
|
||
}
|
||
|
||
// ─── Tables ───────────────────────────────────────────────────────────────
|
||
const TABLES = {
|
||
'tbl-sangrias': { field: 'sangrias', cols: ['hora','retirada','gerente'], enterNav: { hora:'retirada', retirada:'gerente', gerente:'_nextRow' } },
|
||
'tbl-despesas': { field: 'despesas', cols: ['desc','obs','valor'], enterNav: { desc:'obs', obs:'valor', valor:'_nextRow' } },
|
||
'tbl-pix': { field: 'pixcnpj', cols: ['nome','valor'], enterNav: { nome:'valor', valor:'_nextRow' } },
|
||
'tbl-vales': { field: 'vales', cols: ['nome','valor'], enterNav: { nome:'valor', valor:'_nextRow' } },
|
||
'tbl-receber': { field: 'receber', cols: ['nome','valor'], enterNav: { nome:'valor', valor:'_nextRow' } },
|
||
'tbl-cancelamentos': { field: 'cancelamentos', cols: ['numero','valor','motivo'], enterNav: { numero:'valor', valor:'motivo', motivo:'_nextRow' } },
|
||
'tbl-cartao-credito': { field: 'cartoes.credito', cols: ['valor'], enterNav: { valor:'_nextRow' } },
|
||
'tbl-cartao-debito': { field: 'cartoes.debito', cols: ['valor'], enterNav: { valor:'_nextRow' } },
|
||
'tbl-cartao-alimentacao': { field: 'cartoes.alimentacao', cols: ['valor'], enterNav: { valor:'_nextRow' } },
|
||
'tbl-cartao-pix': { field: 'cartoes.pix', cols: ['valor'], enterNav: { valor:'_nextRow' } },
|
||
};
|
||
|
||
function getField(tblId) {
|
||
const parts = TABLES[tblId].field.split('.');
|
||
let cur = estado;
|
||
for (const p of parts) cur = cur[p];
|
||
return cur;
|
||
}
|
||
|
||
function makeRow(tblId, rowIndex, rowData) {
|
||
const cfg = TABLES[tblId];
|
||
const tr = document.createElement('tr');
|
||
cfg.cols.forEach(col => {
|
||
const td = document.createElement('td');
|
||
if (col === 'valor' || col === 'retirada') td.className = 'num';
|
||
const inp = document.createElement('input');
|
||
inp.type = 'text';
|
||
// For numeric cols that hold money, format with 2 decimals
|
||
// For text cols (hora, desc, obs, nome, gerente, motivo, numero) keep as-is
|
||
const numericCols = ['valor', 'retirada'];
|
||
inp.value = rowData[col] !== undefined && rowData[col] !== null
|
||
? (numericCols.includes(col) ? (rowData[col] === 0 ? '' : fmtNum(rowData[col])) : (rowData[col] || ''))
|
||
: '';
|
||
inp.placeholder = col === 'valor' || col === 'retirada' ? '0,00' : (col === 'hora' ? 'HH:MM' : '');
|
||
inp.dataset.tbl = tblId;
|
||
inp.dataset.row = rowIndex;
|
||
inp.dataset.col = col;
|
||
// datalist only for fields that should offer suggestions from config
|
||
if (col === 'gerente') inp.setAttribute('list', 'dl-gerente');
|
||
if (col === 'desc') inp.setAttribute('list', 'dl-despesa');
|
||
if (col === 'nome' && tblId === 'tbl-vales') inp.setAttribute('list', 'dl-vales');
|
||
// tbl-pix nome = free text (no datalist)
|
||
// tbl-receber nome = free text (no datalist)
|
||
// tbl-cancelamentos numero + motivo = free text (no datalist)
|
||
inp.addEventListener('input', onTableInput);
|
||
inp.addEventListener('blur', onCellBlur);
|
||
inp.addEventListener('keydown', onCellKeydown);
|
||
td.appendChild(inp);
|
||
tr.appendChild(td);
|
||
});
|
||
const delTd = document.createElement('td');
|
||
const delBtn = document.createElement('button');
|
||
delBtn.className = 'row-del';
|
||
delBtn.innerHTML = '✕';
|
||
delBtn.dataset.tbl = tblId;
|
||
delBtn.dataset.row = rowIndex;
|
||
delBtn.addEventListener('click', onDeleteRow);
|
||
delTd.appendChild(delBtn);
|
||
tr.appendChild(delTd);
|
||
return tr;
|
||
}
|
||
|
||
function renderTable(tblId) {
|
||
const cfg = TABLES[tblId];
|
||
const tbody = document.querySelector(`#${tblId} tbody`);
|
||
if (!tbody) return;
|
||
tbody.innerHTML = '';
|
||
getField(tblId).forEach((r, i) => { tbody.appendChild(makeRow(tblId, i, r)); });
|
||
}
|
||
|
||
function onTableInput(e) {
|
||
const { tbl, row, col } = e.target.dataset;
|
||
const field = getField(tbl);
|
||
if (!field || !field[row]) return;
|
||
if (col === 'valor' || col === 'retirada') {
|
||
field[row][col] = parseNum(e.target.value);
|
||
} else if (col === 'hora') {
|
||
const formatted = fmtTime(e.target.value);
|
||
if (formatted !== e.target.value) e.target.value = formatted;
|
||
field[row][col] = formatted;
|
||
} else {
|
||
// text fields: nome, desc, obs, gerente, motivo, numero — keep as string
|
||
field[row][col] = e.target.value;
|
||
}
|
||
updateTotals();
|
||
scheduleSave();
|
||
}
|
||
|
||
function onCellBlur(e) {
|
||
const { col } = e.target.dataset;
|
||
if (col === 'valor' || col === 'retirada') {
|
||
const v = parseNum(e.target.value);
|
||
e.target.value = isNaN(v) || v === 0 ? '' : fmtNum(v);
|
||
updateTotals();
|
||
}
|
||
// For text cols (hora, nome, desc, obs, gerente, motivo, numero) — keep as-is
|
||
}
|
||
|
||
function onCellKeydown(e) {
|
||
if (e.key !== 'Enter') return;
|
||
// 50ms delay so the browser finishes filling the datalist value
|
||
// before we read e.target.value and move focus
|
||
setTimeout(() => {
|
||
const { tbl, row, col } = e.target.dataset;
|
||
const cfg = TABLES[tbl];
|
||
if (!cfg) return;
|
||
const next = cfg.enterNav[col];
|
||
if (next === '_nextRow') {
|
||
const nextRowIndex = parseInt(row) + 1;
|
||
if (nextRowIndex < getField(tbl).length) {
|
||
focusInput(tbl, nextRowIndex, cfg.cols[0]);
|
||
} else {
|
||
addRowAndFocus(tbl, cfg.cols[0]);
|
||
}
|
||
} else if (next) {
|
||
focusInput(tbl, parseInt(row), next);
|
||
}
|
||
}, 50);
|
||
}
|
||
|
||
function focusInput(tblId, rowIndex, col) {
|
||
const sel = `#${tblId} tbody tr:nth-child(${rowIndex + 1}) input[data-col="${col}"]`;
|
||
const inp = document.querySelector(sel);
|
||
if (inp) { inp.focus(); if (col === 'valor' || col === 'retirada' || col === 'hora') inp.select(); }
|
||
}
|
||
|
||
function addRowAndFocus(tblId, focusCol) {
|
||
const cfg = TABLES[tblId];
|
||
const field = getField(tblId);
|
||
const numericCols = ['valor', 'retirada'];
|
||
const empty = {};
|
||
cfg.cols.forEach(c => { empty[c] = numericCols.includes(c) ? 0 : ''; });
|
||
field.push(empty);
|
||
renderTable(tblId);
|
||
focusInput(tblId, field.length - 1, focusCol);
|
||
}
|
||
|
||
function onDeleteRow(e) {
|
||
const { tbl, row } = e.target.dataset;
|
||
const field = getField(tbl);
|
||
if (field && field.length > 1) {
|
||
field.splice(parseInt(row), 1);
|
||
renderTable(tbl);
|
||
updateTotals();
|
||
scheduleSave();
|
||
} else {
|
||
const cfg = TABLES[tbl];
|
||
cfg.cols.forEach(c => { field[0][c] = c === 'valor' || c === 'retirada' ? 0 : ''; });
|
||
renderTable(tbl);
|
||
updateTotals();
|
||
scheduleSave();
|
||
}
|
||
}
|
||
|
||
// Add buttons
|
||
document.querySelectorAll('.add-btn[data-tbl]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const tblId = btn.dataset.tbl;
|
||
const cfg = TABLES[tblId];
|
||
if (!cfg) return;
|
||
const field = getField(tblId);
|
||
const empty = {};
|
||
cfg.cols.forEach(c => { empty[c] = c === 'valor' ? 0 : ''; });
|
||
field.push(empty);
|
||
renderTable(tblId);
|
||
focusInput(tblId, field.length - 1, cfg.cols[0]);
|
||
});
|
||
});
|
||
|
||
// ─── Totals ────────────────────────────────────────────────────────────────
|
||
function updateTotals() {
|
||
// ── Sangrias ──
|
||
const sangriasTotal = estado.sangrias.reduce((s, r) => s + (parseNum(r['retirada'] || 0) || 0), 0);
|
||
setText('total-sangrias', fmt(sangriasTotal));
|
||
|
||
// ── Tabelas de valor ──
|
||
const map = {
|
||
'tbl-despesas':'despesas', 'tbl-pix':'pixcnpj', 'tbl-vales':'vales',
|
||
'tbl-receber':'receber', 'tbl-cancelamentos':'cancelamentos',
|
||
};
|
||
Object.entries(map).forEach(([tblId, fieldName]) => {
|
||
const total = estado[fieldName].reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
setText(tblId.replace('tbl-','total-'), fmt(total));
|
||
});
|
||
|
||
// ── Cartões ──
|
||
const cartCred = estado.cartoes.credito.reduce((s, r) => s + (r.valor || 0), 0);
|
||
const cartDeb = estado.cartoes.debito.reduce((s, r) => s + (r.valor || 0), 0);
|
||
const cartAli = estado.cartoes.alimentacao.reduce((s, r) => s + (r.valor || 0), 0);
|
||
const cartPix = estado.cartoes.pix.reduce((s, r) => s + (r.valor || 0), 0);
|
||
|
||
['credito','debito','alimentacao','pix'].forEach(c => {
|
||
const total = estado.cartoes[c].reduce((s, r) => s + (r.valor || 0), 0);
|
||
setText(`total-cartao-${c}`, fmt(total));
|
||
});
|
||
|
||
const somaCartoes = cartCred + cartDeb + cartAli + cartPix;
|
||
setText('soma-cartoes', fmt(somaCartoes));
|
||
|
||
// ── Saldo de Caixa — auto-populate readonly fields ──
|
||
// Crédito = total cartão crédito
|
||
document.getElementById('sys-credito').value = cartCred > 0 ? fmtNum(cartCred) : '';
|
||
// Débito = total cartão débito
|
||
document.getElementById('sys-debito').value = cartDeb > 0 ? fmtNum(cartDeb) : '';
|
||
// Alimentação = total cartão alimentação
|
||
document.getElementById('sys-alimentacao').value = cartAli > 0 ? fmtNum(cartAli) : '';
|
||
// PIX Cartão = total cartão PIX
|
||
document.getElementById('sys-pix').value = cartPix > 0 ? fmtNum(cartPix) : '';
|
||
// PIX CNPJ = total PIX CNPJ
|
||
const pixCnpjTotal = estado.pixcnpj.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
document.getElementById('sys-pixcnpj').value = pixCnpjTotal > 0 ? fmtNum(pixCnpjTotal) : '';
|
||
// Vales = total vales
|
||
const valesTotal = estado.vales.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
document.getElementById('sys-vales').value = valesTotal > 0 ? fmtNum(valesTotal) : '';
|
||
// À Receber = total receber
|
||
const receberTotal = estado.receber.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
document.getElementById('sys-receber').value = receberTotal > 0 ? fmtNum(receberTotal) : '';
|
||
|
||
// Saldo de Caixa (Sistema) = troco + crédito + débito + alimentação + vales + receber + pixCartão + pixCnpj
|
||
const sysTotal = parseNum(val('sys-troco')) + cartCred + cartDeb + cartAli + valesTotal + receberTotal + cartPix + pixCnpjTotal;
|
||
setText('sys-total', fmt(sysTotal));
|
||
|
||
// ── Fechamento Contado — auto-populate (readonly, filled from tables) ──
|
||
// Dinheiro = total sangrias
|
||
document.getElementById('f-fech-dinheiro').value = sangriasTotal > 0 ? fmtNum(sangriasTotal) : '';
|
||
// Cartões = total cartões (crédito+débito+alimentação+pix)
|
||
document.getElementById('f-fech-cartoes').value = somaCartoes > 0 ? fmtNum(somaCartoes) : '';
|
||
// À Receber = total receber
|
||
document.getElementById('f-fech-receber').value = receberTotal > 0 ? fmtNum(receberTotal) : '';
|
||
|
||
// Fechamento total
|
||
const fd = sangriasTotal;
|
||
const fc = somaCartoes;
|
||
const fr = receberTotal;
|
||
setText('fech-total', fmt(fd + fc + fr));
|
||
|
||
// Diferença = total_fechamento (sysTotal: troco+credito+debito+alimentacao+vales+receber+pixCartao+pixCnpj) - saldo_esperado - cancelamentos
|
||
const cancelTotal = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const diff = sysTotal - parseNum(val('f-saldo-esperado')) - cancelTotal;
|
||
const difEl = document.getElementById('diferenca');
|
||
const badgeEl = document.getElementById('dif-badge');
|
||
if (difEl) {
|
||
difEl.textContent = fmt(diff);
|
||
if (diff === 0) {
|
||
difEl.className = 'dif-value zero';
|
||
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
|
||
} else if (diff < 0 && diff >= -5) {
|
||
difEl.className = 'dif-value neg'; // OK − até −5 de tolerância
|
||
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
|
||
} else if (diff > 0 && diff <= 3) {
|
||
difEl.className = 'dif-value zero'; // OK + até 3
|
||
if (badgeEl) { badgeEl.textContent = '✓ OK'; badgeEl.style.background='#e8f5e9'; badgeEl.style.color='#2e7d32'; badgeEl.style.display='inline'; }
|
||
} else {
|
||
// Sobra mais de R$3 ou falta mais de R$5
|
||
difEl.className = diff > 0 ? 'dif-value pos' : 'dif-value neg';
|
||
if (badgeEl) {
|
||
if (diff > 0) {
|
||
badgeEl.textContent = '⚠ SOBRANDO';
|
||
badgeEl.style.background='#fff3e0';
|
||
badgeEl.style.color='#e65100';
|
||
} else {
|
||
badgeEl.textContent = '⚠ FALTANDO';
|
||
badgeEl.style.background='#ffebee';
|
||
badgeEl.style.color='#c0272d';
|
||
}
|
||
badgeEl.style.display='inline';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── Numeric input: auto ,00 on digit+blur pattern ───────────────────────
|
||
// For fields that use datalist (text input), we need to avoid triggering
|
||
// auto-formatter when the browser is about to fill from datalist.
|
||
// Strategy: detect "comma-only" input and convert to ",00" immediately.
|
||
// For normal digit input: add ,00 on blur (not on every digit).
|
||
|
||
// ─── Numeric input: auto ,00 on digit+blur pattern ───────────────────────
|
||
// These fields are plain text inputs (no datalist). Format on blur only.
|
||
// For sys-troco which is user-editable: also format on Enter keydown.
|
||
|
||
['troco','credito','debito','alimentacao','vales','receber','pix'].forEach(k => {
|
||
const el = document.getElementById(`sys-${k}`);
|
||
if (!el) return;
|
||
|
||
// On input: just track for autosave
|
||
el.addEventListener('input', () => {
|
||
updateTotals();
|
||
scheduleSave();
|
||
});
|
||
|
||
// On keydown Enter: format value directly (Enter does not trigger blur for tabindex=-1)
|
||
el.addEventListener('keydown', e => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
const raw = el.value.trim();
|
||
if (!raw) { el.value = ''; return; }
|
||
const num = parseNum(raw);
|
||
if (isNaN(num) || num === 0) { el.value = ''; }
|
||
else { el.value = fmtNum(num); }
|
||
updateTotals();
|
||
scheduleSave();
|
||
}
|
||
});
|
||
|
||
// On blur: format the value
|
||
el.addEventListener('blur', () => {
|
||
const raw = el.value.trim();
|
||
if (!raw) { el.value = ''; return; }
|
||
const num = parseNum(raw);
|
||
if (isNaN(num) || num === 0) { el.value = ''; }
|
||
else { el.value = fmtNum(num); }
|
||
updateTotals();
|
||
scheduleSave();
|
||
});
|
||
});
|
||
|
||
['f-fech-dinheiro','f-fech-cartoes','f-fech-receber','f-saldo-esperado'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
|
||
el.addEventListener('input', () => {
|
||
const v = el.value;
|
||
if (v.endsWith(',') && !v.includes(',') && v !== ',') {
|
||
el.value = v + '00';
|
||
const pos = v.length;
|
||
setTimeout(() => { el.selectionStart = el.selectionEnd = pos; }, 0);
|
||
}
|
||
updateTotals();
|
||
scheduleSave();
|
||
});
|
||
|
||
el.addEventListener('blur', () => {
|
||
const raw = el.value.trim();
|
||
if (!raw) { el.value = ''; return; }
|
||
const num = parseNum(raw);
|
||
if (isNaN(num) || num === 0) { el.value = ''; }
|
||
else { el.value = fmtNum(num); }
|
||
updateTotals();
|
||
scheduleSave();
|
||
});
|
||
});
|
||
|
||
// ─── Metadata ──────────────────────────────────────────────────────────────
|
||
['f-data','f-operador','f-turno','f-loja'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
el.addEventListener('change', () => {
|
||
estado.data = val('f-data');
|
||
estado.operador = val('f-operador');
|
||
estado.turno = val('f-turno');
|
||
estado.loja = val('f-loja');
|
||
syncHeader();
|
||
scheduleSave();
|
||
});
|
||
});
|
||
|
||
function syncHeader() {
|
||
setText('fechamento-id', `${estado.loja}_${estado.data}_op`);
|
||
}
|
||
|
||
// ─── Datalists ─────────────────────────────────────────────────────────────
|
||
function buildDatalists() {
|
||
// Auto-build vales_nomes from current entries (autofill dinâmico)
|
||
const valesUsed = [...new Set(estado.vales.map(v => v.nome).filter(Boolean))];
|
||
config.vales_nomes = [...new Set([...config.vales_nomes, ...valesUsed])]
|
||
.sort((a, b) => a.localeCompare(b, 'pt-BR'));
|
||
|
||
// Gerentes e Operadores também acumulam dos lançamentos
|
||
const gerentesUsed = [...new Set(estado.sangrias.map(r => r.gerente).filter(Boolean))];
|
||
config.gerentes = [...new Set([...config.gerentes, ...gerentesUsed])]
|
||
.sort((a, b) => a.localeCompare(b, 'pt-BR'));
|
||
|
||
const lists = {
|
||
'dl-operadores': [...new Set([...config.operadores])].sort((a,b) => a.localeCompare(b, 'pt-BR')),
|
||
'dl-gerente': [...new Set([...config.gerentes])].sort((a,b) => a.localeCompare(b, 'pt-BR')),
|
||
'dl-despesa': [...new Set([...config.despesas])].sort((a,b) => a.localeCompare(b, 'pt-BR')),
|
||
// Clientes: escrita livre — sem datalist fixo
|
||
'dl-vales': [...new Set([...config.vales_nomes])].sort((a,b) => a.localeCompare(b, 'pt-BR')),
|
||
};
|
||
Object.entries(lists).forEach(([id, items]) => {
|
||
let dl = document.getElementById(id);
|
||
if (!dl) { dl = document.createElement('datalist'); dl.id = id; document.body.appendChild(dl); }
|
||
dl.innerHTML = items.map(v => `<option value="${v}">`).join('');
|
||
});
|
||
// Clientes: sem datalist (escrita livre)
|
||
const clienteDl = document.getElementById('dl-cliente');
|
||
if (clienteDl) clienteDl.remove();
|
||
}
|
||
|
||
// ─── Init ─────────────────────────────────────────────────────────────────
|
||
const PRE_FILL_TABLES = Object.keys(TABLES);
|
||
|
||
function ensureOneRow(tblId) {
|
||
const cfg = TABLES[tblId];
|
||
if (!cfg) return;
|
||
const field = getField(tblId);
|
||
if (field && field.length === 0) {
|
||
const empty = {};
|
||
cfg.cols.forEach(c => { empty[c] = c === 'valor' || c === 'retirada' ? 0 : ''; });
|
||
field.push(empty);
|
||
}
|
||
}
|
||
|
||
function init() {
|
||
const today = new Date().toISOString().split('T')[0];
|
||
setVal('f-data', today);
|
||
estado.data = today;
|
||
setVal('f-loja', estado.loja || 'União');
|
||
setVal('f-operador', estado.operador || '');
|
||
setVal('f-turno', estado.turno || '');
|
||
syncHeader();
|
||
PRE_FILL_TABLES.forEach(ensureOneRow);
|
||
Object.keys(TABLES).forEach(renderTable);
|
||
updateTotals();
|
||
}
|
||
|
||
// ─── LocalStorage ──────────────────────────────────────────────────────────
|
||
const LS_KEY = 'fc_v7';
|
||
const CFG_KEY = 'fc_cfg';
|
||
|
||
function buildEstadoForBackend() {
|
||
// Calcula totais das tabelas (mesma lógica do updateTotals)
|
||
const sangriasTotal = estado.sangrias.reduce((s, r) => s + (parseNum(r['retirada'] || 0) || 0), 0);
|
||
const despesasTotal = estado.despesas.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
const pixcnpjTotal = estado.pixcnpj.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
const valesTotal = estado.vales.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
const receberTotal = estado.receber.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
const cancelTotal = estado.cancelamentos.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
const cartCred = estado.cartoes.credito.reduce((s, r) => s + (r.valor || 0), 0);
|
||
const cartDeb = estado.cartoes.debito.reduce((s, r) => s + (r.valor || 0), 0);
|
||
const cartAli = estado.cartoes.alimentacao.reduce((s, r) => s + (r.valor || 0), 0);
|
||
const cartPix = estado.cartoes.pix.reduce((s, r) => s + (r.valor || 0), 0);
|
||
|
||
// Monta o objeto exactly as o backend espera (storage.rs → salvar_fechamento)
|
||
return {
|
||
uuid: estado.uuid,
|
||
id_local: estado.id_local,
|
||
loja: estado.loja,
|
||
data: estado.data,
|
||
operador: estado.operador,
|
||
turno: estado.turno,
|
||
saldo_troco: parseNum(val('sys-troco')),
|
||
saldo_esperado: parseNum(val('f-saldo-esperado')),
|
||
fechamento: parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')),
|
||
// Saldos de cartões (do Sys / Saldo de Caixa)
|
||
credito: parseNum(val('sys-credito')) || cartCred,
|
||
debito: parseNum(val('sys-debito')) || cartDeb,
|
||
alimentacao: parseNum(val('sys-alimentacao')) || cartAli,
|
||
pixcnpj: parseNum(val('sys-pixcnpj')) || pixcnpjTotal,
|
||
vales: parseNum(val('sys-vales')) || valesTotal,
|
||
receber: parseNum(val('sys-receber')) || receberTotal,
|
||
// Totais para colunas SQL
|
||
total_sangrias: sangriasTotal,
|
||
total_despesas: despesasTotal,
|
||
total_pixcnpj: pixcnpjTotal,
|
||
total_vales: valesTotal,
|
||
total_areceber: receberTotal,
|
||
total_cancelamentos: cancelTotal,
|
||
total_cartao_credito: cartCred,
|
||
total_cartao_debito: cartDeb,
|
||
total_cartao_alimentacao: cartAli,
|
||
total_cartao_pix: cartPix,
|
||
// Fechamento contado
|
||
fd: parseNum(val('f-fech-dinheiro')),
|
||
fc: parseNum(val('f-fech-cartoes')),
|
||
fr: parseNum(val('f-fech-receber')),
|
||
despesas: estado.despesas.map(r => ({ desc: r.desc || '', obs: r.obs || '', valor: r.valor || 0 })),
|
||
sangrias: estado.sangrias.map(r => ({ hora: r.hora || '', retirada: r.retirada || 0, gerente: r.gerente || '' })),
|
||
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome || '', valor: r.valor || 0 })),
|
||
vales: estado.vales.map(r => ({ nome: r.nome || '', obs: r.obs || '', valor: r.valor || 0 })),
|
||
receber: estado.receber.map(r => ({ nome: r.nome || '', valor: r.valor || 0 })),
|
||
cancelamentos: estado.cancelamentos.map(r => ({ numero: r.numero || '', valor: r.valor || 0, motivo: r.motivo || '' })),
|
||
cartoes: {
|
||
credito: estado.cartoes.credito.map(r => r.valor || 0),
|
||
debito: estado.cartoes.debito.map(r => r.valor || 0),
|
||
alimentacao: estado.cartoes.alimentacao.map(r => r.valor || 0),
|
||
pix: estado.cartoes.pix.map(r => r.valor || 0),
|
||
},
|
||
observacoes: estado.observacoes,
|
||
clientes: estado.clientes,
|
||
frango: estado.frango,
|
||
vendas: estado.vendas,
|
||
sync_status: estado.sync_status,
|
||
};
|
||
}
|
||
|
||
function saveState() {
|
||
// Sync reactive fields from DOM before saving
|
||
estado.operador = val('f-operador');
|
||
estado.turno = val('f-turno');
|
||
estado.loja = val('f-loja');
|
||
estado.saldo_troco = parseNum(val('sys-troco'));
|
||
estado.saldo_esperado = parseNum(val('f-saldo-esperado'));
|
||
estado.fechamento_contado = {
|
||
dinheiro: parseNum(val('f-fech-dinheiro')),
|
||
cartoes: parseNum(val('f-fech-cartoes')),
|
||
receber: parseNum(val('f-fech-receber')),
|
||
};
|
||
estado.clientes = parseInt(document.getElementById('f-clientes').value) || 0;
|
||
estado.frango = parseInt(document.getElementById('f-frango').value) || 0;
|
||
estado.vendas = parseNum(val('f-vendas'));
|
||
estado.updatedAt = new Date().toISOString();
|
||
try { localStorage.setItem(LS_KEY, JSON.stringify(estado)); } catch(e) {}
|
||
}
|
||
function loadState() {
|
||
try {
|
||
const raw = localStorage.getItem(LS_KEY);
|
||
if (!raw) return false;
|
||
const saved = JSON.parse(raw);
|
||
// Restore simple fields
|
||
estado.uuid = saved.uuid || estado.uuid;
|
||
estado.id_local = saved.id_local !== undefined ? saved.id_local : null;
|
||
estado.loja = saved.loja || estado.loja;
|
||
estado.data = saved.data || estado.data;
|
||
estado.operador = saved.operador || estado.operador;
|
||
estado.turno = saved.turno || estado.turno;
|
||
estado.saldo_troco = saved.saldo_troco || 0;
|
||
estado.saldo_esperado = saved.saldo_esperado || 0;
|
||
estado.fechamento = saved.fechamento || 0;
|
||
estado.observacoes = saved.observacoes || '';
|
||
estado.clientes = saved.clientes || 0;
|
||
estado.frango = saved.frango || 0;
|
||
estado.vendas = saved.vendas || 0;
|
||
estado.sync_status = saved.sync_status || 'local';
|
||
// Restore tables (always as arrays of objects)
|
||
estado.despesas = Array.isArray(saved.despesas) ? saved.despesas : [];
|
||
estado.sangrias = Array.isArray(saved.sangrias) ? saved.sangrias : [];
|
||
estado.pixcnpj = Array.isArray(saved.pixcnpj) ? saved.pixcnpj : [];
|
||
estado.vales = Array.isArray(saved.vales) ? saved.vales : [];
|
||
estado.receber = Array.isArray(saved.receber) ? saved.receber : [];
|
||
estado.cancelamentos = Array.isArray(saved.cancelamentos) ? saved.cancelamentos : [];
|
||
// Restore cartoes — each entry must be { valor: number }
|
||
if (saved.cartoes) {
|
||
estado.cartoes.credito = Array.isArray(saved.cartoes.credito)
|
||
? saved.cartoes.credito.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
|
||
: [];
|
||
estado.cartoes.debito = Array.isArray(saved.cartoes.debito)
|
||
? saved.cartoes.debito.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
|
||
: [];
|
||
estado.cartoes.alimentacao = Array.isArray(saved.cartoes.alimentacao)
|
||
? saved.cartoes.alimentacao.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
|
||
: [];
|
||
estado.cartoes.pix = Array.isArray(saved.cartoes.pix)
|
||
? saved.cartoes.pix.map(v => ({ valor: typeof v === 'object' && v !== null ? (v.valor || 0) : (parseNum(v) || 0) }))
|
||
: [];
|
||
}
|
||
// Restore fechamento_contado
|
||
if (saved.fechamento_contado) {
|
||
estado.fechamento_contado = saved.fechamento_contado;
|
||
}
|
||
setVal('f-data', estado.data || '');
|
||
setVal('f-operador', estado.operador || '');
|
||
setVal('f-turno', estado.turno || '');
|
||
setVal('f-loja', estado.loja || 'União');
|
||
setVal('f-obs', estado.observacoes || '');
|
||
setVal('f-saldo-esperado', estado.saldo_esperado === 0 ? '' : fmtNum(estado.saldo_esperado));
|
||
setVal('sys-troco', estado.saldo_troco === 0 ? '' : fmtNum(estado.saldo_troco));
|
||
const fc = estado.fechamento_contado || {};
|
||
setVal('f-fech-dinheiro', fc.dinheiro ? fmtNum(fc.dinheiro) : '');
|
||
setVal('f-fech-cartoes', fc.cartoes ? fmtNum(fc.cartoes) : '');
|
||
setVal('f-fech-receber', fc.receber ? fmtNum(fc.receber) : '');
|
||
syncHeader();
|
||
PRE_FILL_TABLES.forEach(ensureOneRow);
|
||
Object.keys(TABLES).forEach(renderTable);
|
||
updateTotals();
|
||
return true;
|
||
} catch(e) { return false; }
|
||
}
|
||
function loadConfig() {
|
||
try {
|
||
const raw = localStorage.getItem(CFG_KEY);
|
||
if (raw) config = { ...config, ...JSON.parse(raw) };
|
||
} catch(e) {}
|
||
}
|
||
function saveConfig() {
|
||
config.admin_password = document.getElementById('cfg-admin-password').value.trim();
|
||
config.dynamic_password = document.getElementById('cfg-dynamic-password').value.trim();
|
||
try { localStorage.setItem(CFG_KEY, JSON.stringify(config)); } catch(e) {}
|
||
buildDatalists();
|
||
}
|
||
|
||
// ─── Config Modal ──────────────────────────────────────────────────────────
|
||
function openConfig() {
|
||
document.getElementById('modal-config').classList.add('open');
|
||
renderCfgList('operadores', 'cfg-operadores-list');
|
||
renderCfgList('gerentes', 'cfg-gerentes-list');
|
||
renderCfgList('despesas', 'cfg-despesas-list');
|
||
renderCfgList('clientes', 'cfg-clientes-list');
|
||
renderCfgList('vales_nomes', 'cfg-vales-list');
|
||
document.getElementById('cfg-admin-password').value = config.admin_password || '';
|
||
document.getElementById('cfg-dynamic-password').value = config.dynamic_password || '';
|
||
}
|
||
function closeConfig() { document.getElementById('modal-config').classList.remove('open'); }
|
||
function renderCfgList(key, containerId) {
|
||
const container = document.getElementById(containerId);
|
||
container.innerHTML = '';
|
||
config[key].forEach((item, i) => {
|
||
const div = document.createElement('div');
|
||
div.className = 'cfg-item';
|
||
div.innerHTML = `<input type="text" value="${item}" data-key="${key}" data-index="${i}" /><button class="btn-rm" data-key="${key}" data-index="${i}">✕</button>`;
|
||
container.appendChild(div);
|
||
});
|
||
container.querySelectorAll('.btn-rm').forEach(btn => {
|
||
btn.onclick = () => { const { key: k, index: idx } = btn.dataset; config[k].splice(parseInt(idx), 1); renderCfgList(k, containerId); };
|
||
});
|
||
container.querySelectorAll('input').forEach(inp => {
|
||
inp.addEventListener('change', () => { const { key: k, index: idx } = inp.dataset; config[k][parseInt(idx)] = inp.value; });
|
||
});
|
||
}
|
||
|
||
document.getElementById('btn-config').addEventListener('click', openConfig);
|
||
document.getElementById('modal-close').addEventListener('click', closeConfig);
|
||
document.getElementById('modal-config').addEventListener('click', e => { if (e.target.id === 'modal-config') closeConfig(); });
|
||
|
||
['operador','gerente','despesa','cliente','vales'].forEach((_, i) => {
|
||
const btnMap = ['cfg-add-operador','cfg-add-gerente','cfg-add-despesa','cfg-add-cliente','cfg-add-vales'];
|
||
const keyMap = ['operadores','gerentes','despesas','clientes','vales_nomes'];
|
||
const listMap = ['cfg-operadores-list','cfg-gerentes-list','cfg-despesas-list','cfg-clientes-list','cfg-vales-list'];
|
||
document.getElementById(btnMap[i]).addEventListener('click', () => {
|
||
const k = keyMap[i]; config[k].push(''); renderCfgList(k, listMap[i]);
|
||
const inputs = document.getElementById(listMap[i]).querySelectorAll('input');
|
||
inputs[inputs.length - 1].focus();
|
||
});
|
||
});
|
||
|
||
document.getElementById('cfg-salvar').addEventListener('click', () => { saveConfig(); closeConfig(); scheduleSave(); });
|
||
|
||
// ─── Tauri Commands ───────────────────────────────────────────────────────
|
||
let _saveTimer = null;
|
||
function scheduleSave() {
|
||
clearTimeout(_saveTimer);
|
||
_saveTimer = setTimeout(() => { saveState(); if (window.__TAURI__) tauriSaveLocal(); }, 1000);
|
||
}
|
||
|
||
async function tauriSaveLocal() {
|
||
try {
|
||
const data = buildEstadoForBackend();
|
||
await window.__TAURI__.core.invoke('salvar_fechamento', { estado: data });
|
||
} catch(e) { console.warn('salvar_fechamento:', e); }
|
||
}
|
||
|
||
async function tauriEnviar() {
|
||
const data = buildEstadoForBackend();
|
||
// 1. Salva local
|
||
await window.__TAURI__.core.invoke('salvar_fechamento', { estado: data }).catch(e => console.warn('local save err:', e));
|
||
// 2. Envia Supabase — debug completo
|
||
console.log('=== Enviando para Supabase ===');
|
||
console.log('URL:', 'https://supabase.ofrangao.com.br/rest/v1/fechamentos');
|
||
console.log('Payload:', JSON.stringify(data, null, 2));
|
||
try {
|
||
const result = await window.__TAURI__.core.invoke('sb_salvar_fechamento', { estado: data });
|
||
console.log('Supabase resposta:', JSON.stringify(result));
|
||
return result;
|
||
} catch(e) {
|
||
console.error('Erro sb_salvar_fechamento:', JSON.stringify(e));
|
||
// Tenta debug: qual era o payload exato?
|
||
console.error('Payload que falhou:', JSON.stringify(data));
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
function buildConfirmHTML(data) {
|
||
// Relatório completo — mesmo layout do PDF
|
||
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
|
||
const fmtNeg = v => (v < 0 ? '-' : '') + 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
|
||
// Diferença = fechamento - saldo_esperado - cancelamentos
|
||
const diferenca = data.diferenca !== undefined ? data.diferenca
|
||
: ((data.fechamento || 0) - (data.saldo_esperado || 0) - (data.cancelamentos || 0));
|
||
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmtNeg(diferenca);
|
||
const diffOk = diferenca === 0;
|
||
|
||
const sangrias_arr = Array.isArray(data.sangrias_arr) ? data.sangrias_arr : [];
|
||
const despesas_arr = Array.isArray(data.despesas_arr) ? data.despesas_arr : [];
|
||
const pixcnpj_arr = Array.isArray(data.pixcnpj_arr) ? data.pixcnpj_arr : [];
|
||
const vales_arr = Array.isArray(data.vales_arr) ? data.vales_arr : [];
|
||
const receber_arr = Array.isArray(data.receber_arr) ? data.receber_arr : [];
|
||
const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
|
||
|
||
const cartCred_arr = Array.isArray(data.cartoes_credito) ? data.cartoes_credito : [];
|
||
const cartDeb_arr = Array.isArray(data.cartoes_debito) ? data.cartoes_debito : [];
|
||
const cartAli_arr = Array.isArray(data.cartoes_alimentacao) ? data.cartoes_alimentacao : [];
|
||
const cartPix_arr = Array.isArray(data.cartoes_pix) ? data.cartoes_pix : [];
|
||
|
||
const totalSangrias = sangrias_arr.reduce((s,r) => s+(r.retirada||0)+(r.valor||0), 0);
|
||
const totalDespesas = despesas_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||
const totalPixCnpj = pixcnpj_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||
const totalVales = vales_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||
const totalReceber = receber_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||
const totalCancel = cancelamentos_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||
|
||
const cartCred = cartCred_arr.reduce((s,v) => s+(v||0), 0);
|
||
const cartDeb = cartDeb_arr.reduce((s,v) => s+(v||0), 0);
|
||
const cartAli = cartAli_arr.reduce((s,v) => s+(v||0), 0);
|
||
const cartPix = cartPix_arr.reduce((s,v) => s+(v||0), 0);
|
||
const totalCartoes = cartCred + cartDeb + cartAli + cartPix;
|
||
|
||
const fdin = data.fechamento_dinheiro || 0;
|
||
const fcarts = data.fechamento_cartoes || 0;
|
||
const frec = data.fechamento_receber || 0;
|
||
const ftotal = data.fechamento || (fdin + fcarts + frec);
|
||
const sysTotal = data.sys_total || 0;
|
||
const saldoEsperado = data.saldo_esperado || 0;
|
||
|
||
// Cartoes table
|
||
const maxRows = Math.max(cartCred_arr.length, cartDeb_arr.length, cartAli_arr.length, cartPix_arr.length);
|
||
let cartTableRows = '';
|
||
for (let i = 0; i < maxRows; i++) {
|
||
const cr = cartCred_arr[i] || 0;
|
||
const db = cartDeb_arr[i] || 0;
|
||
const al = cartAli_arr[i] || 0;
|
||
const px = cartPix_arr[i] || 0;
|
||
cartTableRows += `<tr>
|
||
<td>${cr > 0 ? fmt(cr) : '-'}</td>
|
||
<td>${db > 0 ? fmt(db) : '-'}</td>
|
||
<td>${al > 0 ? fmt(al) : '-'}</td>
|
||
<td>${px > 0 ? fmt(px) : '-'}</td>
|
||
</tr>`;
|
||
}
|
||
cartTableRows += `<tr class="total-row">
|
||
<td>${cartCred > 0 ? fmt(cartCred) : '-'}</td>
|
||
<td>${cartDeb > 0 ? fmt(cartDeb) : '-'}</td>
|
||
<td>${cartAli > 0 ? fmt(cartAli) : '-'}</td>
|
||
<td>${cartPix > 0 ? fmt(cartPix) : '-'}</td>
|
||
</tr>`;
|
||
|
||
let html = `<!DOCTYPE html>
|
||
<html><head>
|
||
<meta charset="UTF-8">
|
||
<title>Fechamento de Caixa</title>
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: 'Courier New', monospace; font-size: 18px; font-weight: bold !important; width: 80mm; margin: 0 auto; padding: 8px; font-weight: bold !important; print-color-adjust: exact; -webkit-print-color-adjust: exact; }
|
||
.header { text-align: center; margin-bottom: 8px; }
|
||
.header h1 { font-size: 24px; font-weight: bold !important; }
|
||
.header h2 { font-size: 18px; font-weight: bold !important; }
|
||
hr { border: none; border-top: 2px solid #000; margin: 6px 0; }
|
||
.section-title { font-weight: bold !important; font-size: 18px; border-top: 2px solid #000; padding-top: 4px; margin-top: 8px; }
|
||
.row { display: flex; justify-content: space-between; padding: 2px 0; font-size: 18px; font-weight: bold !important; }
|
||
.row.total-row { font-weight: bold !important; }
|
||
.cart-table { table-layout: fixed; width: 100%; border-collapse: collapse; font-size: 18px; margin-top: 4px; font-weight: bold !important; }
|
||
.cart-table th { font-weight: bold !important; border-bottom: 2px solid #000; padding: 3px; font-size: 18px; width: 25%; }
|
||
.cart-table td { padding: 3px 5px; text-align: right; font-weight: bold !important; font-size: 18px; }
|
||
.cart-table td:first-child { text-align: left; }
|
||
.cart-table tr.total-row { font-weight: bold !important; border-top: 2px solid #000; }
|
||
.subtotal { font-size: 18px; margin-top: 4px; }
|
||
.subtotal .row { padding: 1px 0; font-weight: bold !important; }
|
||
.diff-ok { color: #2e7d32; font-weight: bold !important; }
|
||
.diff-bad { color: #c0272d; font-weight: bold !important; }
|
||
.obs { font-size: 16px; color: #555; margin-top: 6px; white-space: pre-wrap; font-weight: bold !important; }
|
||
.footer { text-align: center; font-size: 14px; color: #555; margin-top: 8px; font-weight: bold !important; }
|
||
@media print { body { background: #fff; font-weight: bold !important; font-size: 18px !important; } }
|
||
</head><body>
|
||
<div class="header">
|
||
<h1>${data.loja}</h1>
|
||
<h2>Fechamento de Caixa</h2>
|
||
</div>
|
||
<hr>
|
||
<div class="row"><span>Data:</span><span>${data.data}</span></div>
|
||
<div class="row"><span>Operador:</span><span>${data.operador}</span></div>
|
||
<div class="row"><span>Turno:</span><span>${data.turno}</span></div>
|
||
|
||
${sangrias_arr.length > 0 ? `<div class="section-title">SANGRIAS</div>
|
||
${sangrias_arr.map(s => `<div class="row"><span>${(s.hora||'')}: ${s.gerente||''}</span><span>${fmt((s.retirada||0)+(s.valor||0))}</span></div>`).join('')}
|
||
<div class="row total-row"><span>TOTAL SANGRIAS:</span><span>${fmt(totalSangrias)}</span></div>` : ''}
|
||
|
||
${despesas_arr.length > 0 ? `<div class="section-title">DESPESAS</div>
|
||
${despesas_arr.map(d => `<div class="row"><span>${d.desc||''}${d.obs ? ` (${d.obs})` : ''}</span><span>${fmt(d.valor||0)}</span></div>`).join('')}
|
||
<div class="row total-row"><span>TOTAL DESPESAS:</span><span>${fmt(totalDespesas)}</span></div>` : ''}
|
||
|
||
${pixcnpj_arr.length > 0 ? `<div class="section-title">PIX CNPJ</div>
|
||
${pixcnpj_arr.map(p => `<div class="row"><span>${p.nome||''}</span><span>${fmt(p.valor||0)}</span></div>`).join('')}
|
||
<div class="row total-row"><span>TOTAL PIX CNPJ:</span><span>${fmt(totalPixCnpj)}</span></div>` : ''}
|
||
|
||
${vales_arr.length > 0 ? `<div class="section-title">VALES</div>
|
||
${vales_arr.map(v => `<div class="row"><span>${v.nome||''}${v.obs ? ` (${v.obs})` : ''}</span><span>${fmt(v.valor||0)}</span></div>`).join('')}
|
||
<div class="row total-row"><span>TOTAL VALES:</span><span>${fmt(totalVales)}</span></div>` : ''}
|
||
|
||
${receber_arr.length > 0 ? `<div class="section-title">A RECEBER</div>
|
||
${receber_arr.map(r => `<div class="row"><span>${r.nome||''}</span><span>${fmt(r.valor||0)}</span></div>`).join('')}
|
||
<div class="row total-row"><span>TOTAL A RECEBER:</span><span>${fmt(totalReceber)}</span></div>` : ''}
|
||
|
||
${cancelamentos_arr.length > 0 ? `<div class="section-title">CANCELAMENTOS</div>
|
||
${cancelamentos_arr.map(c => `<div class="row"><span>${c.numero ? `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>
|
||
|
||
<div class="section-title">CONTADORES</div>
|
||
<div class="row"><span>Clientes:</span><span>${data.clientes || 0}</span></div>
|
||
<div class="row"><span>Frango Assado:</span><span>${data.frango || 0}</span></div>
|
||
|
||
${data.observacoes ? `<div class="obs">${data.observacoes}</div>` : ''}
|
||
|
||
<div class="footer">Gerado em ${new Date().toLocaleString('pt-BR')}</div>
|
||
</body></html>`;
|
||
}
|
||
|
||
// ─── Print Preview Window ──────────────────────────────────────────────────
|
||
function buildPrintHTML(data) {
|
||
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
|
||
const fmtNeg = v => (v < 0 ? '-' : '') + 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
|
||
// Diferença = Total Sistema - Saldo Esperado - Cancelamentos
|
||
const sysTotalCalc = (data.sysTotal || 0);
|
||
const saldoEsperadoCalc = (data.saldo_esperado || 0);
|
||
const cancelamentosCalc = (data.cancelamentos || 0);
|
||
const diferenca = sysTotalCalc - saldoEsperadoCalc - cancelamentosCalc;
|
||
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmtNeg(diferenca);
|
||
const diffColor = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
|
||
|
||
// Arrays — support both array-of-objects and flat totals
|
||
const sangrias_arr = Array.isArray(data.sangrias_arr) ? data.sangrias_arr : [];
|
||
const despesas_arr = Array.isArray(data.despesas_arr) ? data.despesas_arr : [];
|
||
const pixcnpj_arr = Array.isArray(data.pixcnpj_arr) ? data.pixcnpj_arr : [];
|
||
const vales_arr = Array.isArray(data.vales_arr) ? data.vales_arr : [];
|
||
const receber_arr = Array.isArray(data.receber_arr) ? data.receber_arr : [];
|
||
const cancelamentos_arr = Array.isArray(data.cancelamentos_arr) ? data.cancelamentos_arr : [];
|
||
|
||
// Cartoes arrays from estado
|
||
const cartCred_arr = Array.isArray(data.cartoes_credito) ? data.cartoes_credito : [];
|
||
const cartDeb_arr = Array.isArray(data.cartoes_debito) ? data.cartoes_debito : [];
|
||
const cartAli_arr = Array.isArray(data.cartoes_alimentacao) ? data.cartoes_alimentacao : [];
|
||
const cartPix_arr = Array.isArray(data.cartoes_pix) ? data.cartoes_pix : [];
|
||
|
||
// Flat totals
|
||
const totalSangrias = sangrias_arr.reduce((s,r) => s+(r.retirada||0)+(r.valor||0), 0);
|
||
const totalDespesas = despesas_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||
const totalPixCnpj = pixcnpj_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||
const totalVales = vales_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||
const totalReceber = receber_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||
const totalCancel = cancelamentos_arr.reduce((s,r) => s+(r.valor||0), 0);
|
||
|
||
const cartCred = cartCred_arr.reduce((s,v) => s+(v||0), 0);
|
||
const cartDeb = cartDeb_arr.reduce((s,v) => s+(v||0), 0);
|
||
const cartAli = cartAli_arr.reduce((s,v) => s+(v||0), 0);
|
||
const cartPix = cartPix_arr.reduce((s,v) => s+(v||0), 0);
|
||
const totalCartoes = cartCred + cartDeb + cartAli + cartPix;
|
||
|
||
// Closed-form fields
|
||
const fdin = data.fechamento_dinheiro || 0;
|
||
const fcarts = data.fechamento_cartoes || 0;
|
||
const frec = data.fechamento_receber || 0;
|
||
const ftotal = data.fechamento || (fdin + fcarts + frec);
|
||
|
||
// Build section rows for sangrias, despesas, etc.
|
||
function section(title, lines, total, fmtFn) {
|
||
if (!lines.length && total <= 0) return '';
|
||
let html = `<div class="section-title">${title}</div>`;
|
||
lines.forEach(l => { if (l.valor > 0 || l.retirada > 0) { html += `<div class="row"><span>${l.label||''}</span><span>${fmtFn(l.valor||l.retirada||0)}</span></div>`; } });
|
||
if (total > 0) html += `<div class="row total-row"><span>TOTAL ${title}:</span><span>${fmtFn(total)}</span></div>`;
|
||
return html;
|
||
}
|
||
|
||
// Sangrias rows
|
||
const sangriaLines = sangrias_arr.map(s => ({
|
||
label: (s.hora ? s.hora + ': ' : '') + (s.gerente ? `(${s.gerente})` : ''),
|
||
valor: (s.retirada||0)+(s.valor||0)
|
||
})).filter(l => l.valor > 0);
|
||
|
||
// Despesas rows
|
||
const despLines = despesas_arr.map(d => ({
|
||
label: (d.desc||'') + (d.obs ? ` (${d.obs})` : ''),
|
||
valor: d.valor||0
|
||
})).filter(l => l.valor > 0);
|
||
|
||
// PIX CNPJ rows
|
||
const pixLines = pixcnpj_arr.map(p => ({
|
||
label: p.nome||'PIX CNPJ',
|
||
valor: p.valor||0
|
||
})).filter(l => l.valor > 0);
|
||
|
||
// Vales rows
|
||
const valesLines = vales_arr.map(v => ({
|
||
label: (v.nome||'') + (v.obs ? ` (${v.obs})` : ''),
|
||
valor: v.valor||0
|
||
})).filter(l => l.valor > 0);
|
||
|
||
// A Receber rows
|
||
const recLines = receber_arr.map(r => ({
|
||
label: r.nome||'',
|
||
valor: r.valor||0
|
||
})).filter(l => l.valor > 0);
|
||
|
||
// Cancelamentos rows
|
||
const cancLines = cancelamentos_arr.map(c => ({
|
||
label: (c.numero ? `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: 18px; font-weight: bold !important; width: 80mm; margin: 0 auto; padding: 8px; print-color-adjust: exact; -webkit-print-color-adjust: exact; }
|
||
.header { text-align: center; margin-bottom: 8px; }
|
||
.header h1 { font-size: 24px; font-weight: bold !important; }
|
||
.header h2 { font-size: 18px; font-weight: bold !important; }
|
||
hr { border: none; border-top: 2px solid #000; margin: 6px 0; }
|
||
.section-title { font-weight: bold !important; font-size: 18px; border-top: 2px solid #000; padding-top: 4px; margin-top: 8px; }
|
||
.row { display: flex; justify-content: space-between; padding: 2px 0; font-size: 18px; font-weight: bold !important; }
|
||
.row.total-row { font-weight: bold !important; }
|
||
.cart-table { table-layout: fixed; width: 100%; border-collapse: collapse; font-size: 18px; margin-top: 4px; font-weight: bold !important; }
|
||
.cart-table th { font-weight: bold !important; border-bottom: 2px solid #000; padding: 3px; font-size: 18px; width: 25%; }
|
||
.cart-table td { padding: 3px 5px; text-align: right; font-weight: bold !important; font-size: 18px; }
|
||
.cart-table td:first-child { text-align: left; }
|
||
.cart-table tr.total-row { font-weight: bold !important; border-top: 2px solid #000; }
|
||
.subtotal { font-size: 18px; margin-top: 4px; }
|
||
.subtotal .row { padding: 1px 0; font-weight: bold !important; }
|
||
.diff-ok { color: #2e7d32; font-weight: bold !important; }
|
||
.diff-bad { color: #c0272d; font-weight: bold !important; }
|
||
.obs { font-size: 16px; color: #555; margin-top: 6px; white-space: pre-wrap; font-weight: bold !important; }
|
||
.footer { text-align: center; font-size: 14px; color: #555; margin-top: 8px; font-weight: bold !important; }
|
||
@media print { body { background: #fff; font-weight: bold !important; font-size: 18px !important; } }
|
||
</style>
|
||
</head><body>
|
||
<div class="header">
|
||
<h1>${data.loja}</h1>
|
||
<h2>Fechamento de Caixa</h2>
|
||
</div>
|
||
<hr>
|
||
<div class="row"><span>Data:</span><span>${data.data}</span></div>
|
||
<div class="row"><span>Operador:</span><span>${data.operador}</span></div>
|
||
<div class="row"><span>Turno:</span><span>${data.turno}</span></div>
|
||
|
||
${sangriaLines.length > 0 ? `<div class="section-title">SANGRIAS</div>
|
||
${sangriaLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
|
||
<div class="row total-row"><span>TOTAL SANGRIAS:</span><span>${fmt(totalSangrias)}</span></div>` : ''}
|
||
|
||
${despLines.length > 0 ? `<div class="section-title">DESPESAS</div>
|
||
${despLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
|
||
<div class="row total-row"><span>TOTAL DESPESAS:</span><span>${fmt(totalDespesas)}</span></div>` : ''}
|
||
|
||
${pixLines.length > 0 ? `<div class="section-title">PIX CNPJ</div>
|
||
${pixLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
|
||
<div class="row total-row"><span>TOTAL PIX CNPJ:</span><span>${fmt(totalPixCnpj)}</span></div>` : ''}
|
||
|
||
${valesLines.length > 0 ? `<div class="section-title">VALES</div>
|
||
${valesLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
|
||
<div class="row total-row"><span>TOTAL VALES:</span><span>${fmt(totalVales)}</span></div>` : ''}
|
||
|
||
${recLines.length > 0 ? `<div class="section-title">A RECEBER</div>
|
||
${recLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
|
||
<div class="row total-row"><span>TOTAL A RECEBER:</span><span>${fmt(totalReceber)}</span></div>` : ''}
|
||
|
||
${cancLines.length > 0 ? `<div class="section-title">CANCELAMENTOS</div>
|
||
${cancLines.map(l => `<div class="row"><span>${l.label}</span><span>${fmt(l.valor)}</span></div>`).join('')}
|
||
<div class="row total-row"><span>TOTAL:</span><span>${fmt(totalCancel)}</span></div>` : ''}
|
||
|
||
<div class="section-title">DETALHAMENTO DE CARTÕES</div>
|
||
<table class="cart-table">
|
||
<thead>
|
||
<tr>
|
||
<th>CRÉDITO</th><th>DÉBITO</th><th>ALIMENT.</th><th>PIX</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${cartTableRows}
|
||
</tbody>
|
||
</table>
|
||
<div class="subtotal"><div class="row total-row"><span>Soma Cartões:</span><span>${fmt(totalCartoes)}</span></div></div>
|
||
|
||
<div class="section-title">SALDO DE CAIXA (SISTEMA) — RESUMO</div>
|
||
<div class="row"><span>Troco</span><span>${fmt(data.saldo_troco||0)}</span></div>
|
||
<div class="row"><span>Crédito</span><span>${fmt(cartCred)}</span></div>
|
||
<div class="row"><span>Débito</span><span>${fmt(cartDeb)}</span></div>
|
||
<div class="row"><span>Alimentação</span><span>${fmt(cartAli)}</span></div>
|
||
<div class="row"><span>Vales</span><span>${fmt(totalVales)}</span></div>
|
||
<div class="row"><span>À Receber</span><span>${fmt(totalReceber)}</span></div>
|
||
<div class="row"><span>PIX Cartão</span><span>${fmt(cartPix)}</span></div>
|
||
<div class="row"><span>PIX CNPJ</span><span>${fmt(totalPixCnpj)}</span></div>
|
||
<div class="row total-row"><span>TOTAL SISTEMA</span><span>${fmt(sysTotal)}</span></div>
|
||
<div class="row"><span>Saldo Esperado</span><span>${fmt(saldoEsperado)}</span></div>
|
||
<div class="row"><span>DIFERENÇA</span><span class="${diffOk ? 'diff-ok' : 'diff-bad'}">${diffStr}${diffOk ? ' ✓ OK' : ''}</span></div>
|
||
|
||
<div class="section-title">FECHAMENTO (CONTADO)</div>
|
||
<div class="row"><span>Dinheiro:</span><span>${fmt(fdin)}</span></div>
|
||
<div class="row"><span>Cartões:</span><span>${fmt(fcarts)}</span></div>
|
||
<div class="row"><span>À Receber:</span><span>${fmt(frec)}</span></div>
|
||
<div class="row total-row"><span>TOTAL:</span><span>${fmt(ftotal)}</span></div>
|
||
|
||
<div class="section-title">CONTADORES</div>
|
||
<div class="row"><span>Clientes:</span><span>${data.clientes || 0}</span></div>
|
||
<div class="row"><span>Frango Assado:</span><span>${data.frango || 0}</span></div>
|
||
<div class="row"><span>Vendas:</span><span>${fmt(data.vendas || 0)}</span></div>
|
||
|
||
${data.observacoes ? `<div class="obs">${data.observacoes}</div>` : ''}
|
||
|
||
<div class="footer">Gerado em ${new Date().toLocaleString('pt-BR')}</div>
|
||
</body></html>`;
|
||
|
||
return html;
|
||
}
|
||
|
||
// ─── Relatório de Impressão (layout exato do usuário) ──────────────────────
|
||
function buildReportHTML(data) {
|
||
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
|
||
const fmtNeg = v => (v < 0 ? '-' : '') + 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
|
||
|
||
const cartCred = data.credito || 0;
|
||
const cartDeb = data.debito || 0;
|
||
const cartAli = data.alimentacao || 0;
|
||
const cartPix = data.voucher || 0;
|
||
const sangrias = data.sangrias || 0;
|
||
const despesas = data.despesas || 0;
|
||
const vales = data.vales || 0;
|
||
const receber = data.areceber || 0;
|
||
const pixcnpj = data.pixcnpj || 0;
|
||
const cancelamentos = data.cancelamentos || 0;
|
||
const fechamento = data.fechamento || 0;
|
||
const sysTotal = data.sys_total || 0;
|
||
const saldoEsp = data.saldo_esperado || 0;
|
||
const diferenca = data.diferenca !== undefined ? data.diferenca
|
||
: (fechamento - saldoEsp - cancelamentos);
|
||
const diffStr = diferenca >= 0 ? `+${fmt(diferenca)}` : fmtNeg(diferenca);
|
||
const diffColor = diferenca === 0 ? '#2e7d32' : '#c0272d';
|
||
|
||
return `<!DOCTYPE html>
|
||
<html><head>
|
||
<meta charset="UTF-8">
|
||
<title>Relatório de Fechamento</title>
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: 'Courier New', monospace; font-size: 18px; font-weight: bold; width: 80mm; margin: 0 auto; padding: 8px; }
|
||
.header { text-align: center; margin-bottom: 10px; }
|
||
hr { border: none; border-top: 1px dashed #000; margin: 6px 0; }
|
||
.row { display: flex; justify-content: space-between; padding: 3px 0; font-size: 18px; font-weight: bold; }
|
||
.row .val { font-weight: bold; }
|
||
.footer { text-align: center; font-size: 13px; color: #555; margin-top: 8px; }
|
||
@media print { body { margin: 0; font-size: 18px !important; font-weight: bold !important; } }
|
||
</style>
|
||
</head><body>
|
||
<div class="header">
|
||
<div>📅 Data: ${data.data}</div>
|
||
<div>👤 Operador: ${data.operador}</div>
|
||
<div>⏰ Turno: ${data.turno}</div>
|
||
<div>🏪 Loja: ${data.loja}</div>
|
||
</div>
|
||
<hr>
|
||
<div class="row"><span>💸 Sangrias:</span><span class="val">${fmt(sangrias)}</span></div>
|
||
<div class="row"><span>📝 Despesas:</span><span class="val">${fmt(despesas)}</span></div>
|
||
<div class="row"><span>💰 Vales:</span><span class="val">${fmt(vales)}</span></div>
|
||
<div class="row"><span>👥 À Receber:</span><span class="val">${fmt(receber)}</span></div>
|
||
<div class="row"><span>🔵 PIX CNPJ:</span><span class="val">${fmt(pixcnpj)}</span></div>
|
||
<div class="row"><span>💳 Crédito:</span><span class="val">${fmt(cartCred)}</span></div>
|
||
<div class="row"><span>💳 Débito:</span><span class="val">${fmt(cartDeb)}</span></div>
|
||
<div class="row"><span>💳 Alimentação:</span><span class="val">${fmt(cartAli)}</span></div>
|
||
<div class="row"><span>❌ Cancelamentos:</span><span class="val">${fmt(cancelamentos)}</span></div>
|
||
<hr>
|
||
<div class="row"><span>📋 Fechamento (contado):</span><span class="val">${fmt(fechamento)}</span></div>
|
||
<div class="row"><span>🖥️ Total Sistema:</span><span class="val">${fmt(sysTotal)}</span></div>
|
||
<hr>
|
||
<div class="row"><span>🎯 Saldo Esperado (contado):</span><span class="val">${fmt(saldoEsp)}</span></div>
|
||
<div class="row"><span>📐 Diferença:</span><span class="val" style="color:${diffColor}">${diffStr}</span></div>
|
||
<hr>
|
||
<div class="row"><span>👥 Clientes:</span><span class="val">${data.clientes || 0}</span></div>
|
||
<div class="row"><span>🍗 Frango Assado:</span><span class="val">${data.frango || 0}</span></div>
|
||
<div class="row"><span>💰 Vendas:</span><span class="val">${fmt(data.vendas || 0)}</span></div>
|
||
<hr>
|
||
<div style="text-align:center;font-size:12px;color:#888">ID: ${data.loja}_${data.data}_${data.operador}</div>
|
||
<div class="footer">Gerado em ${new Date().toLocaleString('pt-BR')}</div>
|
||
</body></html>`;
|
||
}
|
||
|
||
function openPrintPreview(data, mode) {
|
||
const html = mode === 'confirm' ? buildConfirmHTML(data)
|
||
: mode === 'report' ? buildReportHTML(data)
|
||
: buildPrintHTML(data);
|
||
// Remove preview anterior se existir
|
||
const existing = document.getElementById('print-preview-overlay');
|
||
if (existing) document.body.removeChild(existing);
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.id = 'print-preview-overlay';
|
||
overlay.style.cssText = `
|
||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||
background: rgba(0,0,0,0.85); z-index: 99999;
|
||
display: flex; flex-direction: column; align-items: center;
|
||
font-family: 'Courier New', monospace;
|
||
`;
|
||
|
||
const previewStyle = `
|
||
<style>
|
||
body { width: 80mm; margin: 0 auto; font-family: 'Courier New', monospace; font-size: 6px; background: #fff; padding: 10px; font-weight: bold; }
|
||
.header { text-align: center; margin-bottom: 8px; }
|
||
.header h1 { font-size: 18px; font-weight: bold; }
|
||
.header h2 { font-size: 6px; font-weight: bold; }
|
||
hr { border: none; border-top: 1px dashed #000; margin: 6px 0; }
|
||
.section-title { font-weight: bold; font-size: 6px; border-top: 1px dashed #000; padding-top: 4px; margin-top: 6px; }
|
||
.row { display: flex; justify-content: space-between; padding: 2px 0; font-size: 6px; font-weight: bold; }
|
||
table { width: 100%; border-collapse: collapse; font-size: 6px; font-weight: bold; }
|
||
td { padding: 2px 4px; vertical-align: top; font-weight: bold; }
|
||
td:nth-child(2) { text-align: right; }
|
||
td:nth-child(3) { text-align: right; color: #555; font-size: 5px; }
|
||
.grand-total { font-size: 18px; font-weight: bold; text-align: center; padding: 6px; border: 2px solid #000; margin: 8px 0; }
|
||
.obs { font-size: 5px; color: #555; margin-top: 6px; white-space: pre-wrap; font-weight: normal; }
|
||
.footer { text-align: center; font-size: 5px; color: #555; margin-top: 8px; font-weight: normal; }
|
||
@media print { body { background: #fff; font-size: 6px !important; font-weight: bold !important; } }
|
||
</style>
|
||
`;
|
||
|
||
const fullHtml = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Recibo</title>${previewStyle}</head><body>${html}</body></html>`;
|
||
|
||
const iframe = document.createElement('iframe');
|
||
iframe.style.cssText = 'width:360px; height:600px; border:2px solid #555; margin-top:16px; background:#fff;';
|
||
iframe.id = 'print-iframe';
|
||
|
||
const btnFechar = document.createElement('button');
|
||
btnFechar.textContent = '✕ Fechar';
|
||
btnFechar.style.cssText = `
|
||
margin-top: 12px; padding: 10px 32px;
|
||
background: #444; color: #fff; border: none; border-radius: 6px;
|
||
font-size: 15px; cursor: pointer;
|
||
`;
|
||
btnFechar.onclick = () => {
|
||
document.body.removeChild(overlay);
|
||
};
|
||
|
||
const btnImprimir = document.createElement('button');
|
||
btnImprimir.textContent = '🖨️ Imprimir';
|
||
btnImprimir.style.cssText = `
|
||
margin-top: 8px; padding: 10px 32px;
|
||
background: #e65100; color: #fff; border: none; border-radius: 6px;
|
||
font-size: 15px; cursor: pointer;
|
||
`;
|
||
btnImprimir.onclick = () => {
|
||
iframe.contentWindow.focus();
|
||
iframe.contentWindow.print();
|
||
};
|
||
|
||
overlay.appendChild(iframe);
|
||
overlay.appendChild(btnImprimir);
|
||
overlay.appendChild(btnFechar);
|
||
document.body.appendChild(overlay);
|
||
|
||
const iframeDoc = iframe.contentWindow.document;
|
||
iframeDoc.open();
|
||
iframeDoc.write(fullHtml);
|
||
iframeDoc.close();
|
||
}
|
||
|
||
function triggerWindowPrint() {
|
||
// Chama window.print() na janela pai (o próprio app)
|
||
window.print();
|
||
}
|
||
|
||
// ─── buildReciboData — lê do FORMULÁRIO principal (não do estado carregado do BD) ───
|
||
// Importante: quando editing, o estado estápopulado pelo abrirFechamentoEdicao, não do BD
|
||
|
||
// ─── buildReciboData — lê do FORMULÁRIO principal ───────────────────────────
|
||
// usado tanto para novo fechamento quanto para edição
|
||
function buildReciboData() {
|
||
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
|
||
const cartDeb = estado.cartoes.debito.reduce((s,r) => s+(r.valor||0), 0);
|
||
const cartAli = estado.cartoes.alimentacao.reduce((s,r) => s+(r.valor||0), 0);
|
||
const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
|
||
const pixCnpjTotal = estado.pixcnpj.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const sangriasTotal = estado.sangrias.reduce((s,r) => s+(parseNum(r.retirada||0)||0), 0);
|
||
const despesasTotal = estado.despesas.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const valesTotal = estado.vales.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const receberTotal = estado.receber.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const cancelTotal = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
|
||
// Lê do DOM — mas se estiver vazio usa estado (registro anterior)
|
||
const trocoDom = parseNum(val('sys-troco'));
|
||
const saldoTroco = trocoDom || estado.saldo_troco || 0;
|
||
const saldoCred = parseNum(val('sys-credito')) || cartCred;
|
||
const saldoDeb = parseNum(val('sys-debito')) || cartDeb;
|
||
const saldoAli = parseNum(val('sys-alimentacao')) || cartAli;
|
||
const saldoPixCartao = parseNum(val('sys-pix')) || cartPix;
|
||
const saldoPixCnpj = parseNum(val('sys-pixcnpj')) || pixCnpjTotal;
|
||
const saldoVales = parseNum(val('sys-vales')) || valesTotal;
|
||
const saldoReceber = parseNum(val('sys-receber')) || receberTotal;
|
||
|
||
const fechamentoDinheiro = parseNum(val('f-fech-dinheiro')) || estado.fechamento_contado?.dinheiro || 0;
|
||
const fechamentoCartoes = parseNum(val('f-fech-cartoes')) || estado.fechamento_contado?.cartoes || 0;
|
||
const fechamentoReceber = parseNum(val('f-fech-receber')) || estado.fechamento_contado?.receber || 0;
|
||
const fechamentoCounted = fechamentoDinheiro + fechamentoCartoes + fechamentoReceber;
|
||
const saldoEsperado = parseNum(val('f-saldo-esperado')) || estado.saldo_esperado || 0;
|
||
|
||
// Saldo de Caixa (Sistema) = troco + crédito + débito + alimentação + voucher pix + pixcnpj + vales + receber
|
||
const sysTotal = saldoTroco + cartCred + cartDeb + cartAli + cartPix + pixCnpjTotal + valesTotal + receberTotal;
|
||
// Diferença = saldo de caixa (sistema) - saldo_esperado - cancelamentos
|
||
const diferenca = sysTotal - saldoEsperado - cancelTotal;
|
||
return {
|
||
uuid: estado.uuid,
|
||
id_local: estado.id_local,
|
||
id: estado.id,
|
||
loja: estado.loja,
|
||
data: estado.data,
|
||
operador: estado.operador,
|
||
turno: estado.turno,
|
||
saldo_troco: saldoTroco,
|
||
saldo_esperado: saldoEsperado,
|
||
fechamento: fechamentoCounted,
|
||
diferenca: diferenca,
|
||
sys_total: sysTotal,
|
||
fechamento_dinheiro: fechamentoDinheiro,
|
||
fechamento_cartoes: fechamentoCartoes,
|
||
fechamento_receber: fechamentoReceber,
|
||
// Flat totals for ReciboData struct (Rust) — must be numbers
|
||
credito: cartCred,
|
||
debito: cartDeb,
|
||
alimentacao: cartAli,
|
||
voucher: cartPix,
|
||
pixcnpj: pixCnpjTotal,
|
||
sangrias: sangriasTotal,
|
||
despesas: despesasTotal,
|
||
vales: valesTotal,
|
||
areceber: receberTotal,
|
||
cancelamentos: cancelTotal,
|
||
// Full arrays for preview / Supabase
|
||
despesas_arr: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
|
||
sangrias_arr: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
|
||
pixcnpj_arr: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||
vales_arr: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })),
|
||
receber_arr: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||
cancelamentos_arr: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
|
||
cartoes_credito: estado.cartoes.credito.map(r => r.valor || 0),
|
||
cartoes_debito: estado.cartoes.debito.map(r => r.valor || 0),
|
||
cartoes_alimentacao: estado.cartoes.alimentacao.map(r => r.valor || 0),
|
||
cartoes_pix: estado.cartoes.pix.map(r => r.valor || 0),
|
||
total_cartao_credito: cartCred,
|
||
total_cartao_debito: cartDeb,
|
||
total_cartao_alimentacao: cartAli,
|
||
total_cartao_pix: cartPix,
|
||
total_pixcnpj: pixCnpjTotal,
|
||
total_sangrias: sangriasTotal,
|
||
total_despesas: despesasTotal,
|
||
total_vales: valesTotal,
|
||
total_areceber: receberTotal,
|
||
total_cancelamentos: cancelTotal,
|
||
observacoes: estado.observacoes,
|
||
clientes: estado.clientes,
|
||
frango: estado.frango,
|
||
vendas: estado.vendas || 0,
|
||
edited_at: new Date().toISOString(),
|
||
edited_by: config.admin_user || 'admin',
|
||
sync_status: estado.sync_status,
|
||
};
|
||
}
|
||
|
||
async function tauriRecibo() {
|
||
const data = buildReciboData();
|
||
|
||
// Tenta ESC/POS via Tauri primeiro; se falhar, so lanca erro (preview tratado pelo caller)
|
||
try {
|
||
return await window.__TAURI__.core.invoke('imprimir_recibo', { data: JSON.stringify(data) });
|
||
} catch(e) {
|
||
console.warn('ESC/POS falhou:', e);
|
||
throw e; // re-lanca para o caller tratar o preview
|
||
}
|
||
}
|
||
|
||
async function tauriCarregarRascunho() {
|
||
const loja = val('f-loja') || 'União';
|
||
const data = val('f-data');
|
||
if (!data) return null;
|
||
try {
|
||
return await window.__TAURI__.core.invoke('carregar_rascunho', { loja, data });
|
||
} catch(e) { console.warn('carregar_rascunho:', e); return null; }
|
||
}
|
||
|
||
// ─── Confirm Modal ────────────────────────────────────────────────────────
|
||
function openConfirmModal() {
|
||
console.log('[DEBUG] openConfirmModal chamado');
|
||
const operador = val('f-operador').trim();
|
||
const turno = val('f-turno').trim();
|
||
const loja = val('f-loja').trim();
|
||
console.log('[DEBUG] openConfirmModal campos:', {operador, turno, loja});
|
||
if (!operador || !turno || !loja) {
|
||
console.log('[DEBUG] openConfirmModal: campos faltando, alert');
|
||
alert('⚠️ Preencha Operador, Turno e Loja antes de enviar.');
|
||
return;
|
||
}
|
||
console.log('[DEBUG] openConfirmModal: campos OK, montando modal');
|
||
// Build summary
|
||
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
|
||
const cartDeb = estado.cartoes.debito.reduce((s,r) => s+(r.valor||0), 0);
|
||
const cartAli = estado.cartoes.alimentacao.reduce((s,r) => s+(r.valor||0), 0);
|
||
const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
|
||
const sangrias = estado.sangrias.reduce((s,r) => s+(parseNum(r.retirada||0)||0), 0);
|
||
const despesas = estado.despesas.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const vales = estado.vales.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const receber = estado.receber.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const cancelamentos = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const pixcnpj = estado.pixcnpj.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const fechamento = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
|
||
const saldoEsp = parseNum(val('f-saldo-esperado'));
|
||
const sysTotal = parseNum(val('sys-troco')) + cartCred + cartDeb + cartAli + cartPix + vales + receber + pixcnpj;
|
||
const diferenca = sysTotal - saldoEsp - cancelamentos;
|
||
const diffClass = diferenca === 0 ? '#2e7d32' : (diferenca > 0 ? '#e65100' : '#c0272d');
|
||
document.getElementById('confirm-summary').innerHTML =
|
||
`<div style="margin-bottom:6px;font-size:17px"><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 style="font-size:16px">💸 Sangrias: <strong>${fmt(sangrias)}</strong></div>
|
||
<div style="font-size:16px">📝 Despesas: <strong>${fmt(despesas)}</strong></div>
|
||
<div style="font-size:16px">💰 Vales: <strong>${fmt(vales)}</strong></div>
|
||
<div style="font-size:16px">👥 À Receber: <strong>${fmt(receber)}</strong></div>
|
||
<div style="font-size:16px">🔵 PIX CNPJ: <strong>${fmt(pixcnpj)}</strong></div>
|
||
<div style="font-size:16px">💳 Crédito: <strong>${fmt(cartCred)}</strong></div>
|
||
<div style="font-size:16px">💳 Débito: <strong>${fmt(cartDeb)}</strong></div>
|
||
<div style="font-size:16px">💳 Alimentação: <strong>${fmt(cartAli)}</strong></div>
|
||
<div style="font-size:16px">❌ Cancelamentos: <strong>${fmt(cancelamentos)}</strong></div>
|
||
<div style="font-size:16px">👥 Clientes: <strong>${estado.clientes || 0}</strong></div>
|
||
<div style="font-size:16px">🍗 Frango Assado: <strong>${estado.frango || 0}</strong></div>
|
||
<div style="font-size:16px">💰 Vendas: <strong>${fmt(estado.vendas || 0)}</strong></div>
|
||
<div style="font-size:16px">📋 Fechamento (contado): <strong>${fmt(fechamento)}</strong></div>
|
||
<div style="font-size:16px">🖥️ Total Sistema: <strong>${fmt(sysTotal)}</strong></div>
|
||
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
|
||
<div style="font-size:16px">🎯 Saldo Esperado (contado): <strong>${fmt(saldoEsp)}</strong></div>
|
||
<div style="margin-top:4px;font-size:16px">📐 Diferença: <strong style="color:${diffClass}">${fmt(diferenca)}</strong></div>
|
||
<div style="margin-top:8px;font-size:12px;color:#888">ID: ${estado.loja}_${estado.data}_${estado.operador}</div>`;
|
||
console.log('[DEBUG] openConfirmModal: tentando abrir modal');
|
||
document.getElementById('modal-confirm').classList.add('open');
|
||
}
|
||
|
||
function closeConfirmModal() {
|
||
document.getElementById('modal-confirm').classList.remove('open');
|
||
}
|
||
|
||
document.getElementById('confirm-close').addEventListener('click', closeConfirmModal);
|
||
document.getElementById('confirm-cancel').addEventListener('click', closeConfirmModal);
|
||
document.getElementById('modal-confirm').addEventListener('click', e => { if (e.target.id === 'modal-confirm') closeConfirmModal(); });
|
||
|
||
// Print from confirm modal — abre preview idêntico ao modal de confirmação
|
||
document.getElementById('confirm-print').addEventListener('click', async () => {
|
||
// Usa buildReciboData para não duplicar lógica e ter todos os campos certos
|
||
let data;
|
||
try {
|
||
data = buildReciboData();
|
||
} catch(e) {
|
||
console.error('Erro ao montar dados do recibo:', e);
|
||
alert('Erro ao montar dados do recibo.');
|
||
return;
|
||
}
|
||
// Fecha modal APÓS gerar preview (não antes)
|
||
closeConfirmModal();
|
||
openPrintPreview(data, 'report');
|
||
});
|
||
|
||
// Confirm send
|
||
document.getElementById('confirm-send').addEventListener('click', async () => {
|
||
closeConfirmModal();
|
||
const btn = document.getElementById('btn-enviar');
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Enviando...';
|
||
try {
|
||
const r = await tauriEnviar();
|
||
estado.sync_status = 'synced';
|
||
saveState();
|
||
alert('✅ Enviado com sucesso!\n\n📋 Tabela: fechamentos_web\n🌐 Servidor: supabase.ofrangao.com.br\n\nAbra o Supabase Studio para verificar.');
|
||
} catch(e) {
|
||
await tauriSaveLocal();
|
||
const msg = e?.message || e?.toString?.() || String(e);
|
||
alert(
|
||
'⚠️ Erro ao enviar para o Supabase.\n\n' +
|
||
'📋 Tabela: fechamentos_web\n' +
|
||
'🌐 Servidor: supabase.ofrangao.com.br\n\n' +
|
||
'O servidor pode estar offline. Dados salvos localmente.\n\n' +
|
||
'Erro: ' + msg
|
||
);
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.textContent = '💾 Enviar Fechamento';
|
||
}
|
||
});
|
||
|
||
// ─── Buttons ───────────────────────────────────────────────────────────────
|
||
document.getElementById('f-obs').addEventListener('input', e => { estado.observacoes = e.target.value; scheduleSave(); });
|
||
document.getElementById('f-clientes').addEventListener('input', e => { estado.clientes = parseInt(e.target.value) || 0; scheduleSave(); });
|
||
document.getElementById('f-frango').addEventListener('input', e => { estado.frango = parseInt(e.target.value) || 0; scheduleSave(); });
|
||
|
||
document.getElementById('btn-recibo').addEventListener('click', async () => {
|
||
const btn = document.getElementById('btn-recibo');
|
||
btn.disabled = true;
|
||
try {
|
||
await tauriRecibo();
|
||
} catch(e) {
|
||
console.warn('ESC/POS falhou:', e);
|
||
}
|
||
try {
|
||
const data = buildReciboData();
|
||
openPrintPreview(data, 'recibo');
|
||
} catch(e2) {
|
||
console.error('Preview falhou:', e2);
|
||
alert('Erro ao gerar preview do recibo.');
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
});
|
||
|
||
document.getElementById('btn-limpar').addEventListener('click', () => {
|
||
const overlay = document.createElement('div');
|
||
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:999999;display:flex;align-items:center;justify-content:center;';
|
||
overlay.id = 'clear-modal';
|
||
overlay.innerHTML = `
|
||
<div style="background:#fff;border-radius:10px;padding:28px;width:340px;text-align:center">
|
||
<div style="font-size:20px;margin-bottom:12px">⚠️ Limpar Rascunho</div>
|
||
<div style="font-size:13px;color:#555;margin-bottom:20px;line-height:1.5">Tem certeza?<br>Irá <strong>apagar todas as informações</strong> lançadas para iniciar uma nova.</div>
|
||
<div style="display:flex;gap:10px;justify-content:center">
|
||
<button id="clear-cancel-btn" style="padding:9px 20px;background:#888;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px">Cancelar</button>
|
||
<button id="clear-ok-btn" style="padding:9px 20px;background:#c0272d;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px">Apagar tudo</button>
|
||
</div>
|
||
</div>`;
|
||
document.body.appendChild(overlay);
|
||
document.getElementById('clear-cancel-btn').addEventListener('click', () => overlay.remove());
|
||
document.getElementById('clear-ok-btn').addEventListener('click', () => {
|
||
overlay.remove();
|
||
localStorage.removeItem(LS_KEY);
|
||
estado = {
|
||
uuid: crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(),
|
||
loja: estado.loja, data: estado.data, operador: '', turno: '',
|
||
saldo_troco: 0, fechamento: 0, saldo_esperado: 0, diferenca: 0,
|
||
despesas: [], sangrias: [], pixcnpj: [], vales: [], receber: [], cancelamentos: [],
|
||
cartoes: { credito: [], debito: [], alimentacao: [], pix: [] },
|
||
fechamento_contado: { dinheiro: 0, cartoes: 0, receber: 0 },
|
||
observacoes: '', id_local: null, sync_status: 'local'
|
||
};
|
||
estado.sangrias = []; estado.despesas = []; estado.pixcnpj = [];
|
||
estado.vales = []; estado.receber = []; estado.cancelamentos = [];
|
||
estado.cartoes = { credito: [], debito: [], alimentacao: [], pix: [] };
|
||
estado.fechamento_contado = { dinheiro: 0, cartoes: 0, receber: 0 };
|
||
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('f-vendas', ''); setVal('f-clientes', ''); setVal('f-frango', '');
|
||
setVal('sys-troco', ''); setVal('sys-total', '');
|
||
setVal('sys-credito', ''); setVal('sys-debito', '');
|
||
setVal('sys-alimentacao', ''); setVal('sys-pixcnpj', '');
|
||
Object.keys(TABLES).forEach(renderTable);
|
||
updateTotals();
|
||
syncUI();
|
||
showToast('Rascunho limpo.', 'info');
|
||
});
|
||
});
|
||
|
||
// ─── Dia Anterior — seleção + consulta + edição de registros do Supabase ───
|
||
const btnAnterior = document.getElementById('btn-anterior');
|
||
if (btnAnterior) {
|
||
btnAnterior.addEventListener('click', async () => {
|
||
const hasTauri = !!(window.__TAURI__ && window.__TAURI__.core);
|
||
|
||
let recentList = [];
|
||
if (hasTauri) {
|
||
try {
|
||
const loja = val('f-loja') || 'União';
|
||
recentList = await window.__TAURI__.core.invoke('sb_listar_recentes', { loja, limite: 50 });
|
||
} catch(e) {
|
||
console.warn('sb_listar_recentes falhou:', e);
|
||
}
|
||
}
|
||
|
||
if (!recentList || recentList.length === 0) {
|
||
alert('Nenhum fechamento encontrado no servidor para esta loja.');
|
||
return;
|
||
}
|
||
|
||
buildConsultaModal(recentList);
|
||
});
|
||
}
|
||
|
||
// Monta o modal de consulta — lista de fechamentos do Supabase
|
||
function buildConsultaModal(recentList) {
|
||
const old = document.getElementById('modal-consulta');
|
||
if (old) document.body.removeChild(old);
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.id = 'modal-consulta';
|
||
overlay.style.cssText = `
|
||
position:fixed;top:0;left:0;right:0;bottom:0;
|
||
background:rgba(0,0,0,0.6);z-index:99998;
|
||
display:flex;align-items:center;justify-content:center;
|
||
`;
|
||
|
||
const fmtDate = d => {
|
||
if (!d) return '';
|
||
const [y,m,day] = d.split('-');
|
||
return `${day}/${m}/${y}`;
|
||
};
|
||
const fmtMoney = v => v != null ? `R$ ${parseFloat(v).toFixed(2).replace('.', ',')}` : 'R$ 0,00';
|
||
const statusBadge = s => {
|
||
if (s === 'ok') return '<span style="color:#2e7d32;font-weight:bold">✓ OK</span>';
|
||
if (s === 'sobrando') return '<span style="color:#e65100;font-weight:bold">⚠ SOBRANDO</span>';
|
||
if (s === 'faltando') return '<span style="color:#c0272d;font-weight:bold">⚠ FALTANDO</span>';
|
||
return s || '?';
|
||
};
|
||
|
||
// Calcula saldo de caixa real (mesma fórmula do relatório)
|
||
// SALDO CAIXA = soma dos campos individuais (total do sistema no relatório)
|
||
// Lê do top-level E do JSON dados (compatibilidade com registros antigos)
|
||
// SALDO CAIXA = soma dos campos individuais (total do sistema no relatório)
|
||
// Prioriza top-level, só usa dados.dados se top-level for 0/missing (compat old records)
|
||
const f = (item, dados, field) => parseFloat(item[field]||dados[field]||0);
|
||
const calcSaldoCaixa = item => {
|
||
const dados = item.dados || {};
|
||
return f(item,dados,'saldo_troco')
|
||
+ f(item,dados,'saldo_credito')
|
||
+ f(item,dados,'saldo_debito')
|
||
+ f(item,dados,'saldo_alimentacao')
|
||
+ f(item,dados,'saldo_vales')
|
||
+ f(item,dados,'saldo_areceber')
|
||
+ f(item,dados,'total_pixcnpj')
|
||
+ f(item,dados,'total_cartao_pix');
|
||
};
|
||
const calcCancelamentos = item => {
|
||
const dados = item.dados || {};
|
||
return (parseFloat(item.total_cancelamentos)||0)
|
||
+ (parseFloat(dados.total_cancelamentos)||0)
|
||
+ (parseFloat(dados.cancelamentos)||0); // old format: array of cancelamentos
|
||
};
|
||
|
||
// Ordena por data desc e pega os últimos 5 dias distintos
|
||
const sortedDesc = [...recentList].sort((a,b) => b.data.localeCompare(a.data));
|
||
const cincoDias = sortedDesc.slice(0, 5);
|
||
|
||
// Último dia distinto (para placeholder do input)
|
||
const ultimoDia = sortedDesc[0]?.data || '';
|
||
|
||
function renderRows(list) {
|
||
if (!list || list.length === 0) {
|
||
return `<tr><td colspan="8" style="padding:20px;text-align:center;color:#888;font-size:13px">Nenhum registro encontrado.</td></tr>`;
|
||
}
|
||
return list.map((item, i) => {
|
||
const saldoCaixa = calcSaldoCaixa(item);
|
||
const canc = calcCancelamentos(item);
|
||
const diff = saldoCaixa - (parseFloat(item.saldo_esperado)||0) - canc;
|
||
const diffClass = Math.abs(diff) <= 4.50 ? '#2e7d32' : diff > 0 ? '#e65100' : '#c0272d';
|
||
const diffLabel = Math.abs(diff) <= 4.50 ? `✓ OK (${diff===0?'0':(diff>0?'+':'-')+Math.abs(diff).toFixed(2).replace('.',',')})` : diff > 0 ? `+R$ ${diff.toFixed(2).replace('.',',')} SOBRANDO` : `-R$ ${Math.abs(diff).toFixed(2).replace('.',',')} FALTANDO`;
|
||
return `<tr data-idx="${recentList.indexOf(item)}" style="background:${i%2===0?'#fff':'#fafafa'}" onmouseover="this.style.background='#fff3e0'" onmouseout="this.style.background='${i%2===0?'#fff':'#fafafa'}'">
|
||
<td style="padding:6px 8px;border-bottom:1px solid #eee;font-size:12px;cursor:pointer" onclick="openConsultaRegistro(${recentList.indexOf(item)})">${fmtDate(item.data)}</td>
|
||
<td style="padding:6px 8px;border-bottom:1px solid #eee;font-size:12px;cursor:pointer" onclick="openConsultaRegistro(${recentList.indexOf(item)})">${item.operador||'?'}</td>
|
||
<td style="padding:6px 8px;border-bottom:1px solid #eee;font-size:12px;cursor:pointer" onclick="openConsultaRegistro(${recentList.indexOf(item)})">${item.turno||'?'}</td>
|
||
<td style="padding:6px 8px;border-bottom:1px solid #eee;font-size:12px;text-align:right;cursor:pointer" onclick="openConsultaRegistro(${recentList.indexOf(item)})">${fmtMoney(saldoCaixa)}</td>
|
||
<td style="padding:6px 8px;border-bottom:1px solid #eee;font-size:12px;text-align:right;cursor:pointer" onclick="openConsultaRegistro(${recentList.indexOf(item)})">${fmtMoney(item.saldo_esperado)}</td>
|
||
<td style="padding:6px 8px;border-bottom:1px solid #eee;font-size:12px;text-align:right;cursor:pointer" onclick="openConsultaRegistro(${recentList.indexOf(item)})">${fmtMoney(canc)}</td>
|
||
<td style="padding:6px 8px;border-bottom:1px solid #eee;font-size:12px;text-align:center;cursor:pointer;color:${diffClass};font-weight:bold" onclick="openConsultaRegistro(${recentList.indexOf(item)})">${diffLabel}</td>
|
||
<td style="padding:4px 6px;border-bottom:1px solid #eee;text-align:center">
|
||
<button onclick="event.stopPropagation();window.abrirEditaFechamentoIndex(${recentList.indexOf(item)})" style="background:#e65100;color:#fff;border:none;border-radius:4px;padding:4px 10px;cursor:pointer;font-size:11px;white-space:nowrap">✏️ Editar</button>
|
||
</td>
|
||
</tr>`;
|
||
}).join('');
|
||
}
|
||
|
||
overlay.innerHTML = `
|
||
<div style="background:#fff;border-radius:10px;padding:20px;max-width:900px;width:95%;max-height:85vh;overflow-y:auto">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-wrap:wrap;gap:8px">
|
||
<h2 style="margin:0;font-size:16px">📅 Consultar Fechamentos</h2>
|
||
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
||
<span style="font-size:12px;color:#555">De:</span>
|
||
<input type="date" id="consulta-busca-de" value="" max="${ultimoDia}" style="padding:6px 10px;border:1px solid #ccc;border-radius:6px;font-size:13px" />
|
||
<span style="font-size:12px;color:#555">Até:</span>
|
||
<input type="date" id="consulta-busca-ate" value="" max="${ultimoDia}" style="padding:6px 10px;border:1px solid #ccc;border-radius:6px;font-size:13px" />
|
||
<select id="consulta-busca-turno" style="padding:6px 10px;border:1px solid #ccc;border-radius:6px;font-size:13px;background:#fff">
|
||
<option value="">Todos turnos</option>
|
||
<option value="manha">Manhã</option>
|
||
<option value="tarde">Tarde</option>
|
||
<option value="integral">Integral</option>
|
||
</select>
|
||
<button onclick="window._filtrarConsulta()" style="background:#1565c0;color:#fff;border:none;border-radius:6px;padding:6px 14px;cursor:pointer;font-size:13px">🔍 Buscar</button>
|
||
<button onclick="closeConsultaModal()" style="background:none;border:none;font-size:18px;cursor:pointer;padding:4px 8px">✕</button>
|
||
</div>
|
||
</div>
|
||
<div id="consulta-info" style="font-size:11px;color:#888;margin-bottom:8px">Mostrando últimos 5 dias</div>
|
||
<div style="max-height:55vh;overflow-y:auto;border:1px solid #eee;border-radius:6px">
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead style="position:sticky;top:0;background:#f5f5f5;z-index:1">
|
||
<tr>
|
||
<th style="padding:6px 8px;text-align:left;font-size:11px;color:#888">DATA</th>
|
||
<th style="padding:6px 8px;text-align:left;font-size:11px;color:#888">OPERADOR</th>
|
||
<th style="padding:6px 8px;text-align:left;font-size:11px;color:#888">TURNO</th>
|
||
<th style="padding:6px 8px;text-align:right;font-size:11px;color:#888">SALDO CAIXA</th>
|
||
<th style="padding:6px 8px;text-align:right;font-size:11px;color:#888">ESPERADO</th>
|
||
<th style="padding:6px 8px;text-align:right;font-size:11px;color:#888">CANCEL.</th>
|
||
<th style="padding:6px 8px;text-align:center;font-size:11px;color:#888">DIFERENÇA</th>
|
||
<th style="padding:4px 6px;text-align:center;font-size:11px;color:#888">AÇÃO</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="consulta-tbody">${renderRows(cincoDias)}</tbody>
|
||
</table>
|
||
</div>
|
||
<div style="margin-top:8px;font-size:11px;color:#888;text-align:center">
|
||
Mostrando últimos 5 dias. Use a <strong>busca por data</strong> para encontrar registros mais antigos.
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(overlay);
|
||
|
||
// Função de busca por período
|
||
window._filtrarConsulta = () => {
|
||
const de = document.getElementById('consulta-busca-de')?.value;
|
||
const ate = document.getElementById('consulta-busca-ate')?.value;
|
||
const turno = document.getElementById('consulta-busca-turno')?.value || '';
|
||
const tbody = document.getElementById('consulta-tbody');
|
||
const info = document.getElementById('consulta-info');
|
||
if (!tbody) return;
|
||
let filtrados = [...recentList];
|
||
if (de) filtrados = filtrados.filter(item => item.data >= de);
|
||
if (ate) filtrados = filtrados.filter(item => item.data <= ate);
|
||
if (turno) filtrados = filtrados.filter(item => (item.turno||'').toLowerCase() === turno);
|
||
if (de || ate || turno) {
|
||
tbody.innerHTML = renderRows(filtrados);
|
||
const label = `${filtrados.length} registro${filtrados.length!==1?'s':''}${de&&ate?` de ${fmtDate(de)} a ${fmtDate(ate)}`:de?` a partir de ${fmtDate(de)}`:ate?` até ${fmtDate(ate)}`:''}`;
|
||
if (info) info.textContent = label;
|
||
} else {
|
||
tbody.innerHTML = renderRows(cincoDias);
|
||
if (info) info.textContent = 'Mostrando últimos 5 dias';
|
||
}
|
||
};
|
||
|
||
// Enter na busca
|
||
['consulta-busca-de','consulta-busca-ate'].forEach(id => {
|
||
document.getElementById(id)?.addEventListener('keydown', e => { if (e.key === 'Enter') window._filtrarConsulta(); });
|
||
});
|
||
|
||
window._recentList = recentList;
|
||
window._consultaAll = recentList;
|
||
}
|
||
|
||
// Abre a janela de edição completa de um registro pelo índice na lista
|
||
window.abrirEditaFechamentoIndex = async function(idx) {
|
||
const item = window._recentList ? window._recentList[idx] : null;
|
||
if (!item) return;
|
||
|
||
const hasTauri = !!(window.__TAURI__ && window.__TAURI__.core);
|
||
if (!hasTauri) { alert('Tauri não disponível.'); return; }
|
||
|
||
let full = null;
|
||
try {
|
||
if (item.id) {
|
||
try { full = await window.__TAURI__.core.invoke('sb_buscar_por_id', { id: item.id }); } catch(e) {}
|
||
}
|
||
if (!full && item.uuid) {
|
||
try { full = await window.__TAURI__.core.invoke('sb_buscar_por_uuid', { uuid: item.uuid }); } catch(e) {}
|
||
}
|
||
if (!full && item.id_fechamento) {
|
||
try { full = await window.__TAURI__.core.invoke('sb_buscar_por_id_fechamento', { idFechamento: item.id_fechamento }); } catch(e) {}
|
||
}
|
||
} catch(e) {
|
||
console.warn('Erro buscando registro:', e);
|
||
}
|
||
|
||
if (!full) { alert('Registro não encontrado.'); return; }
|
||
|
||
// Adiciona id_fechamento se não vier do banco
|
||
if (!full.id_fechamento && full.loja && full.data && full.operador) {
|
||
full.id_fechamento = (full.loja + '_' + full.data + '_' + full.operador).toLowerCase().replace(/ /g, '_');
|
||
}
|
||
|
||
closeConsultaModal();
|
||
if (!await askPassword()) return;
|
||
window.abrirEditaFechamento(full);
|
||
};
|
||
|
||
function closeConsultaModal() {
|
||
const m = document.getElementById('modal-consulta');
|
||
if (m) document.body.removeChild(m);
|
||
window._recentList = null;
|
||
window._currentConsulta = null;
|
||
}
|
||
|
||
// Abre o relatório de um registro selecionado
|
||
window.openConsultaRegistro = async function(idx) {
|
||
const item = window._recentList[idx];
|
||
if (!item) return;
|
||
closeConsultaModal();
|
||
|
||
const hasTauri = !!(window.__TAURI__ && window.__TAURI__.core);
|
||
if (!hasTauri) { alert('Tauri não disponível.'); return; }
|
||
|
||
let full = null;
|
||
try {
|
||
// 1. Tentar por id numérico (campo "id" da tabela, pk)
|
||
if (item.id) {
|
||
try { full = await window.__TAURI__.core.invoke('sb_buscar_por_id', { id: item.id }); } catch(e) {}
|
||
}
|
||
// 2. Tentar por uuid externo
|
||
if (!full && item.uuid) {
|
||
try { full = await window.__TAURI__.core.invoke('sb_buscar_por_uuid', { uuid: item.uuid }); } catch(e) {}
|
||
}
|
||
// 3. Tentar por id_fechamento (ex: uniao_2026-06-11_filipe)
|
||
if (!full && item.id_fechamento) {
|
||
try { full = await window.__TAURI__.core.invoke('sb_buscar_por_id_fechamento', { idFechamento: item.id_fechamento }); } catch(e) {}
|
||
}
|
||
} catch(e) {
|
||
console.warn('Erro buscando registro:', e);
|
||
}
|
||
|
||
if (!full) { alert('Registro não encontrado.'); return; }
|
||
|
||
// Preserva o id numerico (pk da tabela) antes de guardar o registro completo
|
||
window._currentConsulta = full;
|
||
window._currentConsultaId = item.id;
|
||
buildRelatorioModal(full);
|
||
};
|
||
|
||
// Monta o modal de relatório (readonly) com opção de editar
|
||
function buildRelatorioModal(full) {
|
||
const old = document.getElementById('modal-relatorio');
|
||
if (old) document.body.removeChild(old);
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.id = 'modal-relatorio';
|
||
overlay.style.cssText = `
|
||
position:fixed;top:0;left:0;right:0;bottom:0;
|
||
background:rgba(0,0,0,0.6);z-index:99998;
|
||
display:flex;align-items:center;justify-content:center;
|
||
`;
|
||
|
||
const fmt = v => v != null ? parseFloat(v).toFixed(2).replace('.', ',') : '0,00';
|
||
const fmtMoney = v => `R$ ${fmt(v)}`;
|
||
|
||
const dados = full.dados || {};
|
||
|
||
// Campos do sistema
|
||
const troco = parseFloat(full.saldo_troco || 0);
|
||
const credito = parseFloat(full.saldo_credito || 0);
|
||
const debito = parseFloat(full.saldo_debito || 0);
|
||
const alimentacao = parseFloat(full.saldo_alimentacao || 0);
|
||
const vales = parseFloat(full.saldo_vales || 0);
|
||
const pixCartao = parseFloat(full.total_cartao_pix || 0);
|
||
const pixCnpj = parseFloat(full.total_pixcnpj || 0);
|
||
const sysTotal = troco + credito + debito + alimentacao + vales + (parseFloat(full.saldo_areceber)||0) + pixCartao + pixCnpj;
|
||
|
||
// Campos contados
|
||
const fd = parseFloat(full.fechamento_dinheiro || 0);
|
||
const fc = parseFloat(full.fechamento_cartoes || 0);
|
||
const fr = parseFloat(full.fechamento_areceber || 0);
|
||
const fechamentoContado = fd + fc + fr;
|
||
|
||
// Despesas e sangrias
|
||
const totalDespesas = parseFloat(full.total_despesas || 0);
|
||
const totalSangrias = parseFloat(full.total_sangrias || 0);
|
||
const totalCancelamentos = parseFloat(full.total_cancelamentos || 0);
|
||
|
||
// Esperado e diferença — calculada: Total Sistema - Saldo Esperado - Cancelamentos
|
||
const saldoEsperado = parseFloat(full.saldo_esperado || 0);
|
||
const diferenca = sysTotal - saldoEsperado - totalCancelamentos;
|
||
const statusDif = diferenca === 0 ? 'ok' : (diferenca > 0 ? 'sobrando' : 'faltando');
|
||
|
||
const diffClass = statusDif === 'ok' ? '#2e7d32' : (statusDif === 'sobrando' ? '#e65100' : '#c0272d');
|
||
const diffLabel = statusDif === 'ok' ? '✓ OK' : (statusDif === 'sobrando' ? '⚠ SOBRANDO' : (statusDif === 'faltando' ? '⚠ FALTANDO' : statusDif));
|
||
|
||
const operador = full.operador || '?';
|
||
const turno = full.turno || '?';
|
||
const data = full.data || '?';
|
||
const loja = full.loja || '?';
|
||
const obs = full.observacoes || '';
|
||
const clientes = full.clientes || 0;
|
||
const frango = full.frango || 0;
|
||
const atualizado = full.atualizado_em ? new Date(full.atualizado_em).toLocaleString('pt-BR') : '?';
|
||
|
||
const totalGeral = fd + fc + fr;
|
||
|
||
overlay.innerHTML = `
|
||
<div id="relatorio-inner" style="background:#fff;border-radius:10px;padding:20px;max-width:520px;width:95%;max-height:90vh;overflow-y:auto;font-family:monospace;font-size:13px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
|
||
<h2 style="margin:0;font-size:15px">🧾 Relatório — ${loja.toUpperCase()}</h2>
|
||
<button onclick="closeRelatorioModal()" style="background:none;border:none;font-size:18px;cursor:pointer">✕</button>
|
||
</div>
|
||
|
||
<div style="background:#f8f8f8;border-radius:6px;padding:10px;margin-bottom:10px;font-size:12px">
|
||
<div><strong>📅 Data:</strong> ${data} <strong>Turno:</strong> ${turno} <strong>Operador:</strong> ${operador}</div>
|
||
<div><strong>Atualizado:</strong> ${atualizado}</div>
|
||
</div>
|
||
|
||
<div style="border:1px solid #ddd;border-radius:6px;overflow:hidden;margin-bottom:10px">
|
||
<div style="background:#e65100;color:#fff;padding:4px 8px;font-weight:bold;font-size:12px">FECHAMENTO DE CAIXA</div>
|
||
<div style="padding:8px">
|
||
<div style="display:flex;justify-content:space-between"><span>Troco (+)</span><span>${fmtMoney(troco)}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>Credito (+)</span><span>${fmtMoney(credito)}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>Debito (+)</span><span>${fmtMoney(debito)}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>Alimentacao (+)</span><span>${fmtMoney(alimentacao)}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>Vales (+)</span><span>${fmtMoney(vales)}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>A Receber (+)</span><span>${fmtMoney(parseFloat(full.saldo_areceber||0))}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>PIX Cartao (+)</span><span>${fmtMoney(pixCartao)}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>PIX CNPJ (+)</span><span>${fmtMoney(pixCnpj)}</span></div>
|
||
<div style="border-top:1px solid #ddd;margin-top:4px;padding-top:4px;display:flex;justify-content:space-between;font-weight:bold"><span>TOTAL SISTEMA</span><span style="color:#e65100">${fmtMoney(sysTotal)}</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="border:1px solid #ddd;border-radius:6px;overflow:hidden;margin-bottom:10px">
|
||
<div style="background:#1565c0;color:#fff;padding:4px 8px;font-weight:bold;font-size:12px">SALDO DE CAIXA (CONTADO)</div>
|
||
<div style="padding:8px">
|
||
<div style="display:flex;justify-content:space-between"><span>Dinheiro</span><span>${fmtMoney(fd)}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>Cartoes</span><span>${fmtMoney(fc)}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>A Receber</span><span>${fmtMoney(fr)}</span></div>
|
||
<div style="border-top:1px solid #ddd;margin-top:4px;padding-top:4px;display:flex;justify-content:space-between;font-weight:bold"><span>TOTAL CONTADO</span><span>${fmtMoney(totalGeral)}</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="border:1px solid #ddd;border-radius:6px;overflow:hidden;margin-bottom:10px">
|
||
<div style="background:#333;color:#fff;padding:4px 8px;font-weight:bold;font-size:12px">RESULTADO</div>
|
||
<div style="padding:8px">
|
||
<div style="display:flex;justify-content:space-between"><span>Saldo Esperado</span><span>${fmtMoney(saldoEsperado)}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>Cancelamentos</span><span>- ${fmtMoney(totalCancelamentos)}</span></div>
|
||
<div style="display:flex;justify-content:space-between;border-top:1px solid #ddd;margin-top:4px;padding-top:4px">
|
||
<span>Diferenca</span>
|
||
<span style="color:${diffClass};font-weight:bold">${fmtMoney(Math.abs(diferenca))} ${diferenca >= 0 ? '(+)' : '(-)'}</span>
|
||
</div>
|
||
<div style="text-align:center;margin-top:4px;font-weight:bold;color:${diffClass}">${diffLabel}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="border:1px solid #ddd;border-radius:6px;overflow:hidden;margin-bottom:10px">
|
||
<div style="background:#555;color:#fff;padding:4px 8px;font-weight:bold;font-size:12px">DEMAIS TOTAIS</div>
|
||
<div style="padding:8px">
|
||
<div style="display:flex;justify-content:space-between"><span>Total Sangrias</span><span>${fmtMoney(totalSangrias)}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>Total Despesas</span><span>${fmtMoney(totalDespesas)}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>Clientes</span><span>${clientes}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>Frango Assado</span><span>${frango}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>Vendas</span><span>${fmtMoney(full.vendas || 0)}</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
${obs ? `<div style="background:#fff8e1;border-radius:6px;padding:8px;margin-bottom:10px;font-size:12px"><strong>Obs:</strong> ${obs}</div>` : ''}
|
||
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<button onclick="window.printRelatorio()" style="flex:1;padding:8px;background:#1565c0;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px">🖨️ Imprimir</button>
|
||
<button onclick="window.toggleEditConsulta()" style="flex:1;padding:8px;background:#e65100;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px" id="btn-edit-consulta">✏️ Editar (senha)</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(overlay);
|
||
window._editMode = false;
|
||
}
|
||
|
||
// Fecha o modal de relatório
|
||
function closeRelatorioModal() {
|
||
const m = document.getElementById('modal-relatorio');
|
||
if (m) document.body.removeChild(m);
|
||
window._currentConsulta = null;
|
||
window._editMode = false;
|
||
}
|
||
|
||
// Restaura o estado completo do JSONB 'dados' e volta ao formulário em modo de edição
|
||
window.abrirFechamentoEdicao = function(full) {
|
||
const dados = full.dados || full;
|
||
if (!dados) { alert('Dados detalhados não encontrados neste registro.'); return; }
|
||
|
||
// IDs do registro — CRÍTICO para o salvamento via Supabase
|
||
estado.uuid = full.uuid || null;
|
||
estado.id = full.id || full.uuid || null;
|
||
|
||
// Preenche o estado global com todos os campos do JSONB
|
||
estado.loja = dados.loja || full.loja || '';
|
||
estado.data = dados.data || full.data || '';
|
||
estado.operador = dados.operador || full.operador || '';
|
||
estado.turno = dados.turno || full.turno || '';
|
||
estado.clientes = dados.clientes ?? full.clientes ?? 0;
|
||
estado.frango = dados.frango ?? full.frango ?? 0;
|
||
estado.vendas = dados.vendas ?? full.vendas ?? 0;
|
||
estado.observacoes = dados.observacoes || full.observacoes || '';
|
||
estado.saldo_esperado = dados.saldo_esperado ?? full.saldo_esperado ?? 0;
|
||
// Campos de sistema — vem dos campos flat do Supabase
|
||
estado.saldo_troco = parseFloat(full.saldo_troco || dados.saldo_troco || 0);
|
||
estado.saldo_credito = parseFloat(full.saldo_credito || dados.saldo_credito || 0);
|
||
estado.saldo_debito = parseFloat(full.saldo_debito || dados.saldo_debito || 0);
|
||
estado.saldo_alimentacao = parseFloat(full.saldo_alimentacao || dados.saldo_alimentacao || 0);
|
||
estado.saldo_vales = parseFloat(full.saldo_vales || dados.saldo_vales || 0);
|
||
estado.saldo_areceber = parseFloat(full.saldo_areceber || dados.saldo_areceber || 0);
|
||
estado.total_cartao_pix = parseFloat(full.total_cartao_pix || dados.total_cartao_pix || 0);
|
||
estado.total_pixcnpj = parseFloat(full.total_pixcnpj || dados.total_pixcnpj || 0);
|
||
|
||
// Listas detalhadas
|
||
estado.sangrias = (dados.sangrias || []).map(r => ({ hora: r.hora||r.Hora||'', retirada: r.retirada||r.Retirada||r.valor||0, gerente: r.gerente||r.Gerente||'' }));
|
||
estado.despesas = (dados.despesas || []).map(r => ({ desc: r.desc||r.Desc||'', obs: r.obs||r.Obs||'', valor: r.valor||r.Valor||0 }));
|
||
estado.vales = (dados.vales || []).map(r => ({ nome: r.nome||r.Nome||'', valor: r.valor||r.Valor||0 }));
|
||
estado.pixcnpj = (dados.pixcnpj || []).map(r => ({ desc: r.desc||'', valor: r.valor||r.Valor||0 }));
|
||
estado.receber = (dados.receber || []).map(r => ({ nome: r.nome||r.Nome||'', valor: r.valor||r.Valor||0 }));
|
||
estado.cancelamentos = (dados.cancelamentos || []).map(r => ({ numero: r.numero||r.Numero||'', valor: r.valor||r.Valor||0 }));
|
||
estado.cartoes = {
|
||
credito: (dados.cartoes?.credito || dados.cartoes_credito || []).map(r => ({ valor: typeof r === 'object' ? (r.valor ?? r.Valor ?? 0) : (parseFloat(r) || 0) })),
|
||
debito: (dados.cartoes?.debito || dados.cartoes_debito || []).map(r => ({ valor: typeof r === 'object' ? (r.valor ?? r.Valor ?? 0) : (parseFloat(r) || 0) })),
|
||
alimentacao:(dados.cartoes?.alimentacao|| dados.cartoes_alimentacao || []).map(r => ({ valor: typeof r === 'object' ? (r.valor ?? r.Valor ?? 0) : (parseFloat(r) || 0) })),
|
||
pix: (dados.cartoes?.pix || dados.cartoes_pix || []).map(r => ({ valor: typeof r === 'object' ? (r.valor ?? r.Valor ?? 0) : (parseFloat(r) || 0) }))
|
||
};
|
||
estado.pixcnpj = (dados.pixcnpj || []).map(r => ({ nome: r.nome || r.Nome || r.desc || r.Desc || '', valor: typeof r === 'object' ? (r.valor ?? r.Valor ?? 0) : (parseFloat(r) || 0) }));
|
||
estado.fechamento_contado = dados.fechamento_contado || { dinheiro: 0, cartoes: 0, receber: 0 };
|
||
|
||
// Fecha o modal de consulta
|
||
closeRelatorioModal();
|
||
closeConsultaModal();
|
||
|
||
// Campos de sistema (saldo de caixa)
|
||
setVal('sys-troco', fmtNum(estado.saldo_troco));
|
||
setVal('sys-credito', fmtNum(estado.saldo_credito));
|
||
setVal('sys-debito', fmtNum(estado.saldo_debito));
|
||
setVal('sys-alimentacao',fmtNum(estado.saldo_alimentacao));
|
||
setVal('sys-pixcnpj', fmtNum(estado.total_pixcnpj));
|
||
// Fechamento contado
|
||
setVal('f-fech-dinheiro', estado.fechamento_contado?.dinheiro || '');
|
||
setVal('f-fech-cartoes', estado.fechamento_contado?.cartoes || '');
|
||
setVal('f-fech-receber', estado.fechamento_contado?.receber || '');
|
||
setVal('f-saldo-esperado', estado.saldo_esperado || '');
|
||
|
||
// Preenche os campos do formulário
|
||
setVal('f-loja', estado.loja);
|
||
setVal('f-data', estado.data);
|
||
setVal('f-operador', estado.operador);
|
||
setVal('f-turno', estado.turno);
|
||
setVal('f-clientes', estado.clientes);
|
||
setVal('f-frango', estado.frango);
|
||
setVal('f-vendas', estado.vendas);
|
||
|
||
// Renderiza todas as tabelas com os dados restaurados
|
||
renderTable('tbl-sangrias', estado.sangrias);
|
||
renderTable('tbl-despesas', estado.despesas);
|
||
renderTable('tbl-vales', estado.vales);
|
||
renderTable('tbl-pix', estado.pixcnpj);
|
||
renderTable('tbl-receber', estado.receber);
|
||
renderTable('tbl-cancelamentos',estado.cancelamentos);
|
||
renderTable('tbl-cartao-credito', estado.cartoes.credito);
|
||
renderTable('tbl-cartao-debito', estado.cartoes.debito);
|
||
renderTable('tbl-cartao-alimentacao',estado.cartoes.alimentacao);
|
||
renderTable('tbl-cartao-pix', estado.cartoes.pix);
|
||
|
||
// Recalcula totais
|
||
updateTotals();
|
||
|
||
// Rola para o topo do formulário
|
||
window.scrollTo(0, 0);
|
||
|
||
alert('✅ Caixa carregado para edição. Verifique todos os valores antes de enviar.');
|
||
};
|
||
|
||
// Abre a tela de edição (modal separado) — requer senha admin
|
||
window.toggleEditConsulta = async function() {
|
||
const full = window._currentConsulta;
|
||
if (!full) return;
|
||
|
||
// Se já está editando, apenas fecha
|
||
if (window._editModalOpen) { closeEditModal(); return; }
|
||
|
||
// Pede senha — sempre, uma vez (aceita fixa ou dinâmica)
|
||
if (!await askPassword()) return;
|
||
|
||
openEditModal(full);
|
||
closeRelatorioModal();
|
||
};
|
||
|
||
// Calcula senha dinâmica do dia: dia e mês como 2 dígitos cada, concatenados
|
||
// ex: 17/12 → "1712"
|
||
function getDynamicPw() {
|
||
const d = new Date();
|
||
const day = String(d.getDate()).padStart(2, '0');
|
||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||
return day + month;
|
||
}
|
||
|
||
function askPassword() {
|
||
return new Promise(resolve => {
|
||
const fixedPw = config.admin_password;
|
||
const dynPw = getDynamicPw();
|
||
const overlay = document.createElement('div');
|
||
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:999999;display:flex;align-items:center;justify-content:center;';
|
||
overlay.id = 'pw-modal';
|
||
overlay.innerHTML = `
|
||
<div style="background:#fff;border-radius:10px;padding:24px;width:300px;text-align:center">
|
||
<div style="font-size:18px;margin-bottom:16px">🔐 Senha Admin</div>
|
||
<input type="password" id="pw-input" placeholder="Digite a senha..." style="width:100%;padding:10px;font-size:14px;border:1px solid #ccc;border-radius:6px;box-sizing:border-box;margin-bottom:12px" />
|
||
<div id="pw-error" style="color:#c0272d;font-size:12px;margin-bottom:8px;display:none">Senha incorreta</div>
|
||
<div style="display:flex;gap:8px;justify-content:center">
|
||
<button id="pw-cancel-btn" style="padding:8px 16px;background:#888;color:#fff;border:none;border-radius:6px;cursor:pointer">Cancelar</button>
|
||
<button id="pw-ok-btn" style="padding:8px 16px;background:#e65100;color:#fff;border:none;border-radius:6px;cursor:pointer">OK</button>
|
||
</div>
|
||
</div>`;
|
||
document.body.appendChild(overlay);
|
||
const inp = document.getElementById('pw-input');
|
||
inp.focus();
|
||
|
||
document.getElementById('pw-cancel-btn').addEventListener('click', () => {
|
||
document.getElementById('pw-modal').remove();
|
||
resolve(false);
|
||
});
|
||
inp.addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('pw-ok-btn').click(); });
|
||
document.getElementById('pw-ok-btn').addEventListener('click', () => {
|
||
const val = document.getElementById('pw-input').value;
|
||
if (val === fixedPw || val === dynPw || val === config.dynamic_password) {
|
||
document.getElementById('pw-modal').remove();
|
||
resolve(true);
|
||
} else {
|
||
document.getElementById('pw-error').style.display = 'block';
|
||
inp.value = '';
|
||
inp.focus();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Monta HTML de uma grade editável dentro do modal
|
||
function editTableHTML(id, headers, rows, numericCols) {
|
||
const fmtNum = v => {
|
||
const n = parseFloat(v);
|
||
return isNaN(n) ? '' : 'R$ ' + n.toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
||
};
|
||
const hdr = headers.map((h,i) => `<th style="padding:2px 4px;text-align:${numericCols.includes(i)?'right':'left'};font-size:10px;white-space:nowrap">${h}</th>`).join('');
|
||
const body = rows.map((row, ri) => {
|
||
const cells = row.map((val, ci) => {
|
||
const isNum = numericCols.includes(ci);
|
||
const displayVal = (val !== null && val !== undefined && val !== '')
|
||
? (isNum ? fmtNum(val) : val)
|
||
: '';
|
||
const extra = isNum
|
||
? `oninput="window._recalcEditTotals()" onblur="this.value=fmtBR(this)" `
|
||
: '';
|
||
return `<td style="padding:1px 2px"><input data-tbl="${id}" data-row="${ri}" data-col="${ci}" value="${displayVal}" ${extra}${isNum ? `style="width:100%;padding:2px;font-size:11px;text-align:right;border:1px solid #ccc;border-radius:2px"` : `style="width:100%;padding:2px;font-size:11px;border:1px solid #ccc;border-radius:2px"`} /></td>`;
|
||
}).join('');
|
||
return `<tr>${cells}</tr>`;
|
||
}).join('');
|
||
return `<table style="width:100%;border-collapse:collapse;font-size:11px;margin-bottom:4px"><thead><tr>${hdr}</tr></thead><tbody>${body}</tbody></table>`;
|
||
}
|
||
|
||
// Monta uma grade vazia com N linhas editáveis
|
||
function editTableEmpty(id, headers, nRows, numericCols) {
|
||
const emptyRows = Array.from({length: nRows}, () => headers.map(() => ''));
|
||
return editTableHTML(id, headers, emptyRows, numericCols);
|
||
}
|
||
|
||
// Adiciona linha a uma grade editável no modal
|
||
window._editAddRow = function(tblId) {
|
||
const container = document.getElementById('tbl-edit-' + tblId);
|
||
if (!container) return;
|
||
const tbl = container.querySelector('table');
|
||
if (!tbl) return;
|
||
const numericCols = container.dataset.numeric ? JSON.parse(container.dataset.numeric) : [];
|
||
const cols = tbl.querySelector('tr').cells.length;
|
||
const newRowIdx = tbl.rows.length;
|
||
const row = tbl.insertRow(-1);
|
||
for (let c = 0; c < cols; c++) {
|
||
const td = row.insertCell(-1);
|
||
const isNum = numericCols.includes(c);
|
||
td.innerHTML = `<input data-tbl="${tblId}" data-row="${newRowIdx}" data-col="${c}" value="" ${isNum ? `oninput="window._recalcEditTotals()" onblur="this.value=fmtBR(this)" style="width:100%;padding:2px;font-size:11px;text-align:right;border:1px solid #ccc;border-radius:2px"` : `style="width:100%;padding:2px;font-size:11px;border:1px solid #ccc;border-radius:2px"`} />`;
|
||
}
|
||
// Recalcula após adicionar
|
||
window._recalcEditTotals();
|
||
};
|
||
|
||
// Lê todos os dados das grades editáveis do modal
|
||
function readEditTables() {
|
||
const inputs = document.querySelectorAll('#modal-edit input[data-tbl]');
|
||
const tables = {};
|
||
inputs.forEach(inp => {
|
||
const tbl = inp.dataset.tbl;
|
||
const row = parseInt(inp.dataset.row);
|
||
const col = parseInt(inp.dataset.col);
|
||
if (!tables[tbl]) tables[tbl] = [];
|
||
while (tables[tbl].length <= row) tables[tbl].push([]);
|
||
tables[tbl][row][col] = inp.value;
|
||
});
|
||
// Converte valores numéricos
|
||
const numericCols = {
|
||
'credito': [0], 'debito': [0], 'alimentacao': [0], 'pix': [0],
|
||
'vales': [1], 'despesas': [2], 'sangrias': [1],
|
||
'cancelamentos': [1], 'pixcnpj': [1], 'receber': [1]
|
||
};
|
||
Object.keys(tables).forEach(tbl => {
|
||
const ncols = numericCols[tbl] || [];
|
||
tables[tbl] = tables[tbl].filter(row => row.some(c => c !== '' && c != null));
|
||
tables[tbl] = tables[tbl].map(row => row.map((val, ci) => {
|
||
if (ncols.includes(ci)) return parseNum(val);
|
||
return (val || '').trim();
|
||
}));
|
||
});
|
||
return tables;
|
||
}
|
||
|
||
// Recalcula todos os totais do modal de edição e atualiza o display
|
||
window._recalcEditTotals = function() {
|
||
const tables = readEditTables();
|
||
const cartCred_tot = (tables.credito || []).reduce((s, r) => s + (r[0] || 0), 0);
|
||
const cartDeb_tot = (tables.debito || []).reduce((s, r) => s + (r[0] || 0), 0);
|
||
const cartAli_tot = (tables.alimentacao || []).reduce((s, r) => s + (r[0] || 0), 0);
|
||
const cartPix_tot = (tables.pix || []).reduce((s, r) => s + (r[0] || 0), 0);
|
||
const vales_tot = (tables.vales || []).reduce((s, r) => s + (r[1] || 0), 0);
|
||
const desp_tot = (tables.despesas || []).reduce((s, r) => s + (r[2] || 0), 0);
|
||
const sang_tot = (tables.sangrias || []).reduce((s, r) => s + (r[1] || 0), 0);
|
||
const pixcnpj_tot = (tables.pixcnpj || []).reduce((s, r) => s + (r[1] || 0), 0);
|
||
const recv_tot = (tables.receber || []).reduce((s, r) => s + (r[1] || 0), 0);
|
||
const canc_tot = (tables.cancelamentos || []).reduce((s, r) => s + (r[1] || 0), 0);
|
||
|
||
const el = id => document.getElementById(id);
|
||
const fmtTot = v => v > 0 ? 'R$ ' + v.toLocaleString('pt-BR', { minimumFractionDigits: 2 }) : '—';
|
||
|
||
// Atualiza spans do total-box (prefixo "edit-total-") E do resumo rápido (prefixo "tot-")
|
||
if (el('edit-total-credito')) el('edit-total-credito').textContent = fmtTot(cartCred_tot);
|
||
if (el('edit-total-debito')) el('edit-total-debito').textContent = fmtTot(cartDeb_tot);
|
||
if (el('edit-total-alimentacao')) el('edit-total-alimentacao').textContent = fmtTot(cartAli_tot);
|
||
if (el('edit-total-pix')) el('edit-total-pix').textContent = fmtTot(cartPix_tot);
|
||
if (el('edit-total-vales')) el('edit-total-vales').textContent = fmtTot(vales_tot);
|
||
if (el('edit-total-despesas')) el('edit-total-despesas').textContent = fmtTot(desp_tot);
|
||
if (el('edit-total-sangrias')) el('edit-total-sangrias').textContent = fmtTot(sang_tot);
|
||
if (el('edit-total-pixcnpj')) el('edit-total-pixcnpj').textContent = fmtTot(pixcnpj_tot);
|
||
if (el('edit-total-receber')) el('edit-total-receber').textContent = fmtTot(recv_tot);
|
||
if (el('edit-total-cancel')) el('edit-total-cancel').textContent = fmtTot(canc_tot);
|
||
// Atualiza também os spans do total-box da popup (prefixo "tot-")
|
||
if (el('tot-credito')) el('tot-credito').textContent = fmtTot(cartCred_tot);
|
||
if (el('tot-debito')) el('tot-debito').textContent = fmtTot(cartDeb_tot);
|
||
if (el('tot-ali')) el('tot-ali').textContent = fmtTot(cartAli_tot);
|
||
if (el('tot-pix')) el('tot-pix').textContent = fmtTot(cartPix_tot);
|
||
if (el('tot-pixcnpj')) el('tot-pixcnpj').textContent = fmtTot(pixcnpj_tot);
|
||
if (el('tot-vales')) el('tot-vales').textContent = fmtTot(vales_tot);
|
||
if (el('tot-areceber')) el('tot-areceber').textContent = fmtTot(recv_tot);
|
||
if (el('tot-sangrias')) el('tot-sangrias').textContent = fmtTot(sang_tot);
|
||
if (el('tot-despesas')) el('tot-despesas').textContent = fmtTot(desp_tot);
|
||
if (el('tot-cancel')) el('tot-cancel').textContent = fmtTot(canc_tot);
|
||
|
||
// Atualiza campos readonly do saldo de caixa com totais calculados
|
||
const setReadonly = (id, v) => { if (el(id)) el(id).value = fmtTot(v); };
|
||
setReadonly('edit-sld-credito', cartCred_tot);
|
||
setReadonly('edit-sld-debito', cartDeb_tot);
|
||
setReadonly('edit-sld-alimentacao', cartAli_tot);
|
||
setReadonly('edit-sld-vales', vales_tot);
|
||
setReadonly('edit-sld-pixcnpj', pixcnpj_tot);
|
||
setReadonly('edit-sld-areceber', recv_tot);
|
||
setReadonly('edit-sld-pixcartao', cartPix_tot);
|
||
|
||
// Calcula Saldo Caixa e Diferença em tempo real
|
||
const trocoVal = parseNum(el('edit-troco')?.value || '0');
|
||
const esperadoVal = parseNum(el('edit-esperado')?.value || '0');
|
||
const saldoCaixa = trocoVal + cartCred_tot + cartDeb_tot + cartAli_tot + cartPix_tot + pixcnpj_tot + vales_tot + recv_tot;
|
||
const diferenca = saldoCaixa - esperadoVal - canc_tot;
|
||
|
||
if (el('edit-saldo-caixa')) {
|
||
el('edit-saldo-caixa').textContent = saldoCaixa > 0 ? fmtTot(saldoCaixa) : 'R$ 0,00';
|
||
}
|
||
if (el('edit-diferenca')) {
|
||
const fmtDif = v => v >= 0 ? `R$ ${v.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}` : `-R$ ${Math.abs(v).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`;
|
||
el('edit-diferenca').textContent = fmtDif(diferenca);
|
||
// Alerta visual se diferença != 0
|
||
if (el('edit-diferenca-box')) {
|
||
el('edit-diferenca-box').style.background = diferenca === 0 ? '#e8f5e9' : diferenca > 0 ? '#fff3e0' : '#ffebee';
|
||
}
|
||
}
|
||
};
|
||
|
||
// Modal de edição completo com todas as grades
|
||
function openEditModal(full) {
|
||
window._editModalOpen = true;
|
||
// Salva o registro ANTES de fechar o modal de relatório — usa variável separada para não conflitar
|
||
window._editingRecord = full;
|
||
|
||
const dados = full.dados || {};
|
||
|
||
// helper para fmt dinheiro
|
||
const fmtMR = v => {
|
||
const n = parseFloat(v||0);
|
||
if (isNaN(n)) return '';
|
||
if (n === 0) return 'R$ 0,00';
|
||
return 'R$ ' + n.toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
||
};
|
||
|
||
// helper inline para fmt table cell (já em editTableHTML)
|
||
|
||
const editedInfo = full.edited_at
|
||
? `<div style="background:#fff8e1;border-radius:4px;padding:6px 10px;font-size:11px;color:#666;margin-bottom:8px">
|
||
<strong>⚠️ Última edição:</strong> ${full.edited_at.replace('T',' ').substring(0,16)} por <strong>${full.edited_by||'?'}</strong>
|
||
</div>`
|
||
: '';
|
||
|
||
// --- Cartões ---
|
||
const cartCred_rows = (dados.cartoes?.credito || full.cartoes_credito || []).map(r => [typeof r === 'object' ? r.valor : r]);
|
||
const cartDeb_rows = (dados.cartoes?.debito || full.cartoes_debito || []).map(r => [typeof r === 'object' ? r.valor : r]);
|
||
const cartAli_rows = (dados.cartoes?.alimentacao || full.cartoes_alimentacao || []).map(r => [typeof r === 'object' ? r.valor : r]);
|
||
const cartPix_rows = (dados.cartoes?.pix || full.cartoes_pix || []).map(r => [typeof r === 'object' ? r.valor : r]);
|
||
|
||
const cartCredHTML = editTableHTML('credito', ['Valor (R$)'], cartCred_rows.length ? cartCred_rows : [['']], [0]);
|
||
const cartDebHTML = editTableHTML('debito', ['Valor (R$)'], cartDeb_rows.length ? cartDeb_rows : [['']], [0]);
|
||
const cartAliHTML = editTableHTML('alimentacao', ['Valor (R$)'], cartAli_rows.length ? cartAli_rows : [['']], [0]);
|
||
const cartPixHTML = editTableHTML('pix', ['Valor (R$)'], cartPix_rows.length ? cartPix_rows : [['']], [0]);
|
||
|
||
// --- Vales ---
|
||
const vales_rows = (dados.vales || []).map(r => [r.nome||'', r.valor||0, r.obs||'']);
|
||
const valesHTML = editTableHTML('vales', ['Nome', 'Valor (R$)', 'Obs'], vales_rows.length ? vales_rows : [['','','']], [1]);
|
||
|
||
// --- Despesas ---
|
||
const despesas_rows = (dados.despesas || []).map(r => [r.desc||'', r.obs||'', r.valor||0]);
|
||
const despesasHTML = editTableHTML('despesas', ['Descrição', 'Obs', 'Valor (R$)'], despesas_rows.length ? despesas_rows : [['','','']], [2]);
|
||
|
||
// --- Sangrias ---
|
||
const sangrias_rows = (dados.sangrias || []).map(r => [r.hora||'', r.retirada||r.valor||0, r.gerente||'']);
|
||
const sangriasHTML = editTableHTML('sangrias', ['Hora', 'Valor (R$)', 'Gerente'], sangrias_rows.length ? sangrias_rows : [['','','']], [1]);
|
||
|
||
// --- Cancelamentos ---
|
||
const cancel_rows = (dados.cancelamentos || []).map(r => [r.numero||'', r.valor||0, r.motivo||'']);
|
||
const cancelHTML = editTableHTML('cancelamentos', ['Número', 'Valor (R$)', 'Motivo'], cancel_rows.length ? cancel_rows : [['','','']], [1]);
|
||
|
||
// --- PIX CNPJ ---
|
||
const pixcnpj_rows = (dados.pixcnpj || []).map(r => [r.nome||r.desc||'', r.valor||0]);
|
||
const pixcnpjHTML = editTableHTML('pixcnpj', ['Nome', 'Valor (R$)'], pixcnpj_rows.length ? pixcnpj_rows : [['','']], [1]);
|
||
|
||
// --- A Receber ---
|
||
const receber_rows = (dados.receber || []).map(r => [r.nome||'', r.valor||0]);
|
||
const receberHTML = editTableHTML('receber', ['Nome', 'Valor (R$)'], receber_rows.length ? receber_rows : [['','']], [1]);
|
||
|
||
// --- Totais (só leitura formatado) — calcula dos dados reais ---
|
||
const totaisInfo = (() => {
|
||
const calcCred = (dados.cartoes?.credito || full.cartoes_credito || []).reduce((s,r) => s+(parseFloat(typeof r==='object'?r.valor:r)||0), 0);
|
||
const calcDeb = (dados.cartoes?.debito || full.cartoes_debito || []).reduce((s,r) => s+(parseFloat(typeof r==='object'?r.valor:r)||0), 0);
|
||
const calcAli = (dados.cartoes?.alimentacao || full.cartoes_alimentacao || []).reduce((s,r) => s+(parseFloat(typeof r==='object'?r.valor:r)||0), 0);
|
||
const calcPix = (dados.cartoes?.pix || full.cartoes_pix || []).reduce((s,r) => s+(parseFloat(typeof r==='object'?r.valor:r)||0), 0);
|
||
const calcSang = (dados.sangrias || []).reduce((s,r) => s+(parseFloat(r.retirada||r.valor||0)||0), 0);
|
||
const calcDesp = (dados.despesas || []).reduce((s,r) => s+(parseFloat(r.valor||0)||0), 0);
|
||
const calcVales = (dados.vales || []).reduce((s,r) => s+(parseFloat(r.valor||0)||0), 0);
|
||
const calcPixCnpj = (dados.pixcnpj || []).reduce((s,r) => s+(parseFloat(r.valor||0)||0), 0);
|
||
const calcRecv = (dados.receber || []).reduce((s,r) => s+(parseFloat(r.valor||0)||0), 0);
|
||
const calcCanc = (dados.cancelamentos || []).reduce((s,r) => s+(parseFloat(r.valor||0)||0), 0);
|
||
return `
|
||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:4px;background:#f5f5f5;border-radius:6px;padding:8px;margin-bottom:8px;font-size:11px">
|
||
<div><strong>Total Crédito:</strong> <span id="edit-total-credito">${fmtMR(calcCred)}</span></div>
|
||
<div><strong>Total Débito:</strong> <span id="edit-total-debito">${fmtMR(calcDeb)}</span></div>
|
||
<div><strong>Total Alimentação:</strong> <span id="edit-total-alimentacao">${fmtMR(calcAli)}</span></div>
|
||
<div><strong>Total PIX:</strong> <span id="edit-total-pix">${fmtMR(calcPix)}</span></div>
|
||
<div><strong>Total Sangrias:</strong> <span id="edit-total-sangrias">${fmtMR(calcSang)}</span></div>
|
||
<div><strong>Total Despesas:</strong> <span id="edit-total-despesas">${fmtMR(calcDesp)}</span></div>
|
||
<div><strong>Total Vales:</strong> <span id="edit-total-vales">${fmtMR(calcVales)}</span></div>
|
||
<div><strong>Total PIX CNPJ:</strong> <span id="edit-total-pixcnpj">${fmtMR(calcPixCnpj)}</span></div>
|
||
<div><strong>Total Receber:</strong> <span id="edit-total-receber">${fmtMR(calcRecv)}</span></div>
|
||
<div><strong>Total Cancel:</strong> <span id="edit-total-cancel">${fmtMR(calcCanc)}</span></div>
|
||
</div>`;
|
||
})();
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.id = 'modal-edit';
|
||
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:99998;overflow-y:auto';
|
||
overlay.innerHTML = `
|
||
<div style="background:#fff;margin:16px auto;border-radius:12px;padding:16px;max-width:800px;width:95%;font-size:13px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;position:sticky;top:0;background:#fff;z-index:1">
|
||
<h2 style="margin:0;font-size:15px;color:#e65100">✏️ Editar Fechamento Completo</h2>
|
||
<button onclick="closeEditModal()" style="background:none;border:none;font-size:18px;cursor:pointer;padding:4px 8px">✕</button>
|
||
</div>
|
||
|
||
${editedInfo}
|
||
|
||
<!-- Identificação -->
|
||
<div style="background:#f5f5f5;border-radius:6px;padding:8px;margin-bottom:10px">
|
||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:8px">
|
||
<div><label style="font-size:10px;color:#555">Data:</label><input type="date" id="edit-data" value="${full.data||''}" style="width:100%;padding:5px;border:1px solid #ccc;border-radius:4px" /></div>
|
||
<div><label style="font-size:10px;color:#555">Loja:</label>
|
||
<select id="edit-loja" style="width:100%;padding:5px;border:1px solid #ccc;border-radius:4px">
|
||
<option value="alianca" ${(full.loja||'').toLowerCase()==='alianca'?'selected':''}>Aliança</option>
|
||
<option value="uniao" ${(full.loja||'').toLowerCase()==='uniao'?'selected':''}>União</option>
|
||
</select>
|
||
</div>
|
||
<div><label style="font-size:10px;color:#555">Turno:</label>
|
||
<select id="edit-turno" style="width:100%;padding:5px;border:1px solid #ccc;border-radius:4px">
|
||
<option value="manha" ${(full.turno||'')==='manha'?'selected':''}>Manhã</option>
|
||
<option value="tarde" ${(full.turno||'')==='tarde'?'selected':''}>Tarde</option>
|
||
<option value="integral" ${(full.turno||'')==='integral'?'selected':''}>Integral</option>
|
||
</select>
|
||
</div>
|
||
<div><label style="font-size:10px;color:#555">Operador:</label><input type="text" id="edit-operador" value="${full.operador||''}" style="width:100%;padding:5px;border:1px solid #ccc;border-radius:4px" /></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Totais (leitura) -->
|
||
${totaisInfo}
|
||
|
||
<!-- Cartões de Crédito -->
|
||
<div style="margin-bottom:12px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
||
<strong style="font-size:12px;color:#1565c0">💳 Cartão de Crédito</strong>
|
||
<button onclick="_editAddRow('credito')" style="background:#1565c0;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
|
||
</div>
|
||
<div id="tbl-edit-credito" data-numeric="[0]">${cartCredHTML}</div>
|
||
</div>
|
||
|
||
<!-- Cartões de Débito -->
|
||
<div style="margin-bottom:12px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
||
<strong style="font-size:12px;color:#2e7d32">💳 Cartão de Débito</strong>
|
||
<button onclick="_editAddRow('debito')" style="background:#2e7d32;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
|
||
</div>
|
||
<div id="tbl-edit-debito" data-numeric="[0]">${cartDebHTML}</div>
|
||
</div>
|
||
|
||
<!-- Alimentação -->
|
||
<div style="margin-bottom:12px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
||
<strong style="font-size:12px;color:#e65100">🍽️ Alimentação</strong>
|
||
<button onclick="_editAddRow('alimentacao')" style="background:#e65100;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
|
||
</div>
|
||
<div id="tbl-edit-alimentacao" data-numeric="[0]">${cartAliHTML}</div>
|
||
</div>
|
||
|
||
<!-- PIX -->
|
||
<div style="margin-bottom:12px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
||
<strong style="font-size:12px;color:#7b1fa2">📱 PIX</strong>
|
||
<button onclick="_editAddRow('pix')" style="background:#7b1fa2;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
|
||
</div>
|
||
<div id="tbl-edit-pix" data-numeric="[0]">${cartPixHTML}</div>
|
||
</div>
|
||
|
||
<!-- Vales -->
|
||
<div style="margin-bottom:12px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
||
<strong style="font-size:12px;color:#c62828">📋 Vales</strong>
|
||
<button onclick="_editAddRow('vales')" style="background:#c62828;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
|
||
</div>
|
||
<div id="tbl-edit-vales" data-numeric="[1]">${valesHTML}</div>
|
||
</div>
|
||
|
||
<!-- Despesas -->
|
||
<div style="margin-bottom:12px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
||
<strong style="font-size:12px;color:#ef6c00">💸 Despesas</strong>
|
||
<button onclick="_editAddRow('despesas')" style="background:#ef6c00;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
|
||
</div>
|
||
<div id="tbl-edit-despesas" data-numeric="[2]">${despesasHTML}</div>
|
||
</div>
|
||
|
||
<!-- Sangrias -->
|
||
<div style="margin-bottom:12px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
||
<strong style="font-size:12px;color:#00695c">🏧 Sangrias</strong>
|
||
<button onclick="_editAddRow('sangrias')" style="background:#00695c;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
|
||
</div>
|
||
<div id="tbl-edit-sangrias" data-numeric="[1]">${sangriasHTML}</div>
|
||
</div>
|
||
|
||
<!-- PIX CNPJ -->
|
||
<div style="margin-bottom:12px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
||
<strong style="font-size:12px;color:#6a1b9a">PIX CNPJ</strong>
|
||
<button onclick="_editAddRow('pixcnpj')" style="background:#6a1b9a;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
|
||
</div>
|
||
<div id="tbl-edit-pixcnpj" data-numeric="[1]">${pixcnpjHTML}</div>
|
||
</div>
|
||
|
||
<!-- A Receber -->
|
||
<div style="margin-bottom:12px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
||
<strong style="font-size:12px;color:#283593">📬 A Receber</strong>
|
||
<button onclick="_editAddRow('receber')" style="background:#283593;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
|
||
</div>
|
||
<div id="tbl-edit-receber" data-numeric="[1]">${receberHTML}</div>
|
||
</div>
|
||
|
||
<!-- Cancelamentos -->
|
||
<div style="margin-bottom:12px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
||
<strong style="font-size:12px;color:#777">❌ Cancelamentos</strong>
|
||
<button onclick="_editAddRow('cancelamentos')" style="background:#777;color:#fff;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px">+ Linha</button>
|
||
</div>
|
||
<div id="tbl-edit-cancelamentos" data-numeric="[1]">${cancelHTML}</div>
|
||
</div>
|
||
|
||
<!-- Saldo de Caixa (SÓ TROCO é editável — restante é soma automática) -->
|
||
<div style="margin-bottom:12px;background:#f5f5f5;border-radius:6px;padding:8px">
|
||
<strong style="font-size:12px;color:#333;margin-bottom:4px;display:block">💰 Saldo de Caixa <span style="font-size:10px;color:#888">(apenas Troco é editável)</span></strong>
|
||
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:6px">
|
||
<div><label style="font-size:10px;color:#555">Troco:</label><input type="text" id="edit-troco" value="${fmtMR(full.saldo_troco)}" style="width:100%;padding:5px;border:1px solid #2e7d32;border-radius:4px;text-align:right" oninput="window._recalcEditTotals()" onblur="this.value=fmtBR(this)" /></div>
|
||
<div><label style="font-size:10px;color:#555">Saldo Esperado:</label><input type="text" id="edit-esperado" value="${fmtMR(full.saldo_esperado)}" style="width:100%;padding:5px;border:1px solid #e65100;border-radius:4px;text-align:right;font-weight:bold" oninput="window._recalcEditTotals()" onblur="this.value=fmtBR(this)" /></div>
|
||
<div><label style="font-size:10px;color:#888">Crédito:</label><input type="text" id="edit-sld-credito" value="${fmtMR(full.saldo_credito)}" readonly tabindex="-1" style="width:100%;padding:5px;border:1px solid #ddd;border-radius:4px;text-align:right;background:#eee;color:#666" /></div>
|
||
<div><label style="font-size:10px;color:#888">Débito:</label><input type="text" id="edit-sld-debito" value="${fmtMR(full.saldo_debito)}" readonly tabindex="-1" style="width:100%;padding:5px;border:1px solid #ddd;border-radius:4px;text-align:right;background:#eee;color:#666" /></div>
|
||
<div><label style="font-size:10px;color:#888">Alimentação:</label><input type="text" id="edit-sld-alimentacao" value="${fmtMR(full.saldo_alimentacao)}" readonly tabindex="-1" style="width:100%;padding:5px;border:1px solid #ddd;border-radius:4px;text-align:right;background:#eee;color:#666" /></div>
|
||
<div><label style="font-size:10px;color:#888">Vales:</label><input type="text" id="edit-sld-vales" value="${fmtMR(full.saldo_vales)}" readonly tabindex="-1" style="width:100%;padding:5px;border:1px solid #ddd;border-radius:4px;text-align:right;background:#eee;color:#666" /></div>
|
||
<div><label style="font-size:10px;color:#888">PIX CNPJ:</label><input type="text" id="edit-sld-pixcnpj" value="${fmtMR(full.total_pixcnpj)}" readonly tabindex="-1" style="width:100%;padding:5px;border:1px solid #ddd;border-radius:4px;text-align:right;background:#eee;color:#666" /></div>
|
||
<div><label style="font-size:10px;color:#888">À Receber:</label><input type="text" id="edit-sld-areceber" value="${fmtMR(full.saldo_areceber)}" readonly tabindex="-1" style="width:100%;padding:5px;border:1px solid #ddd;border-radius:4px;text-align:right;background:#eee;color:#666" /></div>
|
||
<div><label style="font-size:10px;color:#888">PIX Cartão:</label><input type="text" id="edit-sld-pixcartao" value="${fmtMR(full.total_cartao_pix)}" readonly tabindex="-1" style="width:100%;padding:5px;border:1px solid #ddd;border-radius:4px;text-align:right;background:#eee;color:#666" /></div>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:6px">
|
||
<div style="background:#e3f2fd;border-radius:4px;padding:6px 8px;text-align:right">
|
||
<span style="font-size:11px;color:#1565c0">SALDO CAIXA:</span>
|
||
<span id="edit-saldo-caixa" style="font-size:16px;font-weight:bold;color:#1565c0;margin-left:8px">R$ 0,00</span>
|
||
</div>
|
||
<div id="edit-diferenca-box" style="background:#fff3e0;border-radius:4px;padding:6px 8px;text-align:right">
|
||
<span style="font-size:11px;color:#e65100">DIFERENÇA:</span>
|
||
<span id="edit-diferenca" style="font-size:16px;font-weight:bold;color:#e65100;margin-left:8px">R$ 0,00</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Contadores -->
|
||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;margin-bottom:12px">
|
||
<div><label style="font-size:10px;color:#555">Clientes:</label><input type="number" id="edit-clientes" value="${dados.clientes||full.clientes||0}" min="0" style="width:100%;padding:5px;border:1px solid #ccc;border-radius:4px" /></div>
|
||
<div><label style="font-size:10px;color:#555">Frango Assado:</label><input type="number" id="edit-frango" value="${dados.frango||full.frango||0}" min="0" style="width:100%;padding:5px;border:1px solid #ccc;border-radius:4px" /></div>
|
||
<div><label style="font-size:10px;color:#555">Vendas (R$):</label><input type="text" id="edit-vendas" value="${fmtMR(dados.vendas||full.vendas||0)}" style="width:100%;padding:5px;border:1px solid #e65100;border-radius:4px;text-align:right;font-weight:bold" onblur="this.value=fmtBR(this)" /></div>
|
||
</div>
|
||
|
||
<!-- Observações -->
|
||
<div style="margin-bottom:12px">
|
||
<label style="font-size:10px;color:#555">Observações:</label>
|
||
<textarea id="edit-obs" style="width:100%;padding:5px;border:1px solid #ccc;border-radius:4px;font-size:13px;height:60px">${full.observacoes||''}</textarea>
|
||
</div>
|
||
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<button onclick="window.salvarEdicaoModal()" style="flex:1;padding:10px;background:#2e7d32;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;font-weight:bold">💾 Salvar Alterações</button>
|
||
<button onclick="closeEditModal()" style="padding:10px 16px;background:#888;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px">Cancelar</button>
|
||
</div>
|
||
</div>`;
|
||
|
||
document.body.appendChild(overlay);
|
||
// Calcula totais iniciais após renderizar o DOM
|
||
setTimeout(() => window._recalcEditTotals(), 50);
|
||
}
|
||
|
||
// Formata campo de dinheiro brasileiro
|
||
function fmtBR(inp) {
|
||
const v = parseNum(inp.value);
|
||
if (isNaN(v)) return '';
|
||
if (v === 0) return 'R$ 0,00';
|
||
return 'R$ ' + v.toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
||
}
|
||
|
||
// Fecha modal de edição
|
||
function closeEditModal() {
|
||
const m = document.getElementById('modal-edit');
|
||
if (m) document.body.removeChild(m);
|
||
window._editModalOpen = false;
|
||
}
|
||
|
||
// ─── Janela de edição completa (usa modal interno, não popup) ─────────────────
|
||
// Reaproveita openEditModal que já tem todas as tabelas editáveis,
|
||
// grades de cartões/vales/despesas/sangrias/pixcnjp/receber/cancelamentos,
|
||
// e conexão Tauri funcionando. Apenas pré-preenche o registro e abre.
|
||
window.abrirEditaFechamento = async function(full) {
|
||
if (!full) { alert('Registro não fornecido.'); return; }
|
||
// Garante id_fechamento
|
||
if (!full.id_fechamento && full.loja && full.data && full.operador) {
|
||
full.id_fechamento = (full.loja + '_' + full.data + '_' + full.operador).toLowerCase().replace(/ /g, '_');
|
||
}
|
||
|
||
// Atualiza estado com o registro carregado para que buildReciboData funcione corretamente
|
||
const dados = full.dados || {};
|
||
estado = {
|
||
uuid: full.uuid || crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(),
|
||
id_local: full.id_local || null,
|
||
id: full.id || null,
|
||
loja: full.loja || estado.loja,
|
||
data: full.data || estado.data,
|
||
operador: full.operador || '',
|
||
turno: full.turno || 'manha',
|
||
saldo_troco: parseNum(full.saldo_troco || 0),
|
||
fechamento: parseNum(full.fechamento_dinheiro || 0) + parseNum(full.fechamento_cartoes || 0) + parseNum(full.fechamento_areceber || 0),
|
||
saldo_esperado: parseNum(full.saldo_esperado || 0),
|
||
diferenca: parseNum(full.diferenca || 0),
|
||
clientes: parseInt(full.clientes || 0),
|
||
frango: parseInt(full.frango || 0),
|
||
vendas: parseNum(full.vendas || 0),
|
||
observacoes: full.observacoes || '',
|
||
sync_status: full.sync_status || 'synced',
|
||
// Cartões
|
||
cartoes: {
|
||
credito: (dados.cartoes?.credito || full.cartoes_credito || []).map(v => ({ valor: parseNum(v) || 0 })),
|
||
debito: (dados.cartoes?.debito || full.cartoes_debito || []).map(v => ({ valor: parseNum(v) || 0 })),
|
||
alimentacao: (dados.cartoes?.alimentacao || full.cartoes_alimentacao || []).map(v => ({ valor: parseNum(v) || 0 })),
|
||
pix: (dados.cartoes?.pix || full.cartoes_pix || []).map(v => ({ valor: parseNum(v) || 0 })),
|
||
},
|
||
// Demais arrays
|
||
vales: (dados.vales || full.vales || []).map(r => ({ nome: r.nome||'', valor: parseNum(r.valor)||0, obs: r.obs||'' })),
|
||
despesas: (dados.despesas || full.despesas || []).map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: parseNum(r.valor)||0 })),
|
||
sangrias: (dados.sangrias || full.sangrias || []).map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada||r.valor)||0, gerente: r.gerente||'' })),
|
||
cancelamentos: (dados.cancelamentos || full.cancelamentos || []).map(r => ({ numero: r.numero||'', valor: parseNum(r.valor)||0, motivo: r.motivo||'' })),
|
||
pixcnpj: (dados.pixcnpj || full.pixcnpj || []).map(r => ({ nome: r.nome||'', valor: parseNum(r.valor)||0 })),
|
||
receber: (dados.receber || full.receber || []).map(r => ({ nome: r.nome||'', valor: parseNum(r.valor)||0 })),
|
||
fechamento_contado: {
|
||
dinheiro: parseNum(full.fechamento_dinheiro || 0),
|
||
cartoes: parseNum(full.fechamento_cartoes || 0),
|
||
receber: parseNum(full.fechamento_areceber || 0),
|
||
},
|
||
};
|
||
|
||
// Preenche os campos do formulário principal para que o botão Recibo funcione
|
||
setVal('sys-troco', fmtNum(estado.saldo_troco));
|
||
setVal('sys-credito', fmtNum(estado.saldo_credito));
|
||
setVal('sys-debito', fmtNum(estado.saldo_debito));
|
||
setVal('sys-alimentacao', fmtNum(estado.saldo_alimentacao));
|
||
setVal('sys-pixcnpj', fmtNum(estado.total_pixcnpj));
|
||
setVal('f-saldo-esperado', estado.saldo_esperado || '');
|
||
setVal('f-obs', estado.observacoes || '');
|
||
setVal('f-operador', estado.operador || '');
|
||
setVal('f-turno', estado.turno || '');
|
||
setVal('f-clientes', estado.clientes || '');
|
||
setVal('f-frango', estado.frango || '');
|
||
setVal('f-vendas', estado.vendas || '');
|
||
setVal('f-loja', estado.loja || '');
|
||
setVal('f-data', estado.data || '');
|
||
setVal('f-fech-dinheiro', estado.fechamento_contado?.dinheiro || '');
|
||
setVal('f-fech-cartoes', estado.fechamento_contado?.cartoes || '');
|
||
setVal('f-fech-receber', estado.fechamento_contado?.receber || '');
|
||
|
||
// Renderiza todas as grades
|
||
renderTable('tbl-sangrias', estado.sangrias);
|
||
renderTable('tbl-despesas', estado.despesas);
|
||
renderTable('tbl-vales', estado.vales);
|
||
renderTable('tbl-pix', estado.pixcnpj);
|
||
renderTable('tbl-receber', estado.receber);
|
||
renderTable('tbl-cancelamentos', estado.cancelamentos);
|
||
renderTable('tbl-cartao-credito', estado.cartoes.credito);
|
||
renderTable('tbl-cartao-debito', estado.cartoes.debito);
|
||
renderTable('tbl-cartao-alimentacao',estado.cartoes.alimentacao);
|
||
renderTable('tbl-cartao-pix', estado.cartoes.pix);
|
||
|
||
// Atualiza totais
|
||
updateTotals();
|
||
|
||
// Usa o mesmo modal de edição completo que já funciona — só abre com o registro
|
||
openEditModal(full);
|
||
};
|
||
|
||
// Salva edições do modal de edição completo
|
||
window.salvarEdicaoModal = async function() {
|
||
console.log('[DEBUG] salvarEdicaoModal called');
|
||
const full = window._editingRecord;
|
||
if (!full) { console.log('[DEBUG] _editingRecord is null'); alert('Nenhum registro selecionado.'); return; }
|
||
console.log('[DEBUG] full.uuid:', full.uuid, 'full.id:', full.id);
|
||
|
||
const p = v => parseNum(v);
|
||
const inp = id => { const el = document.getElementById(id); return el ? el.value : ''; };
|
||
|
||
// Lê todas as grades editáveis
|
||
const tables = readEditTables();
|
||
|
||
// Calcula totais dos cartões
|
||
const cartCred_tot = (tables.credito || []).reduce((s, r) => s + (r[0] || 0), 0);
|
||
const cartDeb_tot = (tables.debito || []).reduce((s, r) => s + (r[0] || 0), 0);
|
||
const cartAli_tot = (tables.alimentacao || []).reduce((s, r) => s + (r[0] || 0), 0);
|
||
const cartPix_tot = (tables.pix || []).reduce((s, r) => s + (r[0] || 0), 0);
|
||
const vales_tot = (tables.vales || []).reduce((s, r) => s + (r[1] || 0), 0);
|
||
const desp_tot = (tables.despesas || []).reduce((s, r) => s + (r[2] || 0), 0);
|
||
const sang_tot = (tables.sangrias || []).reduce((s, r) => s + (r[1] || 0), 0);
|
||
const pixcnpj_tot = (tables.pixcnpj || []).reduce((s, r) => s + (r[1] || 0), 0);
|
||
const recv_tot = (tables.receber || []).reduce((s, r) => s + (r[1] || 0), 0);
|
||
const canc_tot = (tables.cancelamentos || []).reduce((s, r) => s + (r[1] || 0), 0);
|
||
|
||
// Monta estruturas de volta
|
||
const cartoes = {
|
||
credito: (tables.credito || []).map(r => r[0]),
|
||
debito: (tables.debito || []).map(r => r[0]),
|
||
alimentacao: (tables.alimentacao || []).map(r => r[0]),
|
||
pix: (tables.pix || []).map(r => r[0]),
|
||
};
|
||
|
||
const vales = (tables.vales || []).map(r => ({ nome: r[0], valor: r[1], obs: r[2]||'' }));
|
||
const despesas = (tables.despesas || []).map(r => ({ desc: r[0], obs: r[1]||'', valor: r[2] }));
|
||
const sangrias = (tables.sangrias || []).map(r => ({ hora: r[0], retirada: r[1], gerente: r[2]||'' }));
|
||
const cancelamentos = (tables.cancelamentos || []).map(r => ({ numero: r[0], valor: r[1], motivo: r[2]||'' }));
|
||
const pixcnpj = (tables.pixcnpj || []).map(r => ({ nome: r[0], valor: r[1] }));
|
||
const receber = (tables.receber || []).map(r => ({ nome: r[0], valor: r[1] }));
|
||
|
||
const now = new Date().toISOString();
|
||
const operador = estado.operador || full.operador || '';
|
||
|
||
const updates = {
|
||
// Chave natural — essencial para o Supabase encontrar o registro
|
||
id_fechamento: inp('edit-loja') + '_' + inp('edit-data') + '_' + inp('edit-operador'),
|
||
|
||
// Identificação
|
||
loja: inp('edit-loja'),
|
||
data: inp('edit-data'),
|
||
turno: inp('edit-turno'),
|
||
operador: inp('edit-operador'),
|
||
|
||
// Cartões (como arrays de números crus)
|
||
cartoes_credito: cartoes.credito,
|
||
cartoes_debito: cartoes.debito,
|
||
cartoes_alimentacao: cartoes.alimentacao,
|
||
cartoes_pix: cartoes.pix,
|
||
|
||
// Totais de cartões
|
||
total_cartao_credito: cartCred_tot,
|
||
total_cartao_debito: cartDeb_tot,
|
||
total_cartao_alimentacao: cartAli_tot,
|
||
total_cartao_pix: cartPix_tot,
|
||
|
||
// Saldo de caixa — soma automática dos itens editados (não dos campos readonly)
|
||
saldo_troco: p(inp('edit-troco')),
|
||
saldo_credito: cartCred_tot,
|
||
saldo_debito: cartDeb_tot,
|
||
saldo_alimentacao: cartAli_tot,
|
||
saldo_vales: vales_tot,
|
||
saldo_areceber: recv_tot,
|
||
|
||
// Totais
|
||
total_vales: vales_tot,
|
||
total_despesas: desp_tot,
|
||
total_sangrias: sang_tot,
|
||
total_pixcnpj: pixcnpj_tot,
|
||
total_areceber: recv_tot,
|
||
total_cancelamentos: canc_tot,
|
||
total_cartao_pix: cartPix_tot,
|
||
|
||
// Diferença calculada
|
||
diferenca: p(inp('edit-troco')) + cartCred_tot + cartDeb_tot + cartAli_tot + cartPix_tot + pixcnpj_tot + vales_tot + recv_tot
|
||
- p(inp('edit-esperado')) - canc_tot,
|
||
|
||
// Fechamento (campos calculados que o formato antigo usava no dados)
|
||
fechamento_dinheiro: p(inp('edit-troco')) + vales_tot + sang_tot - desp_tot - canc_tot,
|
||
fechamento_cartoes: cartCred_tot + cartDeb_tot + cartAli_tot + cartPix_tot,
|
||
fechamento_areceber: recv_tot,
|
||
|
||
// Lançamentos detalhados (JSONB — gravados no campo `dados`)
|
||
// Merge completo com dados existentes para preservar campos antigos (fc, fd, fr,
|
||
// uuid, id_local, sync_status, fechamento, etc.) e sobrepor com valores atuais.
|
||
dados: (() => {
|
||
const existing = full.dados;
|
||
const base = {
|
||
cartoes,
|
||
vales,
|
||
despesas,
|
||
sangrias,
|
||
cancelamentos,
|
||
pixcnpj,
|
||
receber,
|
||
clientes: parseInt(inp('edit-clientes') || '0'),
|
||
frango: parseInt(inp('edit-frango') || '0'),
|
||
vendas: p(inp('edit-vendas')),
|
||
// Campos que o formato antigo gravava no dados
|
||
credito: cartCred_tot,
|
||
debito: cartDeb_tot,
|
||
alimentacao: cartAli_tot,
|
||
fc: cartCred_tot + cartDeb_tot + cartAli_tot + cartPix_tot,
|
||
fd: sang_tot,
|
||
fr: vales_tot,
|
||
fechamento: p(inp('edit-troco')) + vales_tot + sang_tot - desp_tot - canc_tot,
|
||
saldo_troco: p(inp('edit-troco')),
|
||
total_vales: vales_tot,
|
||
total_despesas: desp_tot,
|
||
total_sangrias: sang_tot,
|
||
total_pixcnpj: pixcnpj_tot,
|
||
total_areceber: recv_tot,
|
||
total_cancelamentos: canc_tot,
|
||
total_cartao_credito: cartCred_tot,
|
||
total_cartao_debito: cartDeb_tot,
|
||
total_cartao_alimentacao: cartAli_tot,
|
||
total_cartao_pix: cartPix_tot,
|
||
saldo_esperado: p(inp('edit-esperado')),
|
||
observacoes: inp('edit-obs') || '',
|
||
data: inp('edit-data') || full.data || '',
|
||
loja: inp('edit-loja') || full.loja || '',
|
||
turno: inp('edit-turno') || full.turno || '',
|
||
operador: inp('edit-operador') || full.operador || '',
|
||
};
|
||
if (typeof existing === 'string') {
|
||
try {
|
||
const parsed = JSON.parse(existing);
|
||
return { ...parsed, ...base };
|
||
} catch { return base; }
|
||
}
|
||
return { ...(existing || {}), ...base };
|
||
})(),
|
||
|
||
// Contadores
|
||
clientes: parseInt(inp('edit-clientes') || '0'),
|
||
frango: parseInt(inp('edit-frango') || '0'),
|
||
vendas: p(inp('edit-vendas')),
|
||
|
||
// Meta
|
||
saldo_esperado: p(inp('edit-esperado')),
|
||
observacoes: inp('edit-obs'),
|
||
|
||
// Edit log
|
||
edited_at: now,
|
||
edited_by: operador,
|
||
};
|
||
|
||
const hasTauri = !!(window.__TAURI__ && window.__TAURI__.core);
|
||
if (!hasTauri) { alert('Tauri não disponível.'); return; }
|
||
|
||
try {
|
||
// Usa id_fechamento que é a chave natural do registro
|
||
const id_fechamento = full.id_fechamento || (full.loja && full.data && full.operador
|
||
? `${full.loja}_${full.data}_${full.operador}`.toLowerCase().replace(/ /g, '_')
|
||
: null);
|
||
if (!id_fechamento) { alert('id_fechamento não encontrado.'); return; }
|
||
console.log('[DEBUG] salvarEdicaoModal id_fechamento:', id_fechamento, 'updates:', JSON.stringify(updates));
|
||
|
||
console.log('[DEBUG] invoking sb_atualizar_fechamento...');
|
||
const result = await window.__TAURI__.core.invoke('sb_atualizar_fechamento', { id_fechamento, updates });
|
||
console.log('[DEBUG] invoke result:', result);
|
||
closeEditModal();
|
||
closeRelatorioModal();
|
||
alert('✅ Atualizado com sucesso!');
|
||
} catch(e) {
|
||
console.error('Erro ao salvar:', e);
|
||
alert('Erro ao salvar: ' + (e?.message || e));
|
||
}
|
||
};
|
||
|
||
// Salva edições de volta ao Supabase
|
||
window.salvarEdicaoConsulta = async function() {
|
||
const full = window._currentConsulta;
|
||
if (!full) return;
|
||
|
||
const p = v => parseFloat((v || '0').replace(',', '.') || '0');
|
||
const inp = id => document.getElementById(id).value;
|
||
|
||
const updates = {
|
||
saldo_troco: p(inp('edit-troco')),
|
||
saldo_credito: p(inp('edit-credito')),
|
||
saldo_debito: p(inp('edit-debito')),
|
||
saldo_alimentacao: p(inp('edit-alimentacao')),
|
||
saldo_vales: p(inp('edit-vales')),
|
||
total_cartao_pix: p(inp('edit-pixcartao')),
|
||
total_pixcnpj: p(inp('edit-pixcnpj')),
|
||
saldo_areceber: p(inp('edit-areceber')),
|
||
fechamento_dinheiro: p(inp('edit-fd')),
|
||
fechamento_cartoes: p(inp('edit-fc')),
|
||
fechamento_areceber: p(inp('edit-fr')),
|
||
total_sangrias: p(inp('edit-sangrias')),
|
||
total_despesas: p(inp('edit-despesas')),
|
||
clientes: parseInt(inp('edit-clientes') || '0'),
|
||
frango: parseInt(inp('edit-frango') || '0'),
|
||
vendas: p(inp('edit-vendas')),
|
||
saldo_esperado: p(inp('edit-esperado')),
|
||
observacoes: inp('edit-obs'),
|
||
edited_at: new Date().toISOString(),
|
||
edited_by: full.operador || '',
|
||
};
|
||
|
||
const hasTauri = !!(window.__TAURI__ && window.__TAURI__.core);
|
||
if (!hasTauri) { alert('Tauri não disponível.'); return; }
|
||
|
||
try {
|
||
const id = document.getElementById('edit-id').value || window._currentConsultaId || full.id;
|
||
if (!id) { alert('ID do registro não encontrado. Não é possível atualizar.'); return; }
|
||
|
||
await window.__TAURI__.core.invoke('sb_atualizar_fechamento', { id, updates });
|
||
alert('✅ Atualizado com sucesso!');
|
||
closeRelatorioModal();
|
||
} catch(e) {
|
||
console.error('Erro ao salvar:', e);
|
||
alert('Erro ao salvar: ' + (e?.message || e));
|
||
}
|
||
};
|
||
|
||
// Converte o registro "full" do Supabase para o formato usado pelo buildPrintHTML
|
||
function consultaToPrintData(full) {
|
||
const fmtDiff = v => 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
|
||
const fmtDiffNeg = v => (v < 0 ? '-' : '') + 'R$ ' + Math.abs(v || 0).toFixed(2).replace('.', ',');
|
||
const fmt = v => 'R$ ' + (v || 0).toFixed(2).replace('.', ',');
|
||
|
||
const cartCred = parseFloat(full.saldo_credito || 0);
|
||
const cartDeb = parseFloat(full.saldo_debito || 0);
|
||
const cartAli = parseFloat(full.saldo_alimentacao || 0);
|
||
const cartPix = parseFloat(full.total_cartao_pix || 0);
|
||
const pixCnpj = parseFloat(full.total_pixcnpj || 0);
|
||
const vales = parseFloat(full.saldo_vales || 0);
|
||
const receber = parseFloat(full.saldo_areceber || 0);
|
||
const troco = parseFloat(full.saldo_troco || 0);
|
||
const sysTotal = troco + cartCred + cartDeb + cartAli + cartPix + pixCnpj + vales + receber;
|
||
const fd = parseFloat(full.fechamento_dinheiro || 0);
|
||
const fc = parseFloat(full.fechamento_cartoes || 0);
|
||
const fr = parseFloat(full.fechamento_areceber || 0);
|
||
const ftotal = fd + fc + fr;
|
||
const saldoEsp = parseFloat(full.saldo_esperado || 0);
|
||
const diferenca = parseFloat(full.diferenca || 0);
|
||
const diffOk = diferenca === 0;
|
||
const diffStr = diferenca >= 0 ? `+${fmtDiff(diferenca)}` : fmtDiffNeg(diferenca);
|
||
const dados = full.dados || {};
|
||
|
||
const despesas_arr = (dados.despesas || []).map(d => ({ desc: d.desc || '', obs: d.obs || '', valor: parseFloat(d.valor || 0) }));
|
||
const sangrias_arr = (dados.sangrias || []).map(s => ({ hora: s.hora || '', retirada: parseFloat(s.retirada || 0), gerente: s.gerente || '', valor: parseFloat(s.valor || 0) }));
|
||
const pixcnpj_arr = (dados.pixcnpj || []).map(p => ({ nome: p.nome || '', valor: parseFloat(p.valor || 0) }));
|
||
const vales_arr = (dados.vales || []).map(v => ({ nome: v.nome || '', obs: v.obs || '', valor: parseFloat(v.valor || 0) }));
|
||
const receber_arr = (dados.receber || []).map(r => ({ nome: r.nome || '', valor: parseFloat(r.valor || 0) }));
|
||
const cancelamentos_arr = (dados.cancelamentos || []).map(c => ({ numero: c.numero || '', valor: parseFloat(c.valor || 0), motivo: c.motivo || '' }));
|
||
const cartoes_credito = (dados.cartoes?.credito || full.cartoes_credito || []).map(v => parseFloat(typeof v === 'object' ? (v.valor ?? 0) : (v || 0)));
|
||
const cartoes_debito = (dados.cartoes?.debito || full.cartoes_debito || []).map(v => parseFloat(typeof v === 'object' ? (v.valor ?? 0) : (v || 0)));
|
||
const cartoes_alimentacao = (dados.cartoes?.alimentacao || full.cartoes_alimentacao || []).map(v => parseFloat(typeof v === 'object' ? (v.valor ?? 0) : (v || 0)));
|
||
const cartoes_pix = (dados.cartoes?.pix || full.cartoes_pix || []).map(v => parseFloat(typeof v === 'object' ? (v.valor ?? 0) : (v || 0)));
|
||
const totalSangrias = sangrias_arr.reduce((s, r) => s + r.retirada + r.valor, 0);
|
||
const totalDespesas = despesas_arr.reduce((s, r) => s + r.valor, 0);
|
||
const totalPixCnpj = pixcnpj_arr.reduce((s, r) => s + r.valor, 0);
|
||
const totalVales = vales_arr.reduce((s, r) => s + r.valor, 0);
|
||
const totalReceber = receber_arr.reduce((s, r) => s + r.valor, 0);
|
||
const totalCancel = cancelamentos_arr.reduce((s, r) => s + r.valor, 0);
|
||
const totalCartoes = cartCred + cartDeb + cartAli + cartPix;
|
||
|
||
return {
|
||
loja: full.loja || '?',
|
||
data: full.data || '?',
|
||
operador: full.operador || '?',
|
||
turno: full.turno || '?',
|
||
saldo_troco: troco,
|
||
saldo_esperado: saldoEsp,
|
||
fechamento: ftotal,
|
||
diferenca: diferenca,
|
||
sys_total: sysTotal,
|
||
fechamento_dinheiro: fd,
|
||
fechamento_cartoes: fc,
|
||
fechamento_areceber: fr,
|
||
credito: cartCred,
|
||
debito: cartDeb,
|
||
alimentacao: cartAli,
|
||
voucher: cartPix,
|
||
pixcnpj: pixCnpj,
|
||
vales: vales,
|
||
areceber: receber,
|
||
cancelamentos: totalCancel,
|
||
despesas_arr,
|
||
sangrias_arr,
|
||
pixcnpj_arr,
|
||
vales_arr,
|
||
receber_arr,
|
||
cancelamentos_arr,
|
||
cartoes_credito,
|
||
cartoes_debito,
|
||
cartoes_alimentacao,
|
||
cartoes_pix,
|
||
totalSangrias,
|
||
totalDespesas,
|
||
totalPixCnpj,
|
||
totalVales,
|
||
totalReceber,
|
||
totalCancel,
|
||
totalCartoes,
|
||
sysTotal,
|
||
saldoEsperado: saldoEsp,
|
||
diffStr,
|
||
diffOk,
|
||
fdin: fd,
|
||
fcarts: fc,
|
||
frec: fr,
|
||
ftotal,
|
||
observacoes: full.observacoes || '',
|
||
clientes: full.clientes || 0,
|
||
frango: full.frango || 0,
|
||
vendas: dados.vendas ?? full.vendas ?? 0,
|
||
};
|
||
}
|
||
|
||
// Imprime o relatório — usa iframe como openPrintPreview (Tauri não bloqueia)
|
||
window.printRelatorio = function() {
|
||
const full = window._currentConsulta;
|
||
if (!full) return;
|
||
const data = consultaToPrintData(full);
|
||
|
||
let html = `<!DOCTYPE html>
|
||
<html><head>
|
||
<meta charset="UTF-8">
|
||
<title>Fechamento de Caixa</title>
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: 'Courier New', monospace; font-size: 18px; width: 80mm; margin: 0 auto; padding: 8px; font-weight: bold; print-color-adjust: exact; -webkit-print-color-adjust: exact; }
|
||
.header { text-align: center; margin-bottom: 8px; }
|
||
.header h1 { font-size: 24px; font-weight: bold; }
|
||
.header h2 { font-size: 18px; font-weight: normal; }
|
||
hr { border: none; border-top: 2px solid #000; margin: 6px 0; }
|
||
.section-title { font-weight: bold; font-size: 17px; border-top: 2px solid #000; padding-top: 4px; margin-top: 8px; }
|
||
.row { display: flex; justify-content: space-between; padding: 2px 0; font-size: 16px; font-weight: bold; }
|
||
.row.total-row { font-weight: bold; }
|
||
.cart-table { table-layout: fixed; width: 100%; border-collapse: collapse; font-size: 16px; margin-top: 4px; font-weight: bold; }
|
||
.cart-table th { font-weight: bold; border-bottom: 2px solid #000; padding: 2px; width: 25%; }
|
||
.cart-table td { padding: 2px 4px; text-align: right; font-weight: bold; }
|
||
.cart-table td:first-child { text-align: left; }
|
||
.cart-table tr.total-row { font-weight: bold; border-top: 2px solid #000; }
|
||
.subtotal { font-size: 16px; margin-top: 4px; }
|
||
.subtotal .row { padding: 2px 0; }
|
||
.diff-ok { color: #2e7d32; font-weight: bold; }
|
||
.diff-bad { color: #c0272d; font-weight: bold; }
|
||
.obs { font-size: 13px; color: #555; margin-top: 6px; white-space: pre-wrap; font-weight: normal; }
|
||
.footer { text-align: center; font-size: 13px; color: #555; margin-top: 8px; }
|
||
@media print { body { background: #fff; font-weight: bold !important; font-size: 18px !important; } }
|
||
</style>
|
||
</head><body>
|
||
${buildPrintHTML(data)}
|
||
</body></html>`;
|
||
|
||
const existing = document.getElementById('print-preview-overlay');
|
||
if (existing) document.body.removeChild(existing);
|
||
const overlay = document.createElement('div');
|
||
overlay.id = 'print-preview-overlay';
|
||
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);z-index:99999;display:flex;flex-direction:column;align-items:center;font-family:\'Courier New\',monospace;';
|
||
const iframe = document.createElement('iframe');
|
||
iframe.style.cssText = 'width:360px;height:600px;border:2px solid #555;margin-top:16px;background:#fff;';
|
||
iframe.id = 'print-iframe';
|
||
const btnFechar = document.createElement('button');
|
||
btnFechar.textContent = '✕ Fechar';
|
||
btnFechar.style.cssText = 'margin-top:12px;padding:10px 32px;background:#444;color:#fff;border:none;border-radius:6px;font-size:15px;cursor:pointer;';
|
||
btnFechar.onclick = () => document.body.removeChild(overlay);
|
||
const btnImprimir = document.createElement('button');
|
||
btnImprimir.textContent = '🖨️ Imprimir';
|
||
btnImprimir.style.cssText = 'margin-top:8px;padding:10px 32px;background:#e65100;color:#fff;border:none;border-radius:6px;font-size:15px;cursor:pointer;';
|
||
btnImprimir.onclick = () => { iframe.contentWindow.focus(); iframe.contentWindow.print(); };
|
||
overlay.appendChild(iframe);
|
||
overlay.appendChild(btnFechar);
|
||
overlay.appendChild(btnImprimir);
|
||
document.body.appendChild(overlay);
|
||
iframe.srcdoc = html;
|
||
};
|
||
|
||
// Intercepta o envio — se sync_status=editing, faz PATCH
|
||
const _originalBtnEnviarHandler = () => {
|
||
const operador = val('f-operador').trim();
|
||
const turno = val('f-turno').trim();
|
||
const loja = val('f-loja').trim();
|
||
if (!operador || !turno || !loja) {
|
||
alert('⚠️ Preencha Operador, Turno e Loja antes de enviar.');
|
||
return;
|
||
}
|
||
openConfirmModal();
|
||
};
|
||
|
||
// Modifica btn-enviar para detectar modo editing
|
||
document.getElementById('btn-enviar').replaceWith(
|
||
Object.assign(document.createElement('button'), {
|
||
id: 'btn-enviar', className: 'btn', innerHTML: '💾 Enviar Fechamento',
|
||
onclick() {
|
||
const operador = val('f-operador').trim();
|
||
const turno = val('f-turno').trim();
|
||
const loja = val('f-loja').trim();
|
||
if (!operador || !turno || !loja) {
|
||
alert('⚠️ Preencha Operador, Turno e Loja antes de enviar.');
|
||
return;
|
||
}
|
||
if (estado.sync_status === 'editing') {
|
||
// Confirmação antes de atualizar
|
||
if (!confirm(`⚠️ ATENÇÃO: vai ATUALIZAR o registro existente no Supabase!\n\nUUID: ${estado.uuid}\n\nContinuar?`)) return;
|
||
salvarEdicaoSupabase();
|
||
} else {
|
||
openConfirmModal();
|
||
}
|
||
}
|
||
})
|
||
);
|
||
|
||
async function salvarEdicaoSupabase() {
|
||
const hasTauri = !!(window.__TAURI__ && window.__TAURI__.core);
|
||
if (!hasTauri) { alert('Tauri não disponível.'); return; }
|
||
const btn = document.getElementById('btn-enviar');
|
||
if (btn) { btn.disabled = true; btn.textContent = '⏳ Salvando...'; }
|
||
|
||
try {
|
||
const data = buildReciboData();
|
||
// Remove campos que não devem ir no PATCH
|
||
delete data.uuid;
|
||
delete data.id_local;
|
||
// Usa o id numérico se disponível, senão usa uuid
|
||
const id = estado.id || estado.uuid;
|
||
console.log('[DEBUG] salvando id:', id, 'data:', JSON.stringify(data));
|
||
const result = await window.__TAURI__.core.invoke('sb_atualizar_fechamento', {
|
||
id: id,
|
||
updates: JSON.stringify(data)
|
||
});
|
||
console.log('[DEBUG] sb_atualizar_fechamento result:', result);
|
||
estado.sync_status = 'synced';
|
||
saveState();
|
||
alert('✅ Registro atualizado com sucesso no Supabase!');
|
||
} catch(e) {
|
||
console.error('Erro ao atualizar:', e);
|
||
alert('❌ Erro ao atualizar no Supabase:\n' + (e?.message || e));
|
||
} finally {
|
||
if (btn) { btn.disabled = false; btn.textContent = '💾 Enviar Fechamento'; }
|
||
}
|
||
}
|
||
|
||
// ─── Boot ─────────────────────────────────────────────────────────────────
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadConfig();
|
||
buildDatalists();
|
||
if (!loadState()) init();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|