Versão em produção - código extraído do container

This commit is contained in:
root
2025-07-27 23:25:13 -03:00
commit 7ff259ef08
63 changed files with 7707 additions and 0 deletions

107
templates/dashboard.html Executable file
View File

@@ -0,0 +1,107 @@
{% extends "index.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h1 style="display: flex; align-items: center; gap: 10px;">
<i class="fas fa-chart-line"></i> Dashboard de Faturas
</h1>
<form method="get" style="margin: 20px 0;">
<label for="cliente">Selecionar Cliente:</label>
<select name="cliente" id="cliente" onchange="this.form.submit()">
<option value="">Todos</option>
{% for c in clientes %}
<option value="{{ c }}" {% if c == cliente_atual %}selected{% endif %}>{{ c }}</option>
{% endfor %}
</select>
</form>
<!-- Cards -->
<div style="display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 30px;">
{% for indicador in indicadores %}
<div style="
flex: 1 1 220px;
background: #2563eb;
color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
">
<strong>{{ indicador.titulo }}</strong>
<div style="font-size: 1.6rem; font-weight: bold; margin-top: 10px;">
{{ indicador.valor }}
</div>
</div>
{% endfor %}
</div>
<h2 style="margin-bottom: 20px;"><i class="fas fa-chart-bar"></i> Análise da Decisão do STF (RE 574.706 15/03/2017)</h2>
<div style="display: flex; flex-wrap: wrap; gap: 20px;">
<div style="flex: 1;">
<h4>% de Faturas com ICMS na Base PIS/COFINS</h4>
<canvas id="graficoICMS"></canvas>
</div>
<div style="flex: 1;">
<h4>Valor Médio de Tributos com ICMS</h4>
<canvas id="graficoValor"></canvas>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const ctx1 = document.getElementById('graficoICMS').getContext('2d');
new Chart(ctx1, {
type: 'bar',
data: {
labels: ['Antes da Decisão', 'Depois da Decisão'],
datasets: [{
label: '% com ICMS na Base',
data: {{ [analise_stf.antes.percentual_com_icms, analise_stf.depois.percentual_com_icms] | tojson }},
backgroundColor: ['#f39c12', '#e74c3c']
}]
},
options: {
responsive: true,
plugins: {
legend: { display: true },
title: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
title: { display: true, text: '%' }
}
}
}
});
const ctx2 = document.getElementById('graficoValor').getContext('2d');
new Chart(ctx2, {
type: 'bar',
data: {
labels: ['Antes da Decisão', 'Depois da Decisão'],
datasets: [{
label: 'Valor Médio de PIS/COFINS com ICMS',
data: {{ [analise_stf.antes.media_valor, analise_stf.depois.media_valor] | tojson }},
backgroundColor: ['#2980b9', '#27ae60']
}]
},
options: {
responsive: true,
plugins: {
legend: { display: true },
title: { display: false }
},
scales: {
y: {
beginAtZero: true,
title: { display: true, text: 'R$' }
}
}
}
});
</script>
{% endblock %}

184
templates/index.html Normal file
View File

