"""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'{esc(status)}' 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}> {esc(label)} {esc(value)} {esc(hint)} """ 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'
{esc(title)}
{esc(detail)}
'