Compare commits
2 Commits
73d51e5938
...
e7c2a64714
| Author | SHA1 | Date | |
|---|---|---|---|
| e7c2a64714 | |||
| e9ed45ba21 |
45
app/main.py
45
app/main.py
@@ -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."}
|
||||||
@@ -4,15 +4,21 @@ from sqlalchemy.dialects.postgresql import UUID
|
|||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app.database import Base
|
from app.database import Base
|
||||||
|
from sqlalchemy import Boolean
|
||||||
|
|
||||||
|
|
||||||
class ParametrosFormula(Base):
|
class ParametrosFormula(Base):
|
||||||
__tablename__ = 'parametros_formula'
|
__tablename__ = "parametros_formula"
|
||||||
__table_args__ = {'schema': 'faturas', 'extend_existing': True}
|
__table_args__ = {"schema": "faturas"}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
nome = Column(String)
|
tipo = Column(String(20))
|
||||||
formula = Column(Text)
|
formula = Column(Text)
|
||||||
|
ativo = Column(Boolean)
|
||||||
|
aliquota_icms = Column(Float)
|
||||||
|
incluir_icms = Column(Integer)
|
||||||
|
incluir_pis = Column(Integer)
|
||||||
|
incluir_cofins = Column(Integer)
|
||||||
|
|
||||||
class Fatura(Base):
|
class Fatura(Base):
|
||||||
__tablename__ = "faturas"
|
__tablename__ = "faturas"
|
||||||
|
|||||||
22
app/routes/parametros.py → app/parametros.py
Executable file → Normal file
22
app/routes/parametros.py → app/parametros.py
Executable file → Normal file
@@ -1,12 +1,15 @@
|
|||||||
# parametros.py
|
# parametros.py
|
||||||
from fastapi import APIRouter, HTTPException, Depends
|
from fastapi import APIRouter, Request, HTTPException, Depends
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from database import get_session
|
from app.database import get_session
|
||||||
from models import AliquotaUF, ParametrosFormula, SelicMensal
|
from app.models import AliquotaUF, ParametrosFormula, SelicMensal
|
||||||
from typing import List
|
from typing import List
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import datetime
|
import datetime
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from sqlalchemy.future import select
|
||||||
|
from app.database import AsyncSessionLocal
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -36,6 +39,19 @@ class SelicMensalSchema(BaseModel):
|
|||||||
|
|
||||||
# === Rotas ===
|
# === Rotas ===
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
templates = Jinja2Templates(directory="app/templates")
|
||||||
|
|
||||||
|
@router.get("/parametros")
|
||||||
|
async def parametros_page(request: Request):
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
result = await session.execute(select(ParametrosFormula).where(ParametrosFormula.ativo == True))
|
||||||
|
parametros = result.scalars().first()
|
||||||
|
return templates.TemplateResponse("parametros.html", {
|
||||||
|
"request": request,
|
||||||
|
"parametros": parametros or {}
|
||||||
|
})
|
||||||
|
|
||||||
@router.get("/parametros/aliquotas", response_model=List[AliquotaUFSchema])
|
@router.get("/parametros/aliquotas", response_model=List[AliquotaUFSchema])
|
||||||
def listar_aliquotas(db: AsyncSession = Depends(get_session)):
|
def listar_aliquotas(db: AsyncSession = Depends(get_session)):
|
||||||
return db.query(AliquotaUF).all()
|
return db.query(AliquotaUF).all()
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,32 +1,206 @@
|
|||||||
{% extends "index.html" %}
|
{% extends "index.html" %}
|
||||||
{% block title %}Parâmetros de Cálculo{% endblock %}
|
{% block title %}Parâmetros de Cálculo{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>⚙️ Parâmetros</h1>
|
|
||||||
|
|
||||||
<form method="post">
|
<h1 style="font-size: 1.6rem; margin-bottom: 1rem; display:flex; align-items:center; gap:0.5rem;">
|
||||||
<label for="tipo">Tipo:</label><br>
|
⚙️ Parâmetros de Cálculo
|
||||||
<input type="text" name="tipo" id="tipo" value="{{ parametros.tipo or '' }}" required/><br><br>
|
</h1>
|
||||||
|
|
||||||
<label for="formula">Fórmula:</label><br>
|
<div class="tabs">
|
||||||
<input type="text" name="formula" id="formula" value="{{ parametros.formula or '' }}" required/><br><br>
|
<button class="tab active" onclick="switchTab('formulas')">📄 Fórmulas</button>
|
||||||
|
<button class="tab" onclick="switchTab('selic')">📊 Gestão SELIC</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="aliquota_icms">Alíquota de ICMS (%):</label><br>
|
<!-- ABA FÓRMULAS -->
|
||||||
<input type="number" step="0.01" name="aliquota_icms" id="aliquota_icms" value="{{ parametros.aliquota_icms or '' }}" /><br><br>
|
<div id="formulas" class="tab-content active">
|
||||||
|
<form method="post" class="formulario-box">
|
||||||
|
<div class="grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tipo">Tipo:</label>
|
||||||
|
<input type="text" name="tipo" id="tipo" value="{{ parametros.tipo or '' }}" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="aliquota_icms">Alíquota de ICMS (%):</label>
|
||||||
|
<input type="number" step="0.01" name="aliquota_icms" id="aliquota_icms" value="{{ parametros.aliquota_icms or '' }}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="incluir_icms">Incluir ICMS:</label><br>
|
<div class="form-group">
|
||||||
<input type="checkbox" name="incluir_icms" id="incluir_icms" value="1" {% if parametros.incluir_icms %}checked{% endif %}><br><br>
|
<label for="formula">Fórmula:</label>
|
||||||
|
<div class="editor-box">
|
||||||
|
<select onchange="inserirCampo(this)">
|
||||||
|
<option value="">Inserir campo...</option>
|
||||||
|
<option value="pis_base">pis_base</option>
|
||||||
|
<option value="cofins_base">cofins_base</option>
|
||||||
|
<option value="icms_valor">icms_valor</option>
|
||||||
|
<option value="pis_aliq">pis_aliq</option>
|
||||||
|
<option value="cofins_aliq">cofins_aliq</option>
|
||||||
|
</select>
|
||||||
|
<textarea name="formula" id="formula" rows="3" required>{{ parametros.formula or '' }}</textarea>
|
||||||
|
<div class="actions-inline">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="testarFormula()">🧪 Testar Fórmula</button>
|
||||||
|
<span class="exemplo">Ex: (pis_base - (pis_base - icms_valor)) * pis_aliq</span>
|
||||||
|
</div>
|
||||||
|
<div id="resultado-teste" class="mensagem-info" style="display:none;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="ativo">Ativo:</label><br>
|
<div class="form-group check-group">
|
||||||
<input type="checkbox" name="ativo" id="ativo" value="1" {% if parametros.ativo %}checked{% endif %}><br><br>
|
<label><input type="checkbox" name="incluir_icms" value="1" {% if parametros.incluir_icms %}checked{% endif %}> Incluir ICMS</label>
|
||||||
|
<label><input type="checkbox" name="incluir_pis" value="1" {% if parametros.incluir_pis %}checked{% endif %}> Incluir PIS</label>
|
||||||
|
<label><input type="checkbox" name="incluir_cofins" value="1" {% if parametros.incluir_cofins %}checked{% endif %}> Incluir COFINS</label>
|
||||||
|
<label><input type="checkbox" name="ativo" value="1" {% if parametros.ativo %}checked{% endif %}> Ativo</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" style="padding: 10px 20px; background: #2563eb; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
<button type="submit" class="btn btn-primary">💾 Salvar Parâmetro</button>
|
||||||
Salvar Parâmetros
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if mensagem %}
|
<h3 style="margin-top: 2rem;">📋 Fórmulas Salvas</h3>
|
||||||
<div style="margin-top: 20px; background: #e0f7fa; padding: 10px; border-left: 4px solid #2563eb;">
|
<div class="card-list">
|
||||||
{{ mensagem }}
|
{% for param in lista_parametros %}
|
||||||
|
<div class="param-card">
|
||||||
|
<h4>{{ param.tipo }}</h4>
|
||||||
|
<code>{{ param.formula }}</code>
|
||||||
|
<div class="actions">
|
||||||
|
<a href="?editar={{ param.id }}" class="btn btn-sm">✏️ Editar</a>
|
||||||
|
<a href="?testar={{ param.id }}" class="btn btn-sm btn-secondary">🧪 Testar</a>
|
||||||
|
<a href="/parametros/delete/{{ param.id }}" class="btn btn-sm btn-danger">🗑️ Excluir</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
|
{% else %}<p style="color:gray;">Nenhuma fórmula cadastrada.</p>{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ABA SELIC -->
|
||||||
|
<div id="selic" class="tab-content" style="display:none;">
|
||||||
|
<div class="formulario-box">
|
||||||
|
<h3>📈 Gestão da SELIC</h3>
|
||||||
|
<p>Utilize o botão abaixo para importar os fatores SELIC automaticamente a partir da API do Banco Central.</p>
|
||||||
|
<form method="post" action="/parametros/selic/importar">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="data_maxima">Data máxima para cálculo da SELIC:</label>
|
||||||
|
<input type="date" id="data_maxima" name="data_maxima" value="{{ data_maxima or '' }}" />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">⬇️ Atualizar Fatores SELIC</button>
|
||||||
|
</form>
|
||||||
|
<div class="mensagem-info" style="margin-top:1rem;">Última data coletada da SELIC: <strong>{{ ultima_data_selic }}</strong></div>
|
||||||
|
<table class="selic-table">
|
||||||
|
<thead><tr><th>Competência</th><th>Fator</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in selic_dados %}
|
||||||
|
<tr><td>{{ item.competencia }}</td><td>{{ item.fator }}</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ESTILOS -->
|
||||||
|
<style>
|
||||||
|
.tabs { display: flex; gap: 1rem; margin-bottom: 1rem; }
|
||||||
|
.tab { background: none; border: none; font-weight: bold; cursor: pointer; padding: 0.5rem 1rem; border-bottom: 2px solid transparent; }
|
||||||
|
.tab.active { border-bottom: 2px solid #2563eb; color: #2563eb; }
|
||||||
|
.tab-content { display: none; }
|
||||||
|
.tab-content.active { display: block; }
|
||||||
|
|
||||||
|
.formulario-box {
|
||||||
|
background: #fff;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.05);
|
||||||
|
max-width: 850px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group { margin-bottom: 1rem; }
|
||||||
|
.form-group label { display: block; font-weight: bold; margin-bottom: 0.5rem; }
|
||||||
|
.form-group input[type="text"],
|
||||||
|
.form-group input[type="number"],
|
||||||
|
.form-group input[type="date"],
|
||||||
|
.form-group textarea { width: 100%; padding: 0.6rem; border-radius: 6px; border: 1px solid #ccc; }
|
||||||
|
|
||||||
|
.form-group.check-group { display: flex; gap: 1rem; flex-wrap: wrap; }
|
||||||
|
.check-group label { display: flex; align-items: center; gap: 0.5rem; }
|
||||||
|
|
||||||
|
.editor-box select { margin-bottom: 0.5rem; }
|
||||||
|
.editor-box textarea { font-family: monospace; }
|
||||||
|
.actions-inline { display: flex; gap: 1rem; align-items: center; margin-top: 0.5rem; }
|
||||||
|
|
||||||
|
.btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; }
|
||||||
|
.btn-primary { background-color: #2563eb; color: white; }
|
||||||
|
.btn-secondary { background-color: #6c757d; color: white; }
|
||||||
|
.btn-danger { background-color: #dc3545; color: white; }
|
||||||
|
.btn-sm { font-size: 0.85rem; padding: 0.3rem 0.6rem; }
|
||||||
|
|
||||||
|
.mensagem-info {
|
||||||
|
background: #e0f7fa;
|
||||||
|
padding: 1rem;
|
||||||
|
border-left: 4px solid #2563eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #007b83;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||||
|
|
||||||
|
.card-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.param-card {
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #2563eb;
|
||||||
|
}
|
||||||
|
.param-card code { display: block; margin: 0.5rem 0; color: #333; }
|
||||||
|
.actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||||||
|
|
||||||
|
.selic-table { width: 100%; margin-top: 1rem; border-collapse: collapse; }
|
||||||
|
.selic-table th, .selic-table td { padding: 0.6rem; border-bottom: 1px solid #ccc; }
|
||||||
|
.selic-table th { text-align: left; background: #f1f1f1; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function switchTab(tabId) {
|
||||||
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||||
|
document.getElementById(tabId).classList.add('active');
|
||||||
|
event.target.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function inserirCampo(select) {
|
||||||
|
const campo = select.value;
|
||||||
|
if (campo) {
|
||||||
|
const formula = document.getElementById("formula");
|
||||||
|
const start = formula.selectionStart;
|
||||||
|
const end = formula.selectionEnd;
|
||||||
|
formula.value = formula.value.slice(0, start) + campo + formula.value.slice(end);
|
||||||
|
formula.focus();
|
||||||
|
formula.setSelectionRange(start + campo.length, start + campo.length);
|
||||||
|
select.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testarFormula() {
|
||||||
|
const formula = document.getElementById("formula").value;
|
||||||
|
const vars = {
|
||||||
|
pis_base: 84.38,
|
||||||
|
cofins_base: 84.38,
|
||||||
|
icms_valor: 24.47,
|
||||||
|
pis_aliq: 0.012872,
|
||||||
|
cofins_aliq: 0.059287
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const resultado = eval(formula.replace(/\b(\w+)\b/g, match => vars.hasOwnProperty(match) ? vars[match] : match));
|
||||||
|
document.getElementById("resultado-teste").innerText = "Resultado: R$ " + resultado.toFixed(5);
|
||||||
|
document.getElementById("resultado-teste").style.display = "block";
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById("resultado-teste").innerText = "Erro: " + e.message;
|
||||||
|
document.getElementById("resultado-teste").style.display = "block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
Reference in New Issue
Block a user