2025-07-28 13:29:45 -03:00
|
|
|
{% 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>
|
2025-07-29 14:10:14 -03:00
|
|
|
{% if app_env != "producao" %}
|
|
|
|
|
<button class="btn btn-warning" onclick="limparFaturas()">🧹 Limpar Faturas (Teste)</button>
|
|
|
|
|
{% endif %}
|
2025-07-28 13:29:45 -03:00
|
|
|
</div>
|
|
|
|
|
|
2025-07-29 14:10:14 -03:00
|
|
|
|
2025-07-28 13:29:45 -03:00
|
|
|
<table>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>Arquivo</th>
|
|
|
|
|
<th>Status</th>
|
|
|
|
|
<th>Mensagem</th>
|
2025-07-28 22:31:31 -03:00
|
|
|
<th>Tempo</th>
|
2025-07-28 13:29:45 -03:00
|
|
|
</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();
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-28 22:31:31 -03:00
|
|
|
function renderTable(statusList = []) {
|
|
|
|
|
const rows = statusList.length ? statusList : arquivos.map(file => ({
|
|
|
|
|
nome: file.name,
|
|
|
|
|
status: 'Aguardando',
|
|
|
|
|
mensagem: ''
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
fileTable.innerHTML = rows.length
|
|
|
|
|
? rows.map(file => {
|
|
|
|
|
const status = file.status || 'Aguardando';
|
|
|
|
|
const statusClass = status === 'Concluído' ? 'status-ok'
|
|
|
|
|
: status === 'Erro' ? 'status-error'
|
|
|
|
|
: status === 'Aguardando' ? 'status-warn'
|
|
|
|
|
: 'status-processing';
|
|
|
|
|
return `
|
|
|
|
|
<tr>
|
|
|
|
|
<td>${file.nome}</td>
|
|
|
|
|
<td class="${statusClass}">${status}</td>
|
|
|
|
|
<td>${file.mensagem || '---'}</td>
|
|
|
|
|
<td>${file.tempo || '---'}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
`;
|
|
|
|
|
}).join('')
|
|
|
|
|
: '<tr><td colspan="3" style="text-align:center; color: #aaa;">Nenhum arquivo selecionado.</td></tr>';
|
|
|
|
|
}
|
2025-07-28 13:29:45 -03:00
|
|
|
|
|
|
|
|
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 });
|
2025-07-28 22:31:31 -03:00
|
|
|
|
|
|
|
|
const response = await fetch("/process-queue", { method: "POST" });
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const msg = await response.text();
|
|
|
|
|
throw new Error(`Erro no processamento: ${msg}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
arquivos = []; // <- só limpa se o processamento for bem-sucedido
|
2025-07-28 13:29:45 -03:00
|
|
|
statusInterval = setInterval(updateStatus, 1000);
|
|
|
|
|
} catch (err) {
|
2025-07-28 22:31:31 -03:00
|
|
|
alert("Erro ao processar faturas:\n" + err.message);
|
2025-07-28 13:29:45 -03:00
|
|
|
} finally {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
btn.innerText = "Processar Faturas";
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
}, 1500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-28 22:31:31 -03:00
|
|
|
|
2025-07-28 13:29:45 -03:00
|
|
|
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 = [];
|
2025-07-28 22:31:31 -03:00
|
|
|
document.getElementById("file-input").value = null;
|
2025-07-28 13:29:45 -03:00
|
|
|
renderTable();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function baixarPlanilha() {
|
|
|
|
|
window.open('/export-excel', '_blank');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function gerarRelatorio() {
|
|
|
|
|
window.open('/generate-report', '_blank');
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 14:10:14 -03:00
|
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
updateStatus();
|
2025-07-28 13:29:45 -03:00
|
|
|
|
2025-07-29 14:10:14 -03:00
|
|
|
const dragOverlay = document.getElementById("drag-overlay");
|
|
|
|
|
let dragCounter = 0;
|
2025-07-28 22:31:31 -03:00
|
|
|
|
2025-07-29 14:10:14 -03:00
|
|
|
window.addEventListener("dragenter", e => {
|
|
|
|
|
dragCounter++;
|
|
|
|
|
dragOverlay.classList.add("active");
|
2025-07-28 22:31:31 -03:00
|
|
|
});
|
|
|
|
|
|
2025-07-29 14:10:14 -03:00
|
|
|
window.addEventListener("dragleave", e => {
|
|
|
|
|
dragCounter--;
|
|
|
|
|
if (dragCounter <= 0) {
|
|
|
|
|
dragOverlay.classList.remove("active");
|
|
|
|
|
dragCounter = 0;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
window.addEventListener("dragover", e => {
|
|
|
|
|
e.preventDefault();
|
2025-07-28 22:31:31 -03:00
|
|
|
});
|
|
|
|
|
|
2025-07-29 14:10:14 -03:00
|
|
|
window.addEventListener("drop", e => {
|
2025-07-28 22:31:31 -03:00
|
|
|
e.preventDefault();
|
2025-07-29 14:10:14 -03:00
|
|
|
dragOverlay.classList.remove("active");
|
|
|
|
|
dragCounter = 0;
|
2025-07-28 22:31:31 -03:00
|
|
|
handleFiles(e.dataTransfer.files);
|
|
|
|
|
});
|
2025-07-29 14:10:14 -03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function limparFaturas() {
|
|
|
|
|
if (!confirm("Deseja realmente limpar todas as faturas e arquivos (somente homologação)?")) return;
|
|
|
|
|
|
|
|
|
|
const res = await fetch("/limpar-faturas", { method: "POST" });
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
alert(data.message || "Concluído");
|
|
|
|
|
updateStatus(); // atualiza visual
|
|
|
|
|
}
|
2025-07-28 22:31:31 -03:00
|
|
|
|
2025-07-29 14:10:14 -03:00
|
|
|
window.addEventListener("dragover", e => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
window.addEventListener("drop", e => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
});
|
2025-07-28 13:29:45 -03:00
|
|
|
</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; }
|
2025-07-28 22:31:31 -03:00
|
|
|
|
|
|
|
|
.status-processing {
|
|
|
|
|
color: #0d6efd; /* azul */
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 14:10:14 -03:00
|
|
|
.drag-overlay {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
|
|
|
background-color: rgba(67, 97, 238, 0.7); /* institucional */
|
|
|
|
|
z-index: 9999;
|
|
|
|
|
display: none;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
transition: opacity 0.2s ease-in-out;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.drag-overlay.active {
|
|
|
|
|
display: flex;
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.drag-overlay-content {
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: white;
|
|
|
|
|
font-size: 1.1rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
animation: fadeInUp 0.3s ease;
|
|
|
|
|
text-shadow: 1px 1px 6px rgba(0, 0, 0, 0.4);
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.drag-overlay-content svg {
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
width: 72px;
|
|
|
|
|
height: 72px;
|
|
|
|
|
fill: white;
|
|
|
|
|
filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.4));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes fadeInUp {
|
|
|
|
|
from {
|
|
|
|
|
transform: translateY(10px);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
}
|
|
|
|
|
to {
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-07-28 13:29:45 -03:00
|
|
|
</style>
|
|
|
|
|
|
2025-07-29 14:10:14 -03:00
|
|
|
<div class="drag-overlay" id="drag-overlay">
|
|
|
|
|
<div class="drag-overlay-content">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="#fff" viewBox="0 0 24 24">
|
|
|
|
|
<path d="M12 2C6.477 2 2 6.477 2 12c0 5.522 4.477 10 10 10s10-4.478 10-10c0-5.523-4.477-10-10-10Zm0 13a1 1 0 0 1-1-1v-2.586l-.293.293a1 1 0 0 1-1.414-1.414l2.707-2.707a1 1 0 0 1 1.414 0l2.707 2.707a1 1 0 0 1-1.414 1.414L13 11.414V14a1 1 0 0 1-1 1Z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<p>Solte os arquivos para enviar</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-07-28 13:29:45 -03:00
|
|
|
{% endblock %}
|