Fechamento de Caixa - Tauri 2 initial commit
Build Fechamento de Caixa / build (macos-14, app) (push) Has been cancelled
Build Fechamento de Caixa / build (ubuntu-22.04, deb) (push) Has been cancelled
Build Fechamento de Caixa / build (windows-2022, msi) (push) Has been cancelled

- 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:
root
2026-06-20 13:58:34 +00:00
parent b06462a92d
commit a5cff433a6
20 changed files with 8313 additions and 0 deletions
+958
View File
@@ -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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
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>