Criação da tela de clientes e relatórios
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-08-11 13:14:54 -03:00
parent bcf9861e97
commit 950eb2a826
7 changed files with 595 additions and 181 deletions

View File

@@ -4,6 +4,16 @@
<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>
<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>
@@ -34,7 +44,6 @@
</div>
<div id="tabela-wrapper" class="tabela-wrapper"></div>
</div>
ar
<script>
let arquivos = [];
let statusInterval = null;
@@ -43,18 +52,49 @@ ar
const fileTable = document.getElementById('file-table');
function handleFiles(files) {
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;
// <<< 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();
}
arquivos = [...arquivos, ...files];
renderTable();
}
function renderTable(statusList = []) {
const grupos = ['Aguardando', 'Enviado', 'Erro', 'Duplicado'];
@@ -102,7 +142,20 @@ function renderTable(statusList = []) {
}
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");
if (processamentoFinalizado) {
@@ -120,8 +173,9 @@ async function processar(btn) {
document.getElementById("barra-progresso").style.width = "0%";
for (let i = 0; i < total; i++) {
const file = arquivos[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
@@ -129,14 +183,16 @@ async function processar(btn) {
nome: file.name,
status: "Enviando...",
mensagem: `(${i + 1}/${total})`,
tempo: "---"
tempo: "---",
tamanho: (file.size / 1024).toFixed(1) + " KB",
data: new Date(file.lastModified).toLocaleDateString()
});
renderTable(statusList);
const start = performance.now();
try {
await fetch("/upload-files", { method: "POST", body: formData });
let progresso = Math.round(((i + 1) / total) * 100);
const progresso = Math.round(((i + 1) / total) * 100);
document.getElementById("barra-progresso").style.width = `${progresso}%`;
statusList[i].status = "Enviado";
@@ -147,9 +203,7 @@ async function processar(btn) {
}
renderTable(statusList);
// Delay de 200ms entre cada envio
await new Promise(r => setTimeout(r, 200));
await new Promise(r => setTimeout(r, 200)); // pequeno delay
}
btn.innerText = "⏳ Iniciando processamento...";
@@ -206,23 +260,39 @@ async function processar(btn) {
}
}
function limpar() {
fetch("/clear-all", { method: "POST" });
arquivos = [];
processado = false;
document.getElementById("file-input").value = null;
renderTable();
function limpar() {
fetch("/clear-all", { method: "POST" });
// limpa feedback visual também
document.getElementById("upload-feedback").classList.add("hidden");
document.getElementById("feedback-sucesso").innerText = "";
document.getElementById("feedback-erro").innerText = "";
document.getElementById("feedback-duplicado").innerText = "";
document.getElementById("tabela-wrapper").classList.remove("bloqueada");
processamentoFinalizado = false;
document.getElementById("btn-selecionar").disabled = false;
// 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();
}
}
function baixarPlanilha() {
window.open('/export-excel', '_blank');
@@ -233,6 +303,7 @@ async function processar(btn) {
}
window.addEventListener('DOMContentLoaded', () => {
carregarClientes();
updateStatus();
const dragOverlay = document.getElementById("drag-overlay");
@@ -255,11 +326,15 @@ window.addEventListener('DOMContentLoaded', () => {
e.preventDefault();
});
window.addEventListener("drop", e => {
e.preventDefault();
dragOverlay.classList.remove("active");
dragCounter = 0;
handleFiles(e.dataTransfer.files);
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);
});
});