"use client"; // Primitives partagées du tableau de bord d'administration : aides de format, // récupération JSON, tuiles de stats, interrupteur accessible, graphiques en barres maison. import { useCallback, useEffect, useState, type ReactNode } from "react"; import { AlertTriangle } from "lucide-react"; import { Button, Card, cn } from "@/components/ui"; // ---------- Formats (fr-CA) ---------- export function fmtInt(n: number | null | undefined): string { return (n ?? 0).toLocaleString("fr-CA"); } export function fmtUSD(n: number | null | undefined, digits = 2): string { const v = n ?? 0; return `${v.toLocaleString("fr-CA", { minimumFractionDigits: digits, maximumFractionDigits: digits })} $`; } /** SQLite stocke datetime('now') en UTC sans fuseau — on normalise avant d'afficher. */ export function fmtDate(s: string | null | undefined, withTime = true): string { if (!s) return "—"; const iso = s.includes("T") ? s : s.replace(" ", "T") + "Z"; const d = new Date(iso); if (Number.isNaN(d.getTime())) return s; return d.toLocaleString("fr-CA", withTime ? { dateStyle: "medium", timeStyle: "short" } : { dateStyle: "medium" }); } // ---------- Récupération JSON ---------- export async function postJson(url: string, body?: unknown, method = "POST"): Promise { const res = await fetch(url, { method, headers: body !== undefined ? { "Content-Type": "application/json" } : undefined, body: body !== undefined ? JSON.stringify(body) : undefined, }); const j = (await res.json().catch(() => null)) as { error?: string } | null; if (!res.ok) throw new Error(j?.error || `Erreur ${res.status}`); return j as T; } export function useFetchJson(url: string) { const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const reload = useCallback(async () => { setLoading(true); setError(null); try { const res = await fetch(url); const j = (await res.json().catch(() => null)) as (T & { error?: string }) | null; if (!res.ok || !j) throw new Error(j?.error || `Erreur ${res.status}`); setData(j); } catch (e) { setError(e instanceof Error ? e.message : "Erreur inattendue."); } finally { setLoading(false); } }, [url]); useEffect(() => { reload(); }, [reload]); return { data, error, loading, reload, setData }; } // ---------- Bannière d'erreur ---------- export function ErrorBanner({ message, onRetry }: { message: string; onRetry?: () => void }) { return (
{message} {onRetry && ( )}
); } // ---------- Message de succès transitoire ---------- export function SuccessFlash({ message }: { message: string | null }) { if (!message) return null; return (
{message}
); } // ---------- Tuile de statistique ---------- export function StatTile({ label, value, sub, tone = "default", }: { label: string; value: ReactNode; sub?: ReactNode; tone?: "default" | "red" | "amber" | "green"; }) { const tones = { default: "text-fg", red: "text-red-600 dark:text-red-400", amber: "text-amber-600 dark:text-amber-400", green: "text-emerald-600 dark:text-emerald-400", }; return (

{label}

{value}

{sub &&

{sub}

}
); } // ---------- Interrupteur accessible ---------- export function Toggle({ checked, onChange, disabled, label, }: { checked: boolean; onChange: (v: boolean) => void; disabled?: boolean; label?: string; }) { return ( ); } // ---------- Graphique en barres vertical (CSS, sans dépendance) ---------- export type BarDatum = { label: string; value: number; hint?: string }; export function BarChart({ data, height = 150, barClass = "bg-brand-500", formatValue = (v: number) => fmtInt(v), emptyLabel = "Aucune donnée sur la période.", }: { data: BarDatum[]; height?: number; barClass?: string; formatValue?: (v: number) => string; emptyLabel?: string; }) { const max = Math.max(...data.map((d) => d.value), 0); if (data.length === 0 || max <= 0) { return

{emptyLabel}

; } return (
{data.map((d, i) => (
0 ? barClass : "bg-surface-3 dark:bg-brand-900/60")} style={{ height: d.value > 0 ? `${Math.max(3, (d.value / max) * 100)}%` : "2px" }} />
{d.label}{" "} {formatValue(d.value)} {d.hint && · {d.hint}}
))}
{data[0]?.label} {data[data.length - 1]?.label}
); } // ---------- Barre horizontale (maîtrise, erreurs récurrentes) ---------- export function HBarRow({ label, meta, value, display, barClass = "bg-brand-500", muted = false, }: { label: ReactNode; meta?: ReactNode; value: number; // 0..1 display: ReactNode; barClass?: string; muted?: boolean; }) { return (
{label} {display}
{meta}
); } // ---------- En-tête de section ---------- export function SectionTitle({ children, sub }: { children: ReactNode; sub?: ReactNode }) { return (

{children}

{sub &&

{sub}

}
); } // ---------- Squelette de tableau ---------- export function TableSkeleton({ rows = 6 }: { rows?: number }) { return (
{Array.from({ length: rows }).map((_, i) => (
))}
); }