Fechamento de Caixa - Tauri 2 initial commit
- Storage: SQLite local via rusqlite - Supabase: REST API via reqwest - Printer: USB ESC/POS for TM-T20 - Frontend: vanilla HTML (form-v9.3 ported) - Build: GitHub Actions for Linux/Windows/macOS
This commit is contained in:
+958
@@ -0,0 +1,958 @@
|
||||
<!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>
|
||||
/* ─── Reset & Base ─────────────────────────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #0d0d0d;
|
||||
--surface: #181818;
|
||||
--surface2: #242424;
|
||||
--border: #333;
|
||||
--text: #e0e0e0;
|
||||
--muted: #888;
|
||||
--accent: #e63946;
|
||||
--accent2: #f4a261;
|
||||
--success: #2a9d8f;
|
||||
--warning: #e9c46a;
|
||||
--danger: #e63946;
|
||||
--radius: 8px;
|
||||
--font: 'Segoe UI', system-ui, sans-serif;
|
||||
}
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
font-family: var(--font);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ─── Header ──────────────────────────────────────────────────────── */
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
header .logo {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
}
|
||||
header .subtitle { font-size: 0.8rem; color: var(--muted); }
|
||||
.header-spacer { flex: 1; }
|
||||
.sync-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
}
|
||||
.sync-badge.synced { border-color: var(--success); color: var(--success); }
|
||||
.sync-badge.error { border-color: var(--danger); color: var(--danger); }
|
||||
|
||||
/* ─── Toolbar ──────────────────────────────────────────────────────── */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.toolbar select, .toolbar input {
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: var(--radius);
|
||||
padding: 5px 10px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.toolbar button {
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: var(--radius);
|
||||
padding: 5px 14px;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.toolbar button:hover { border-color: var(--accent); color: var(--accent); }
|
||||
.toolbar button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||
.toolbar button.primary:hover { background: #c1121f; }
|
||||
|
||||
/* ─── Main Form Area ───────────────────────────────────────────────── */
|
||||
.main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
/* ─── Form Sections ────────────────────────────────────────────────── */
|
||||
.section {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: var(--surface2);
|
||||
border-bottom: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
gap: 8px;
|
||||
}
|
||||
.section-header:hover { background: #2a2a2a; }
|
||||
.section-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--accent2);
|
||||
}
|
||||
.section-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 7px;
|
||||
border-radius: 10px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
display: none;
|
||||
}
|
||||
.section-badge.visible { display: inline; }
|
||||
.section-toggle { margin-left: auto; color: var(--muted); font-size: 0.8rem; }
|
||||
.section-body { padding: 12px; }
|
||||
|
||||
/* ─── Form Grid ────────────────────────────────────────────────────── */
|
||||
.grid { display: grid; gap: 8px; }
|
||||
.grid-2 { grid-template-columns: 1fr 1fr; }
|
||||
.grid-3 { grid-template-columns: 1fr 1fr 1fr; }
|
||||
.grid-4 { grid-template-columns: 1fr 1fr 1fr 1fr; }
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
input, select, textarea {
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: var(--radius);
|
||||
padding: 7px 10px;
|
||||
font-size: 0.9rem;
|
||||
font-family: var(--font);
|
||||
width: 100%;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
input:focus, select:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
input[type="number"] { font-variant-numeric: tabular-nums; }
|
||||
input:read-only { opacity: 0.6; cursor: not-allowed; }
|
||||
.input-group { display: flex; gap: 6px; }
|
||||
.input-group input { flex: 1; }
|
||||
|
||||
/* ─── Table Editor ─────────────────────────────────────────────────── */
|
||||
.table-wrap { overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
||||
th {
|
||||
background: var(--surface2);
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 5px 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
td { padding: 3px 8px; border-bottom: 1px solid #222; vertical-align: middle; }
|
||||
tr:hover td { background: #1e1e1e; }
|
||||
td input, td select {
|
||||
padding: 4px 6px;
|
||||
font-size: 0.82rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
td input[type="number"] { text-align: right; width: 110px; }
|
||||
td input[placeholder] { width: 140px; }
|
||||
.btn-remove {
|
||||
width: 26px; height: 26px;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.btn-remove:hover { border-color: var(--danger); color: var(--danger); background: rgba(230,57,70,0.1); }
|
||||
.btn-add-row {
|
||||
width: 100%;
|
||||
padding: 6px;
|
||||
margin-top: 6px;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--border);
|
||||
color: var(--muted);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.btn-add-row:hover { border-color: var(--accent2); color: var(--accent2); }
|
||||
|
||||
/* ─── Diferença Display ────────────────────────────────────────────── */
|
||||
.resultado {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius);
|
||||
margin-top: 8px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.resultado.ok { background: rgba(42,157,143,0.15); border: 1px solid var(--success); }
|
||||
.resultado.diff { background: rgba(230,57,70,0.15); border: 1px solid var(--danger); }
|
||||
.resultado .label { font-size: 0.8rem; color: var(--muted); text-transform: uppercase; }
|
||||
.resultado .value { font-size: 1.4rem; font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
.resultado.ok .value { color: var(--success); }
|
||||
.resultado.diff .value { color: var(--danger); }
|
||||
|
||||
/* ─── Action Bar ───────────────────────────────────────────────────── */
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: var(--surface);
|
||||
border-top: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.actions button {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
padding: 10px 16px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface2);
|
||||
color: var(--text);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.actions button:hover { border-color: var(--accent); }
|
||||
.actions button.btn-enviar {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
.actions button.btn-enviar:hover { background: #c1121f; }
|
||||
.actions button:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
/* ─── Toast ────────────────────────────────────────────────────────── */
|
||||
#toast {
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(20px);
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.85rem;
|
||||
opacity: 0;
|
||||
transition: all 0.3s;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
max-width: 90vw;
|
||||
}
|
||||
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
#toast.success { border-color: var(--success); color: var(--success); }
|
||||
#toast.error { border-color: var(--danger); color: var(--danger); }
|
||||
|
||||
/* ─── Loading Overlay ──────────────────────────────────────────────── */
|
||||
#loading {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
z-index: 999;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
#loading.show { display: flex; }
|
||||
.spinner {
|
||||
width: 40px; height: 40px;
|
||||
border: 3px solid var(--border);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* ─── Responsivo ───────────────────────────────────────────────────── */
|
||||
@media (max-width: 700px) {
|
||||
.grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr 1fr; }
|
||||
.toolbar { gap: 6px; }
|
||||
.actions button { min-width: 100px; font-size: 0.8rem; }
|
||||
}
|
||||
|
||||
/* ─── Print Preview Modal ─────────────────────────────────────────── */
|
||||
#modal-print {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.8);
|
||||
z-index: 500;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
#modal-print.show { display: flex; }
|
||||
#modal-print .paper {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
width: 280px;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
#modal-print .paper .p-title { text-align: center; font-weight: bold; font-size: 14px; }
|
||||
#modal-print .paper .p-section { margin-top: 8px; }
|
||||
#modal-print .paper .p-row { display: flex; justify-content: space-between; }
|
||||
#modal-print .paper .p-divider { border-top: 1px dashed #000; margin: 4px 0; }
|
||||
#modal-print .actions { background: transparent; border: none; justify-content: center; gap: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Header -->
|
||||
<header>
|
||||
<span class="logo">🍗 Fechamento de Caixa</span>
|
||||
<span class="subtitle" id="loja-label">—</span>
|
||||
<div class="header-spacer"></div>
|
||||
<span class="sync-badge" id="sync-badge">● Local</span>
|
||||
</header>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="toolbar">
|
||||
<select id="sel-loja">
|
||||
<option value="">Loja…</option>
|
||||
<option value="Uniao">União</option>
|
||||
<option value="Alianca">Aliança</option>
|
||||
</select>
|
||||
<input type="date" id="inp-data">
|
||||
<select id="sel-turno">
|
||||
<option value="">Turno…</option>
|
||||
<option value="1">1º Turno</option>
|
||||
<option value="2">2º Turno</option>
|
||||
</select>
|
||||
<select id="sel-operador">
|
||||
<option value="">Operador…</option>
|
||||
</select>
|
||||
<div class="header-spacer"></div>
|
||||
<button onclick="carregarRascunho()">↺ Carregar</button>
|
||||
<button onclick="listarRecentes()">📋 Recentes</button>
|
||||
<button onclick="abrirConfig()">⚙</button>
|
||||
</div>
|
||||
|
||||
<!-- Main Form -->
|
||||
<div class="main" id="main-form">
|
||||
|
||||
<!-- Entradas -->
|
||||
<div class="section" id="sec-entradas">
|
||||
<div class="section-header" onclick="toggleSec('entradas')">
|
||||
<span class="section-title">Entradas</span>
|
||||
<span class="section-badge" id="badge-entradas">0</span>
|
||||
<span class="section-toggle" id="toggle-entradas">▼</span>
|
||||
</div>
|
||||
<div class="section-body" id="body-entradas">
|
||||
<div class="grid grid-3">
|
||||
<label>Saldo Troco<input type="number" id="f-saldo-troco" step="0.01" placeholder="0,00"></label>
|
||||
<label>Fechamento (Dinheiro)<input type="number" id="f-fechamento" step="0.01" placeholder="0,00"></label>
|
||||
<label>Saldo Esperado<input type="number" id="f-saldo-esperado" step="0.01" placeholder="0,00" readonly></label>
|
||||
</div>
|
||||
<div class="resultado" id="res-diferenca">
|
||||
<span class="label">Diferença</span>
|
||||
<span class="value" id="val-diferenca">R$ 0,00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sangrias / Despesas -->
|
||||
<div class="section" id="sec-despesas">
|
||||
<div class="section-header" onclick="toggleSec('despesas')">
|
||||
<span class="section-title">Sangrias / Despesas</span>
|
||||
<span class="section-badge" id="badge-despesas">0</span>
|
||||
<span class="section-toggle" id="toggle-despesas">▼</span>
|
||||
</div>
|
||||
<div class="section-body" id="body-despesas">
|
||||
<div class="table-wrap">
|
||||
<table id="tbl-despesas">
|
||||
<thead><tr>
|
||||
<th>Descrição</th><th>Valor</th><th></th>
|
||||
</tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<button class="btn-add-row" onclick="addDespesa()">+ Adicionar despesa</button>
|
||||
</div>
|
||||
<div class="grid grid-3" style="margin-top:10px">
|
||||
<label>Total Sangrias<input type="number" id="f-total-sangrias" step="0.01" readonly></label>
|
||||
<label>Total Despesas<input type="number" id="f-total-despesas" step="0.01" readonly></label>
|
||||
<label>Vales<input type="number" id="f-vales" step="0.01"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cartões -->
|
||||
<div class="section" id="sec-cartoes">
|
||||
<div class="section-header" onclick="toggleSec('cartoes')">
|
||||
<span class="section-title">Cartões / Voucher</span>
|
||||
<span class="section-badge" id="badge-cartoes">0</span>
|
||||
<span class="section-toggle" id="toggle-cartoes">▼</span>
|
||||
</div>
|
||||
<div class="section-body" id="body-cartoes">
|
||||
<div class="grid grid-4">
|
||||
<label>Crédito<input type="number" id="f-credito" step="0.01" placeholder="0,00"></label>
|
||||
<label>Débito<input type="number" id="f-debito" step="0.01" placeholder="0,00"></label>
|
||||
<label>Alimentação<input type="number" id="f-alimentacao" step="0.01" placeholder="0,00"></label>
|
||||
<label>Voucher<input type="number" id="f-voucher" step="0.01" placeholder="0,00"></label>
|
||||
</div>
|
||||
<div style="margin-top:8px">
|
||||
<label>Cancelamentos<input type="number" id="f-cancelamentos" step="0.01" placeholder="0,00"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- A Receber -->
|
||||
<div class="section" id="sec-receber">
|
||||
<div class="section-header" onclick="toggleSec('receber')">
|
||||
<span class="section-title">A Receber</span>
|
||||
<span class="section-badge" id="badge-receber">0</span>
|
||||
<span class="section-toggle" id="toggle-receber">▼</span>
|
||||
</div>
|
||||
<div class="section-body" id="body-receber">
|
||||
<div class="table-wrap">
|
||||
<table id="tbl-receber">
|
||||
<thead><tr><th>Descrição</th><th>Valor</th><th></th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<button class="btn-add-row" onclick="addReceber()">+ Adicionar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Observaes -->
|
||||
<div class="section" id="sec-obs">
|
||||
<div class="section-header" onclick="toggleSec('obs')">
|
||||
<span class="section-title">Observaes / Info</span>
|
||||
<span class="section-toggle" id="toggle-obs">▼</span>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<textarea id="f-observacoes" rows="3" placeholder="Observaes do fechamento…"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /.main -->
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="actions">
|
||||
<button onclick="salvarRascunho()" id="btn-salvar">💾 Salvar</button>
|
||||
<button onclick="limparForm()" id="btn-limpar">🗑 Limpar</button>
|
||||
<button onclick="abrirImpressao()" id="btn-imprimir">🖨 Imprimir</button>
|
||||
<button onclick="enviarFechamento()" class="btn-enviar" id="btn-enviar">📤 Enviar</button>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div id="toast"></div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div id="loading"><div class="spinner"></div><span id="loading-msg">Aguarde…</span></div>
|
||||
|
||||
<!-- Config Modal -->
|
||||
<div id="modal-config" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.8);z-index:600;align-items:center;justify-content:center;">
|
||||
<div style="background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:24px;width:360px;max-width:90vw;">
|
||||
<h3 style="margin-bottom:16px;font-size:1rem;">⚙ Configuraes</h3>
|
||||
<label style="margin-bottom:12px">Impressora USB
|
||||
<select id="cfg-impressora" style="margin-top:4px">
|
||||
<option value="">— Detectar automaticamente —</option>
|
||||
</select>
|
||||
</label>
|
||||
<div style="display:flex;gap:8px;margin-top:16px">
|
||||
<button onclick="detectarImpressora()" style="flex:1;padding:8px;border-radius:var(--radius);background:var(--surface2);border:1px solid var(--border);color:var(--text);cursor:pointer;">Detectar</button>
|
||||
<button onclick="fecharConfig()" style="flex:1;padding:8px;border-radius:var(--radius);background:var(--accent);border:1px solid var(--accent);color:#fff;cursor:pointer;">Fechar</button>
|
||||
</div>
|
||||
<div id="cfg-status" style="margin-top:10px;font-size:0.8rem;color:var(--muted);"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Print Preview Modal -->
|
||||
<div id="modal-print">
|
||||
<div class="paper" id="paper-content"></div>
|
||||
<div class="actions" style="position:absolute;bottom:20px;left:0;right:0;">
|
||||
<button onclick="imprimirAgora()">🖨 Imprimir</button>
|
||||
<button onclick="fecharPrintPreview()">Cancelar</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
let estado = {
|
||||
uuid: null,
|
||||
id_local: null,
|
||||
loja: '',
|
||||
data: '',
|
||||
operador: '',
|
||||
turno: '',
|
||||
saldo_troco: 0,
|
||||
fechamento: 0,
|
||||
saldo_esperado: 0,
|
||||
diferenca: 0,
|
||||
despesas: [], // [{desc, valor}]
|
||||
total_sangrias: 0,
|
||||
total_despesas: 0,
|
||||
vales: 0,
|
||||
cartoes: {credito:0, debito:0, alimentacao:0, voucher:0, cancelamentos:0},
|
||||
receber: [], // [{desc, valor}]
|
||||
observacoes: 'rascunho'
|
||||
};
|
||||
let autoSaveTimer = null;
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────
|
||||
async function init() {
|
||||
// Defaults: loja do config, data hoje, turno 1
|
||||
const cfg = await invoke('get_config').catch(() => ({}));
|
||||
if (cfg.loja) {
|
||||
document.getElementById('sel-loja').value = cfg.loja;
|
||||
estado.loja = cfg.loja;
|
||||
}
|
||||
document.getElementById('inp-data').value = new Date().toISOString().split('T')[0];
|
||||
estado.data = document.getElementById('inp-data').value;
|
||||
|
||||
// Load operadores from server
|
||||
await carregarOperadores();
|
||||
|
||||
// Add Enter key handler for all value inputs to add new row
|
||||
document.querySelectorAll('input').forEach(inp => {
|
||||
inp.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' && inp.type === 'number') {
|
||||
e.preventDefault();
|
||||
inp.blur();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-calculate on input change
|
||||
['f-saldo-troco','f-fechamento','f-credito','f-debito','f-alimentacao','f-voucher','f-cancelamentos','f-vales'].forEach(id => {
|
||||
document.getElementById(id)?.addEventListener('input', calcular);
|
||||
});
|
||||
|
||||
calcular();
|
||||
document.getElementById('loja-label').textContent = estado.loja || '—';
|
||||
}
|
||||
|
||||
// ── Store ─────────────────────────────────────────────────────────────────
|
||||
async function invokeSafe(cmd, args = {}) {
|
||||
try {
|
||||
return await invoke(cmd, args);
|
||||
} catch(e) {
|
||||
console.error(`[${cmd}]`, e);
|
||||
toast(`Erro: ${e}`, 'error');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Auto-save (local SQLite) ─────────────────────────────────────────────
|
||||
function scheduleAutoSave() {
|
||||
clearTimeout(autoSaveTimer);
|
||||
autoSaveTimer = setTimeout(() => salvarRascunho(true), 1500);
|
||||
}
|
||||
|
||||
function calcular() {
|
||||
const sto = parseFloat(val('f-saldo-troco')) || 0;
|
||||
const fec = parseFloat(val('f-fechamento')) || 0;
|
||||
const esp = sto + fec;
|
||||
document.getElementById('f-saldo-esperado').value = fmt(esp);
|
||||
|
||||
estado.saldo_troco = sto;
|
||||
estado.fechamento = fec;
|
||||
estado.saldo_esperado = esp;
|
||||
|
||||
const dif = fec - esp;
|
||||
estado.diferenca = dif;
|
||||
const el = document.getElementById('res-diferenca');
|
||||
const vl = document.getElementById('val-diferenca');
|
||||
el.className = 'resultado ' + (Math.abs(dif) < 0.01 ? 'ok' : 'diff');
|
||||
vl.textContent = (dif >= 0 ? '+' : '') + fmt(dif);
|
||||
|
||||
// Totals
|
||||
estado.despesas = getTableRows('tbl-despesas').map(r => ({desc: r[0], valor: parseFloat(r[1])||0}));
|
||||
estado.total_sangrias = estado.despesas.filter(d => d.desc.includes('Sangria')).reduce((s,d) => s+d.valor, 0);
|
||||
estado.total_despesas = estado.despesas.filter(d => !d.desc.includes('Sangria')).reduce((s,d) => s+d.valor, 0);
|
||||
estado.vales = parseFloat(val('f-vales')) || 0;
|
||||
estado.cartoes = {
|
||||
credo: parseFloat(val('f-credito')) || 0,
|
||||
debito: parseFloat(val('f-debito')) || 0,
|
||||
alimentacao: parseFloat(val('f-alimentacao')) || 0,
|
||||
voucher: parseFloat(val('f-voucher')) || 0,
|
||||
cancelamentos: parseFloat(val('f-cancelamentos')) || 0
|
||||
};
|
||||
estado.receber = getTableRows('tbl-receber').map(r => ({desc: r[0], valor: parseFloat(r[1])||0}));
|
||||
estado.observacoes = val('f-observacoes') || 'rascunho';
|
||||
estado.loja = val('sel-loja');
|
||||
estado.data = val('inp-data');
|
||||
estado.operador = val('sel-operador');
|
||||
estado.turno = val('sel-turno');
|
||||
|
||||
updateBadges();
|
||||
}
|
||||
|
||||
function updateBadges() {
|
||||
const d = estado.despesas.length;
|
||||
const r = estado.receber.length;
|
||||
const bD = document.getElementById('badge-despesas');
|
||||
const bR = document.getElementById('badge-receber');
|
||||
bD.textContent = d; bD.className = 'section-badge' + (d ? ' visible' : '');
|
||||
bR.textContent = r; bR.className = 'section-badge' + (r ? ' visible' : '');
|
||||
}
|
||||
|
||||
// ── Salvar Rascunho ──────────────────────────────────────────────────────
|
||||
async function salvarRascunho(silent = false) {
|
||||
calcular();
|
||||
try {
|
||||
const result = await invokeSafe('salvar_local', { estado: JSON.stringify(estado) });
|
||||
estado.id_local = result.id;
|
||||
estado.uuid = result.uuid;
|
||||
if (!silent) toast('Rascunho salvo (local)', 'success');
|
||||
atualizarSyncBadge('local');
|
||||
} catch(e) {
|
||||
if (!silent) toast('Erro ao salvar rascunho', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Carregar Rascunho ───────────────────────────────────────────────────
|
||||
async function carregarRascunho() {
|
||||
const loja = val('sel-loja');
|
||||
const data = val('inp-data');
|
||||
if (!loja || !data) { toast('Selecione loja e data primeiro', 'error'); return; }
|
||||
showLoad('Carregando rascunho…');
|
||||
try {
|
||||
const r = await invokeSafe('carregar_rascunho', { loja, data });
|
||||
if (!r) { toast('Nenhum rascunho encontrado', ''); hideLoad(); return; }
|
||||
Object.assign(estado, r);
|
||||
// Restore form fields
|
||||
document.getElementById('f-saldo-troco').value = r.saldo_troco || '';
|
||||
document.getElementById('f-fechamento').value = r.fechamento || '';
|
||||
document.getElementById('f-vales').value = r.vales || '';
|
||||
document.getElementById('f-credito').value = r.cartoes?.credito || '';
|
||||
document.getElementById('f-debito').value = r.cartoes?.debito || '';
|
||||
document.getElementById('f-alimentacao').value = r.cartoes?.alimentacao || '';
|
||||
document.getElementById('f-voucher').value = r.cartoes?.voucher || '';
|
||||
document.getElementById('f-cancelamentos').value = r.cartoes?.cancelamentos || '';
|
||||
document.getElementById('f-observacoes').value = r.observacoes || '';
|
||||
document.getElementById('sel-operador').value = r.operador || '';
|
||||
document.getElementById('sel-turno').value = r.turno || '';
|
||||
// Restore tables
|
||||
restoreTable('tbl-despesas', (r.despesas||[]).map(d => [d.desc, d.valor]));
|
||||
restoreTable('tbl-receber', (r.receber||[]).map(d => [d.desc, d.valor]));
|
||||
calcular();
|
||||
toast('Rascunho carregado', 'success');
|
||||
} catch(e) { /* toast already shown */ }
|
||||
hideLoad();
|
||||
}
|
||||
|
||||
// ── Enviar (sync to Supabase) ─────────────────────────────────────────────
|
||||
async function enviarFechamento() {
|
||||
calcular();
|
||||
if (!estado.loja || !estado.data) { toast('Loja e data so obrigatorios', 'error'); return; }
|
||||
showLoad('Enviando ao servidor…');
|
||||
try {
|
||||
await invokeSafe('enviar_fechamento', { estado: JSON.stringify(estado) });
|
||||
toast('Enviado com sucesso!', 'success');
|
||||
atualizarSyncBadge('synced');
|
||||
} catch(e) { toast(`Erro ao enviar: ${e}`, 'error'); }
|
||||
hideLoad();
|
||||
}
|
||||
|
||||
// ── Impresso ─────────────────────────────────────────────────────────────
|
||||
function abrirImpressao() {
|
||||
calcular();
|
||||
const p = document.getElementById('paper-content');
|
||||
const dif = estado.diferenca;
|
||||
p.innerHTML = `
|
||||
<div class="p-title">FECHAMENTO DE CAIXA</div>
|
||||
<div style="text-align:center">${estado.loja} — ${estado.data} — Turno ${estado.turno}</div>
|
||||
<div style="text-align:center">Operador: ${estado.operador}</div>
|
||||
<div class="p-divider"></div>
|
||||
<div class="p-section">
|
||||
<div class="p-row"><span>Saldo Troco:</span><span>${fmt(estado.saldo_troco)}</span></div>
|
||||
<div class="p-row"><span>Fechamento:</span><span>${fmt(estado.fechamento)}</span></div>
|
||||
<div class="p-row"><span>Saldo Esperado:</span><span>${fmt(estado.saldo_esperado)}</span></div>
|
||||
<div class="p-row" style="font-weight:bold;color:${Math.abs(dif)<0.01?'green':'red'}">
|
||||
<span>Diferenca:</span><span>${dif>=0?'+':''}${fmt(dif)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-divider"></div>
|
||||
<div class="p-section">
|
||||
<div class="p-row"><span>Credito:</span><span>${fmt(estado.cartoes.credito)}</span></div>
|
||||
<div class="p-row"><span>Debito:</span><span>${fmt(estado.cartoes.debito)}</span></div>
|
||||
<div class="p-row"><span>Alimentacao:</span><span>${fmt(estado.cartoes.alimentacao)}</span></div>
|
||||
<div class="p-row"><span>Voucher:</span><span>${fmt(estado.cartoes.voucher)}</span></div>
|
||||
<div class="p-row"><span>Cancelamentos:</span><span>${fmt(estado.cartoes.cancelamentos)}</span></div>
|
||||
</div>
|
||||
<div class="p-divider"></div>
|
||||
<div class="p-section">
|
||||
<div class="p-row"><span>Sangrias:</span><span>${fmt(estado.total_sangrias)}</span></div>
|
||||
<div class="p-row"><span>Despesas:</span><span>${fmt(estado.total_despesas)}</span></div>
|
||||
<div class="p-row"><span>Vales:</span><span>${fmt(estado.vales)}</span></div>
|
||||
</div>
|
||||
${estado.observacoes && estado.observacoes!='rascunho' ? `<div class="p-divider"></div><div>Obs: ${estado.observacoes}</div>` : ''}
|
||||
<div class="p-divider"></div>
|
||||
<div style="text-align:center;font-size:10px">${new Date().toLocaleString('pt-BR')}</div>
|
||||
`;
|
||||
document.getElementById('modal-print').classList.add('show');
|
||||
}
|
||||
|
||||
async function imprimirAgora() {
|
||||
document.getElementById('modal-print').classList.remove('show');
|
||||
showLoad('Imprimindo…');
|
||||
try {
|
||||
await invokeSafe('imprimir_fechamento', { estado: JSON.stringify(estado) });
|
||||
toast('Impresso com sucesso!', 'success');
|
||||
} catch(e) { toast(`Erro na impressao: ${e}`, 'error'); }
|
||||
hideLoad();
|
||||
}
|
||||
|
||||
function fecharPrintPreview() {
|
||||
document.getElementById('modal-print').classList.remove('show');
|
||||
}
|
||||
|
||||
// ── Listar Recentes ──────────────────────────────────────────────────────
|
||||
async function listarRecentes() {
|
||||
const loja = val('sel-loja');
|
||||
if (!loja) { toast('Selecione a loja primeiro', 'error'); return; }
|
||||
showLoad('Carregando recentes…');
|
||||
try {
|
||||
const lista = await invokeSafe('listar_recentes', { loja, limite: 20 });
|
||||
const html = lista.map(r => `
|
||||
<tr onclick="selecionarRecente(${r.id})" style="cursor:pointer">
|
||||
<td>${r.data}</td><td>${r.operador||'—'}</td><td>${r.turno}º</td>
|
||||
<td style="text-align:right">${fmt(r.saldo_esperado||0)}</td>
|
||||
<td><span style="font-size:0.7rem;padding:2px 6px;border-radius:8px;background:${r.sync_status==='synced'?'var(--success)':'var(--muted)'};color:#fff">${r.sync_status||'local'}</span></td>
|
||||
</tr>`).join('');
|
||||
const m = document.getElementById('modal-recentes') || createRecentesModal();
|
||||
m.querySelector('tbody').innerHTML = html || '<tr><td colspan=5 style="text-align:center;color:var(--muted)">Nenhum registro</td></tr>';
|
||||
m.classList.add('show');
|
||||
} catch(e) { /* toast */ }
|
||||
hideLoad();
|
||||
}
|
||||
|
||||
function createRecentesModal() {
|
||||
const div = document.createElement('div');
|
||||
div.id = 'modal-recentes';
|
||||
div.style.cssText = 'display:none;position:fixed;inset:0;background:rgba(0,0,0,0.8);z-index:600;align-items:center;justify-content:center;';
|
||||
div.innerHTML = `<div style="background:var(--surface);border:1px solid var(--border);border-radius:12px;width:500px;max-width:90vw;max-height:80vh;overflow:hidden;display:flex;flex-direction:column;">
|
||||
<div style="padding:14px 16px;border-bottom:1px solid var(--border);font-weight:600">📋 Fechamentos Recentes <span style="margin-left:auto;cursor:pointer" onclick="this.closest('#modal-recentes').classList.remove('show')">✕</span></div>
|
||||
<div style="overflow-y:auto;flex:1"><table style="width:100%"><thead><tr><th>Data</th><th>Operador</th><th>Turno</th><th style="text-align:right">Valor</th><th>Status</th></tr></thead><tbody></tbody></table></div>
|
||||
</div>`;
|
||||
document.body.appendChild(div);
|
||||
return div;
|
||||
}
|
||||
|
||||
async function selecionarRecente(id) {
|
||||
document.getElementById('modal-recentes')?.classList.remove('show');
|
||||
showLoad('Carregando…');
|
||||
try {
|
||||
const r = await invokeSafe('buscar_fechamento', { id });
|
||||
Object.assign(estado, r);
|
||||
document.getElementById('f-saldo-troco').value = r.saldo_troco || '';
|
||||
document.getElementById('f-fechamento').value = r.fechamento || '';
|
||||
document.getElementById('f-vales').value = r.vales || '';
|
||||
document.getElementById('f-credito').value = r.cartoes?.credito || '';
|
||||
document.getElementById('f-debito').value = r.cartoes?.debito || '';
|
||||
document.getElementById('f-alimentacao').value = r.cartoes?.alimentacao || '';
|
||||
document.getElementById('f-voucher').value = r.cartoes?.voucher || '';
|
||||
document.getElementById('f-cancelamentos').value = r.cartoes?.cancelamentos || '';
|
||||
document.getElementById('f-observacoes').value = r.observacoes || '';
|
||||
document.getElementById('sel-operador').value = r.operador || '';
|
||||
document.getElementById('sel-turno').value = r.turno || '';
|
||||
restoreTable('tbl-despesas', (r.despesas||[]).map(d => [d.desc, d.valor]));
|
||||
restoreTable('tbl-receber', (r.receber||[]).map(d => [d.desc, d.valor]));
|
||||
calcular();
|
||||
toast('Fechamento carregado', 'success');
|
||||
} catch(e) {}
|
||||
hideLoad();
|
||||
}
|
||||
|
||||
// ── Operadores ───────────────────────────────────────────────────────────
|
||||
async function carregarOperadores() {
|
||||
const loja = val('sel-loja');
|
||||
if (!loja) return;
|
||||
try {
|
||||
const listas = await invoke('carregar_listas', { loja }).catch(() => null);
|
||||
const ops = (listas?.operadores || []).map(o => `<option value="${o}">${o}</option>`).join('');
|
||||
document.getElementById('sel-operador').innerHTML = '<option value="">Operador…</option>' + ops;
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
const val = id => document.getElementById(id)?.value || '';
|
||||
const fmt = v => {
|
||||
v = parseFloat(v) || 0;
|
||||
return 'R$ ' + v.toLocaleString('pt-BR', {minimumFractionDigits: 2, maximumFractionDigits: 2});
|
||||
};
|
||||
|
||||
function toast(msg, cls = '') {
|
||||
const t = document.getElementById('toast');
|
||||
t.textContent = msg; t.className = 'show ' + cls;
|
||||
clearTimeout(t._timer);
|
||||
t._timer = setTimeout(() => t.className = '', 3000);
|
||||
}
|
||||
|
||||
function showLoad(msg = 'Aguarde…') {
|
||||
document.getElementById('loading-msg').textContent = msg;
|
||||
document.getElementById('loading').classList.add('show');
|
||||
}
|
||||
const hideLoad = () => document.getElementById('loading').classList.remove('show');
|
||||
|
||||
function toggleSec(name) {
|
||||
const body = document.getElementById('body-' + name);
|
||||
const toggle = document.getElementById('toggle-' + name);
|
||||
const hidden = body.style.display === 'none';
|
||||
body.style.display = hidden ? '' : 'none';
|
||||
toggle.textContent = hidden ? '▼' : '▲';
|
||||
}
|
||||
|
||||
function getTableRows(tblId) {
|
||||
const rows = [];
|
||||
document.querySelectorAll(`#${tblId} tbody tr`).forEach(tr => {
|
||||
const inputs = tr.querySelectorAll('input');
|
||||
if (inputs[0]?.value) rows.push([inputs[0].value, inputs[1]?.value || '0']);
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
function restoreTable(tblId, data) {
|
||||
const tbody = document.querySelector(`#${tblId} tbody`);
|
||||
tbody.innerHTML = '';
|
||||
(data || []).forEach(([desc, valor]) => addTableRow(tblId, desc, valor));
|
||||
const fn = tblId === 'tbl-despesas' ? addDespesa : addReceber;
|
||||
if (!data?.length) fn();
|
||||
}
|
||||
|
||||
function addDespesa() { addTableRow('tbl-despesas'); }
|
||||
function addReceber() { addTableRow('tbl-receber'); }
|
||||
|
||||
function addTableRow(tblId, desc = '', valor = '') {
|
||||
const tbody = document.querySelector(`#${tblId} tbody`);
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td><input value="${escHtml(desc)}" placeholder="Descricao" onchange="calcular()"></td>
|
||||
<td><input type="number" value="${valor}" step="0.01" placeholder="0,00" oninput="calcular()"></td>
|
||||
<td><button class="btn-remove" onclick="this.closest('tr').remove();calcular()">✕</button></td>`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
const escHtml = s => (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
|
||||
function limparForm() {
|
||||
if (!confirm('Limpar formulario? Rascunho local sera mantido.')) return;
|
||||
document.getElementById('f-saldo-troco').value = '';
|
||||
document.getElementById('f-fechamento').value = '';
|
||||
document.getElementById('f-vales').value = '';
|
||||
document.getElementById('f-credito').value = '';
|
||||
document.getElementById('f-debito').value = '';
|
||||
document.getElementById('f-alimentacao').value = '';
|
||||
document.getElementById('f-voucher').value = '';
|
||||
document.getElementById('f-cancelamentos').value = '';
|
||||
document.getElementById('f-observacoes').value = '';
|
||||
document.querySelectorAll('#tbl-despesas tbody tr, #tbl-receber tbody tr').forEach(r => r.remove());
|
||||
estado.uuid = null;
|
||||
estado.id_local = null;
|
||||
estado.observacoes = 'rascunho';
|
||||
calcular();
|
||||
toast('Formulario limpo');
|
||||
}
|
||||
|
||||
function atualizarSyncBadge(status) {
|
||||
const b = document.getElementById('sync-badge');
|
||||
if (status === 'synced') {
|
||||
b.textContent = '● Sincronizado'; b.className = 'sync-badge synced';
|
||||
} else if (status === 'error') {
|
||||
b.textContent = '● Erro'; b.className = 'sync-badge error';
|
||||
} else {
|
||||
b.textContent = '● Local'; b.className = 'sync-badge';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Config Modal ─────────────────────────────────────────────────────────
|
||||
function abrirConfig() {
|
||||
document.getElementById('modal-config').style.display = 'flex';
|
||||
}
|
||||
function fecharConfig() {
|
||||
document.getElementById('modal-config').style.display = 'none';
|
||||
}
|
||||
async function detectarImpressora() {
|
||||
const status = document.getElementById('cfg-status');
|
||||
status.textContent = 'Detectando…';
|
||||
try {
|
||||
const path = await invoke('detectar_impressora');
|
||||
if (path) {
|
||||
status.textContent = 'Encontrada: ' + path;
|
||||
toast('Impressora detectada!', 'success');
|
||||
} else {
|
||||
status.textContent = 'Nenhuma impressora USB encontrada.';
|
||||
}
|
||||
} catch(e) {
|
||||
status.textContent = 'Erro: ' + e;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tauri event listeners ────────────────────────────────────────────────
|
||||
window.__TAURI__.core.listen('tauri://error', e => {
|
||||
console.error('Tauri error:', e);
|
||||
toast('Erro interno: ' + e, 'error');
|
||||
});
|
||||
|
||||
// ── Wire events ──────────────────────────────────────────────────────────
|
||||
document.getElementById('sel-loja').addEventListener('change', async e => {
|
||||
estado.loja = e.target.value;
|
||||
document.getElementById('loja-label').textContent = estado.loja;
|
||||
await invoke('set_loja', { loja: estado.loja }).catch(() => {});
|
||||
await carregarOperadores();
|
||||
scheduleAutoSave();
|
||||
});
|
||||
['inp-data','sel-turno','sel-operador'].forEach(id => {
|
||||
document.getElementById(id)?.addEventListener('change', scheduleAutoSave);
|
||||
});
|
||||
document.querySelectorAll('input, textarea, select').forEach(el => {
|
||||
el.addEventListener('input', scheduleAutoSave);
|
||||
});
|
||||
|
||||
// Start
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user