Commit inicial - upload de todos os arquivos da pasta
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<!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="/categorias" class="{{ 'active' if request.url.path.startswith('/categorias') else '' }}">Categorias</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>
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ 'Editar categoria' if mode == 'edit' else 'Nova categoria' }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h2>{{ 'Editar categoria' if mode == 'edit' else 'Nova categoria' }}</h2>
|
||||
<p class="muted">{{ 'Ajuste os campos e salve.' if mode == 'edit' else 'Cadastre uma categoria e a palavra-chave usada para reconhecê-la.' }}</p>
|
||||
</div>
|
||||
<a href="/categorias" class="btn btn-ghost btn-sm">Voltar</a>
|
||||
</div>
|
||||
|
||||
<section class="card form-card">
|
||||
<form method="post" action="{{ '/categorias/' ~ categoria.id ~ '/edit' if mode == 'edit' else '/categorias/new' }}" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<label class="field">
|
||||
<span>Categoria</span>
|
||||
<input type="text" name="categoria" value="{{ categoria.categoria if categoria else '' }}" required placeholder="Ex.: Alimentação">
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Palavra-chave</span>
|
||||
<input type="text" name="palavra_chave" value="{{ categoria.palavra_chave if categoria else '' }}" required placeholder="Ex.: MERCADO">
|
||||
<small class="muted">Usada para reconhecer o fornecedor por correspondência (contém, sem diferenciar maiúsculas/minúsculas).</small>
|
||||
</label>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'Salvar alterações' if mode == 'edit' else 'Criar categoria' }}</button>
|
||||
<a href="/categorias" class="btn btn-ghost">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Categorias{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h2>Categorias</h2>
|
||||
<p class="muted">{{ rows | length }} categoria(s)</p>
|
||||
</div>
|
||||
<a href="/categorias/new" class="btn btn-primary btn-sm">+ Nova</a>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
{% if rows %}
|
||||
<div class="table-scroll">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>Categoria</th><th>Palavra-chave</th><th class="r">Ações</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in rows %}
|
||||
<tr>
|
||||
<td>{{ c.categoria }}</td>
|
||||
<td class="muted small">{{ c.palavra_chave }}</td>
|
||||
<td class="r nowrap">
|
||||
<a href="/categorias/{{ c.id }}/edit" class="link">Editar</a>
|
||||
{% if c.id != reserved_id %}
|
||||
<form method="post" action="/categorias/{{ c.id }}/delete" class="inline"
|
||||
onsubmit="return confirm('Excluir esta categoria?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="linkbtn danger">Excluir</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted">Nenhuma categoria cadastrada. <a href="/categorias/new" class="link">Criar a primeira</a>.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,132 @@
|
||||
{% 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="card">
|
||||
<form method="get" action="/" class="filters">
|
||||
<label class="field grow">
|
||||
<span>Categoria</span>
|
||||
<select name="category" onchange="this.form.submit()">
|
||||
<option value="">Todas</option>
|
||||
{% for c in categorias %}
|
||||
<option value="{{ c.id }}" {{ 'selected' if selected_category == c.id | string else '' }}>{{ c.categoria }}</option>
|
||||
{% endfor %}
|
||||
<option value="none" {{ 'selected' if selected_category == 'none' else '' }}>Sem categoria</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="filters-actions">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Filtrar</button>
|
||||
<a href="/" class="btn btn-ghost btn-sm">Limpar</a>
|
||||
</div>
|
||||
</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-3">
|
||||
<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>
|
||||
|
||||
<div class="card">
|
||||
<h3>Gastos por categoria</h3>
|
||||
{% if categories %}
|
||||
<div class="barlist">
|
||||
{% for c in categories %}
|
||||
<div class="barrow">
|
||||
<span class="barlabel" title="{{ c.categoria }}">{{ c.categoria }}</span>
|
||||
<span class="bartrack"><span class="barfill" style="width: {{ c.pct }}%"></span></span>
|
||||
<span class="barval">{{ c.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>Competência</th><th>Fornecedor</th><th>Categoria</th><th class="r">Valor</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for d in recent %}
|
||||
<tr>
|
||||
<td class="nowrap">{{ d.mes | competencia(d.ano) }}</td>
|
||||
<td>{{ d.supplier_name }}</td>
|
||||
<td>{{ d.categoria_nome or '' }}</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 %}
|
||||
@@ -0,0 +1,56 @@
|
||||
{% 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>Competência</span>
|
||||
<span class="field-group">
|
||||
<select name="mes" required>
|
||||
{% for m in meses_competencia %}
|
||||
<option value="{{ loop.index }}" {{ 'selected' if (doc.mes if doc else default_mes) == loop.index else '' }}>{{ m }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="ano" required>
|
||||
{% for a in anos_competencia %}
|
||||
<option value="{{ a }}" {{ 'selected' if (doc.ano if doc else default_ano) == a else '' }}>{{ a }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</span>
|
||||
<small class="muted">Se não selecionado, será usado o mês/ano 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>
|
||||
<label class="field">
|
||||
<span>Categoria</span>
|
||||
<select name="categoria_id">
|
||||
<option value="">Sem categoria</option>
|
||||
{% for c in categorias %}
|
||||
<option value="{{ c.id }}" {{ 'selected' if doc and doc.categoria_id == c.id else '' }}>{{ c.categoria }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</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 %}
|
||||
@@ -0,0 +1,144 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Documentos{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h2>Documentos</h2>
|
||||
<p class="muted">{{ total_count }} 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 (competência)</span>
|
||||
<span class="field-group">
|
||||
<select name="start_mes">
|
||||
<option value="" {{ 'selected' if not filters.start_mes else '' }}>Mês</option>
|
||||
{% for m in meses_competencia %}
|
||||
<option value="{{ loop.index }}" {{ 'selected' if filters.start_mes == loop.index | string else '' }}>{{ m }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="start_ano">
|
||||
<option value="" {{ 'selected' if not filters.start_ano else '' }}>Ano</option>
|
||||
{% for a in anos_competencia %}
|
||||
<option value="{{ a }}" {{ 'selected' if filters.start_ano == a | string else '' }}>{{ a }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</span>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Até (competência)</span>
|
||||
<span class="field-group">
|
||||
<select name="end_mes">
|
||||
<option value="" {{ 'selected' if not filters.end_mes else '' }}>Mês</option>
|
||||
{% for m in meses_competencia %}
|
||||
<option value="{{ loop.index }}" {{ 'selected' if filters.end_mes == loop.index | string else '' }}>{{ m }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="end_ano">
|
||||
<option value="" {{ 'selected' if not filters.end_ano else '' }}>Ano</option>
|
||||
{% for a in anos_competencia %}
|
||||
<option value="{{ a }}" {{ 'selected' if filters.end_ano == a | string else '' }}>{{ a }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</span>
|
||||
</label>
|
||||
<label class="field grow"><span>Fornecedor</span><input type="text" name="supplier" value="{{ filters.supplier }}" placeholder="contém…"></label>
|
||||
<label class="field grow">
|
||||
<span>Categoria</span>
|
||||
<select name="category" onchange="this.form.submit()">
|
||||
<option value="">Todas</option>
|
||||
{% for c in categorias %}
|
||||
<option value="{{ c.id }}" {{ 'selected' if filters.category == c.id | string else '' }}>{{ c.categoria }}</option>
|
||||
{% endfor %}
|
||||
<option value="none" {{ 'selected' if filters.category == 'none' else '' }}>Sem categoria</option>
|
||||
</select>
|
||||
</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 %}
|
||||
{% macro sort_url(key) -%}
|
||||
/documents?start_mes={{ filters.start_mes | urlencode }}&start_ano={{ filters.start_ano | urlencode }}&end_mes={{ filters.end_mes | urlencode }}&end_ano={{ filters.end_ano | urlencode }}&supplier={{ filters.supplier | urlencode }}&category={{ filters.category | urlencode }}&sort={{ key }}&order={{ 'asc' if (sort == key and order == 'desc') else 'desc' }}
|
||||
{%- endmacro %}
|
||||
{% macro sort_arrow(key) -%}
|
||||
{% if sort == key %}{{ ' ▲' if order == 'asc' else ' ▼' }}{% endif %}
|
||||
{%- endmacro %}
|
||||
<div class="table-scroll">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><a href="{{ sort_url('competencia') }}" class="link">Competência{{ sort_arrow('competencia') }}</a></th>
|
||||
<th><a href="{{ sort_url('supplier_name') }}" class="link">Fornecedor{{ sort_arrow('supplier_name') }}</a></th>
|
||||
<th><a href="{{ sort_url('categoria') }}" class="link">Categoria{{ sort_arrow('categoria') }}</a></th>
|
||||
<th class="r"><a href="{{ sort_url('total_paid') }}" class="link">Valor{{ sort_arrow('total_paid') }}</a></th>
|
||||
<th>Origem</th>
|
||||
<th class="r">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in rows %}
|
||||
<tr>
|
||||
<td class="nowrap">{{ d.mes | competencia(d.ano) }}</td>
|
||||
<td>{{ d.supplier_name }}</td>
|
||||
<td>{{ d.categoria_nome or '' }}</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="3">Total registros: {{ total_count }}</td><td class="r money">{{ total | money }}</td><td colspan="2"></td></tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if total_pages > 1 %}
|
||||
{% macro page_url(p) -%}
|
||||
/documents?start_mes={{ filters.start_mes | urlencode }}&start_ano={{ filters.start_ano | urlencode }}&end_mes={{ filters.end_mes | urlencode }}&end_ano={{ filters.end_ano | urlencode }}&supplier={{ filters.supplier | urlencode }}&category={{ filters.category | urlencode }}&sort={{ sort }}&order={{ order }}&page={{ p }}
|
||||
{%- endmacro %}
|
||||
<nav class="pagination" aria-label="Paginação">
|
||||
{% if page <= 1 %}
|
||||
<span class="btn btn-ghost btn-sm disabled" aria-disabled="true">« Anterior</span>
|
||||
{% else %}
|
||||
<a href="{{ page_url(page - 1) }}" class="btn btn-ghost btn-sm">« Anterior</a>
|
||||
{% endif %}
|
||||
{% for p in page_numbers %}
|
||||
{% if p is none %}
|
||||
<span class="pagination-ellipsis">…</span>
|
||||
{% elif p == page %}
|
||||
<span class="pagination-page current" aria-current="page">{{ p }}</span>
|
||||
{% else %}
|
||||
<a href="{{ page_url(p) }}" class="pagination-page">{{ p }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if page >= total_pages %}
|
||||
<span class="btn btn-ghost btn-sm disabled" aria-disabled="true">Próxima »</span>
|
||||
{% else %}
|
||||
<a href="{{ page_url(page + 1) }}" class="btn btn-ghost btn-sm">Próxima »</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% 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 %}
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,105 @@
|
||||
{% 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 or not r.mes or not r.ano) 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 or not r.mes or not r.ano) %}<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>Competência</span>
|
||||
<span class="field-group">
|
||||
<select name="mes" required>
|
||||
<option value="" disabled {{ 'selected' if not r.mes else '' }}>Mês</option>
|
||||
{% for m in meses_competencia %}
|
||||
<option value="{{ loop.index }}" {{ 'selected' if r.mes == loop.index else '' }}>{{ m }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="ano" required>
|
||||
<option value="" disabled {{ 'selected' if not r.ano else '' }}>Ano</option>
|
||||
{% for a in anos_competencia %}
|
||||
<option value="{{ a }}" {{ 'selected' if r.ano == a else '' }}>{{ a }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</span>
|
||||
</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 %}
|
||||
Reference in New Issue
Block a user