Commit inicial - upload de todos os arquivos da pasta

This commit is contained in:
2026-07-24 15:51:10 -03:00
commit efa06def71
44 changed files with 3604 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
<!doctype html>
<html lang="pt-BR" data-theme="">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ app_name }}{% endblock %} · {{ app_name }}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><text y='14' font-size='14'>🧾</text></svg>">
<link rel="stylesheet" href="/static/styles.css">
<script>
// Aplica o tema salvo antes da pintura para evitar flash.
(function () {
try {
var t = localStorage.getItem('theme');
if (t) document.documentElement.setAttribute('data-theme', t);
} catch (e) {}
})();
</script>
</head>
<body>
{% if not hide_nav %}
<header class="topbar">
<a class="brand" href="/">
<span class="brand-mark">🧾</span>
<span>{{ app_name }}</span>
</a>
<nav class="nav">
<a href="/" class="{{ 'active' if request.url.path == '/' else '' }}">Dashboard</a>
<a href="/documents" class="{{ 'active' if request.url.path.startswith('/documents') and 'new' not in request.url.path else '' }}">Documentos</a>
<a href="/documents/new" class="btn btn-primary btn-sm">+ Novo</a>
</nav>
<div class="topbar-right">
<button type="button" class="theme-toggle" id="themeToggle" title="Alternar tema" aria-label="Alternar tema">
<span class="theme-icon"></span>
</button>
{% if current_user %}
<span class="user">{{ current_user }}</span>
<form method="post" action="/logout" class="inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="btn btn-ghost btn-sm">Sair</button>
</form>
{% endif %}
</div>
</header>
{% endif %}
<main class="{{ 'main-centered' if hide_nav else 'main' }}">
{% if flashes %}
<div class="flash-stack">
{% for f in flashes %}
<div class="flash flash-{{ f.level }}">{{ f.message }}</div>
{% endfor %}
</div>
{% endif %}
{% block content %}{% endblock %}
</main>
<script src="/static/app.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
+97
View File
@@ -0,0 +1,97 @@
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<section class="upload-panel">
<div>
<h2>Importar documentos</h2>
<p class="muted">Envie PDFs ou imagens de notas e cupons fiscais. A leitura é feita
{% if openai_enabled %}por IA (OpenAI){% else %}por OCR/heurística local{% endif %},
e você confirma antes de gravar.</p>
</div>
<form method="post" action="/upload" enctype="multipart/form-data" class="upload-form" id="uploadForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label class="filedrop" id="filedrop">
<input type="file" name="files" id="fileInput" multiple accept=".pdf,.jpg,.jpeg,.png" hidden>
<span class="filedrop-icon"></span>
<span class="filedrop-text" id="filedropText">Clique ou arraste arquivos aqui</span>
</label>
<button type="submit" class="btn btn-primary" id="uploadBtn">Enviar e revisar</button>
</form>
</section>
<section class="kpis">
<div class="kpi">
<span class="kpi-label">Total gasto</span>
<strong class="kpi-value money">{{ totals.total | money }}</strong>
<span class="kpi-foot">{{ totals.count }} documento(s)</span>
</div>
<div class="kpi">
<span class="kpi-label">Ticket médio</span>
<strong class="kpi-value">{{ totals.avg | money }}</strong>
<span class="kpi-foot">por documento</span>
</div>
<div class="kpi">
<span class="kpi-label">Meses com registro</span>
<strong class="kpi-value">{{ months | length }}</strong>
<span class="kpi-foot">{{ suppliers | length }} fornecedor(es) no top</span>
</div>
</section>
<section class="grid-2">
<div class="card">
<h3>Gastos por mês</h3>
{% if months %}
<div class="barlist">
{% for m in months %}
<div class="barrow">
<span class="barlabel">{{ m.label }}</span>
<span class="bartrack"><span class="barfill" style="width: {{ m.pct }}%"></span></span>
<span class="barval">{{ m.total | money }}</span>
</div>
{% endfor %}
</div>
{% else %}<p class="muted">Sem dados ainda.</p>{% endif %}
</div>
<div class="card">
<h3>Gastos por fornecedor</h3>
{% if suppliers %}
<div class="barlist">
{% for s in suppliers %}
<div class="barrow">
<span class="barlabel" title="{{ s.supplier_name }}">{{ s.supplier_name }}</span>
<span class="bartrack"><span class="barfill alt" style="width: {{ s.pct }}%"></span></span>
<span class="barval">{{ s.total | money }}</span>
</div>
{% endfor %}
</div>
{% else %}<p class="muted">Sem dados ainda.</p>{% endif %}
</div>
</section>
<section class="card">
<div class="card-head">
<h3>Últimos documentos</h3>
<a href="/documents" class="btn btn-ghost btn-sm">Ver todos</a>
</div>
{% if recent %}
<div class="table-scroll">
<table class="table">
<thead><tr><th>Data</th><th>Fornecedor</th><th class="r">Valor</th><th></th></tr></thead>
<tbody>
{% for d in recent %}
<tr>
<td class="nowrap">{{ d.purchase_date }}</td>
<td>{{ d.supplier_name }}</td>
<td class="r money nowrap">{{ d.total_paid | money }}</td>
<td class="r"><a href="/documents/{{ d.id }}/edit" class="link">editar</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}<p class="muted">Nenhum documento confirmado. Comece importando acima.</p>{% endif %}
</section>
{% endblock %}
+36
View File
@@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block title %}{{ 'Editar documento' if mode == 'edit' else 'Novo lançamento' }}{% endblock %}
{% block content %}
<div class="page-head">
<div>
<h2>{{ 'Editar documento' if mode == 'edit' else 'Novo lançamento' }}</h2>
<p class="muted">{{ 'Ajuste os campos e salve.' if mode == 'edit' else 'Lançamento manual de uma despesa fiscal.' }}</p>
</div>
<a href="/documents" class="btn btn-ghost btn-sm">Voltar</a>
</div>
<section class="card form-card">
<form method="post" action="{{ '/documents/' ~ doc.id ~ '/edit' if mode == 'edit' else '/documents/new' }}" class="form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label class="field">
<span>Data da compra</span>
<input type="date" name="purchase_date" value="{{ doc.purchase_date if doc else '' }}">
<small class="muted">Se deixar em branco, será usado o 1º dia do mês corrente.</small>
</label>
<label class="field">
<span>Fornecedor</span>
<input type="text" name="supplier_name" value="{{ doc.supplier_name if doc else '' }}" required placeholder="Nome do estabelecimento">
</label>
<label class="field">
<span>Valor pago (R$)</span>
<input type="text" inputmode="decimal" name="total_paid" value="{{ '%.2f'|format(doc.total_paid) if doc else '' }}" required placeholder="0,00">
</label>
<div class="form-actions">
<button type="submit" class="btn btn-primary">{{ 'Salvar alterações' if mode == 'edit' else 'Lançar documento' }}</button>
<a href="/documents" class="btn btn-ghost">Cancelar</a>
</div>
</form>
</section>
{% endblock %}
+60
View File
@@ -0,0 +1,60 @@
{% extends "base.html" %}
{% block title %}Documentos{% endblock %}
{% block content %}
<div class="page-head">
<div>
<h2>Documentos</h2>
<p class="muted">{{ rows | length }} registro(s) · total {{ total | money }}</p>
</div>
<a href="/documents/new" class="btn btn-primary btn-sm">+ Novo lançamento</a>
</div>
<section class="card">
<form method="get" action="/documents" class="filters">
<label class="field"><span>De</span><input type="date" name="start" value="{{ filters.start }}"></label>
<label class="field"><span>Até</span><input type="date" name="end" value="{{ filters.end }}"></label>
<label class="field grow"><span>Fornecedor</span><input type="text" name="supplier" value="{{ filters.supplier }}" placeholder="contém…"></label>
<div class="filters-actions">
<button type="submit" class="btn btn-primary btn-sm">Filtrar</button>
<a href="/documents" class="btn btn-ghost btn-sm">Limpar</a>
</div>
</form>
</section>
<section class="card">
{% if rows %}
<div class="table-scroll">
<table class="table">
<thead>
<tr><th>Data</th><th>Fornecedor</th><th class="r">Valor</th><th>Origem</th><th class="r">Ações</th></tr>
</thead>
<tbody>
{% for d in rows %}
<tr>
<td class="nowrap">{{ d.purchase_date }}</td>
<td>{{ d.supplier_name }}</td>
<td class="r money nowrap">{{ d.total_paid | money }}</td>
<td class="muted small">{{ d.source_location }}</td>
<td class="r nowrap">
<a href="/documents/{{ d.id }}/edit" class="link">Editar</a>
<form method="post" action="/documents/{{ d.id }}/delete" class="inline"
onsubmit="return confirm('Excluir este documento?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="linkbtn danger">Excluir</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr><td colspan="2">Total filtrado</td><td class="r money">{{ total | money }}</td><td colspan="2"></td></tr>
</tfoot>
</table>
</div>
{% else %}
<p class="muted">Nenhum documento encontrado. <a href="/documents/new" class="link">Lançar manualmente</a> ou <a href="/" class="link">importar</a>.</p>
{% endif %}
</section>
{% endblock %}
+23
View File
@@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}Entrar{% endblock %}
{% block content %}
<div class="login-card">
<div class="login-head">
<span class="brand-mark lg">🧾</span>
<h1>{{ app_name }}</h1>
<p class="muted">Controle de despesas fiscais</p>
</div>
<form method="post" action="/login" class="form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label class="field">
<span>Usuário</span>
<input type="text" name="username" autocomplete="username" required autofocus>
</label>
<label class="field">
<span>Senha</span>
<input type="password" name="password" autocomplete="current-password" required>
</label>
<button type="submit" class="btn btn-primary btn-block">Entrar</button>
</form>
</div>
{% endblock %}
+92
View File
@@ -0,0 +1,92 @@
{% extends "base.html" %}
{% block title %}Revisar importação{% endblock %}
{% block content %}
<div class="page-head">
<div>
<h2>Revisar antes de importar</h2>
<p class="muted">Confira os documentos extraídos. Corrija os ilegíveis e confirme.</p>
</div>
<a href="/" class="btn btn-ghost btn-sm">Cancelar</a>
</div>
<section class="summary-banner">
<div class="summary-item">
<span class="summary-label">Documentos</span>
<strong class="summary-value">{{ summary.count }}</strong>
</div>
<div class="summary-item">
<span class="summary-label">Valor total</span>
<strong class="summary-value money">{{ summary.total | money }}</strong>
</div>
{% if summary.pendentes %}
<div class="summary-item warn">
<span class="summary-label">Precisam de correção</span>
<strong class="summary-value">{{ summary.pendentes }}</strong>
</div>
{% endif %}
</section>
{% if not rows %}
<div class="card"><p class="muted">Nenhum documento neste lote. <a href="/" class="link">Voltar</a>.</p></div>
{% else %}
<div class="staging-list">
{% for r in rows %}
<article class="staging-row {{ 'row-warn' if (not r.legible or not r.supplier_name or r.total_paid is none) else '' }}">
<div class="staging-badges">
{% if not r.legible %}<span class="badge badge-warn">Ilegível — revise</span>{% endif %}
{% if r.legible and (not r.supplier_name or r.total_paid is none) %}<span class="badge badge-warn">Incompleto — preencha</span>{% endif %}
<span class="badge badge-conf badge-{{ r.confidence }}">{{ r.confidence }}</span>
<span class="badge badge-src">{{ 'IA' if r.extractor == 'openai' else 'OCR local' }}</span>
{% if r.uncertain_fields and r.uncertain_fields != '[]' %}
<span class="badge badge-fields">incertos: {{ r.uncertain_fields | replace('[','') | replace(']','') | replace('"','') }}</span>
{% endif %}
</div>
<form method="post" action="/import/{{ batch.id }}/update/{{ r.id }}" class="staging-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label class="field">
<span>Data</span>
<input type="date" name="purchase_date" value="{{ r.purchase_date }}">
</label>
<label class="field grow">
<span>Fornecedor</span>
<input type="text" name="supplier_name" value="{{ r.supplier_name or '' }}" placeholder="Nome do estabelecimento">
</label>
<label class="field">
<span>Valor (R$)</span>
<input type="text" inputmode="decimal" name="total_paid" value="{{ '%.2f'|format(r.total_paid) if r.total_paid is not none else '' }}" placeholder="0,00">
</label>
<div class="staging-actions">
<button type="submit" class="btn btn-primary btn-sm">Salvar correção</button>
<a href="/files/{{ r.upload_id }}" target="_blank" rel="noopener" class="btn btn-ghost btn-sm">Ver arquivo</a>
</div>
</form>
<form method="post" action="/import/{{ batch.id }}/discard/{{ r.id }}" class="staging-discard"
onsubmit="return confirm('Descartar este documento do lote?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="btn btn-danger btn-sm">Descartar</button>
</form>
<div class="staging-source">{{ r.source_file_name }}</div>
</article>
{% endfor %}
</div>
<div class="confirm-bar">
<div class="muted">
{% if summary.pendentes %}
Corrija os {{ summary.pendentes }} documento(s) pendente(s) antes de importar.
{% else %}
Tudo pronto para importar.
{% endif %}
</div>
<form method="post" action="/import/{{ batch.id }}/confirm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="btn btn-primary btn-lg" {{ 'disabled' if summary.pendentes else '' }}>
Confirmar importação de {{ summary.count }} documento(s) · {{ summary.total | money }}
</button>
</form>
</div>
{% endif %}
{% endblock %}