Criação da tela de clientes e relatórios
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
316
app/main.py
316
app/main.py
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
from fastapi import FastAPI, HTTPException, Request, UploadFile, File
|
||||
from fastapi import FastAPI, HTTPException, Request, UploadFile, File, Depends, Form
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
@@ -25,6 +25,10 @@ from app.models import Fatura, SelicMensal, ParametrosFormula
|
||||
from datetime import date
|
||||
from app.utils import avaliar_formula
|
||||
from app.routes import clientes
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.database import get_session
|
||||
from fastapi import Query
|
||||
from sqlalchemy import select as sqla_select
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
@@ -32,7 +36,7 @@ templates = Jinja2Templates(directory="app/templates")
|
||||
app.state.templates = templates
|
||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
||||
|
||||
UPLOAD_DIR = "uploads/temp"
|
||||
UPLOAD_DIR = os.path.join("app", "uploads", "temp")
|
||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||
|
||||
def _parse_referencia(ref: str):
|
||||
@@ -99,7 +103,27 @@ def _avaliar_formula(texto_formula: str | None, contexto: dict) -> float:
|
||||
# aceita vírgula como decimal vindo do banco
|
||||
if isinstance(v, str):
|
||||
v = v.replace(".", "").replace(",", ".") if re.search(r"[0-9],[0-9]", v) else v
|
||||
expr = re.sub(rf'\b{re.escape(str(campo))}\b', str(v), expr)
|
||||
# nome do campo escapado na regex
|
||||
pat = rf'\b{re.escape(str(campo))}\b'
|
||||
|
||||
# normaliza o valor para número; se não der, vira 0
|
||||
val = v
|
||||
if val is None or val == "":
|
||||
num = 0.0
|
||||
else:
|
||||
if isinstance(val, str):
|
||||
# troca vírgula decimal e remove separador de milhar simples
|
||||
val_norm = val.replace(".", "").replace(",", ".")
|
||||
else:
|
||||
val_norm = val
|
||||
try:
|
||||
num = float(val_norm)
|
||||
except Exception:
|
||||
num = 0.0
|
||||
|
||||
# usa lambda para evitar interpretação de backslashes no replacement
|
||||
expr = re.sub(pat, lambda m: str(num), expr)
|
||||
|
||||
|
||||
try:
|
||||
return float(eval(expr, {"__builtins__": {}}, {}))
|
||||
@@ -113,10 +137,14 @@ async def dashboard(request: Request, cliente: str | None = None):
|
||||
async with AsyncSessionLocal() as session:
|
||||
print("DBG /: abrindo sessão", flush=True)
|
||||
|
||||
r = await session.execute(text(
|
||||
"SELECT DISTINCT nome FROM faturas.faturas ORDER BY nome"
|
||||
))
|
||||
clientes = [c for c, in r.fetchall()]
|
||||
r = await session.execute(text("""
|
||||
SELECT id, nome_fantasia
|
||||
FROM faturas.clientes
|
||||
WHERE ativo = TRUE
|
||||
ORDER BY nome_fantasia
|
||||
"""))
|
||||
clientes = [{"id": id_, "nome": nome} for id_, nome in r.fetchall()]
|
||||
|
||||
print(f"DBG /: clientes={len(clientes)}", flush=True)
|
||||
|
||||
# Fórmulas
|
||||
@@ -135,7 +163,7 @@ async def dashboard(request: Request, cliente: str | None = None):
|
||||
sql = "SELECT * FROM faturas.faturas"
|
||||
params = {}
|
||||
if cliente:
|
||||
sql += " WHERE nome = :cliente"
|
||||
sql += " WHERE cliente_id = :cliente"
|
||||
params["cliente"] = cliente
|
||||
print("DBG /: SQL faturas ->", sql, params, flush=True)
|
||||
|
||||
@@ -254,21 +282,58 @@ def upload_page(request: Request):
|
||||
})
|
||||
|
||||
@app.get("/relatorios", response_class=HTMLResponse)
|
||||
def relatorios_page(request: Request):
|
||||
return templates.TemplateResponse("relatorios.html", {"request": request})
|
||||
async def relatorios_page(request: Request, cliente: str | None = Query(None)):
|
||||
async with AsyncSessionLocal() as session:
|
||||
# Carregar clientes ativos para o combo
|
||||
r_cli = await session.execute(text("""
|
||||
SELECT id, nome_fantasia
|
||||
FROM faturas.clientes
|
||||
WHERE ativo = TRUE
|
||||
ORDER BY nome_fantasia
|
||||
"""))
|
||||
clientes = [{"id": str(row.id), "nome": row.nome_fantasia} for row in r_cli]
|
||||
|
||||
# Carregar faturas (todas ou filtradas por cliente)
|
||||
if cliente:
|
||||
r_fat = await session.execute(text("""
|
||||
SELECT *
|
||||
FROM faturas.faturas
|
||||
WHERE cliente_id = :cid
|
||||
ORDER BY data_processamento DESC
|
||||
"""), {"cid": cliente})
|
||||
else:
|
||||
r_fat = await session.execute(text("""
|
||||
SELECT *
|
||||
FROM faturas.faturas
|
||||
ORDER BY data_processamento DESC
|
||||
"""))
|
||||
|
||||
faturas = r_fat.mappings().all()
|
||||
|
||||
return templates.TemplateResponse("relatorios.html", {
|
||||
"request": request,
|
||||
"clientes": clientes,
|
||||
"cliente_selecionado": cliente or "",
|
||||
"faturas": faturas
|
||||
})
|
||||
|
||||
@app.post("/upload-files")
|
||||
async def upload_files(files: list[UploadFile] = File(...)):
|
||||
async def upload_files(
|
||||
cliente_id: str = Form(...),
|
||||
files: list[UploadFile] = File(...)
|
||||
):
|
||||
for file in files:
|
||||
temp_path = os.path.join(UPLOAD_DIR, f"{uuid.uuid4()}_{file.filename}")
|
||||
with open(temp_path, "wb") as f:
|
||||
shutil.copyfileobj(file.file, f)
|
||||
await fila_processamento.put({
|
||||
"caminho_pdf": temp_path,
|
||||
"nome_original": file.filename
|
||||
"nome_original": file.filename,
|
||||
"cliente_id": cliente_id
|
||||
})
|
||||
return {"message": "Arquivos enviados para fila"}
|
||||
|
||||
|
||||
@app.post("/process-queue")
|
||||
async def process_queue():
|
||||
resultados = await processar_em_lote()
|
||||
@@ -307,116 +372,108 @@ async def clear_all():
|
||||
return {"message": "Fila e arquivos limpos"}
|
||||
|
||||
@app.get("/export-excel")
|
||||
async def export_excel():
|
||||
async def export_excel(
|
||||
tipo: str = Query("geral", regex="^(geral|exclusao_icms|aliquota_icms)$"),
|
||||
cliente: str | None = Query(None)
|
||||
):
|
||||
import pandas as pd
|
||||
|
||||
# 1) Carregar faturas (com filtro por cliente, se houver)
|
||||
async with AsyncSessionLocal() as session:
|
||||
# 1. Coletar faturas e tabela SELIC
|
||||
faturas_result = await session.execute(select(Fatura))
|
||||
stmt = select(Fatura)
|
||||
if cliente:
|
||||
# filtra por cliente_id (UUID em string)
|
||||
stmt = stmt.where(Fatura.cliente_id == cliente)
|
||||
faturas_result = await session.execute(stmt)
|
||||
faturas = faturas_result.scalars().all()
|
||||
|
||||
selic_result = await session.execute(select(SelicMensal))
|
||||
selic_tabela = selic_result.scalars().all()
|
||||
|
||||
# 2. Criar mapa {(ano, mes): percentual}
|
||||
selic_map = {(s.ano, s.mes): float(s.percentual) for s in selic_tabela}
|
||||
hoje = date.today()
|
||||
|
||||
def calcular_fator_selic(ano_inicio, mes_inicio):
|
||||
fator = 1.0
|
||||
ano, mes = ano_inicio, mes_inicio
|
||||
while (ano < hoje.year) or (ano == hoje.year and mes <= hoje.month):
|
||||
percentual = selic_map.get((ano, mes))
|
||||
if percentual:
|
||||
fator *= (1 + percentual / 100)
|
||||
mes += 1
|
||||
if mes > 12:
|
||||
mes = 1
|
||||
ano += 1
|
||||
return fator
|
||||
|
||||
# 3. Buscar fórmulas exatas por nome
|
||||
formula_pis_result = await session.execute(
|
||||
select(ParametrosFormula.formula).where(
|
||||
ParametrosFormula.nome == "Cálculo PIS sobre ICMS",
|
||||
ParametrosFormula.ativo == True
|
||||
).limit(1)
|
||||
)
|
||||
formula_cofins_result = await session.execute(
|
||||
select(ParametrosFormula.formula).where(
|
||||
ParametrosFormula.nome == "Cálculo COFINS sobre ICMS",
|
||||
ParametrosFormula.ativo == True
|
||||
).limit(1)
|
||||
)
|
||||
|
||||
formula_pis = formula_pis_result.scalar_one_or_none()
|
||||
formula_cofins = formula_cofins_result.scalar_one_or_none()
|
||||
|
||||
# 4. Montar dados
|
||||
mes_map = {
|
||||
'JAN': 1, 'FEV': 2, 'MAR': 3, 'ABR': 4, 'MAI': 5, 'JUN': 6,
|
||||
'JUL': 7, 'AGO': 8, 'SET': 9, 'OUT': 10, 'NOV': 11, 'DEZ': 12
|
||||
}
|
||||
|
||||
# 2) Montar dados conforme 'tipo'
|
||||
dados = []
|
||||
|
||||
if tipo == "aliquota_icms":
|
||||
# Campos: (Cliente, UC, Referência, Valor Total, ICMS (%), ICMS (R$),
|
||||
# Base ICMS (R$), Consumo (kWh), Tarifa, Nota Fiscal)
|
||||
for f in faturas:
|
||||
try:
|
||||
if "/" in f.referencia:
|
||||
mes_str, ano_str = f.referencia.split("/")
|
||||
mes = mes_map.get(mes_str.strip().upper())
|
||||
ano = int(ano_str)
|
||||
if not mes or not ano:
|
||||
raise ValueError("Mês ou ano inválido")
|
||||
else:
|
||||
ano = int(f.referencia[:4])
|
||||
mes = int(f.referencia[4:])
|
||||
|
||||
fator = calcular_fator_selic(ano, mes)
|
||||
periodo = f"{mes:02d}/{ano} à {hoje.month:02d}/{hoje.year}"
|
||||
|
||||
contexto = f.__dict__
|
||||
valor_pis_icms = avaliar_formula(formula_pis, contexto) if formula_pis else None
|
||||
valor_cofins_icms = avaliar_formula(formula_cofins, contexto) if formula_cofins else None
|
||||
dados.append({
|
||||
"Nome": f.nome,
|
||||
"Cliente": f.nome,
|
||||
"UC": f.unidade_consumidora,
|
||||
"Referência": f.referencia,
|
||||
"Valor Total": f.valor_total,
|
||||
"ICMS (%)": f.icms_aliq,
|
||||
"ICMS (R$)": f.icms_valor,
|
||||
"Base ICMS (R$)": f.icms_base,
|
||||
"Consumo (kWh)": f.consumo,
|
||||
"Tarifa": f.tarifa,
|
||||
"Nota Fiscal": f.nota_fiscal,
|
||||
})
|
||||
filename = "relatorio_aliquota_icms.xlsx"
|
||||
|
||||
elif tipo == "exclusao_icms":
|
||||
# Campos: (Cliente, UC, Referência, Valor Total, PIS (%), ICMS (%), COFINS (%),
|
||||
# PIS (R$), ICMS (R$), COFINS (R$), Base PIS (R$), Base ICMS (R$),
|
||||
# Base COFINS (R$), Consumo (kWh), Tarifa, Nota Fiscal)
|
||||
for f in faturas:
|
||||
dados.append({
|
||||
"Cliente": f.nome,
|
||||
"UC": f.unidade_consumidora,
|
||||
"Referência": f.referencia,
|
||||
"Valor Total": f.valor_total,
|
||||
"PIS (%)": f.pis_aliq,
|
||||
"ICMS (%)": f.icms_aliq,
|
||||
"COFINS (%)": f.cofins_aliq,
|
||||
"PIS (R$)": f.pis_valor,
|
||||
"ICMS (R$)": f.icms_valor,
|
||||
"COFINS (R$)": f.cofins_valor,
|
||||
"Base PIS (R$)": f.pis_base,
|
||||
"Base ICMS (R$)": f.icms_base,
|
||||
"Base COFINS (R$)": f.cofins_base,
|
||||
"Consumo (kWh)": f.consumo,
|
||||
"Tarifa": f.tarifa,
|
||||
"Nota Fiscal": f.nota_fiscal,
|
||||
})
|
||||
filename = "relatorio_exclusao_icms.xlsx"
|
||||
|
||||
else: # "geral" (mantém seu relatório atual — sem fórmulas SELIC)
|
||||
# Se quiser manter exatamente o que já tinha com SELIC e fórmulas,
|
||||
# você pode copiar sua lógica anterior aqui. Abaixo deixo um "geral"
|
||||
# simplificado com as colunas principais.
|
||||
for f in faturas:
|
||||
dados.append({
|
||||
"Cliente": f.nome,
|
||||
"UC": f.unidade_consumidora,
|
||||
"Referência": f.referencia,
|
||||
"Nota Fiscal": f.nota_fiscal,
|
||||
"Valor Total": f.valor_total,
|
||||
"ICMS (%)": f.icms_aliq,
|
||||
"ICMS (R$)": f.icms_valor,
|
||||
"Base ICMS": f.icms_base,
|
||||
"Base ICMS (R$)": f.icms_base,
|
||||
"PIS (%)": f.pis_aliq,
|
||||
"PIS (R$)": f.pis_valor,
|
||||
"Base PIS": f.pis_base,
|
||||
"Base PIS (R$)": f.pis_base,
|
||||
"COFINS (%)": f.cofins_aliq,
|
||||
"COFINS (R$)": f.cofins_valor,
|
||||
"Base COFINS": f.cofins_base,
|
||||
"Base COFINS (R$)": f.cofins_base,
|
||||
"Consumo (kWh)": f.consumo,
|
||||
"Tarifa": f.tarifa,
|
||||
"Cidade": f.cidade,
|
||||
"Estado": f.estado,
|
||||
"Distribuidora": f.distribuidora,
|
||||
"Data Processamento": f.data_processamento,
|
||||
"Fator SELIC acumulado": fator,
|
||||
"Período SELIC usado": periodo,
|
||||
"PIS sobre ICMS": valor_pis_icms,
|
||||
"Valor Corrigido PIS (ICMS)": valor_pis_icms * fator if valor_pis_icms else None,
|
||||
"COFINS sobre ICMS": valor_cofins_icms,
|
||||
"Valor Corrigido COFINS (ICMS)": valor_cofins_icms * fator if valor_cofins_icms else None,
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Erro ao processar fatura {f.nota_fiscal}: {e}")
|
||||
|
||||
df = pd.DataFrame(dados)
|
||||
filename = "relatorio_geral.xlsx"
|
||||
|
||||
# 3) Gerar excel em memória
|
||||
from io import BytesIO
|
||||
output = BytesIO()
|
||||
df = pd.DataFrame(dados)
|
||||
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
|
||||
df.to_excel(writer, index=False, sheet_name="Faturas Corrigidas")
|
||||
|
||||
df.to_excel(writer, index=False, sheet_name="Relatório")
|
||||
output.seek(0)
|
||||
return StreamingResponse(output, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={
|
||||
"Content-Disposition": "attachment; filename=faturas_corrigidas.xlsx"
|
||||
})
|
||||
|
||||
# 4) Responder
|
||||
return StreamingResponse(
|
||||
output,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename}"'}
|
||||
)
|
||||
|
||||
|
||||
from app.parametros import router as parametros_router
|
||||
@@ -474,3 +531,70 @@ async def limpar_erros():
|
||||
caminho = os.path.join(pasta, nome)
|
||||
if os.path.exists(caminho):
|
||||
os.remove(caminho)
|
||||
|
||||
@app.get("/api/clientes")
|
||||
async def listar_clientes(db: AsyncSession = Depends(get_session)):
|
||||
sql = text("""
|
||||
SELECT id, nome_fantasia, cnpj, ativo
|
||||
FROM faturas.clientes
|
||||
WHERE ativo = TRUE
|
||||
ORDER BY nome_fantasia
|
||||
""")
|
||||
res = await db.execute(sql)
|
||||
rows = res.mappings().all()
|
||||
return [
|
||||
{
|
||||
"id": str(r["id"]),
|
||||
"nome_fantasia": r["nome_fantasia"],
|
||||
"cnpj": r["cnpj"],
|
||||
"ativo": bool(r["ativo"]),
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
@app.get("/api/relatorios")
|
||||
async def api_relatorios(
|
||||
cliente: str | None = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=5, le=200),
|
||||
db: AsyncSession = Depends(get_session),
|
||||
):
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
where = "WHERE cliente_id = :cliente" if cliente else ""
|
||||
params = {"limit": page_size, "offset": offset}
|
||||
if cliente:
|
||||
params["cliente"] = cliente
|
||||
|
||||
sql = text(f"""
|
||||
SELECT id, nome, unidade_consumidora, referencia, nota_fiscal,
|
||||
valor_total, icms_aliq, icms_valor, pis_aliq, pis_valor,
|
||||
cofins_aliq, cofins_valor, distribuidora, data_processamento
|
||||
FROM faturas.faturas
|
||||
{where}
|
||||
ORDER BY data_processamento DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
""")
|
||||
count_sql = text(f"SELECT COUNT(*) AS total FROM faturas.faturas {where}")
|
||||
|
||||
rows = (await db.execute(sql, params)).mappings().all()
|
||||
total = (await db.execute(count_sql, params)).scalar_one()
|
||||
|
||||
items = [{
|
||||
"id": str(r["id"]),
|
||||
"nome": r["nome"],
|
||||
"unidade_consumidora": r["unidade_consumidora"],
|
||||
"referencia": r["referencia"],
|
||||
"nota_fiscal": r["nota_fiscal"],
|
||||
"valor_total": float(r["valor_total"]) if r["valor_total"] is not None else None,
|
||||
"icms_aliq": r["icms_aliq"],
|
||||
"icms_valor": r["icms_valor"],
|
||||
"pis_aliq": r["pis_aliq"],
|
||||
"pis_valor": r["pis_valor"],
|
||||
"cofins_aliq": r["cofins_aliq"],
|
||||
"cofins_valor": r["cofins_valor"],
|
||||
"distribuidora": r["distribuidora"],
|
||||
"data_processamento": r["data_processamento"].isoformat() if r["data_processamento"] else None,
|
||||
} for r in rows]
|
||||
|
||||
return {"items": items, "total": total, "page": page, "page_size": page_size}
|
||||
@@ -1,5 +1,5 @@
|
||||
# 📄 models.py
|
||||
from sqlalchemy import Column, String, Integer, Float, DateTime, Text
|
||||
from sqlalchemy import Column, String, Integer, Float, DateTime, Text, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
@@ -50,6 +50,8 @@ class Fatura(Base):
|
||||
estado = Column(String)
|
||||
distribuidora = Column(String)
|
||||
link_arquivo = Column("link_arquivo", String)
|
||||
cliente_id = Column(UUID(as_uuid=True), ForeignKey("faturas.clientes.id"), nullable=False)
|
||||
|
||||
|
||||
|
||||
class LogProcessamento(Base):
|
||||
|
||||
@@ -93,8 +93,9 @@ async def editar_parametro(param_id: int, request: Request):
|
||||
async with AsyncSessionLocal() as session:
|
||||
param = await session.get(ParametrosFormula, param_id)
|
||||
if param:
|
||||
param.tipo = data.get("tipo", param.tipo)
|
||||
param.nome = data.get("nome", param.nome)
|
||||
param.formula = data.get("formula", param.formula)
|
||||
param.ativo = data.get("ativo", param.ativo)
|
||||
await session.commit()
|
||||
return {"success": True}
|
||||
return {"success": False, "error": "Não encontrado"}
|
||||
@@ -140,21 +141,21 @@ async def adicionar_aliquota(aliq: AliquotaUFSchema, db: AsyncSession = Depends(
|
||||
|
||||
@router.get("/parametros/formulas", response_model=List[ParametrosFormulaSchema])
|
||||
async def listar_formulas(db: AsyncSession = Depends(get_session)):
|
||||
result = await db.execute(select(ParametrosFormula).order_by(ParametrosFormula.tipo))
|
||||
result = await db.execute(select(ParametrosFormula).order_by(ParametrosFormula.nome))
|
||||
return result.scalars().all()
|
||||
|
||||
@router.post("/parametros/formulas")
|
||||
async def salvar_formula(form: ParametrosFormulaSchema, db: AsyncSession = Depends(get_session)):
|
||||
result = await db.execute(
|
||||
select(ParametrosFormula).filter_by(tipo=form.tipo)
|
||||
select(ParametrosFormula).filter_by(nome=form.nome)
|
||||
)
|
||||
existente = result.scalar_one_or_none()
|
||||
|
||||
if existente:
|
||||
existente.formula = form.formula
|
||||
existente.campos = form.campos
|
||||
existente.ativo = form.ativo
|
||||
else:
|
||||
novo = ParametrosFormula(**form.dict())
|
||||
novo = ParametrosFormula(nome=form.nome, formula=form.formula, ativo=form.ativo)
|
||||
db.add(novo)
|
||||
|
||||
await db.commit()
|
||||
|
||||
@@ -56,7 +56,7 @@ def salvar_em_uploads(caminho_pdf_temp, nome_original, nota_fiscal):
|
||||
logger.error(f"Erro ao salvar em uploads: {e}")
|
||||
return caminho_pdf_temp
|
||||
|
||||
async def process_single_file(caminho_pdf_temp: str, nome_original: str):
|
||||
async def process_single_file(caminho_pdf_temp: str, nome_original: str, cliente_id: str | None = None):
|
||||
inicio = time.perf_counter()
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
@@ -89,6 +89,9 @@ async def process_single_file(caminho_pdf_temp: str, nome_original: str):
|
||||
dados['link_arquivo'] = caminho_final
|
||||
|
||||
# Salva fatura
|
||||
dados['cliente_id'] = cliente_id
|
||||
if cliente_id:
|
||||
dados['cliente_id'] = cliente_id
|
||||
fatura = Fatura(**dados)
|
||||
session.add(fatura)
|
||||
|
||||
@@ -125,15 +128,36 @@ async def processar_em_lote():
|
||||
while not fila_processamento.empty():
|
||||
item = await fila_processamento.get()
|
||||
try:
|
||||
resultado = await process_single_file(item['caminho_pdf'], item['nome_original'])
|
||||
resultado = await process_single_file(
|
||||
item['caminho_pdf'],
|
||||
item['nome_original'],
|
||||
item.get('cliente_id')
|
||||
)
|
||||
# tentar tamanho/data do TEMP; se não existir mais, tenta do destino final; senão, 0/""
|
||||
temp_path = item['caminho_pdf']
|
||||
dest_path = (resultado.get("dados") or {}).get("link_arquivo", "")
|
||||
|
||||
def _safe_size(p):
|
||||
try:
|
||||
return os.path.getsize(p) // 1024
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def _safe_mtime(p):
|
||||
try:
|
||||
return time.strftime("%d/%m/%Y", time.localtime(os.path.getmtime(p)))
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
status_arquivos[item['nome_original']] = {
|
||||
"status": resultado.get("status"),
|
||||
"mensagem": resultado.get("mensagem", ""),
|
||||
"tempo": resultado.get("tempo", "---"),
|
||||
"tamanho": os.path.getsize(item['caminho_pdf']) // 1024, # tamanho em KB
|
||||
"data": time.strftime("%d/%m/%Y", time.localtime(os.path.getmtime(item['caminho_pdf'])))
|
||||
"tamanho": _safe_size(temp_path) or _safe_size(dest_path),
|
||||
"data": _safe_mtime(temp_path) or _safe_mtime(dest_path),
|
||||
}
|
||||
|
||||
|
||||
resultados.append(status_arquivos[item['nome_original']])
|
||||
except Exception as e:
|
||||
status_arquivos[item['nome_original']] = {
|
||||
@@ -156,16 +180,20 @@ async def processar_em_lote():
|
||||
erros_txt.append(f"{nome} - {status.get('mensagem', 'Erro desconhecido')}")
|
||||
|
||||
if erros_txt:
|
||||
with open(os.path.join(UPLOADS_DIR, "erros", "erros.txt"), "w", encoding="utf-8") as f:
|
||||
erros_dir = os.path.join(UPLOADS_DIR, "erros")
|
||||
os.makedirs(erros_dir, exist_ok=True) # <- GARANTE A PASTA
|
||||
|
||||
with open(os.path.join(erros_dir, "erros.txt"), "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(erros_txt))
|
||||
|
||||
# Compacta PDFs com erro
|
||||
with ZipFile(os.path.join(UPLOADS_DIR, "erros", "faturas_erro.zip"), "w") as zipf:
|
||||
with ZipFile(os.path.join(erros_dir, "faturas_erro.zip"), "w") as zipf:
|
||||
for nome in status_arquivos:
|
||||
if status_arquivos[nome]['status'] == 'Erro':
|
||||
caminho = os.path.join(UPLOADS_DIR, "temp", nome)
|
||||
if os.path.exists(caminho):
|
||||
zipf.write(caminho, arcname=nome)
|
||||
|
||||
return resultados
|
||||
|
||||
def limpar_arquivos_processados():
|
||||
@@ -192,6 +220,6 @@ async def garantir_selic_para_competencia(session, ano, mes):
|
||||
|
||||
if dados:
|
||||
percentual = float(dados[0]["valor"].replace(",", "."))
|
||||
novo = SelicMensal(ano=ano, mes=mes, fator=percentual)
|
||||
novo = SelicMensal(ano=ano, mes=mes, percentual=percentual)
|
||||
session.add(novo)
|
||||
await session.commit()
|
||||
@@ -100,10 +100,10 @@
|
||||
<form method="get" action="/" style="margin: 6px 0 18px">
|
||||
<div class="combo-wrap">
|
||||
<label for="cliente" style="font-size:13px;color:#374151">Selecionar Cliente:</label>
|
||||
<select class="combo" name="cliente" id="cliente">
|
||||
<select id="cliente" name="cliente" class="combo">
|
||||
<option value="">Todos</option>
|
||||
{% for c in clientes %}
|
||||
<option value="{{ c }}" {% if c == cliente_atual %}selected{% endif %}>{{ c }}</option>
|
||||
<option value="{{ c.id }}" {% if cliente_selecionado == c.id %}selected{% endif %}>{{ c.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -3,43 +3,227 @@
|
||||
{% block content %}
|
||||
<h1>📊 Relatórios</h1>
|
||||
|
||||
<form method="get" style="margin-bottom: 20px;">
|
||||
<label for="cliente">Filtrar por Cliente:</label>
|
||||
<select name="cliente" id="cliente" onchange="this.form.submit()">
|
||||
<div style="display:flex; gap:16px; align-items:flex-end; flex-wrap:wrap; margin: 6px 0 18px;">
|
||||
<div class="combo-wrap">
|
||||
<label for="relatorio-cliente" style="font-size:13px;color:#374151">Selecionar Cliente:</label>
|
||||
<select id="relatorio-cliente" class="combo" style="min-width:340px;">
|
||||
<option value="">Todos</option>
|
||||
{% for c in clientes %}
|
||||
<option value="{{ c }}" {% if c == cliente_atual %}selected{% endif %}>{{ c }}</option>
|
||||
<option value="{{ c.id }}">{{ c.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<a href="/export-excel{% if cliente_atual %}?cliente={{ cliente_atual }}{% endif %}" class="btn btn-primary">
|
||||
📥 Baixar Relatório Corrigido (Excel)
|
||||
</a>
|
||||
<div class="combo-wrap">
|
||||
<label for="tipo-relatorio" style="font-size:13px;color:#374151">Tipo de relatório:</label>
|
||||
<select id="tipo-relatorio" class="combo" style="min-width:240px;">
|
||||
<option value="geral">1. Geral</option>
|
||||
<option value="exclusao_icms">2. Exclusão do ICMS</option>
|
||||
<option value="aliquota_icms">3. Alíquota ICMS (%)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="combo-wrap">
|
||||
<label for="page-size" style="font-size:13px;color:#374151">Itens por página:</label>
|
||||
<select id="page-size" class="combo" style="width:140px;">
|
||||
<option>20</option>
|
||||
<option>50</option>
|
||||
<option>100</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a id="link-excel" class="btn btn-primary" href="/export-excel">📥 Baixar (Excel)</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr style="background: #2563eb; color: white;">
|
||||
<th style="padding: 10px;">Cliente</th>
|
||||
<th>Data</th>
|
||||
<tr>
|
||||
<th>Cliente</th>
|
||||
<th>UC</th>
|
||||
<th>Referência</th>
|
||||
<th>Nota Fiscal</th>
|
||||
<th>Valor Total</th>
|
||||
<th>ICMS na Base</th>
|
||||
<th>Status</th>
|
||||
<th>ICMS (%)</th>
|
||||
<th>ICMS (R$)</th>
|
||||
<th>PIS (R$)</th>
|
||||
<th>COFINS (R$)</th>
|
||||
<th>Distribuidora</th>
|
||||
<th>Processado em</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody id="relatorios-body">
|
||||
{% for f in faturas %}
|
||||
<tr style="background: {{ loop.cycle('#ffffff', '#f0f4f8') }};">
|
||||
<td style="padding: 10px;">{{ f.nome }}</td>
|
||||
<td>{{ f.data_emissao }}</td>
|
||||
<td>R$ {{ '%.2f'|format(f.valor_total)|replace('.', ',') }}</td>
|
||||
<td>{{ 'Sim' if f.com_icms else 'Não' }}</td>
|
||||
<td>{{ f.status }}</td>
|
||||
<tr>
|
||||
<td>{{ f.nome }}</td>
|
||||
<td class="mono">{{ f.unidade_consumidora }}</td>
|
||||
<td class="mono">{{ f.referencia }}</td>
|
||||
<td class="mono">{{ f.nota_fiscal }}</td>
|
||||
<td>R$ {{ '%.2f'|format((f.valor_total or 0.0))|replace('.', ',') }}</td>
|
||||
<td>{{ '%.2f'|format((f.icms_aliq or 0.0))|replace('.', ',') }}</td>
|
||||
<td>R$ {{ '%.2f'|format((f.icms_valor or 0.0))|replace('.', ',') }}</td>
|
||||
<td>R$ {{ '%.2f'|format((f.pis_valor or 0.0))|replace('.', ',') }}</td>
|
||||
<td>R$ {{ '%.2f'|format((f.cofins_valor or 0.0))|replace('.', ',') }}</td>
|
||||
<td>{{ f.distribuidora or '-' }}</td>
|
||||
<td class="muted">{{ f.data_processamento }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="pager" style="display:flex; align-items:center; justify-content:space-between; gap:12px; margin-top:12px;">
|
||||
<div id="range" class="muted">Mostrando 0–0 de 0</div>
|
||||
<div style="display:flex; gap:8px;">
|
||||
<button id="prev" class="btn btn-primary">◀ Anterior</button>
|
||||
<button id="next" class="btn btn-primary">Próxima ▶</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
.combo {
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
padding: 12px 44px 12px 14px;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
color: #111827;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,.06);
|
||||
transition: box-shadow .2s ease, border-color .2s ease, transform .2s ease;
|
||||
}
|
||||
.combo:focus { outline: none; border-color: #2563eb; box-shadow: 0 8px 28px rgba(37,99,235,.18); }
|
||||
.combo-wrap { position: relative; display: inline-flex; align-items: center; gap: 8px; }
|
||||
.combo-wrap:after {
|
||||
content: "▾"; position: absolute; right: 12px; pointer-events: none; color:#6b7280; font-size: 12px;
|
||||
}
|
||||
|
||||
/* tabela no estilo “clientes” */
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 12px 34px rgba(0,0,0,.06);
|
||||
}
|
||||
.table thead th {
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
padding: 12px 14px;
|
||||
text-align: left;
|
||||
}
|
||||
.table tbody td {
|
||||
border-top: 1px solid #eef2f7;
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
.table tbody tr:nth-child(odd){ background:#fafafa; }
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }
|
||||
.muted { color:#6b7280; }
|
||||
|
||||
#pager .btn[disabled]{ opacity:.5; cursor:not-allowed; }
|
||||
</style>
|
||||
<script>
|
||||
let page = 1;
|
||||
let pageSize = 20;
|
||||
let total = 0;
|
||||
|
||||
function updateExcelLink() {
|
||||
const cliente = document.getElementById('relatorio-cliente').value || '';
|
||||
const tipo = document.getElementById('tipo-relatorio').value || 'geral';
|
||||
const params = new URLSearchParams();
|
||||
params.set('tipo', tipo);
|
||||
if (cliente) params.set('cliente', cliente);
|
||||
document.getElementById('link-excel').setAttribute('href', `/export-excel?${params.toString()}`);
|
||||
}
|
||||
|
||||
async function carregarTabela() {
|
||||
const cliente = document.getElementById('relatorio-cliente').value || '';
|
||||
const url = new URL('/api/relatorios', window.location.origin);
|
||||
url.searchParams.set('page', page);
|
||||
url.searchParams.set('page_size', pageSize);
|
||||
if (cliente) url.searchParams.set('cliente', cliente);
|
||||
|
||||
const r = await fetch(url);
|
||||
const data = await r.json();
|
||||
|
||||
total = data.total;
|
||||
renderRows(data.items);
|
||||
updatePager();
|
||||
updateExcelLink();
|
||||
}
|
||||
|
||||
function renderRows(items) {
|
||||
const tbody = document.getElementById('relatorios-body');
|
||||
if (!items.length) {
|
||||
tbody.innerHTML = `<tr><td colspan="11" style="padding:14px;">Nenhum registro encontrado.</td></tr>`;
|
||||
return;
|
||||
}
|
||||
const fmtBRL = (v) => (v || v === 0) ? Number(v).toLocaleString('pt-BR',{style:'currency',currency:'BRL'}) : '';
|
||||
const fmtNum = (v) => (v || v === 0) ? Number(v).toLocaleString('pt-BR') : '';
|
||||
const fmtDate = (iso) => iso ? new Date(iso).toLocaleString('pt-BR') : '';
|
||||
|
||||
tbody.innerHTML = items.map(f => `
|
||||
<tr>
|
||||
<td>${f.nome || ''}</td>
|
||||
<td class="mono">${f.unidade_consumidora || ''}</td>
|
||||
<td class="mono">${f.referencia || ''}</td>
|
||||
<td class="mono">${f.nota_fiscal || ''}</td>
|
||||
<td>${fmtBRL(f.valor_total)}</td>
|
||||
<td>${fmtNum(f.icms_aliq)}</td>
|
||||
<td>${fmtBRL(f.icms_valor)}</td>
|
||||
<td>${fmtBRL(f.pis_valor)}</td>
|
||||
<td>${fmtBRL(f.cofins_valor)}</td>
|
||||
<td>${f.distribuidora || '-'}</td>
|
||||
<td class="muted">${fmtDate(f.data_processamento)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function updatePager() {
|
||||
const start = total ? (page - 1) * pageSize + 1 : 0;
|
||||
const end = Math.min(page * pageSize, total);
|
||||
document.getElementById('range').textContent = `Mostrando ${start}–${end} de ${total}`;
|
||||
document.getElementById('prev').disabled = page <= 1;
|
||||
document.getElementById('next').disabled = page * pageSize >= total;
|
||||
}
|
||||
|
||||
document.getElementById('prev').addEventListener('click', () => {
|
||||
if (page > 1) { page--; carregarTabela(); }
|
||||
});
|
||||
document.getElementById('next').addEventListener('click', () => {
|
||||
if (page * pageSize < total) { page++; carregarTabela(); }
|
||||
});
|
||||
document.getElementById('page-size').addEventListener('change', (e) => {
|
||||
pageSize = parseInt(e.target.value, 10);
|
||||
page = 1;
|
||||
carregarTabela();
|
||||
// não precisa alterar o link aqui
|
||||
});
|
||||
document.getElementById('relatorio-cliente').addEventListener('change', () => {
|
||||
page = 1;
|
||||
carregarTabela();
|
||||
updateExcelLink();
|
||||
});
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const pre = "{{ cliente_selecionado or '' }}";
|
||||
if (pre) document.getElementById('relatorio-cliente').value = pre;
|
||||
updateExcelLink();
|
||||
carregarTabela();
|
||||
});
|
||||
|
||||
document.getElementById('tipo-relatorio').addEventListener('change', () => {
|
||||
updateExcelLink();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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,7 +52,33 @@ ar
|
||||
|
||||
const fileTable = document.getElementById('file-table');
|
||||
|
||||
function handleFiles(files) {
|
||||
|
||||
// <<< 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.";
|
||||
@@ -53,8 +88,13 @@ function handleFiles(files) {
|
||||
}
|
||||
|
||||
arquivos = [...arquivos, ...files];
|
||||
|
||||
// trava o combo após começar a anexar (opcional)
|
||||
document.getElementById('select-cliente').disabled = true;
|
||||
|
||||
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() {
|
||||
function limpar() {
|
||||
fetch("/clear-all", { method: "POST" });
|
||||
|
||||
// reset da fila/estado
|
||||
arquivos = [];
|
||||
processado = false;
|
||||
document.getElementById("file-input").value = null;
|
||||
renderTable();
|
||||
processamentoFinalizado = false;
|
||||
if (statusInterval) { clearInterval(statusInterval); statusInterval = null; }
|
||||
|
||||
// limpa feedback visual também
|
||||
// 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 = "";
|
||||
document.getElementById("tabela-wrapper").classList.remove("bloqueada");
|
||||
processamentoFinalizado = false;
|
||||
document.getElementById("btn-selecionar").disabled = false;
|
||||
|
||||
}
|
||||
// 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,10 +326,14 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
window.addEventListener("drop", e => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user