@@ -0,0 +1,184 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{% block title %}ProcessaWatt{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚡️</text></svg>">
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
:root {
--primary: #2563eb;
--primary-dark: #1e40af;
--sidebar-width: 250px;
--sidebar-collapsed: 80px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Montserrat', sans-serif;
}
body {
display: flex;
min-height: 100vh;
background: #f8fafc;
transition: all 0.3s;
}
.sidebar {
width: var(--sidebar-collapsed);
height: 100vh;
background: linear-gradient(to bottom, var(--primary), var(--primary-dark));
position: fixed;
transition: all 0.3s;
overflow: hidden;
}
.sidebar.expanded {
width: var(--sidebar-width);
}
.logo-container {
height: 80px;
display: flex;
align-items: center;
padding: 0 20px;
border-bottom: 1px solid rgba(255,255,255,0.1);
color: white;
gap: 15px;
cursor: pointer;
}
.logo-icon {
width: 40px;
height: 40px;
background: white;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: var(--primary);
font-size: 1.5rem;
}
.app-name {
font-weight: 600;
font-size: 1.1rem;
opacity: 0;
transition: opacity 0.3s;
white-space: nowrap;
}
.sidebar.expanded .app-name {
opacity: 1;
}
.menu {
padding-top: 20px;
}
.menu-item {
display: flex;
align-items: center;
height: 50px;
color: rgba(255,255,255,0.9);
text-decoration: none;
padding-left: 20px;
transition: all 0.2s;
}
.menu-item:hover {
background: rgba(255,255,255,0.1);
}
.menu-item i {
font-size: 1.2rem;
width: 24px;
}
.menu-item span {
margin-left: 15px;
opacity: 0;
transition: opacity 0.3s;
}
.sidebar.expanded .menu-item span {
opacity: 1;
}
.main-content {
margin-left: var(--sidebar-collapsed);
padding: 30px;
flex-grow: 1;
transition: margin-left 0.3s;
}
.sidebar.expanded ~ .main-content {
margin-left: var(--sidebar-width);
}
.filter-section {
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 5px;
}
.indicator {
padding: 10px;
background: #e5e7eb;
border-radius: 4px;
margin-bottom: 10px;
}
.divider {
margin: 30px 0;
border-top: 1px solid #eee;
}
</style>
</head>
<body>
<!-- Sidebar -->
<div class="sidebar" id="sidebar">
<div class="logo-container" onclick="toggleSidebar()">
<div class="logo-icon">⚡️</div>
<div class="app-name">ProcessaWatt</div>
</div>
<div class="menu">
<a href="/" class="menu-item">
<i class="fas fa-tachometer-alt"></i>
<span>Dashboard</span>
</a>
<a href="/upload" class="menu-item">
<i class="fas fa-upload"></i>
<span>Upload</span>
</a>
<a href="/relatorios" class="menu-item">
<i class="fas fa-chart-bar"></i>
<span>Relatórios</span>
</a>
<a href="/parametros" class="menu-item">
<i class="fas fa-cog"></i>
<span>Parâmetros</span>
</a>
</div>
</div>
<!-- Main Content -->
<div class="main-content">
{% block content %}
{% endblock %}
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('expanded');
}
</script>
</body>
</html>

201
templates/index2.html Executable file
View File

