Ajustes gerais: overlay visual, validação por banco e limpeza segura em homologação

This commit is contained in:
2025-07-29 14:10:14 -03:00
parent 73d51e5938
commit e9ed45ba21
3 changed files with 142 additions and 26 deletions

View File

@@ -4,9 +4,12 @@ from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse, JSONResponse from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
import os, shutil import os, shutil
from sqlalchemy import text
from fastapi import Depends
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from io import BytesIO from io import BytesIO
import pandas as pd import pandas as pd
from app.models import ParametrosFormula
from sqlalchemy.future import select from sqlalchemy.future import select
from app.database import AsyncSessionLocal from app.database import AsyncSessionLocal
from app.models import Fatura from app.models import Fatura
@@ -16,6 +19,7 @@ from app.processor import (
status_arquivos, status_arquivos,
limpar_arquivos_processados limpar_arquivos_processados
) )
from app.parametros import router as parametros_router
app = FastAPI() app = FastAPI()
templates = Jinja2Templates(directory="app/templates") templates = Jinja2Templates(directory="app/templates")
@@ -47,15 +51,25 @@ def dashboard(request: Request):
@app.get("/upload", response_class=HTMLResponse) @app.get("/upload", response_class=HTMLResponse)
def upload_page(request: Request): def upload_page(request: Request):
return templates.TemplateResponse("upload.html", {"request": request}) app_env = os.getenv("APP_ENV", "dev") # Captura variável de ambiente
return templates.TemplateResponse("upload.html", {
"request": request,
"app_env": app_env # Passa para o template
})
@app.get("/relatorios", response_class=HTMLResponse) @app.get("/relatorios", response_class=HTMLResponse)
def relatorios_page(request: Request): def relatorios_page(request: Request):
return templates.TemplateResponse("relatorios.html", {"request": request}) return templates.TemplateResponse("relatorios.html", {"request": request})
@app.get("/parametros", response_class=HTMLResponse) @app.get("/parametros", response_class=HTMLResponse)
def parametros_page(request: Request): async def parametros_page(request: Request):
return templates.TemplateResponse("parametros.html", {"request": request}) async with AsyncSessionLocal() as session:
result = await session.execute(select(ParametrosFormula))
parametros = result.scalars().first()
return templates.TemplateResponse("parametros.html", {
"request": request,
"parametros": parametros or {}
})
@app.post("/upload-files") @app.post("/upload-files")
async def upload_files(files: list[UploadFile] = File(...)): async def upload_files(files: list[UploadFile] = File(...)):
@@ -144,3 +158,28 @@ async def export_excel():
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename=relatorio_faturas.xlsx"} headers={"Content-Disposition": "attachment; filename=relatorio_faturas.xlsx"}
) )
from app.parametros import router as parametros_router
app.include_router(parametros_router)
def is_homolog():
return os.getenv("APP_ENV", "dev") == "homolog"
@app.post("/limpar-faturas")
async def limpar_faturas():
app_env = os.getenv("APP_ENV", "dev")
if app_env not in ["homolog", "dev", "local"]:
return JSONResponse(status_code=403, content={"message": "Operação não permitida neste ambiente."})
async with AsyncSessionLocal() as session:
print("🧪 Limpando faturas do banco...")
await session.execute(text("DELETE FROM faturas.faturas"))
await session.commit()
upload_path = os.path.join("app", "uploads")
for nome in os.listdir(upload_path):
caminho = os.path.join(upload_path, nome)
if os.path.isfile(caminho):
os.remove(caminho)
return {"message": "Faturas e arquivos apagados com sucesso."}

View File

@@ -8,6 +8,7 @@ from app.database import AsyncSessionLocal
from app.models import Fatura, LogProcessamento from app.models import Fatura, LogProcessamento
import time import time
import traceback import traceback
import uuid
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -29,7 +30,7 @@ def remover_arquivo_temp(caminho_pdf):
def salvar_em_uploads(caminho_pdf_temp, nome_original, nota_fiscal): def salvar_em_uploads(caminho_pdf_temp, nome_original, nota_fiscal):
try: try:
extensao = os.path.splitext(nome_original)[1].lower() extensao = os.path.splitext(nome_original)[1].lower()
nome_destino = f"{nota_fiscal}{extensao}" nome_destino = f"{nota_fiscal}_{uuid.uuid4().hex[:6]}{extensao}"
destino_final = os.path.join(UPLOADS_DIR, nome_destino) destino_final = os.path.join(UPLOADS_DIR, nome_destino)
shutil.copy2(caminho_pdf_temp, destino_final) shutil.copy2(caminho_pdf_temp, destino_final)
return destino_final return destino_final

View File

@@ -17,8 +17,12 @@
<button class="btn btn-danger" onclick="limpar()">Limpar Tudo</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="baixarPlanilha()">📅 Abrir Planilha</button>
<button class="btn btn-success" onclick="gerarRelatorio()">📊 Gerar Relatório</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> </div>
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -124,38 +128,54 @@ function renderTable(statusList = []) {
window.open('/generate-report', '_blank'); window.open('/generate-report', '_blank');
} }
const dropZone = document.body; window.addEventListener('DOMContentLoaded', () => {
dropZone.addEventListener('dragover', (e) => { updateStatus();
e.preventDefault();
dropZone.classList.add('dragover'); const dragOverlay = document.getElementById("drag-overlay");
let dragCounter = 0;
window.addEventListener("dragenter", e => {
dragCounter++;
dragOverlay.classList.add("active");
}); });
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover'); window.addEventListener("dragleave", e => {
dragCounter--;
if (dragCounter <= 0) {
dragOverlay.classList.remove("active");
dragCounter = 0;
}
}); });
dropZone.addEventListener('drop', (e) => {
window.addEventListener("dragover", e => {
e.preventDefault(); e.preventDefault();
dropZone.classList.remove('dragover'); });
window.addEventListener("drop", e => {
e.preventDefault();
dragOverlay.classList.remove("active");
dragCounter = 0;
handleFiles(e.dataTransfer.files); handleFiles(e.dataTransfer.files);
}); });
});
window.addEventListener('DOMContentLoaded', updateStatus);
// Adiciona destaque visual à caixa ao arrastar arquivos na tela toda async function limparFaturas() {
window.addEventListener('dragover', (e) => { 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(); e.preventDefault();
document.getElementById('upload-box').classList.add('dragover'); });
});
window.addEventListener('dragleave', () => { window.addEventListener("drop", e => {
document.getElementById('upload-box').classList.remove('dragover');
});
window.addEventListener('drop', (e) => {
e.preventDefault(); e.preventDefault();
document.getElementById('upload-box').classList.remove('dragover'); });
handleFiles(e.dataTransfer.files);
});
</script> </script>
<style> <style>
@@ -224,6 +244,62 @@ function renderTable(statusList = []) {
font-weight: bold; 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;
}
}
</style> </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>
{% endblock %} {% endblock %}