|
|
|
|
@@ -8,100 +8,184 @@
|
|
|
|
|
<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>
|
|
|
|
|
<button class="btn btn-primary" id="btn-selecionar" 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-primary pulse" onclick="limpar()" style="font-weight: bold;">🔁 Novo Processo</button>
|
|
|
|
|
<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 %}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<table>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>Arquivo</th>
|
|
|
|
|
<th>Status</th>
|
|
|
|
|
<th>Mensagem</th>
|
|
|
|
|
<th>Tempo</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody id="file-table">
|
|
|
|
|
<tr><td colspan="3" style="text-align: center; color: #aaa;">Nenhum arquivo selecionado.</td></tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
let arquivos = [];
|
|
|
|
|
let statusInterval = null;
|
|
|
|
|
let processado = false;
|
|
|
|
|
let processamentoFinalizado = false;
|
|
|
|
|
|
|
|
|
|
const fileTable = document.getElementById('file-table');
|
|
|
|
|
|
|
|
|
|
function handleFiles(files) {
|
|
|
|
|
arquivos = [...arquivos, ...files];
|
|
|
|
|
renderTable();
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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>';
|
|
|
|
|
arquivos = [...arquivos, ...files];
|
|
|
|
|
renderTable();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function processar(btn) {
|
|
|
|
|
if (arquivos.length === 0) return alert("Nenhum arquivo selecionado.");
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
btn.innerText = "⏳ Processando...";
|
|
|
|
|
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 formData = new FormData();
|
|
|
|
|
arquivos.forEach(file => formData.append("files", file));
|
|
|
|
|
const htmlGrupos = grupos.map(grupo => {
|
|
|
|
|
const rows = dados.filter(f => f.status === grupo);
|
|
|
|
|
if (!rows.length) return '';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await fetch("/upload-files", { method: "POST", body: formData });
|
|
|
|
|
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.tempo || '---'}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
`).join('');
|
|
|
|
|
|
|
|
|
|
const response = await fetch("/process-queue", { method: "POST" });
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const msg = await response.text();
|
|
|
|
|
throw new Error(`Erro no processamento: ${msg}`);
|
|
|
|
|
}
|
|
|
|
|
return `
|
|
|
|
|
<details open class="grupo-status">
|
|
|
|
|
<summary><strong>${grupo}</strong> (${rows.length})</summary>
|
|
|
|
|
<table class="grupo-tabela"><tbody>${linhas}</tbody></table>
|
|
|
|
|
</details>
|
|
|
|
|
`;
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
arquivos = []; // <- só limpa se o processamento for bem-sucedido
|
|
|
|
|
statusInterval = setInterval(updateStatus, 1000);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
alert("Erro ao processar faturas:\n" + err.message);
|
|
|
|
|
} finally {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
btn.innerText = "Processar Faturas";
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
}, 1500);
|
|
|
|
|
}
|
|
|
|
|
document.getElementById("tabela-wrapper").innerHTML = htmlGrupos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function processar(btn) {
|
|
|
|
|
if (arquivos.length === 0) return alert("Nenhum arquivo selecionado.");
|
|
|
|
|
document.getElementById("tabela-wrapper").classList.add("bloqueada");
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
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];
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append("files", file);
|
|
|
|
|
|
|
|
|
|
// Atualiza status visual antes do envio
|
|
|
|
|
statusList.push({
|
|
|
|
|
nome: file.name,
|
|
|
|
|
status: "Enviando...",
|
|
|
|
|
mensagem: `(${i + 1}/${total})`,
|
|
|
|
|
tempo: "---"
|
|
|
|
|
});
|
|
|
|
|
renderTable(statusList);
|
|
|
|
|
|
|
|
|
|
const start = performance.now();
|
|
|
|
|
try {
|
|
|
|
|
await fetch("/upload-files", { method: "POST", body: formData });
|
|
|
|
|
let 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`;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
statusList[i].status = "Erro";
|
|
|
|
|
statusList[i].mensagem = err.message;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderTable(statusList);
|
|
|
|
|
|
|
|
|
|
// Delay de 200ms entre cada envio
|
|
|
|
|
await new Promise(r => setTimeout(r, 200));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function updateStatus() {
|
|
|
|
|
const res = await fetch("/get-status");
|
|
|
|
|
@@ -116,8 +200,19 @@ function renderTable(statusList = []) {
|
|
|
|
|
function limpar() {
|
|
|
|
|
fetch("/clear-all", { method: "POST" });
|
|
|
|
|
arquivos = [];
|
|
|
|
|
processado = false;
|
|
|
|
|
document.getElementById("file-input").value = null;
|
|
|
|
|
renderTable();
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function baixarPlanilha() {
|
|
|
|
|
@@ -176,6 +271,15 @@ window.addEventListener("dragover", e => {
|
|
|
|
|
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%";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
@@ -290,6 +394,129 @@ window.addEventListener("drop", e => {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
@@ -302,4 +529,14 @@ window.addEventListener("drop", e => {
|
|
|
|
|
</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>
|
|
|
|
|
|
|
|
|
|
{% endblock %}
|