d623d11e23
1. PIX CNPJ: linha separada no Saldo de Caixa + incluído na soma sys-total 2. Casas decimais: fmtNum sempre 2 casas (padrão dinheiro) 3. btn-anterior: agora busca do Supabase via sb_carregar_rascunho e repovoa todas as tabelas 4. Impressão: reimplementado usb_windows (WriteFile no Windows); adicionado modo teste (grava .bin) 5. Supabase: feedback de erro com console.error; storage salvar_agora persiste TODAS as tabelas (sangrias, pixcnpj, vales, cancelamentos)
1431 lines
68 KiB
HTML
1431 lines
68 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="pt-BR">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Fechamento de Caixa — O Frangão</title>
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
:root {
|
||
--orange: #e65100;
|
||
--orange-dark: #bf360c;
|
||
--orange-light: #ff8a50;
|
||
--orange-pale: #fff3e0;
|
||
--orange-row: #fff0e0;
|
||
--red: #c0272d;
|
||
--bg: #ffffff;
|
||
--surface: #f7f7f7;
|
||
--surface2: #f0f0f0;
|
||
--border: #e0e0e0;
|
||
--text: #1a1a1a;
|
||
--muted: #777;
|
||
--green: #2e7d32;
|
||
--radius: 6px;
|
||
--font: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||
}
|
||
html, body { height: 100%; overflow: visible; }
|
||
body { font-family: var(--font); background: var(--bg); color: var(--text); display: flex; flex-direction: column; }
|
||
|
||
/* ── HEADER ─────────────────────────────────────────────────────────── */
|
||
.header {
|
||
background: linear-gradient(135deg, #bf360c 0%, #e65100 50%, #ff8a50 100%);
|
||
color: #fff; padding: 8px 18px;
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
flex-shrink: 0; gap: 14px;
|
||
position: sticky; top: 0; z-index: 100;
|
||
}
|
||
.header-left { display: flex; align-items: center; gap: 14px; flex: 1; }
|
||
.header-title { font-size: 15px; font-weight: 700; white-space: nowrap; }
|
||
.header-logo { height: 38px; width: auto; max-width: 140px; object-fit: contain; border-radius: 6px; }
|
||
.header-id { font-size: 11px; font-family: monospace; opacity: 0.9; white-space: nowrap; }
|
||
.header .gear-btn {
|
||
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
|
||
color: #fff; border-radius: var(--radius); padding: 5px 11px;
|
||
cursor: pointer; font-size: 13px; display: flex; align-items: center; gap: 5px;
|
||
font-weight: 600; font-family: var(--font); transition: background 0.2s;
|
||
}
|
||
.header .gear-btn:hover { background: rgba(255,255,255,0.25); }
|
||
|
||
/* ── META BAR ────────────────────────────────────────────────────────── */
|
||
.meta-bar {
|
||
background: var(--surface); border-bottom: 1px solid var(--border);
|
||
padding: 8px 18px; display: flex; gap: 14px; align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
.meta-field { display: flex; flex-direction: column; gap: 2px; }
|
||
.meta-label { font-size: 10px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.meta-input, .meta-select {
|
||
border: 1px solid var(--border); border-radius: var(--radius);
|
||
padding: 5px 9px; font-size: 13px; background: #fff; color: var(--text);
|
||
min-width: 115px; outline: none; font-family: var(--font);
|
||
}
|
||
.meta-input:focus, .meta-select:focus { border-color: var(--orange); box-shadow: 0 0 0 2px var(--orange-pale); }
|
||
.meta-select { cursor: pointer; }
|
||
|
||
/* ── MAIN 2 COLUMNS — single scroll, no internal scrollbars ──────────── */
|
||
.main { flex: 1; display: grid; grid-template-columns: 1fr 1fr; overflow: visible; }
|
||
.col { padding: 12px 16px; overflow: visible; }
|
||
.col-left { border-right: 1px solid var(--border); }
|
||
.col-right { background: var(--surface); display: flex; flex-direction: column; }
|
||
.col-right .section { flex-shrink: 0; }
|
||
.col-right .section:last-child { flex: 1; }
|
||
|
||
/* ── SECTION ─────────────────────────────────────────────────────────── */
|
||
.section { margin-bottom: 14px; }
|
||
.section-header {
|
||
font-size: 11px; font-weight: 700; color: var(--orange);
|
||
text-transform: uppercase; letter-spacing: 0.7px; padding: 4px 0;
|
||
margin-bottom: 5px; border-bottom: 2px solid var(--orange);
|
||
display: flex; align-items: center; gap: 5px;
|
||
}
|
||
|
||
/* ── TABLE ─────────────────────────────────────────────────────────── */
|
||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
th {
|
||
background: var(--surface2); font-weight: 700; color: var(--muted);
|
||
text-align: left; font-size: 10px; text-transform: uppercase;
|
||
letter-spacing: 0.4px; padding: 5px 8px; border: 1px solid var(--border); white-space: nowrap;
|
||
}
|
||
td { padding: 4px 6px; border: 1px solid var(--border); vertical-align: middle; }
|
||
td input, td select {
|
||
width: 100%; border: none; background: transparent;
|
||
font-size: 13px; padding: 4px 2px; outline: none; font-family: var(--font); color: var(--text);
|
||
}
|
||
td input:focus, td select:focus { background: #fffde7; border-radius: 3px; }
|
||
td.num { text-align: right; }
|
||
td.num input { text-align: right; }
|
||
tr.total-row td { background: var(--orange-row); font-weight: 700; color: var(--orange); font-size: 13px; }
|
||
tfoot td { background: var(--surface2); font-weight: 700; font-size: 13px; }
|
||
tfoot td:last-child { text-align: right; }
|
||
|
||
.row-del {
|
||
width: 26px; height: 26px; border: none; background: transparent;
|
||
color: #ccc; cursor: pointer; font-size: 14px; border-radius: 4px;
|
||
display: flex; align-items: center; justify-content: center; padding: 0;
|
||
}
|
||
.row-del:hover { background: #fee2e2; color: var(--red); }
|
||
|
||
.add-btn {
|
||
display: inline-flex; align-items: center; gap: 4px;
|
||
background: var(--orange); color: #fff; border: none; border-radius: var(--radius);
|
||
padding: 5px 12px; font-size: 11px; cursor: pointer; font-weight: 600; margin-top: 5px;
|
||
}
|
||
.add-btn:hover { background: var(--orange-dark); }
|
||
|
||
/* ── SYS BOX ─────────────────────────────────────────────────────────── */
|
||
.sys-grid { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
||
.sys-row {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
padding: 7px 12px; border-bottom: 1px solid var(--border);
|
||
font-size: 13px; background: #fff;
|
||
}
|
||
.sys-row:nth-child(even) { background: var(--surface); }
|
||
.sys-row:last-child { border-bottom: none; }
|
||
.sys-row.total { background: var(--orange-row); font-weight: 700; color: var(--orange); }
|
||
.sys-row .lbl { color: var(--muted); font-size: 12px; }
|
||
.sys-row.total .lbl { color: var(--orange); }
|
||
.sys-row .val { font-variant-numeric: tabular-nums; font-weight: 700; font-size: 13px; }
|
||
.sys-row.total .val { color: var(--orange); font-size: 14px; }
|
||
.sys-row input {
|
||
border: none; background: #e8f5e9; text-align: right; font-size: 13px;
|
||
font-family: var(--font); font-weight: 700; color: #2e7d32;
|
||
width: 90px; padding: 2px 4px; outline: none; border-radius: 3px;
|
||
}
|
||
.sys-row input:focus { background: #c8e6c9; }
|
||
.sys-row input.editable {
|
||
background: #fffde7; color: var(--text); cursor: text;
|
||
}
|
||
.sys-row input.editable:focus { background: #fff9c4; }
|
||
|
||
/* ── FECHAMENTO COUNTED ──────────────────────────────────────────────── */
|
||
.fech-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
|
||
.fech-item { border: 1px solid var(--border); border-radius: var(--radius); padding: 8px 12px; background: #fff; }
|
||
.fech-item-label { font-size: 10px; text-transform: uppercase; color: var(--muted); font-weight: 700; letter-spacing: 0.4px; margin-bottom: 4px; }
|
||
.fech-item input {
|
||
border: none; font-size: 17px; font-weight: 700; color: var(--text);
|
||
width: 100%; font-family: var(--font); outline: none; background: transparent; text-align: right;
|
||
}
|
||
.fech-item input:focus { background: #fffde7; border-radius: 3px; }
|
||
.fech-total {
|
||
border: 1px solid var(--border); border-radius: var(--radius);
|
||
padding: 9px 14px; background: var(--orange-row);
|
||
display: flex; justify-content: space-between; align-items: center; margin-top: 8px;
|
||
}
|
||
.fech-total-label { font-size: 11px; font-weight: 700; color: var(--orange); text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.fech-total-value { font-size: 16px; font-weight: 700; color: var(--orange); font-variant-numeric: tabular-nums; }
|
||
|
||
/* ── DIFERENÇA ─────────────────────────────────────────────────────────── */
|
||
.dif-box {
|
||
border: 2px solid var(--border); border-radius: var(--radius);
|
||
padding: 10px 16px; display: flex; justify-content: space-between; align-items: center;
|
||
}
|
||
.dif-label { font-size: 12px; font-weight: 700; color: var(--text); text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.dif-value { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
||
.dif-value.pos, .dif-value.zero { color: var(--green); }
|
||
.dif-value.neg { color: var(--red); }
|
||
|
||
/* ── SALDO ESPERADO ─────────────────────────────────────────────────── */
|
||
.saldo-esperado-wrap { margin-top: 6px; }
|
||
.saldo-esperado-wrap label { font-size: 10px; text-transform: uppercase; color: var(--muted); font-weight: 700; letter-spacing: 0.4px; display: block; margin-bottom: 3px; }
|
||
.saldo-esperado-wrap input {
|
||
border: 1px solid var(--border); border-radius: var(--radius);
|
||
padding: 8px 10px; font-size: 16px; font-weight: 700; width: 100%;
|
||
font-family: var(--font); outline: none; text-align: right; color: var(--text);
|
||
}
|
||
.saldo-esperado-wrap input:focus { border-color: var(--orange); box-shadow: 0 0 0 2px var(--orange-pale); }
|
||
|
||
/* ── CARDS (4 separate blocks) ─────────────────────────────────────── */
|
||
.cards-section {
|
||
background: var(--surface); border-top: 1px solid var(--border);
|
||
padding: 10px 18px; flex-shrink: 0;
|
||
}
|
||
.cards-header {
|
||
font-size: 11px; font-weight: 700; color: var(--orange);
|
||
text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 8px;
|
||
padding-bottom: 4px; border-bottom: 2px solid var(--orange);
|
||
}
|
||
.cards-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
|
||
.card-block { border: 1px solid var(--border); border-radius: var(--radius); overflow: visible; }
|
||
.card-block-header {
|
||
background: var(--orange); color: #fff; text-align: center;
|
||
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
||
letter-spacing: 0.6px; padding: 6px 4px; border-radius: var(--radius) var(--radius) 0 0;
|
||
}
|
||
.card-block table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
.card-block td { padding: 3px 6px; border: 1px solid var(--border); vertical-align: middle; }
|
||
.card-block td input {
|
||
width: 100%; border: none; background: transparent;
|
||
font-size: 13px; padding: 4px 2px; outline: none; font-family: var(--font); color: var(--text);
|
||
}
|
||
.card-block td input:focus { background: #fffde7; border-radius: 3px; }
|
||
.card-block td.num { text-align: right; }
|
||
.card-block td.num input { text-align: right; }
|
||
.card-block tfoot td { background: var(--orange-pale); font-weight: 700; color: var(--orange); font-size: 13px; text-align: right; }
|
||
.card-block .add-btn { width: calc(100% - 12px); justify-content: center; margin: 5px 6px 6px; }
|
||
.soma-row {
|
||
display: flex; justify-content: flex-end; align-items: center;
|
||
gap: 12px; padding: 7px 10px; background: var(--orange-pale);
|
||
border-radius: var(--radius); font-size: 13px; font-weight: 700; color: var(--orange); margin-top: 6px;
|
||
}
|
||
.soma-row span:last-child { font-size: 16px; }
|
||
|
||
/* ── FOOTER ─────────────────────────────────────────────────────────── */
|
||
.footer {
|
||
background: var(--surface); border-top: 1px solid var(--border);
|
||
padding: 8px 18px; display: flex; align-items: center; gap: 10px;
|
||
flex-shrink: 0; flex-wrap: wrap;
|
||
}
|
||
.auto-save { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--muted); margin-right: auto; }
|
||
.dot { width: 8px; height: 8px; border-radius: 50%; background: #4caf50; }
|
||
.btn {
|
||
border: 1px solid var(--border); background: #fff; border-radius: var(--radius);
|
||
padding: 7px 14px; font-size: 12px; cursor: pointer; display: flex;
|
||
align-items: center; gap: 6px; font-weight: 600; font-family: var(--font); color: var(--text);
|
||
}
|
||
.btn:hover { background: var(--surface2); }
|
||
.btn-danger { background: var(--orange); color: #fff; border-color: var(--orange); font-size: 13px; padding: 8px 16px; }
|
||
.btn-danger:hover { background: var(--orange-dark); }
|
||
|
||
/* ── MODAL ──────────────────────────────────────────────────────────── */
|
||
.modal-overlay {
|
||
display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5);
|
||
z-index: 9999; justify-content: center; align-items: center;
|
||
}
|
||
.modal-overlay.open { display: flex; }
|
||
.modal {
|
||
background: #fff; border-radius: 10px; width: 560px; max-width: 95vw;
|
||
max-height: 85vh; display: flex; flex-direction: column;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.3); overflow: hidden;
|
||
}
|
||
.modal-header {
|
||
background: linear-gradient(135deg, #bf360c, #e65100);
|
||
color: #fff; padding: 13px 20px;
|
||
display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
|
||
}
|
||
.modal-header h2 { font-size: 15px; font-weight: 700; }
|
||
.modal-close {
|
||
background: rgba(255,255,255,0.2); border: none; color: #fff;
|
||
border-radius: 4px; width: 28px; height: 28px; cursor: pointer;
|
||
font-size: 16px; display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.modal-close:hover { background: rgba(255,255,255,0.35); }
|
||
.modal-body { flex: 1; overflow-y: auto; padding: 16px 20px; }
|
||
.cfg-section { margin-bottom: 18px; }
|
||
.cfg-section-title {
|
||
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
||
letter-spacing: 0.6px; color: var(--orange);
|
||
padding-bottom: 4px; margin-bottom: 8px; border-bottom: 1px solid var(--border);
|
||
}
|
||
.cfg-item { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
|
||
.cfg-item input[type="text"] {
|
||
flex: 1; border: 1px solid var(--border); border-radius: var(--radius);
|
||
padding: 6px 10px; font-size: 13px; font-family: var(--font); outline: none;
|
||
}
|
||
.cfg-item input[type="text"]:focus { border-color: var(--orange); }
|
||
.cfg-item .btn-rm {
|
||
width: 26px; height: 26px; border: none; background: #fee2e2;
|
||
color: var(--red); border-radius: 4px; cursor: pointer; font-size: 13px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.btn-add-config {
|
||
display: inline-flex; align-items: center; gap: 4px;
|
||
background: var(--orange); color: #fff; border: none; border-radius: var(--radius);
|
||
padding: 5px 12px; font-size: 11px; cursor: pointer; font-weight: 600; margin-top: 4px;
|
||
}
|
||
.btn-add-config:hover { background: var(--orange-dark); }
|
||
.modal-footer {
|
||
padding: 12px 20px; border-top: 1px solid var(--border);
|
||
display: flex; justify-content: flex-end; gap: 10px; flex-shrink: 0;
|
||
}
|
||
.modal-footer .btn-primary {
|
||
background: var(--orange); color: #fff; border: none; border-radius: var(--radius);
|
||
padding: 8px 20px; font-size: 13px; cursor: pointer; font-weight: 600; font-family: var(--font);
|
||
}
|
||
.modal-footer .btn-primary:hover { background: var(--orange-dark); }
|
||
|
||
/* ── Scrollbar ────────────────────────────────────────────────────── */
|
||
::-webkit-scrollbar { width: 7px; }
|
||
::-webkit-scrollbar-track { background: var(--surface); }
|
||
::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ═══ HEADER ════════════════════════════════════════════════════════════ -->
|
||
<div class="header">
|
||
<div class="header-left">
|
||
<span class="header-title">Fechamento de Caixa</span>
|
||
<img class="header-logo" id="store-logo" src="./logo.png" alt="Logo" />
|
||
<span class="header-id" id="fechamento-id">—</span>
|
||
</div>
|
||
<button class="gear-btn" id="btn-config" title="Configurar">⚙️ Configurar</button>
|
||
</div>
|
||
|
||
<!-- ═══ META BAR ════════════════════════════════════════════════════════ -->
|
||
<div class="meta-bar">
|
||
<div class="meta-field">
|
||
<span class="meta-label">Data</span>
|
||
<input type="date" class="meta-input" id="f-data" />
|
||
</div>
|
||
<div class="meta-field">
|
||
<span class="meta-label">Operador</span>
|
||
<input type="text" class="meta-input" id="f-operador" list="dl-operadores" placeholder="Ex: Isabelle" />
|
||
</div>
|
||
<div class="meta-field">
|
||
<span class="meta-label">Turno</span>
|
||
<select class="meta-select" id="f-turno">
|
||
<option value="">—</option>
|
||
<option value="integral">Integral</option>
|
||
<option value="manhã">Manhã</option>
|
||
<option value="tarde">Tarde</option>
|
||
</select>
|
||
</div>
|
||
<div class="meta-field">
|
||
<span class="meta-label">Loja</span>
|
||
<select class="meta-select" id="f-loja">
|
||
<option value="União">União</option>
|
||
<option value="Aliança">Aliança</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ MAIN 2 COLUMNS ════════════════════════════════════════════════ -->
|
||
<div class="main">
|
||
|
||
<!-- ── LEFT: entradas, sangrias, despesas, pix, vales, receber, cancelamentos ── -->
|
||
<div class="col col-left">
|
||
|
||
<!-- SANGRIAS -->
|
||
<div class="section">
|
||
<div class="section-header">💸 Sangrias</div>
|
||
<table id="tbl-sangrias"><thead><tr><th>Hora</th><th>Retirada (R$)</th><th>Gerente</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td colspan="2">TOTAL</td><td class="num" id="total-sangrias">R$ 0,00</td><td></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-sangrias">+ Adicionar sangria</button>
|
||
</div>
|
||
|
||
<!-- DESPESAS -->
|
||
<div class="section">
|
||
<div class="section-header">📝 Despesas</div>
|
||
<table id="tbl-despesas"><thead><tr><th>Descrição</th><th>Obs</th><th>Valor (R$)</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td colspan="2">TOTAL</td><td class="num" id="total-despesas">R$ 0,00</td><td></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-despesas">+ Adicionar despesa</button>
|
||
</div>
|
||
|
||
<!-- PIX CNPJ -->
|
||
<div class="section">
|
||
<div class="section-header">💳 PIX CNPJ</div>
|
||
<table id="tbl-pix"><thead><tr><th>Nome</th><th>Valor (R$)</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td>TOTAL</td><td class="num" id="total-pix">R$ 0,00</td><td></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-pix">+ Adicionar PIX</button>
|
||
</div>
|
||
|
||
<!-- VALES -->
|
||
<div class="section">
|
||
<div class="section-header">💰 Vales (Funcionários)</div>
|
||
<table id="tbl-vales"><thead><tr><th>Nome</th><th>Valor (R$)</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td>TOTAL</td><td class="num" id="total-vales">R$ 0,00</td><td></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-vales">+ Adicionar vale</button>
|
||
</div>
|
||
|
||
<!-- À RECEBER -->
|
||
<div class="section">
|
||
<div class="section-header">👥 À Receber (Clientes)</div>
|
||
<table id="tbl-receber"><thead><tr><th>Nome</th><th>Valor (R$)</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td>TOTAL</td><td class="num" id="total-receber">R$ 0,00</td><td></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-receber">+ Adicionar cliente</button>
|
||
</div>
|
||
|
||
<!-- CANCELAMENTOS -->
|
||
<div class="section">
|
||
<div class="section-header">❌ Cancelamentos</div>
|
||
<table id="tbl-cancelamentos"><thead><tr><th>Número</th><th>Valor (R$)</th><th>Motivo</th><th style="width:26px"></th></tr></thead><tbody></tbody><tfoot><tr class="total-row"><td colspan="1">TOTAL</td><td class="num" id="total-cancelamentos">R$ 0,00</td><td colspan="2"></td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-cancelamentos">+ Adicionar cancelamento</button>
|
||
</div>
|
||
|
||
</div><!-- /col-left -->
|
||
|
||
<!-- ── RIGHT: saldo caixa, fechamento contado, diferença, obs ── -->
|
||
<div class="col col-right">
|
||
|
||
<!-- SALDO DE CAIXA -->
|
||
<div class="section">
|
||
<div class="section-header">🖥️ Saldo de Caixa (Valores do Sistema)</div>
|
||
<div class="sys-grid">
|
||
<div class="sys-row"><span class="lbl">Troco (+)</span><input type="text" id="sys-troco" class="editable" placeholder="0,00" /></div>
|
||
<div class="sys-row"><span class="lbl">Crédito (+)</span><input type="text" id="sys-credito" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">Débito (+)</span><input type="text" id="sys-debito" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">Alimentação (+)</span><input type="text" id="sys-alimentacao" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">Vales (+)</span><input type="text" id="sys-vales" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">À Receber (+)</span><input type="text" id="sys-receber" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">PIX Cartão (+)</span><input type="text" id="sys-pix" readonly tabindex="-1" /></div>
|
||
<div class="sys-row"><span class="lbl">PIX CNPJ (+)</span><input type="text" id="sys-pixcnpj" readonly tabindex="-1" /></div>
|
||
<div class="sys-row total"><span class="lbl">TOTAL</span><span class="val" id="sys-total">R$ 0,00</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SALDO ESPERADO -->
|
||
<div class="section">
|
||
<div class="section-header">🎯 Saldo Esperado</div>
|
||
<div class="saldo-esperado-wrap">
|
||
<label>Informe o saldo esperado (contado em caixa)</label>
|
||
<input type="text" id="f-saldo-esperado" placeholder="0,00" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- FECHAMENTO (VALORES CONTADOS) -->
|
||
<div class="section">
|
||
<div class="section-header">🛒 Fechamento (Valores Contados)</div>
|
||
<div class="fech-grid">
|
||
<div class="fech-item"><div class="fech-item-label">Dinheiro</div><input type="text" id="f-fech-dinheiro" placeholder="0,00" /></div>
|
||
<div class="fech-item"><div class="fech-item-label">Cartões</div><input type="text" id="f-fech-cartoes" placeholder="0,00" /></div>
|
||
<div class="fech-item"><div class="fech-item-label">À Receber</div><input type="text" id="f-fech-receber" placeholder="0,00" /></div>
|
||
</div>
|
||
<div class="fech-total">
|
||
<span class="fech-total-label">Total Programa</span>
|
||
<span class="fech-total-value" id="fech-total">R$ 0,00</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DIFERENÇA -->
|
||
<div class="section">
|
||
<div class="dif-box">
|
||
<span class="dif-label">Diferença</span>
|
||
<span class="dif-value zero" id="diferenca">R$ 0,00</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- OBS -->
|
||
<div class="section" style="flex:1;display:flex;flex-direction:column">
|
||
<div class="section-header">📝 Observações</div>
|
||
<textarea id="f-obs" rows="4" style="flex:1;width:100%;min-height:80px;border:1px solid var(--border);border-radius:var(--radius);padding:10px;font-size:15px;font-family:var(--font);resize:none" placeholder="Observações..."></textarea>
|
||
</div>
|
||
|
||
</div><!-- /col-right -->
|
||
</div><!-- /main -->
|
||
|
||
<!-- ═══ CARTÕES ════════════════════════════════════════════════════════ -->
|
||
<div class="cards-section">
|
||
<div class="cards-header">💳 Cartões</div>
|
||
<div class="cards-grid">
|
||
|
||
<div class="card-block">
|
||
<div class="card-block-header">Crédito</div>
|
||
<table id="tbl-cartao-credito"><thead><tr><th>Valor (R$)</th></tr></thead><tbody></tbody><tfoot><tr><td id="total-cartao-credito">R$ 0,00</td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-cartao-credito">+</button>
|
||
</div>
|
||
|
||
<div class="card-block">
|
||
<div class="card-block-header">Débito</div>
|
||
<table id="tbl-cartao-debito"><thead><tr><th>Valor (R$)</th></tr></thead><tbody></tbody><tfoot><tr><td id="total-cartao-debito">R$ 0,00</td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-cartao-debito">+</button>
|
||
</div>
|
||
|
||
<div class="card-block">
|
||
<div class="card-block-header">Alimentação</div>
|
||
<table id="tbl-cartao-alimentacao"><thead><tr><th>Valor (R$)</th></tr></thead><tbody></tbody><tfoot><tr><td id="total-cartao-alimentacao">R$ 0,00</td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-cartao-alimentacao">+</button>
|
||
</div>
|
||
|
||
<div class="card-block">
|
||
<div class="card-block-header">PIX</div>
|
||
<table id="tbl-cartao-pix"><thead><tr><th>Valor (R$)</th></tr></thead><tbody></tbody><tfoot><tr><td id="total-cartao-pix">R$ 0,00</td></tr></tfoot></table>
|
||
<button class="add-btn" data-tbl="tbl-cartao-pix">+</button>
|
||
</div>
|
||
|
||
</div>
|
||
<div class="soma-row">
|
||
<span>Soma Cartões (todas as colunas)</span>
|
||
<span id="soma-cartoes">R$ 0,00</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ FOOTER ════════════════════════════════════════════════════════ -->
|
||
<div class="footer">
|
||
<div class="auto-save"><span class="dot"></span> Salvamento automático activo</div>
|
||
<button class="btn" id="btn-anterior">✏️ Dia anterior</button>
|
||
<button class="btn" id="btn-recibo">🖨️ Recibo 80mm</button>
|
||
<button class="btn" id="btn-limpar">🗑️ Limpar rascunho</button>
|
||
<button class="btn btn-danger" id="btn-enviar">💾 Enviar Fechamento</button>
|
||
</div>
|
||
|
||
<!-- ═══ CONFIG MODAL ══════════════════════════════════════════════════ -->
|
||
<div class="modal-overlay" id="modal-config">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h2>⚙️ Configurar Preenchimento Automático</h2>
|
||
<button class="modal-close" id="modal-close">✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="cfg-section">
|
||
<div class="cfg-section-title">👤 Operadores</div>
|
||
<div id="cfg-operadores-list"></div>
|
||
<button class="btn-add-config" id="cfg-add-operador">+ Adicionar operador</button>
|
||
</div>
|
||
<div class="cfg-section">
|
||
<div class="cfg-section-title">👔 Gerentes</div>
|
||
<div id="cfg-gerentes-list"></div>
|
||
<button class="btn-add-config" id="cfg-add-gerente">+ Adicionar gerente</button>
|
||
</div>
|
||
<div class="cfg-section">
|
||
<div class="cfg-section-title">📝 Despesas (sugestões)</div>
|
||
<div id="cfg-despesas-list"></div>
|
||
<button class="btn-add-config" id="cfg-add-despesa">+ Adicionar despesa</button>
|
||
</div>
|
||
<div class="cfg-section">
|
||
<div class="cfg-section-title">👥 Clientes (À Receber)</div>
|
||
<div id="cfg-clientes-list"></div>
|
||
<button class="btn-add-config" id="cfg-add-cliente">+ Adicionar cliente</button>
|
||
</div>
|
||
<div class="cfg-section">
|
||
<div class="cfg-section-title">💰 Vales (nomes para autofill)</div>
|
||
<div id="cfg-vales-list"></div>
|
||
<button class="btn-add-config" id="cfg-add-vales">+ Adicionar nome</button>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn-primary" id="cfg-salvar">Salvar Configuração</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ CONFIRM MODAL ══════════════════════════════════════════════════ -->
|
||
<div class="modal-overlay" id="modal-confirm">
|
||
<div class="modal" style="width:520px">
|
||
<div class="modal-header">
|
||
<h2>⚠️ Confirmar envio do fechamento</h2>
|
||
<button class="modal-close" id="confirm-close">✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p style="font-size:13px;color:#444;margin-bottom:14px">
|
||
Tem certeza que deseja <strong>enviar este fechamento e encerrar o caixa?</strong>
|
||
</p>
|
||
<div id="confirm-summary" style="border:1px solid #b3d9f7;border-radius:8px;padding:12px;background:#f0f7ff;font-size:12px;font-family:monospace">
|
||
</div>
|
||
<p style="font-size:11px;color:#888;margin-top:10px">
|
||
Após enviar, <strong>não dá pra editar este fechamento</strong> sem a senha do Filipe.
|
||
</p>
|
||
</div>
|
||
<div class="modal-footer" style="justify-content:space-between">
|
||
<button class="btn" id="confirm-print" style="color:#333">
|
||
🖨️ Imprimir Relatório
|
||
</button>
|
||
<div style="display:flex;gap:10px">
|
||
<button class="btn" id="confirm-cancel" style="border-color:var(--red);color:var(--red)">✕ Cancelar</button>
|
||
<button class="btn btn-danger" id="confirm-send">✓ Sim, enviar e encerrar</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ JAVASCRIPT ══════════════════════════════════════════════════ -->
|
||
<script>
|
||
'use strict';
|
||
|
||
// ─── Config ────────────────────────────────────────────────────────────────
|
||
let config = {
|
||
operadores: ['Isabelle', 'Silvanea', 'Rafael'],
|
||
gerentes: ['Carlos', 'Ana', 'João'],
|
||
despesas: ['Energia', 'Água', 'Manutenção', 'Limpeza', 'Insumos'],
|
||
clientes: [], // preenchimento livre — não é datalist fixo
|
||
vales_nomes: [], // nomes usados em vales ( histórico para autofill)
|
||
};
|
||
|
||
// ─── App State ────────────────────────────────────────────────────────────
|
||
let estado = {
|
||
uuid: crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(),
|
||
loja: 'União', data: '', operador: '', turno: '',
|
||
saldo_troco: 0, fechamento: 0, saldo_esperado: 0, diferenca: 0,
|
||
despesas: [], sangrias: [], pixcnpj: [], vales: [], receber: [], cancelamentos: [],
|
||
cartoes: { credito: [], debito: [], alimentacao: [], pix: [] },
|
||
fechamento_contado: { dinheiro: 0, cartoes: 0, receber: 0 },
|
||
observacoes: '', id_local: null, sync_status: 'local'
|
||
};
|
||
|
||
// ─── Helpers ──────────────────────────────────────────────────────────────
|
||
const fmt = v => 'R$ ' + (parseFloat(v) || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
||
const parseNum = v => {
|
||
// Already a number? Return directly (don't treat '.' as thousand separator)
|
||
if (typeof v === 'number') return isNaN(v) ? 0 : v;
|
||
const s = String(v || '0').trim();
|
||
if (!s) return 0;
|
||
// Brazilian format: 1.234,56 → 1234.56
|
||
// Strategy: if string has comma, treat as BRL (decimal comma)
|
||
// If no comma but has dot, treat as US (decimal dot)
|
||
const lastComma = s.lastIndexOf(',');
|
||
const lastDot = s.lastIndexOf('.');
|
||
const hasCommaDecimal = lastComma > lastDot;
|
||
if (hasCommaDecimal) {
|
||
const intPart = s.slice(0, lastComma).replace(/\./g, '').replace(/,/g, '');
|
||
const decPart = s.slice(lastComma + 1);
|
||
return parseFloat(intPart + '.' + decPart) || 0;
|
||
}
|
||
// No comma: US-style number, just strip any dots (thousand sep)
|
||
return parseFloat(s.replace(/,/g, '').replace(/\./g, '')) || 0;
|
||
};
|
||
const setText = (id, t) => { const e = document.getElementById(id); if (e) e.textContent = t; };
|
||
const setVal = (id, v) => { const e = document.getElementById(id); if (e) e.value = v; };
|
||
const val = id => { const e = document.getElementById(id); return e ? e.value : ''; };
|
||
const fmtNum = v => {
|
||
const n = typeof v === 'number' ? v : parseNum(v);
|
||
if (isNaN(n)) return '';
|
||
// Always 2 decimal places for money
|
||
return n.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
};
|
||
|
||
// Format time: 4 digits → HH:MM
|
||
function fmtTime(v) {
|
||
const digits = (v || '').replace(/\D/g, '').slice(0, 4);
|
||
if (digits.length <= 2) return digits;
|
||
return digits.slice(0, 2) + ':' + digits.slice(2);
|
||
}
|
||
|
||
// ─── Tables ───────────────────────────────────────────────────────────────
|
||
const TABLES = {
|
||
'tbl-sangrias': { field: 'sangrias', cols: ['hora','retirada','gerente'], enterNav: { hora:'retirada', retirada:'gerente', gerente:'_nextRow' } },
|
||
'tbl-despesas': { field: 'despesas', cols: ['desc','obs','valor'], enterNav: { desc:'obs', obs:'valor', valor:'_nextRow' } },
|
||
'tbl-pix': { field: 'pixcnpj', cols: ['nome','valor'], enterNav: { nome:'valor', valor:'_nextRow' } },
|
||
'tbl-vales': { field: 'vales', cols: ['nome','valor'], enterNav: { nome:'valor', valor:'_nextRow' } },
|
||
'tbl-receber': { field: 'receber', cols: ['nome','valor'], enterNav: { nome:'valor', valor:'_nextRow' } },
|
||
'tbl-cancelamentos': { field: 'cancelamentos', cols: ['numero','valor','motivo'], enterNav: { numero:'valor', valor:'motivo', motivo:'_nextRow' } },
|
||
'tbl-cartao-credito': { field: 'cartoes.credito', cols: ['valor'], enterNav: { valor:'_nextRow' } },
|
||
'tbl-cartao-debito': { field: 'cartoes.debito', cols: ['valor'], enterNav: { valor:'_nextRow' } },
|
||
'tbl-cartao-alimentacao': { field: 'cartoes.alimentacao', cols: ['valor'], enterNav: { valor:'_nextRow' } },
|
||
'tbl-cartao-pix': { field: 'cartoes.pix', cols: ['valor'], enterNav: { valor:'_nextRow' } },
|
||
};
|
||
|
||
function getField(tblId) {
|
||
const parts = TABLES[tblId].field.split('.');
|
||
let cur = estado;
|
||
for (const p of parts) cur = cur[p];
|
||
return cur;
|
||
}
|
||
|
||
function makeRow(tblId, rowIndex, rowData) {
|
||
const cfg = TABLES[tblId];
|
||
const tr = document.createElement('tr');
|
||
cfg.cols.forEach(col => {
|
||
const td = document.createElement('td');
|
||
if (col === 'valor' || col === 'retirada') td.className = 'num';
|
||
const inp = document.createElement('input');
|
||
inp.type = 'text';
|
||
inp.value = rowData[col] !== undefined
|
||
? (rowData[col] === 0 ? '' : fmtNum(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;
|
||
if (col === 'gerente') inp.setAttribute('list', 'dl-gerente');
|
||
if (col === 'desc') inp.setAttribute('list', 'dl-despesa');
|
||
// tbl-receber: clientes free-text (sem datalist)
|
||
// tbl-vales: usa dl-vales
|
||
if (col === 'nome' && tblId === 'tbl-vales') inp.setAttribute('list', 'dl-vales');
|
||
if (col === 'nome' && tblId === 'tbl-pix') inp.setAttribute('list', 'dl-generico');
|
||
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 {
|
||
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();
|
||
}
|
||
}
|
||
|
||
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 empty = {};
|
||
cfg.cols.forEach(c => { empty[c] = c === 'valor' || c === 'retirada' ? 0 : ''; });
|
||
field.push(empty);
|
||
renderTable(tblId);
|
||
focusInput(tblId, field.length - 1, focusCol);
|
||
}
|
||
|
||
function onDeleteRow(e) {
|
||
const { tbl, row } = e.target.dataset;
|
||
const field = getField(tbl);
|
||
if (field && field.length > 1) {
|
||
field.splice(parseInt(row), 1);
|
||
renderTable(tbl);
|
||
updateTotals();
|
||
scheduleSave();
|
||
} else {
|
||
const cfg = TABLES[tbl];
|
||
cfg.cols.forEach(c => { field[0][c] = c === 'valor' || c === 'retirada' ? 0 : ''; });
|
||
renderTable(tbl);
|
||
updateTotals();
|
||
scheduleSave();
|
||
}
|
||
}
|
||
|
||
// Add buttons
|
||
document.querySelectorAll('.add-btn[data-tbl]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const tblId = btn.dataset.tbl;
|
||
const cfg = TABLES[tblId];
|
||
if (!cfg) return;
|
||
const field = getField(tblId);
|
||
const empty = {};
|
||
cfg.cols.forEach(c => { empty[c] = c === 'valor' ? 0 : ''; });
|
||
field.push(empty);
|
||
renderTable(tblId);
|
||
focusInput(tblId, field.length - 1, cfg.cols[0]);
|
||
});
|
||
});
|
||
|
||
// ─── Totals ────────────────────────────────────────────────────────────────
|
||
function updateTotals() {
|
||
// ── Sangrias ──
|
||
const sangriasTotal = estado.sangrias.reduce((s, r) => s + (parseNum(r['retirada'] || 0) || 0), 0);
|
||
setText('total-sangrias', fmt(sangriasTotal));
|
||
|
||
// ── Tabelas de valor ──
|
||
const map = {
|
||
'tbl-despesas':'despesas', 'tbl-pix':'pixcnpj', 'tbl-vales':'vales',
|
||
'tbl-receber':'receber', 'tbl-cancelamentos':'cancelamentos',
|
||
};
|
||
Object.entries(map).forEach(([tblId, fieldName]) => {
|
||
const total = estado[fieldName].reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
setText(tblId.replace('tbl-','total-'), fmt(total));
|
||
});
|
||
|
||
// ── Cartões ──
|
||
const cartCred = estado.cartoes.credito.reduce((s, r) => s + (r.valor || 0), 0);
|
||
const cartDeb = estado.cartoes.debito.reduce((s, r) => s + (r.valor || 0), 0);
|
||
const cartAli = estado.cartoes.alimentacao.reduce((s, r) => s + (r.valor || 0), 0);
|
||
const cartPix = estado.cartoes.pix.reduce((s, r) => s + (r.valor || 0), 0);
|
||
|
||
['credito','debito','alimentacao','pix'].forEach(c => {
|
||
const total = estado.cartoes[c].reduce((s, r) => s + (r.valor || 0), 0);
|
||
setText(`total-cartao-${c}`, fmt(total));
|
||
});
|
||
|
||
const somaCartoes = cartCred + cartDeb + cartAli + cartPix;
|
||
setText('soma-cartoes', fmt(somaCartoes));
|
||
|
||
// ── Saldo de Caixa — auto-populate readonly fields ──
|
||
// Crédito = total cartão crédito
|
||
document.getElementById('sys-credito').value = cartCred > 0 ? fmtNum(cartCred) : '';
|
||
// Débito = total cartão débito
|
||
document.getElementById('sys-debito').value = cartDeb > 0 ? fmtNum(cartDeb) : '';
|
||
// Alimentação = total cartão alimentação
|
||
document.getElementById('sys-alimentacao').value = cartAli > 0 ? fmtNum(cartAli) : '';
|
||
// PIX Cartão = total cartão PIX
|
||
document.getElementById('sys-pix').value = cartPix > 0 ? fmtNum(cartPix) : '';
|
||
// PIX CNPJ = total PIX CNPJ
|
||
const pixCnpjTotal = estado.pixcnpj.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
document.getElementById('sys-pixcnpj').value = pixCnpjTotal > 0 ? fmtNum(pixCnpjTotal) : '';
|
||
// Vales = total vales
|
||
const valesTotal = estado.vales.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
document.getElementById('sys-vales').value = valesTotal > 0 ? fmtNum(valesTotal) : '';
|
||
// À Receber = total receber
|
||
const receberTotal = estado.receber.reduce((s, r) => s + (parseNum(r['valor'] || 0) || 0), 0);
|
||
document.getElementById('sys-receber').value = receberTotal > 0 ? fmtNum(receberTotal) : '';
|
||
|
||
// SYS total = troco + crédito + débito + alimentação + vales + receber + pixCartão + pixCnpj
|
||
const sysTotal = parseNum(val('sys-troco')) + cartCred + cartDeb + cartAli + valesTotal + receberTotal + cartPix + pixCnpjTotal;
|
||
setText('sys-total', fmt(sysTotal));
|
||
|
||
// ── Fechamento Contado — auto-populate (readonly, filled from tables) ──
|
||
// Dinheiro = total sangrias
|
||
document.getElementById('f-fech-dinheiro').value = sangriasTotal > 0 ? fmtNum(sangriasTotal) : '';
|
||
// Cartões = total cartões (crédito+débito+alimentação+pix)
|
||
document.getElementById('f-fech-cartoes').value = somaCartoes > 0 ? fmtNum(somaCartoes) : '';
|
||
// À Receber = total receber
|
||
document.getElementById('f-fech-receber').value = receberTotal > 0 ? fmtNum(receberTotal) : '';
|
||
|
||
// Fechamento total
|
||
const fd = sangriasTotal;
|
||
const fc = somaCartoes;
|
||
const fr = receberTotal;
|
||
setText('fech-total', fmt(fd + fc + fr));
|
||
|
||
// Diferença = Saldo Caixa − Saldo Esperado
|
||
const saldoCaixa = sysTotal;
|
||
const diff = saldoCaixa - parseNum(val('f-saldo-esperado'));
|
||
const difEl = document.getElementById('diferenca');
|
||
if (difEl) {
|
||
difEl.textContent = fmt(diff);
|
||
if (diff === 0) {
|
||
difEl.className = 'dif-value zero';
|
||
} else if (diff < 0 && diff >= -5) {
|
||
difEl.className = 'dif-value neg'; // OK − até −5
|
||
} else if (diff > 0 && diff <= 3) {
|
||
difEl.className = 'dif-value zero'; // OK + até 3
|
||
} else {
|
||
difEl.className = 'dif-value pos'; // Sobra > +3
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── Numeric input: auto ,00 on digit+blur pattern ───────────────────────
|
||
// For fields that use datalist (text input), we need to avoid triggering
|
||
// auto-formatter when the browser is about to fill from datalist.
|
||
// Strategy: detect "comma-only" input and convert to ",00" immediately.
|
||
// For normal digit input: add ,00 on blur (not on every digit).
|
||
|
||
// ─── Numeric input: auto ,00 on digit+blur pattern ───────────────────────
|
||
// These fields are plain text inputs (no datalist). Format on blur only.
|
||
// For sys-troco which is user-editable: also format on Enter keydown.
|
||
|
||
['troco','credito','debito','alimentacao','vales','receber','pix'].forEach(k => {
|
||
const el = document.getElementById(`sys-${k}`);
|
||
if (!el) return;
|
||
|
||
// On input: just track for autosave
|
||
el.addEventListener('input', () => {
|
||
updateTotals();
|
||
scheduleSave();
|
||
});
|
||
|
||
// On keydown Enter: format value directly (Enter does not trigger blur for tabindex=-1)
|
||
el.addEventListener('keydown', e => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
const raw = el.value.trim();
|
||
if (!raw) { el.value = ''; return; }
|
||
const num = parseNum(raw);
|
||
if (isNaN(num) || num === 0) { el.value = ''; }
|
||
else { el.value = fmtNum(num); }
|
||
updateTotals();
|
||
scheduleSave();
|
||
}
|
||
});
|
||
|
||
// On blur: format the value
|
||
el.addEventListener('blur', () => {
|
||
const raw = el.value.trim();
|
||
if (!raw) { el.value = ''; return; }
|
||
const num = parseNum(raw);
|
||
if (isNaN(num) || num === 0) { el.value = ''; }
|
||
else { el.value = fmtNum(num); }
|
||
updateTotals();
|
||
scheduleSave();
|
||
});
|
||
});
|
||
|
||
['f-fech-dinheiro','f-fech-cartoes','f-fech-receber','f-saldo-esperado'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
|
||
el.addEventListener('input', () => {
|
||
const v = el.value;
|
||
if (v.endsWith(',') && !v.includes(',') && v !== ',') {
|
||
el.value = v + '00';
|
||
const pos = v.length;
|
||
setTimeout(() => { el.selectionStart = el.selectionEnd = pos; }, 0);
|
||
}
|
||
updateTotals();
|
||
scheduleSave();
|
||
});
|
||
|
||
el.addEventListener('blur', () => {
|
||
const raw = el.value.trim();
|
||
if (!raw) { el.value = ''; return; }
|
||
const num = parseNum(raw);
|
||
if (isNaN(num) || num === 0) { el.value = ''; }
|
||
else { el.value = fmtNum(num); }
|
||
updateTotals();
|
||
scheduleSave();
|
||
});
|
||
});
|
||
|
||
// ─── Metadata ──────────────────────────────────────────────────────────────
|
||
['f-data','f-operador','f-turno','f-loja'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
el.addEventListener('change', () => {
|
||
estado.data = val('f-data');
|
||
estado.operador = val('f-operador');
|
||
estado.turno = val('f-turno');
|
||
estado.loja = val('f-loja');
|
||
syncHeader();
|
||
scheduleSave();
|
||
});
|
||
});
|
||
|
||
function syncHeader() {
|
||
setText('fechamento-id', `${estado.loja}_${estado.data}_op`);
|
||
}
|
||
|
||
// ─── Datalists ─────────────────────────────────────────────────────────────
|
||
function buildDatalists() {
|
||
// Auto-build vales_nomes from current entries (autofill dinâmico)
|
||
const valesUsed = [...new Set(estado.vales.map(v => v.nome).filter(Boolean))];
|
||
config.vales_nomes = [...new Set([...config.vales_nomes, ...valesUsed])]
|
||
.sort((a, b) => a.localeCompare(b, 'pt-BR'));
|
||
|
||
// Gerentes e Operadores também acumulam dos lançamentos
|
||
const gerentesUsed = [...new Set(estado.sangrias.map(r => r.gerente).filter(Boolean))];
|
||
config.gerentes = [...new Set([...config.gerentes, ...gerentesUsed])]
|
||
.sort((a, b) => a.localeCompare(b, 'pt-BR'));
|
||
|
||
const lists = {
|
||
'dl-operadores': [...new Set([...config.operadores])].sort((a,b) => a.localeCompare(b, 'pt-BR')),
|
||
'dl-gerente': [...new Set([...config.gerentes])].sort((a,b) => a.localeCompare(b, 'pt-BR')),
|
||
'dl-despesa': [...new Set([...config.despesas])].sort((a,b) => a.localeCompare(b, 'pt-BR')),
|
||
// Clientes: escrita livre — sem datalist fixo
|
||
'dl-vales': [...new Set([...config.vales_nomes])].sort((a,b) => a.localeCompare(b, 'pt-BR')),
|
||
};
|
||
Object.entries(lists).forEach(([id, items]) => {
|
||
let dl = document.getElementById(id);
|
||
if (!dl) { dl = document.createElement('datalist'); dl.id = id; document.body.appendChild(dl); }
|
||
dl.innerHTML = items.map(v => `<option value="${v}">`).join('');
|
||
});
|
||
// Clientes: sem datalist (escrita livre)
|
||
const clienteDl = document.getElementById('dl-cliente');
|
||
if (clienteDl) clienteDl.remove();
|
||
}
|
||
|
||
// ─── Init ─────────────────────────────────────────────────────────────────
|
||
const PRE_FILL_TABLES = Object.keys(TABLES);
|
||
|
||
function ensureOneRow(tblId) {
|
||
const cfg = TABLES[tblId];
|
||
if (!cfg) return;
|
||
const field = getField(tblId);
|
||
if (field && field.length === 0) {
|
||
const empty = {};
|
||
cfg.cols.forEach(c => { empty[c] = c === 'valor' || c === 'retirada' ? 0 : ''; });
|
||
field.push(empty);
|
||
}
|
||
}
|
||
|
||
function init() {
|
||
const today = new Date().toISOString().split('T')[0];
|
||
setVal('f-data', today);
|
||
estado.data = today;
|
||
setVal('f-loja', estado.loja);
|
||
syncHeader();
|
||
PRE_FILL_TABLES.forEach(ensureOneRow);
|
||
Object.keys(TABLES).forEach(renderTable);
|
||
updateTotals();
|
||
}
|
||
|
||
// ─── LocalStorage ──────────────────────────────────────────────────────────
|
||
const LS_KEY = 'fc_v7';
|
||
const CFG_KEY = 'fc_cfg';
|
||
|
||
function buildEstadoForBackend() {
|
||
// Monta o objeto exactly as o backend espera (storage.rs → salvar_fechamento)
|
||
return {
|
||
uuid: estado.uuid,
|
||
id_local: estado.id_local,
|
||
loja: estado.loja,
|
||
data: estado.data,
|
||
operador: estado.operador,
|
||
turno: estado.turno,
|
||
saldo_troco: parseNum(val('sys-troco')), // troco do sistema
|
||
saldo_esperado: parseNum(val('f-saldo-esperado')),
|
||
fechamento: parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber')),
|
||
diferenca: parseNum(val('f-saldo-esperado')) - (parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'))),
|
||
despesas: estado.despesas.map(r => ({ desc: r.desc || '', obs: r.obs || '', valor: r.valor || 0 })),
|
||
sangrias: estado.sangrias.map(r => ({ hora: r.hora || '', retirada: r.retirada || 0, gerente: r.gerente || '' })),
|
||
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome || '', valor: r.valor || 0 })),
|
||
vales: estado.vales.map(r => ({ nome: r.nome || '', obs: r.obs || '', valor: r.valor || 0 })),
|
||
receber: estado.receber.map(r => ({ nome: r.nome || '', valor: r.valor || 0 })),
|
||
cancelamentos: estado.cancelamentos.map(r => ({ numero: r.numero || '', valor: r.valor || 0, motivo: r.motivo || '' })),
|
||
cartoes: {
|
||
credito: estado.cartoes.credito.map(r => r.valor || 0),
|
||
debito: estado.cartoes.debito.map(r => r.valor || 0),
|
||
alimentacao: estado.cartoes.alimentacao.map(r => r.valor || 0),
|
||
pix: estado.cartoes.pix.map(r => r.valor || 0),
|
||
},
|
||
observacoes: estado.observacoes,
|
||
sync_status: estado.sync_status,
|
||
};
|
||
}
|
||
|
||
function saveState() {
|
||
// Sync reactive fields from DOM before saving
|
||
estado.operador = val('f-operador');
|
||
estado.turno = val('f-turno');
|
||
estado.loja = val('f-loja');
|
||
estado.saldo_troco = parseNum(val('sys-troco'));
|
||
estado.saldo_esperado = parseNum(val('f-saldo-esperado'));
|
||
estado.fechamento_contado = {
|
||
dinheiro: parseNum(val('f-fech-dinheiro')),
|
||
cartoes: parseNum(val('f-fech-cartoes')),
|
||
receber: parseNum(val('f-fech-receber')),
|
||
};
|
||
estado.updatedAt = new Date().toISOString();
|
||
try { localStorage.setItem(LS_KEY, JSON.stringify(estado)); } catch(e) {}
|
||
}
|
||
function loadState() {
|
||
try {
|
||
const raw = localStorage.getItem(LS_KEY);
|
||
if (!raw) return false;
|
||
const saved = JSON.parse(raw);
|
||
// Merge carefully (cartoes may be in old format)
|
||
Object.assign(estado, saved);
|
||
setVal('f-data', estado.data || '');
|
||
setVal('f-operador', estado.operador || '');
|
||
setVal('f-turno', estado.turno || '');
|
||
setVal('f-loja', estado.loja || 'União');
|
||
setVal('f-obs', estado.observacoes || '');
|
||
setVal('f-saldo-esperado', estado.saldo_esperado === 0 ? '' : fmtNum(estado.saldo_esperado));
|
||
setVal('sys-troco', estado.saldo_troco === 0 ? '' : fmtNum(estado.saldo_troco));
|
||
setVal('f-fech-dinheiro', estado.fechamento_contado && estado.fechamento_contado.dinheiro ? fmtNum(estado.fechamento_contado.dinheiro) : '');
|
||
setVal('f-fech-cartoes', estado.fechamento_contado && estado.fechamento_contado.cartoes ? fmtNum(estado.fechamento_contado.cartoes) : '');
|
||
setVal('f-fech-receber', estado.fechamento_contado && estado.fechamento_contado.receber ? fmtNum(estado.fechamento_contado.receber) : '');
|
||
syncHeader();
|
||
PRE_FILL_TABLES.forEach(ensureOneRow);
|
||
Object.keys(TABLES).forEach(renderTable);
|
||
updateTotals();
|
||
return true;
|
||
} catch(e) { return false; }
|
||
}
|
||
function loadConfig() {
|
||
try {
|
||
const raw = localStorage.getItem(CFG_KEY);
|
||
if (raw) config = { ...config, ...JSON.parse(raw) };
|
||
} catch(e) {}
|
||
}
|
||
function saveConfig() {
|
||
try { localStorage.setItem(CFG_KEY, JSON.stringify(config)); } catch(e) {}
|
||
buildDatalists();
|
||
}
|
||
|
||
// ─── Config Modal ──────────────────────────────────────────────────────────
|
||
function openConfig() {
|
||
document.getElementById('modal-config').classList.add('open');
|
||
renderCfgList('operadores', 'cfg-operadores-list');
|
||
renderCfgList('gerentes', 'cfg-gerentes-list');
|
||
renderCfgList('despesas', 'cfg-despesas-list');
|
||
renderCfgList('clientes', 'cfg-clientes-list');
|
||
renderCfgList('vales_nomes', 'cfg-vales-list');
|
||
}
|
||
function closeConfig() { document.getElementById('modal-config').classList.remove('open'); }
|
||
function renderCfgList(key, containerId) {
|
||
const container = document.getElementById(containerId);
|
||
container.innerHTML = '';
|
||
config[key].forEach((item, i) => {
|
||
const div = document.createElement('div');
|
||
div.className = 'cfg-item';
|
||
div.innerHTML = `<input type="text" value="${item}" data-key="${key}" data-index="${i}" /><button class="btn-rm" data-key="${key}" data-index="${i}">✕</button>`;
|
||
container.appendChild(div);
|
||
});
|
||
container.querySelectorAll('.btn-rm').forEach(btn => {
|
||
btn.onclick = () => { const { key: k, index: idx } = btn.dataset; config[k].splice(parseInt(idx), 1); renderCfgList(k, containerId); };
|
||
});
|
||
container.querySelectorAll('input').forEach(inp => {
|
||
inp.addEventListener('change', () => { const { key: k, index: idx } = inp.dataset; config[k][parseInt(idx)] = inp.value; });
|
||
});
|
||
}
|
||
|
||
document.getElementById('btn-config').addEventListener('click', openConfig);
|
||
document.getElementById('modal-close').addEventListener('click', closeConfig);
|
||
document.getElementById('modal-config').addEventListener('click', e => { if (e.target.id === 'modal-config') closeConfig(); });
|
||
|
||
['operador','gerente','despesa','cliente','vales'].forEach((_, i) => {
|
||
const btnMap = ['cfg-add-operador','cfg-add-gerente','cfg-add-despesa','cfg-add-cliente','cfg-add-vales'];
|
||
const keyMap = ['operadores','gerentes','despesas','clientes','vales_nomes'];
|
||
const listMap = ['cfg-operadores-list','cfg-gerentes-list','cfg-despesas-list','cfg-clientes-list','cfg-vales-list'];
|
||
document.getElementById(btnMap[i]).addEventListener('click', () => {
|
||
const k = keyMap[i]; config[k].push(''); renderCfgList(k, listMap[i]);
|
||
const inputs = document.getElementById(listMap[i]).querySelectorAll('input');
|
||
inputs[inputs.length - 1].focus();
|
||
});
|
||
});
|
||
|
||
document.getElementById('cfg-salvar').addEventListener('click', () => { saveConfig(); closeConfig(); scheduleSave(); });
|
||
|
||
// ─── Tauri Commands ───────────────────────────────────────────────────────
|
||
let _saveTimer = null;
|
||
function scheduleSave() {
|
||
clearTimeout(_saveTimer);
|
||
_saveTimer = setTimeout(() => { saveState(); if (window.__TAURI__) tauriSaveLocal(); }, 1000);
|
||
}
|
||
|
||
async function tauriSaveLocal() {
|
||
try {
|
||
const data = buildEstadoForBackend();
|
||
await window.__TAURI__.core.invoke('salvar_fechamento', { estado: data });
|
||
} catch(e) { console.warn('salvar_fechamento:', e); }
|
||
}
|
||
|
||
async function tauriEnviar() {
|
||
const data = buildEstadoForBackend();
|
||
// 1. Salva local
|
||
await window.__TAURI__.core.invoke('salvar_fechamento', { estado: data }).catch(e => console.warn('local save err:', e));
|
||
// 2. Envia Supabase
|
||
try {
|
||
const result = await window.__TAURI__.core.invoke('sb_salvar_fechamento', { estado: data });
|
||
console.log('Supabase salvo:', result);
|
||
return result;
|
||
} catch(e) {
|
||
console.error('Erro sb_salvar_fechamento:', e);
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
async function tauriRecibo() {
|
||
// Build flat totals for the Rust ReciboData struct
|
||
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
|
||
const cartDeb = estado.cartoes.debito.reduce((s,r) => s+(r.valor||0), 0);
|
||
const cartAli = estado.cartoes.alimentacao.reduce((s,r) => s+(r.valor||0), 0);
|
||
const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
|
||
const pixCnpjTotal = estado.pixcnpj.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const sangriasTotal = estado.sangrias.reduce((s,r) => s+(parseNum(r.retirada||0)||0), 0);
|
||
const despesasTotal = estado.despesas.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const valesTotal = estado.vales.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const receberTotal = estado.receber.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const cancelTotal = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const fechamentoCounted = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
|
||
const saldoEsp = parseNum(val('f-saldo-esperado'));
|
||
const sysTroco = parseNum(val('sys-troco'));
|
||
|
||
const data = {
|
||
uuid: estado.uuid,
|
||
id_local: estado.id_local,
|
||
loja: estado.loja,
|
||
data: estado.data,
|
||
operador: estado.operador,
|
||
turno: estado.turno,
|
||
saldo_troco: sysTroco,
|
||
saldo_esperado: saldoEsp,
|
||
fechamento: fechamentoCounted,
|
||
diferenca: sysTroco - saldoEsp,
|
||
despesas: estado.despesas.map(r => ({ desc: r.desc||'', obs: r.obs||'', valor: r.valor||0 })),
|
||
sangrias: estado.sangrias.map(r => ({ hora: r.hora||'', retirada: parseNum(r.retirada)||0, gerente: r.gerente||'' })),
|
||
pixcnpj: estado.pixcnpj.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||
vales: estado.vales.map(r => ({ nome: r.nome||'', obs: r.obs||'', valor: r.valor||0 })),
|
||
receber: estado.receber.map(r => ({ nome: r.nome||'', valor: r.valor||0 })),
|
||
cancelamentos: estado.cancelamentos.map(r => ({ numero: r.numero||'', valor: r.valor||0, motivo: r.motivo||'' })),
|
||
cartoes: { credito: cartCred, debito: cartDeb, alimentacao: cartAli, pix: cartPix },
|
||
// Flat totals for ReciboData struct (Rust)
|
||
credito: cartCred,
|
||
debito: cartDeb,
|
||
alimentacao: cartAli,
|
||
voucher: cartPix,
|
||
pixcnpj: pixCnpjTotal,
|
||
sangrias: sangriasTotal,
|
||
despesas: despesasTotal,
|
||
vales: valesTotal,
|
||
areceber: receberTotal,
|
||
cancelamentos: cancelTotal,
|
||
observacoes: estado.observacoes,
|
||
sync_status: estado.sync_status,
|
||
};
|
||
return await window.__TAURI__.core.invoke('imprimir_recibo', { estado: JSON.stringify(data) });
|
||
}
|
||
|
||
async function tauriCarregarRascunho() {
|
||
const loja = val('f-loja') || 'União';
|
||
const data = val('f-data');
|
||
if (!data) return null;
|
||
try {
|
||
return await window.__TAURI__.core.invoke('carregar_rascunho', { loja, data });
|
||
} catch(e) { console.warn('carregar_rascunho:', e); return null; }
|
||
}
|
||
|
||
// ─── Confirm Modal ────────────────────────────────────────────────────────
|
||
function openConfirmModal() {
|
||
const operador = val('f-operador').trim();
|
||
const turno = val('f-turno').trim();
|
||
const loja = val('f-loja').trim();
|
||
if (!operador || !turno || !loja) {
|
||
alert('⚠️ Preencha Operador, Turno e Loja antes de enviar.');
|
||
return;
|
||
}
|
||
// Build summary
|
||
const cartCred = estado.cartoes.credito.reduce((s,r) => s+(r.valor||0), 0);
|
||
const cartDeb = estado.cartoes.debito.reduce((s,r) => s+(r.valor||0), 0);
|
||
const cartAli = estado.cartoes.alimentacao.reduce((s,r) => s+(r.valor||0), 0);
|
||
const cartPix = estado.cartoes.pix.reduce((s,r) => s+(r.valor||0), 0);
|
||
const sangrias = estado.sangrias.reduce((s,r) => s+(parseNum(r.retirada||0)||0), 0);
|
||
const despesas = estado.despesas.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const vales = estado.vales.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const receber = estado.receber.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const cancelamentos = estado.cancelamentos.reduce((s,r) => s+(parseNum(r.valor||0)||0), 0);
|
||
const fechamento = parseNum(val('f-fech-dinheiro')) + parseNum(val('f-fech-cartoes')) + parseNum(val('f-fech-receber'));
|
||
const saldoEsp = parseNum(val('f-saldo-esperado'));
|
||
document.getElementById('confirm-summary').innerHTML =
|
||
`<div style="margin-bottom:6px"><strong>📅 Data:</strong> ${estado.data} <strong>👤 Operador:</strong> ${operador} <strong>⏰ Turno:</strong> ${turno} <strong>🏪 Loja:</strong> ${loja}</div>
|
||
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
|
||
<div>💸 Sangrias: <strong>${fmt(sangrias)}</strong></div>
|
||
<div>📝 Despesas: <strong>${fmt(despesas)}</strong></div>
|
||
<div>💰 Vales: <strong>${fmt(vales)}</strong></div>
|
||
<div>👥 À Receber: <strong>${fmt(receber)}</strong></div>
|
||
<div>💳 Crédito: <strong style="color:#c0272d">${fmt(cartCred)}</strong></div>
|
||
<div>💳 Débito: <strong>${fmt(cartDeb)}</strong></div>
|
||
<div>💳 Alimentação: <strong>${fmt(cartAli)}</strong></div>
|
||
<div>❌ Cancelamentos: <strong>${fmt(cancelamentos)}</strong></div>
|
||
<div>📋 Fechamento: <strong>${fmt(fechamento)}</strong></div>
|
||
<hr style="border:none;border-top:1px dashed #b3d9f7;margin:6px 0">
|
||
<div>🎯 Saldo Esperado: <strong>${fmt(saldoEsp)}</strong></div>
|
||
<div style="margin-top:4px;font-size:10px;color:#888">ID: ${estado.loja}_${estado.data}_${estado.operador}</div>`;
|
||
document.getElementById('modal-confirm').classList.add('open');
|
||
}
|
||
|
||
function closeConfirmModal() {
|
||
document.getElementById('modal-confirm').classList.remove('open');
|
||
}
|
||
|
||
document.getElementById('confirm-close').addEventListener('click', closeConfirmModal);
|
||
document.getElementById('confirm-cancel').addEventListener('click', closeConfirmModal);
|
||
document.getElementById('modal-confirm').addEventListener('click', e => { if (e.target.id === 'modal-confirm') closeConfirmModal(); });
|
||
|
||
// Print from confirm modal
|
||
document.getElementById('confirm-print').addEventListener('click', async () => {
|
||
closeConfirmModal();
|
||
const btn = document.getElementById('btn-recibo');
|
||
btn.disabled = true;
|
||
try {
|
||
await tauriRecibo();
|
||
alert('✅ Impressão enviada!');
|
||
} catch(e) {
|
||
alert('Erro ao imprimir: ' + (e.message || e));
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
});
|
||
|
||
// 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!');
|
||
} catch(e) {
|
||
await tauriSaveLocal();
|
||
alert('Erro ao enviar: ' + (e.message || e) + '\n(Dados salvos localmente)');
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.textContent = '💾 Enviar Fechamento';
|
||
}
|
||
});
|
||
|
||
// ─── Buttons ───────────────────────────────────────────────────────────────
|
||
document.getElementById('btn-enviar').addEventListener('click', () => {
|
||
openConfirmModal();
|
||
});
|
||
|
||
document.getElementById('btn-recibo').addEventListener('click', async () => {
|
||
const btn = document.getElementById('btn-recibo');
|
||
btn.disabled = true;
|
||
try {
|
||
await tauriRecibo();
|
||
alert('✅ Impressão enviada!');
|
||
} catch(e) {
|
||
alert('Erro ao imprimir: ' + (e.message || e));
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
});
|
||
|
||
document.getElementById('btn-limpar').addEventListener('click', () => {
|
||
if (!confirm('Limpar rascunho? Esta acção não pode ser revertida.')) return;
|
||
localStorage.removeItem(LS_KEY);
|
||
location.reload();
|
||
});
|
||
|
||
document.getElementById('btn-anterior').addEventListener('click', async () => {
|
||
const d = new Date(estado.data); d.setDate(d.getDate() - 1);
|
||
const prevDate = d.toISOString().split('T')[0];
|
||
await tauriSaveLocal();
|
||
setVal('f-data', prevDate);
|
||
estado.data = prevDate;
|
||
estado.uuid = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
|
||
estado.id_local = null;
|
||
syncHeader();
|
||
|
||
// Try Supabase first (source of truth for submitted closings)
|
||
let rascunho = null;
|
||
try {
|
||
rascunho = await window.__TAURI__.core.invoke('sb_carregar_rascunho', {
|
||
loja: val('f-loja'),
|
||
data: prevDate
|
||
});
|
||
} catch(e) {
|
||
console.warn('sb_carregar_rascunho falhou, tentando local:', e);
|
||
}
|
||
|
||
// Fallback: local SQLite
|
||
if (!rascunho) {
|
||
try {
|
||
rascunho = await window.__TAURI__.core.invoke('carregar_rascunho', {
|
||
loja: val('f-loja'),
|
||
data: prevDate
|
||
});
|
||
} catch(e) {
|
||
console.warn('carregar_rascunho local falhou:', e);
|
||
}
|
||
}
|
||
|
||
if (rascunho) {
|
||
// Meta fields
|
||
if (rascunho.operador) { setVal('f-operador', rascunho.operador); estado.operador = rascunho.operador; }
|
||
if (rascunho.turno) { setVal('f-turno', rascunho.turno); estado.turno = rascunho.turno; }
|
||
if (rascunho.saldo_troco) setVal('sys-troco', fmtNum(rascunho.saldo_troco));
|
||
if (rascunho.saldo_esperado) setVal('f-saldo-esperado', fmtNum(rascunho.saldo_esperado));
|
||
if (rascunho.observacoes) { setVal('f-obs', rascunho.observacoes); estado.observacoes = rascunho.observacoes; }
|
||
|
||
// All tables from Supabase/local
|
||
if (rascunho.despesas && Array.isArray(rascunho.despesas)) {
|
||
estado.despesas = rascunho.despesas.map(d => ({ desc: d.desc || d.descricao || '', obs: d.obs || '', valor: parseNum(d.valor) || 0 }));
|
||
}
|
||
if (rascunho.sangrias && Array.isArray(rascunho.sangrias)) {
|
||
estado.sangrias = rascunho.sangrias.map(s => ({ hora: s.hora || '', retirada: parseNum(s.retirada) || 0, gerente: s.gerente || '' }));
|
||
}
|
||
if (rascunho.pixcnpj && Array.isArray(rascunho.pixcnpj)) {
|
||
estado.pixcnpj = rascunho.pixcnpj.map(p => ({ nome: p.nome || '', valor: parseNum(p.valor) || 0 }));
|
||
}
|
||
if (rascunho.vales && Array.isArray(rascunho.vales)) {
|
||
estado.vales = rascunho.vales.map(v => ({ nome: v.nome || '', obs: v.obs || '', valor: parseNum(v.valor) || 0 }));
|
||
}
|
||
if (rascunho.receber && Array.isArray(rascunho.receber)) {
|
||
estado.receber = rascunho.receber.map(r => ({ nome: r.nome || '', valor: parseNum(r.valor) || 0 }));
|
||
}
|
||
if (rascunho.cancelamentos && Array.isArray(rascunho.cancelamentos)) {
|
||
estado.cancelamentos = rascunho.cancelamentos.map(c => ({ numero: c.numero || '', valor: parseNum(c.valor) || 0, motivo: c.motivo || '' }));
|
||
}
|
||
if (rascunho.cartoes) {
|
||
estado.cartoes.credito = (rascunho.cartoes.credito || []).map(v => ({ valor: parseNum(v) || 0 }));
|
||
estado.cartoes.debito = (rascunho.cartoes.debito || []).map(v => ({ valor: parseNum(v) || 0 }));
|
||
estado.cartoes.alimentacao = (rascunho.cartoes.alimentacao || []).map(v => ({ valor: parseNum(v) || 0 }));
|
||
estado.cartoes.pix = (rascunho.cartoes.pix || []).map(v => ({ valor: parseNum(v) || 0 }));
|
||
}
|
||
} else {
|
||
// No data for previous day: clear everything
|
||
estado.despesas = [];
|
||
estado.sangrias = [];
|
||
estado.pixcnpj = [];
|
||
estado.vales = [];
|
||
estado.receber = [];
|
||
estado.cancelamentos = [];
|
||
estado.cartoes = { credito: [], debito: [], alimentacao: [], pix: [] };
|
||
setVal('sys-troco', '');
|
||
setVal('f-saldo-esperado', '');
|
||
setVal('f-obs', '');
|
||
}
|
||
|
||
PRE_FILL_TABLES.forEach(ensureOneRow);
|
||
Object.keys(TABLES).forEach(renderTable);
|
||
updateTotals();
|
||
scheduleSave();
|
||
});
|
||
|
||
|
||
document.getElementById('f-obs').addEventListener('input', e => { estado.observacoes = e.target.value; scheduleSave(); });
|
||
|
||
// ─── Boot ─────────────────────────────────────────────────────────────────
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadConfig();
|
||
buildDatalists();
|
||
if (!loadState()) init();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|