Files
clientflow_backend/app/admin_ui/components.py
2026-06-09 22:55:58 +01:00

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>'