Files
app_faturas/app/templates/upload.html

634 lines
18 KiB
HTML
Raw Permalink Normal View History

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>
<!-- Seletor de Cliente (obrigatório) -->
<div style="display:flex; gap:12px; align-items:center; margin: 0 0 14px 0;">
<label for="select-cliente" style="font-weight:600;">Cliente:</label>
<select id="select-cliente" style="min-width:320px; padding:.6rem .8rem; border:1px solid #ddd; border-radius:10px;">
<option value="">— Selecione um cliente —</option>
</select>
<span id="cliente-aviso" class="muted">Selecione o cliente antes de anexar/ processar.</span>
</div>
2025-07-28 13:29:45 -03:00
<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" id="btn-selecionar" onclick="document.getElementById('file-input').click()">Selecionar Arquivos</button>
2025-07-28 13:29:45 -03:00
<input type="file" id="file-input" accept=".pdf" multiple onchange="handleFiles(this.files)" style="display:none;" />
2025-07-28 13:29:45 -03:00
</div>
<div class="buttons">
<button class="btn btn-primary" onclick="processar(this)">Processar Faturas</button>
<button class="btn btn-primary pulse" onclick="limpar()" style="font-weight: bold;">🔁 Novo Processo</button>
{% if status_resultados|selectattr("status", "equalto", "Erro")|list %}
<div style="margin-top: 2rem;">
<a class="btn btn-danger" href="/erros/download">⬇️ Baixar Faturas com Erro (.zip)</a>
<a class="btn btn-secondary" href="/erros/log">📄 Ver Log de Erros (.txt)</a>
</div>
{% endif %}
<!--
2025-07-28 13:29:45 -03:00
<button class="btn btn-success" onclick="baixarPlanilha()">📅 Abrir Planilha</button>
<button class="btn btn-success" onclick="gerarRelatorio()">📊 Gerar Relatório</button>
-->
{% 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>
<div id="overlay-bloqueio" class="overlay-bloqueio hidden">
⏳ Tabela bloqueada até finalizar o processo
<div id="barra-progresso" class="barra-processamento"></div>
</div>
<div id="tabela-wrapper" class="tabela-wrapper"></div>
</div>
2025-07-28 13:29:45 -03:00
<script>
let arquivos = [];
let statusInterval = null;
let processado = false;
let processamentoFinalizado = false;
2025-07-28 13:29:45 -03:00
const fileTable = document.getElementById('file-table');
// <<< NOVO: carrega clientes ativos no combo
async function carregarClientes() {
try {
const r = await fetch('/api/clientes'); // se quiser só ativos: /api/clientes?ativos=true
if (!r.ok) throw new Error('Falha ao carregar clientes');
const lista = await r.json();
const sel = document.getElementById('select-cliente');
sel.innerHTML = `<option value="">— Selecione um cliente —</option>` +
lista.map(c => `<option value="${c.id}">${c.nome_fantasia}${c.cnpj ? ' — ' + c.cnpj : ''}</option>`).join('');
} catch (e) {
console.error(e);
alert('Não foi possível carregar a lista de clientes.');
}
}
function clienteSelecionado() {
return (document.getElementById('select-cliente')?.value || '').trim();
}
// <<< AJUSTE: impedir anexar sem cliente
function handleFiles(files) {
if (!clienteSelecionado()) {
alert('Selecione um cliente antes de anexar os arquivos.');
return;
}
if (processado) {
document.getElementById("feedback-sucesso").innerText = "";
document.getElementById("feedback-erro").innerText = "⚠️ Conclua ou inicie um novo processo antes de adicionar mais arquivos.";
document.getElementById("feedback-duplicado").innerText = "";
document.getElementById("upload-feedback").classList.remove("hidden");
return;
}
arquivos = [...arquivos, ...files];
// trava o combo após começar a anexar (opcional)
document.getElementById('select-cliente').disabled = true;
renderTable();
2025-07-28 13:29:45 -03:00
}
function renderTable(statusList = []) {
const grupos = ['Aguardando', 'Enviado', 'Erro', 'Duplicado'];
const dados = statusList.length ? statusList : arquivos.map(file => ({
nome: file.name,
status: 'Aguardando',
mensagem: '',
tempo: '---',
tamanho: (file.size / 1024).toFixed(1) + " KB",
data: new Date(file.lastModified).toLocaleDateString()
}));
const htmlGrupos = grupos.map(grupo => {
const rows = dados.filter(f => f.status === grupo);
if (!rows.length) return '';
const linhas = rows.map(file => `
<tr>
<td>${file.nome}<br><small>${file.tamanho} • ${file.data}</small></td>
<td class="${grupo === 'Concluído' ? 'status-ok' :
grupo === 'Erro' ? 'status-error' :
grupo === 'Aguardando' ? 'status-warn' :
'status-processing'}">
${grupo === 'Concluído' ? '✔️' :
grupo === 'Erro' ? '❌' :
grupo === 'Duplicado' ? '📄' :
'⌛'} ${file.status}
</td>
<td>
${file.mensagem ? `<div class="status-msg">${file.mensagem}</div>` : ""}
${file.tempo || '---'}
</td>
</tr>
`).join('');
return `
<details open class="grupo-status">
<summary><strong>${grupo}</strong> (${rows.length})</summary>
<table class="grupo-tabela"><tbody>${linhas}</tbody></table>
</details>
`;
}).join('');
document.getElementById("tabela-wrapper").innerHTML = htmlGrupos;
}
2025-07-28 13:29:45 -03:00
async function processar(btn) {
if (!clienteSelecionado()) {
alert("Selecione um cliente antes de processar.");
return;
}
if (arquivos.length === 0) return alert("Nenhum arquivo selecionado.");
// Confirmação
const clienteTxt = document.querySelector('#select-cliente option:checked')?.textContent || '';
if (!confirm(`Confirmar processamento de ${arquivos.length} arquivo(s) para o cliente:\n\n${clienteTxt}`)) {
return;
}
const clienteId = clienteSelecionado();
document.getElementById("tabela-wrapper").classList.add("bloqueada");
2025-07-28 13:29:45 -03:00
if (processamentoFinalizado) {
showPopup("⚠️ Conclua ou inicie um novo processo antes de processar novamente.");
return;
}
btn.disabled = true;
btn.innerText = "⏳ Enviando arquivos...";
const statusList = [];
const total = arquivos.length;
2025-07-28 13:29:45 -03:00
document.getElementById("overlay-bloqueio").classList.remove("hidden");
document.getElementById("barra-progresso").style.width = "0%";
for (let i = 0; i < total; i++) {
const file = arquivos[i]; // <- declare 'file' ANTES de usar
const formData = new FormData();
formData.append("cliente_id", clienteId); // <- usa o cache do cliente
formData.append("files", file);
// Atualiza status visual antes do envio
statusList.push({
nome: file.name,
status: "Enviando...",
mensagem: `(${i + 1}/${total})`,
tempo: "---",
tamanho: (file.size / 1024).toFixed(1) + " KB",
data: new Date(file.lastModified).toLocaleDateString()
});
renderTable(statusList);
const start = performance.now();
2025-07-28 13:29:45 -03:00
try {
await fetch("/upload-files", { method: "POST", body: formData });
const progresso = Math.round(((i + 1) / total) * 100);
document.getElementById("barra-progresso").style.width = `${progresso}%`;
statusList[i].status = "Enviado";
statusList[i].tempo = `${((performance.now() - start) / 1000).toFixed(2)}s`;
2025-07-28 13:29:45 -03:00
} catch (err) {
statusList[i].status = "Erro";
statusList[i].mensagem = err.message;
2025-07-28 13:29:45 -03:00
}
renderTable(statusList);
await new Promise(r => setTimeout(r, 200)); // pequeno delay
}
btn.innerText = "⏳ Iniciando processamento...";
try {
const res = await fetch("/process-queue", { method: "POST" });
if (!res.ok) throw new Error(await res.text());
statusInterval = setInterval(updateStatus, 1000);
// Mensagem final após pequeno delay
setTimeout(async () => {
const res = await fetch("/get-status");
const data = await res.json();
const finalStatus = data.files;
const concluidos = finalStatus.filter(f => f.status === "Concluído").length;
const erros = finalStatus.filter(f => f.status === "Erro").length;
const duplicados = finalStatus.filter(f => f.status === "Duplicado").length;
document.getElementById("feedback-sucesso").innerText = `✔️ ${concluidos} enviados com sucesso`;
document.getElementById("feedback-erro").innerText = `❌ ${erros} com erro(s)`;
document.getElementById("feedback-duplicado").innerText = `📄 ${duplicados } duplicado(s)`;
document.getElementById("tabela-wrapper").classList.remove("bloqueada");
document.getElementById("upload-feedback").classList.remove("hidden");
processamentoFinalizado = true;
}, 1000);
} catch (e) {
document.getElementById("feedback-sucesso").innerText = "";
document.getElementById("feedback-erro").innerText = `❌ Erro ao iniciar: ${e.message}`;
document.getElementById("upload-feedback").classList.remove("hidden");
document.getElementById("overlay-bloqueio").classList.add("hidden");
} finally {
processado = true;
document.getElementById("btn-selecionar").disabled = true;
btn.innerText = "Processar Faturas";
btn.disabled = false;
2025-07-28 13:29:45 -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" });
// reset da fila/estado
arquivos = [];
processado = false;
processamentoFinalizado = false;
if (statusInterval) { clearInterval(statusInterval); statusInterval = null; }
// reset dos inputs/visual
document.getElementById("file-input").value = null;
document.getElementById("tabela-wrapper").classList.remove("bloqueada");
document.getElementById("overlay-bloqueio").classList.add("hidden");
document.getElementById("barra-progresso").style.width = "0%";
document.getElementById("btn-selecionar").disabled = false;
// 🔓 permitir mudar o cliente novamente
const sel = document.getElementById("select-cliente");
sel.disabled = false;
sel.value = ""; // <- se NÃO quiser limpar a escolha anterior, remova esta linha
document.getElementById("cliente-aviso").textContent =
"Selecione o cliente antes de anexar/ processar.";
// limpar feedback
document.getElementById("upload-feedback").classList.add("hidden");
document.getElementById("feedback-sucesso").innerText = "";
document.getElementById("feedback-erro").innerText = "";
document.getElementById("feedback-duplicado").innerText = "";
// limpar tabela
renderTable();
}
2025-07-28 13:29:45 -03:00
function baixarPlanilha() {
window.open('/export-excel', '_blank');
}
function gerarRelatorio() {
window.open('/generate-report', '_blank');
}
window.addEventListener('DOMContentLoaded', () => {
carregarClientes();
updateStatus();
2025-07-28 13:29:45 -03:00
const dragOverlay = document.getElementById("drag-overlay");
let dragCounter = 0;
window.addEventListener("dragenter", e => {
dragCounter++;
dragOverlay.classList.add("active");
});
window.addEventListener("dragleave", e => {
dragCounter--;
if (dragCounter <= 0) {
dragOverlay.classList.remove("active");
dragCounter = 0;
}
});
window.addEventListener("dragover", e => {
e.preventDefault();
});
window.addEventListener("drop", e => {
e.preventDefault();
dragOverlay.classList.remove("active");
dragCounter = 0;
if (!clienteSelecionado()) {
alert('Selecione um cliente antes de anexar os arquivos.');
return;
}
handleFiles(e.dataTransfer.files);
});
});
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
}
window.addEventListener("dragover", e => {
e.preventDefault();
});
window.addEventListener("drop", e => {
e.preventDefault();
});
function fecharFeedback() {
document.getElementById("upload-feedback").classList.add("hidden");
document.getElementById("tabela-faturas")?.classList.remove("tabela-bloqueada");
document.getElementById("tabela-wrapper").classList.remove("bloqueada");
document.getElementById("overlay-bloqueio").classList.add("hidden");
document.getElementById("barra-progresso").style.width = "0%";
}
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; }
.status-processing {
color: #0d6efd; /* azul */
font-weight: bold;
}
.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;
}
}
.feedback-popup {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 99999;
}
.feedback-content {
background-color: #fff;
padding: 2rem;
border-radius: 12px;
text-align: center;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.25);
}
.feedback-content h3 {
margin-bottom: 1rem;
}
.feedback-content p {
margin: 0.25rem 0;
font-weight: 500;
}
.hidden {
display: none;
}
.tabela-bloqueada {
pointer-events: none;
opacity: 0.4;
filter: grayscale(0.4);
}
.tabela-wrapper {
position: relative;
}
.overlay-bloqueio {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 2rem 2rem 3rem 2rem;
border-radius: 12px;
font-weight: bold;
font-size: 1rem;
color: #555;
text-align: center;
z-index: 999;
box-shadow: 0 0 6px rgba(0, 0, 0, 0.15);
max-width: 90%;
}
#barra-progresso {
margin-top: 1.2rem;
height: 6px;
width: 0%;
background: linear-gradient(270deg, #4361ee, #66bbff, #4361ee);
background-size: 600% 600%;
animation: animarBarra 1.5s linear infinite;
border-radius: 8px;
transition: width 0.3s ease-in-out;
}
.grupo-status {
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 8px;
background: #fff;
padding: 0.5rem 1rem;
}
.grupo-status summary {
font-size: 1rem;
cursor: pointer;
margin-bottom: 0.5rem;
}
.grupo-tabela {
width: 100%;
border-collapse: collapse;
}
.grupo-tabela td {
padding: 0.75rem;
border-top: 1px solid #eee;
}
.barra-processamento {
height: 5px;
width: 0%; /* importante iniciar assim */
background: linear-gradient(270deg, #4361ee, #66bbff, #4361ee);
background-size: 600% 600%;
animation: animarBarra 1.5s linear infinite;
position: fixed;
top: 0;
left: 0;
z-index: 9999;
}
@keyframes animarBarra {
0% { background-position: 0% 50%; }
100% { background-position: 100% 50%; }
}
.hidden {
display: none !important;
}
.pulse {
animation: pulseAnim 1.8s infinite;
}
@keyframes pulseAnim {
0% { transform: scale(1); }
50% { transform: scale(1.06); }
100% { transform: scale(1); }
}
.status-msg {
color: #dc3545;
font-size: 0.8rem;
margin-top: 0.25rem;
white-space: pre-wrap;
}
2025-07-28 13:29:45 -03:00
</style>
<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>
<div id="upload-feedback" class="feedback-popup hidden">
<div class="feedback-content">
<h3>📦 Upload concluído!</h3>
<p id="feedback-sucesso">✔️ 0 enviados com sucesso</p>
<p id="feedback-erro">❌ 0 com erro(s)</p>
<p id="feedback-duplicado">📄 0 duplicado(s)</p>
<button onclick="fecharFeedback()" class="btn btn-primary" style="margin-top: 1rem;">OK</button>
</div>
</div>
2025-07-28 13:29:45 -03:00
{% endblock %}