// Vrai-Prix — méthode du coût : console d'administration (/cout/admin) — jeton côté client envoyé en // Authorization: Bearer aux routes /api/admin/cost/*. Connecteurs, anomalies, mappings IA, benchmarks, usage IA. "use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import { useLang } from "@/components/LangContext"; import { fmtDate } from "./Provenance"; type J = Record; const KEY = "vrai-prix-cout-admin-token"; export default function AdminConsole() { const { lang } = useLang(); const fr = lang === "fr"; const [token, setToken] = useState(""); const [ready, setReady] = useState(false); const [tab, setTab] = useState<"connectors" | "anomalies" | "mappings" | "benchmarks" | "usage">("connectors"); const [data, setData] = useState(null); const [err, setErr] = useState(null); const [busy, setBusy] = useState(null); const [csv, setCsv] = useState(""); const [log, setLog] = useState([]); useEffect(() => { const id = setTimeout(() => { try { setToken(window.sessionStorage.getItem(KEY) ?? ""); } catch { /* ignore */ } setReady(true); }, 0); return () => clearTimeout(id); }, []); const call = async (path: string, init?: RequestInit): Promise => { const r = await fetch(`/api/admin/cost/${path}`, { ...init, headers: { ...(init?.headers ?? {}), Authorization: `Bearer ${token}`, "Content-Type": "application/json" } }); const d = (await r.json().catch(() => ({}))) as J; if (!r.ok) throw new Error(String(d.error ?? `${r.status} ${r.statusText}`)); return d; }; const load = async (t = tab) => { if (!token) return; setErr(null); try { setData(await call(t === "benchmarks" ? "benchmarks/import" : t)); try { window.sessionStorage.setItem(KEY, token); } catch { /* ignore */ } } catch (e) { setErr(e instanceof Error ? e.message : String(e)); setData(null); } }; const act = async (label: string, path: string, body?: unknown) => { setBusy(label); try { const d = await call(path, { method: "POST", body: body ? JSON.stringify(body) : undefined }); setLog((l) => [`${new Date().toLocaleTimeString()} ${label} → ${JSON.stringify(d).slice(0, 300)}`, ...l].slice(0, 30)); await load(); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } finally { setBusy(null); } }; const rows = (k: string): J[] => (Array.isArray(data?.[k]) ? (data![k] as J[]) : []); const s = (v: unknown) => (v == null ? "—" : String(v)); return (
{fr ? "accès protégé — jeton serveur" : "protected — server token"}
{fr ? "Administration" : "Administration"}

{fr ? "Base de coûts — connecteurs, anomalies, mappings" : "Cost database — connectors, anomalies, mappings"}

{err &&

{err}

}
{data && ( <>
{(["connectors", "anomalies", "mappings", "benchmarks", "usage"] as const).map((t) => ())}
{tab === "connectors" && (
{rows("connectors").map((c, i) => ( ))}
{fr ? "Source" : "Source"}{fr ? "État" : "Status"}{fr ? "Licence" : "Licence"}{fr ? "Dernier crawl" : "Last crawl"}Obs.{fr ? "Erreurs" : "Errors"}
{s(c.name)}
{s(c.key)} · {s(c.frequency)}
{s(c.status)} {s(c.license)}{c.active === 0 || c.active === false ? ` · ${fr ? "inactif" : "inactive"}` : ""} {c.lastSync ? fmtDate(String(c.lastSync), fr) : "—"}{c.lastError ? <>
{String(c.lastError).slice(0, 80)} : null}
{s(c.observations)} {s(c.errors ?? 0)}
{rows("runs").length > 0 &&

{fr ? "Dernières exécutions" : "Recent runs"}

{rows("runs").map((r, i) => ())}
{fr ? "Connecteur" : "Connector"}{fr ? "Début" : "Start"}{fr ? "État" : "Status"}PagesObs.{fr ? "Acceptées" : "Accepted"}{fr ? "Rejetées" : "Rejected"}{fr ? "Inchangées" : "Unchanged"}{fr ? "Erreurs" : "Errors"}
{s(r.connector)}{s(r.started_at ?? r.startedAt)}{s(r.status)}{s(r.pages)}{s(r.observations)}{s(r.accepted)}{s(r.rejected)}{s(r.unchanged)}{s(r.errors)}
}
)} {tab === "anomalies" && (
{rows("anomalies").map((a, i) => ())} {!rows("anomalies").length && }
{fr ? "Article" : "Item"}{fr ? "Source" : "Source"}{fr ? "Prix" : "Price"}{fr ? "Date" : "Date"}{fr ? "Motif" : "Reason"}URL
{s(a.item ?? a.itemCode ?? a.canonical_code)}{s(a.source)}{s(a.price ?? a.total_cost)}{s(a.date ?? a.observation_date)}{s(a.reason ?? a.outlier_reason ?? a.reject_reason)}{a.url ? ↗ : "—"}
{fr ? "Aucune anomalie." : "No anomaly."}
)} {tab === "mappings" && (
{rows("mappings").map((m, i) => ())} {!rows("mappings").length && }
{fr ? "Produit" : "Product"}{fr ? "Article proposé" : "Proposed item"}{fr ? "Méthode" : "Method"}{fr ? "Confiance" : "Confidence"}{fr ? "État" : "Status"}
{s(m.product_title ?? m.title)} ↗
{s(m.source)} · {fr ? "emballage" : "pack"} {s(m.pack_qty ?? m.packQty)} {s(m.pack_unit ?? m.packUnit)}
{s(m.proposed_item_code ?? m.itemCode)}{s(m.method)}
{String(m.rationale ?? "").slice(0, 120)}
{Math.round(Number(m.confidence ?? 0) * (Number(m.confidence) <= 1 ? 100 : 1))} %{s(m.status)}
{fr ? "Aucun mapping en attente." : "No pending mapping."}
)} {tab === "benchmarks" && (

{fr ? "Import CSV (téléchargement légal requis — ex. Altus Canadian Cost Guide)" : "CSV import (legal download required — e.g. Altus Canadian Cost Guide)"}

source,building_type,market,unit,low,high,year,notes