117 lines
4.2 KiB
Python
117 lines
4.2 KiB
Python
"""Reusable HTML helpers for the admin UI refactor.
|
|
|
|
Keep these helpers dependency-light so pages can migrate from
|
|
`admin_dashboard.py` gradually.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import html
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
from typing import Any
|
|
|
|
|
|
def esc(value: Any) -> str:
|
|
return html.escape("" if value is None else str(value))
|
|
|
|
|
|
def money(value: Any, currency: str = "EUR") -> str:
|
|
try:
|
|
amount = Decimal(str(value or "0"))
|
|
return f"{amount:,.2f} {currency}".replace(",", "X").replace(".", ",").replace("X", ".")
|
|
except Exception:
|
|
return f"{esc(value)} {esc(currency)}"
|
|
|
|
|
|
def fmt_dt(value: Any) -> str:
|
|
if not value:
|
|
return "—"
|
|
if isinstance(value, datetime):
|
|
return value.strftime("%Y-%m-%d %H:%M")
|
|
return esc(value)
|
|
|
|
|
|
def status_chip(status: str | None) -> str:
|
|
status = (status or "unknown").lower()
|
|
css = {
|
|
"pending": "cf-chip-orange",
|
|
"processing": "cf-chip-purple",
|
|
"sent": "cf-chip-green",
|
|
"failed": "cf-chip-red",
|
|
"blocked": "cf-chip-red",
|
|
"dry_run": "cf-chip-gray",
|
|
"ignored": "cf-chip-gray",
|
|
"cancelled": "cf-chip-gray",
|
|
"created": "cf-chip-blue",
|
|
"issued": "cf-chip-green",
|
|
"converted": "cf-chip-green",
|
|
}.get(status, "cf-chip-gray")
|
|
return f'<span class="cf-chip {css}">{esc(status)}</span>'
|
|
|
|
|
|
def kpi_icon_for(label: str) -> str:
|
|
text = (label or "").lower()
|
|
if "oportun" in text or "pipeline" in text:
|
|
return "bi-funnel"
|
|
if "valor" in text or "pagamento" in text or "finance" in text or "fatura" in text:
|
|
return "bi-currency-euro"
|
|
if "document" in text or "orçamento" in text or "orcamento" in text:
|
|
return "bi-file-earmark-text"
|
|
if "task" in text or "tarefa" in text or "pendente" in text:
|
|
return "bi-list-check"
|
|
if "email" in text or "comunica" in text or ("cliente" in text and "sem" in text):
|
|
return "bi-envelope"
|
|
if "erro" in text or "invál" in text or "inval" in text or "bloque" in text:
|
|
return "bi-exclamation-triangle"
|
|
if "envio" in text or "packlink" in text or "opera" in text or "prepar" in text:
|
|
return "bi-truck"
|
|
if "cliente" in text:
|
|
return "bi-person-vcard"
|
|
if "produto" in text or "artigo" in text or "categoria" in text or "jasmin" in text:
|
|
return "bi-box-seam"
|
|
if "integra" in text or "sistema" in text or "ambiente" in text or "estado" in text:
|
|
return "bi-puzzle"
|
|
if "retry" in text or "retries" in text:
|
|
return "bi-arrow-clockwise"
|
|
return "bi-grid"
|
|
|
|
|
|
def kpi_tone_for(label: str, hint: str = "") -> str:
|
|
text = f"{label or ''} {hint or ''}".lower()
|
|
if "erro" in text or "falh" in text or "invál" in text or "inval" in text:
|
|
return "cf-kpi-tone-red"
|
|
if "pagamento" in text or "valor" in text or "finance" in text or "€" in text:
|
|
return "cf-kpi-tone-green"
|
|
if "envio" in text or "packlink" in text or "pendente" in text or "aguarda" in text or "incompleto" in text or "bloque" in text:
|
|
return "cf-kpi-tone-orange"
|
|
if "cliente" in text or "comunica" in text or "email" in text:
|
|
return "cf-kpi-tone-purple"
|
|
return ""
|
|
|
|
|
|
def kpi_card(label: str, value: object, href: str | None = None, hint: str = "", icon: str | None = None, tone: str | None = None) -> str:
|
|
tag = "a" if href else "div"
|
|
href_attr = f' href="{esc(href)}"' if href else ""
|
|
reset_class = " text-reset text-decoration-none" if href else ""
|
|
icon_class = icon or kpi_icon_for(label)
|
|
tone_class = tone or kpi_tone_for(label, hint)
|
|
return f"""
|
|
<{tag} class="cf-card cf-kpi{reset_class} {esc(tone_class)}"{href_attr}>
|
|
<span class="cf-kpi-icon" aria-hidden="true"><i class="bi {esc(icon_class)}"></i></span>
|
|
<span>{esc(label)}</span>
|
|
<strong>{esc(value)}</strong>
|
|
<small>{esc(hint)}</small>
|
|
</{tag}>
|
|
"""
|
|
|
|
|
|
def alert_box(title: str, detail: str, tone: str = "warning") -> str:
|
|
css = {
|
|
"danger": "alert-danger",
|
|
"warning": "alert-warning",
|
|
"info": "alert-info",
|
|
"success": "alert-success",
|
|
}.get(tone, "alert-warning")
|
|
return f'<div class="alert {css} border-0"><strong>{esc(title)}</strong><div>{esc(detail)}</div></div>'
|
|
|