@@ -0,0 +1,201 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Processador de Faturas</title>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600&display=swap" rel="stylesheet"/>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Montserrat', sans-serif; }
body { background-color: #f7f9fc; padding: 2rem; color: #333; }
.nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }
.nav h1 { font-size: 1.5rem; color: #4361ee; }
.nav ul { display: flex; list-style: none; gap: 1.5rem; }
.nav li a { text-decoration: none; color: #333; font-weight: 600; }
.nav li a:hover { color: #4361ee; }
.upload-box {
background: #fff;
border: 2px dashed #ccc;
padding: 2rem;
text-align: center;
border-radius: 10px;
margin-bottom: 2rem;
}
.upload-box.dragover {
background-color: #eef2ff;
border-color: #4361ee;
}
.buttons { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; margin-bottom: 2rem; }
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.btn:hover { opacity: 0.9; }
.btn-primary { background-color: #4361ee; color: white; }
.btn-success { background-color: #198754; color: white; }
.btn-danger { background-color: #dc3545; color: white; }
.btn-secondary { background-color: #6c757d; color: white; }
table {
width: 100%;
border-collapse: collapse;
background: #fff;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 0 10px rgba(0,0,0,0.05);
}
th, td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background-color: #f2f2f2;
font-size: 0.85rem;
color: #555;
text-transform: uppercase;
letter-spacing: 1px;
}
#file-input { display: none; }
.status-ok { color: #198754; }
.status-error { color: #dc3545; }
.status-warn { color: #ffc107; }
footer {
text-align: center;
margin-top: 3rem;
font-size: 0.8rem;
color: #aaa;
}
</style>
</head>
<body>
<nav class="nav">
<h1>📄 Processador de Faturas</h1>
<ul>
<li><a href="/">Upload</a></li>
<li><a href="/dashboard">Dashboard</a></li>
<li><a href="/relatorios">Relatórios</a></li>
<li><a href="/parametros">Parâmetros</a></li>
</ul>
</nav>
<div class="upload-box" id="upload-box">
<h3>Arraste faturas em PDF aqui ou clique para selecionar</h3>
<p style="color: gray; font-size: 0.9rem;">Apenas PDFs textuais (não escaneados)</p>
<br />
<button class="btn btn-primary" onclick="document.getElementById('file-input').click()">Selecionar Arquivos</button>
<input type="file" id="file-input" accept=".pdf" multiple onchange="handleFiles(this.files)" />
</div>
<div class="buttons">
<button class="btn btn-primary" onclick="processar()">Processar Faturas</button>
<button class="btn btn-danger" onclick="limpar()">Limpar Tudo</button>
<button class="btn btn-success" onclick="baixarPlanilha()">📥 Abrir Planilha</button>
<button class="btn btn-success" onclick="gerarRelatorio()">📊 Gerar Relatório</button>
</div>
<table>
<thead>
<tr>
<th>Arquivo</th>
<th>Status</th>
<th>Mensagem</th>
</tr>
</thead>
<tbody id="file-table">
<tr><td colspan="3" style="text-align: center; color: #aaa;">Nenhum arquivo selecionado.</td></tr>
</tbody>
</table>
<footer>
Sistema desenvolvido para análise tributária de faturas (PIS/COFINS/ICMS) com correção SELIC.
</footer>
<script>
let arquivos = [];
let statusInterval = null;
const fileTable = document.getElementById('file-table');
function handleFiles(files) {
arquivos = [...arquivos, ...files];
renderTable();
}
function renderTable(statusList = []) {
const rows = statusList.length ? statusList : arquivos.map(file => ({ nome: file.name, status: 'Aguardando', mensagem: '' }));
fileTable.innerHTML = rows.length
? rows.map(file => `
<tr>
<td>${file.nome}</td>
<td class="${file.status === 'Concluido' ? 'status-ok' : file.status === 'Erro' ? 'status-error' : 'status-warn'}">${file.status}</td>
<td>${file.mensagem || '---'}</td>
</tr>
`).join('')
: '<tr><td colspan="3" style="text-align:center; color: #aaa;">Nenhum arquivo selecionado.</td></tr>';
}
async function processar() {
if (arquivos.length === 0) return alert("Nenhum arquivo selecionado.");
const formData = new FormData();
arquivos.forEach(file => formData.append("files", file));
await fetch("/upload-files", { method: "POST", body: formData });
await fetch("/process-queue", { method: "POST" });
arquivos = [];
statusInterval = setInterval(updateStatus, 1000);
}
async function updateStatus() {
const res = await fetch("/get-status");
const data = await res.json();
renderTable(data.files);
if (!data.is_processing && statusInterval) {
clearInterval(statusInterval);
}
}
function limpar() {
fetch("/clear-all", { method: "POST" });
arquivos = [];
renderTable();
}
function baixarPlanilha() {
window.open('/download-spreadsheet', '_blank');
}
function gerarRelatorio() {
window.open('/generate-report', '_blank');
}
const dropZone = document.getElementById('upload-box');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
window.addEventListener('DOMContentLoaded', updateStatus);
</script>
</body>
</html>

26
templates/parametros.html Executable file
View File

@@ -0,0 +1,26 @@
{% extends "index.html" %}
{% block title %}Parâmetros de Cálculo{% endblock %}
{% block content %}
<h1>⚙️ Parâmetros</h1>
<form method="post">
<label for="aliquota_icms">Alíquota de ICMS (%):</label><br>
<input type="number" step="0.01" name="aliquota_icms" id="aliquota_icms" value="{{ parametros.aliquota_icms or '' }}" required/><br><br>
<label for="formula_pis">Fórmula PIS:</label><br>
<input type="text" name="formula_pis" id="formula_pis" value="{{ parametros.formula_pis or '' }}" required/><br><br>
<label for="formula_cofins">Fórmula COFINS:</label><br>
<input type="text" name="formula_cofins" id="formula_cofins" value="{{ parametros.formula_cofins or '' }}" required/><br><br>
<button type="submit" style="padding: 10px 20px; background: #2563eb; color: white; border: none; border-radius: 4px; cursor: pointer;">
Salvar Parâmetros
</button>
</form>
{% if mensagem %}
<div style="margin-top: 20px; background: #e0f7fa; padding: 10px; border-left: 4px solid #2563eb;">
{{ mensagem }}
</div>
{% endif %}
{% endblock %}

38
templates/relatorios.html Executable file
View File

@@ -0,0 +1,38 @@
{% extends "index.html" %}
{% block title %}Relatórios{% endblock %}
{% block content %}
<h1>📊 Relatórios</h1>
<form method="get" style="margin-bottom: 20px;">
<label for="cliente">Filtrar por Cliente:</label>
<select name="cliente" id="cliente" onchange="this.form.submit()">
<option value="">Todos</option>
{% for c in clientes %}
<option value="{{ c }}" {% if c == cliente_atual %}selected{% endif %}>{{ c }}</option>
{% endfor %}
</select>
</form>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background: #2563eb; color: white;">
<th style="padding: 10px;">Cliente</th>
<th>Data</th>
<th>Valor Total</th>
<th>ICMS na Base</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for f in faturas %}
<tr style="background: {{ loop.cycle('#ffffff', '#f0f4f8') }};">
<td style="padding: 10px;">{{ f.nome }}</td>
<td>{{ f.data_emissao }}</td>
<td>R$ {{ '%.2f'|format(f.valor_total)|replace('.', ',') }}</td>
<td>{{ 'Sim' if f.com_icms else 'Não' }}</td>
<td>{{ f.status }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

184
templates/upload.html Executable file
View File

@@ -0,0 +1,184 @@
{% extends "index.html" %}
{% block title %}Upload de Faturas{% endblock %}
{% block content %}
<h1 style="font-size: 1.5rem; margin-bottom: 1rem;">📤 Upload de Faturas</h1>
<div class="upload-box" id="upload-box">
<h3>Arraste faturas em PDF aqui ou clique para selecionar</h3>
<p style="color: gray; font-size: 0.9rem;">Apenas PDFs textuais (não escaneados)</p>
<br />
<button class="btn btn-primary" onclick="document.getElementById('file-input').click()">Selecionar Arquivos</button>
<input type="file" id="file-input" accept=".pdf" multiple onchange="handleFiles(this.files)" style="display:none;" />
</div>
<div class="buttons">
<button class="btn btn-primary" onclick="processar(this)">Processar Faturas</button>
<button class="btn btn-danger" onclick="limpar()">Limpar Tudo</button>
<button class="btn btn-success" onclick="baixarPlanilha()">📅 Abrir Planilha</button>
<button class="btn btn-success" onclick="gerarRelatorio()">📊 Gerar Relatório</button>
</div>
<table>
<thead>
<tr>
<th>Arquivo</th>
<th>Status</th>
<th>Mensagem</th>
</tr>
</thead>
<tbody id="file-table">
<tr><td colspan="3" style="text-align: center; color: #aaa;">Nenhum arquivo selecionado.</td></tr>
</tbody>
</table>
<script>
let arquivos = [];
let statusInterval = null;
const fileTable = document.getElementById('file-table');
function handleFiles(files) {
arquivos = [...arquivos, ...files];
renderTable();
}
function renderTable(statusList = []) {
const rows = statusList.length ? statusList : arquivos.map(file => ({ nome: file.name, status: 'Aguardando', mensagem: '' }));
fileTable.innerHTML = rows.length
? rows.map(file => `
<tr>
<td>${file.nome}</td>
<td class="${file.status === 'Concluido' ? 'status-ok' : file.status === 'Erro' ? 'status-error' : 'status-warn'}">${file.status}</td>
<td>${file.mensagem || '---'}</td>
</tr>
`).join('')
: '<tr><td colspan="3" style="text-align:center; color: #aaa;">Nenhum arquivo selecionado.</td></tr>';
}
async function processar(btn) {
if (arquivos.length === 0) return alert("Nenhum arquivo selecionado.");
btn.disabled = true;
btn.innerText = "⏳ Processando...";
const formData = new FormData();
arquivos.forEach(file => formData.append("files", file));
try {
await fetch("/upload-files", { method: "POST", body: formData });
await fetch("/process-queue", { method: "POST" });
arquivos = [];
statusInterval = setInterval(updateStatus, 1000);
} catch (err) {
alert("Erro ao processar faturas.");
} finally {
setTimeout(() => {
btn.innerText = "Processar Faturas";
btn.disabled = false;
}, 1500);
}
}
async function updateStatus() {
const res = await fetch("/get-status");
const data = await res.json();
renderTable(data.files);
if (!data.is_processing && statusInterval) {
clearInterval(statusInterval);
}
}
function limpar() {
fetch("/clear-all", { method: "POST" });
arquivos = [];
renderTable();
}
function baixarPlanilha() {
window.open('/export-excel', '_blank');
}
function gerarRelatorio() {
window.open('/generate-report', '_blank');
}
const dropZone = document.getElementById('upload-box');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
window.addEventListener('DOMContentLoaded', updateStatus);
</script>
<style>
.upload-box {
background: #fff;
border: 2px dashed #ccc;
padding: 2rem;
text-align: center;
border-radius: 10px;
margin-bottom: 2rem;
}
.upload-box.dragover {
background-color: #eef2ff;
border-color: #4361ee;
}
.buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 2rem;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.btn:hover { opacity: 0.9; }
.btn-primary { background-color: #4361ee; color: white; }
.btn-success { background-color: #198754; color: white; }
.btn-danger { background-color: #dc3545; color: white; }
table {
width: 100%;
border-collapse: collapse;
background: #fff;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 0 10px rgba(0,0,0,0.05);
}
th, td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background-color: #f2f2f2;
font-size: 0.85rem;
color: #555;
text-transform: uppercase;
letter-spacing: 1px;
}
.status-ok { color: #198754; }
.status-error { color: #dc3545; }
.status-warn { color: #ffc107; }
</style>
{% endblock %}