"use client"; // Paramètres : budgets d'usage, politique d'intégrité académique, recherche // croisée inter-cours et partage de conversations. import { useEffect, useState } from "react"; import { Save } from "lucide-react"; import { PageHeader } from "@/components/app-shell"; import { Button, Card, Input, Label, Skeleton, Spinner } from "@/components/ui"; import { ErrorBanner, SectionTitle, SuccessFlash, Toggle, fmtUSD, useFetchJson } from "./shared"; type Budgets = { dailyPerUserUSD: number; monthlyPerUserUSD: number; monthlyGlobalUSD: number; dailyRequestsPerUser: number; }; type Settings = { budgets: Budgets; defaultBudgets: Budgets; integrityPolicy: { enabled: boolean; examLockdown: boolean }; crossCourseEnabled: boolean; sharingEnabled: boolean; }; const BUDGET_FIELDS: { key: keyof Budgets; label: string; description: string; step: string; isInt?: boolean }[] = [ { key: "dailyPerUserUSD", label: "Budget quotidien par étudiant·e ($)", description: "Dépense maximale d'appels de modèles par personne et par jour. Au-delà, seule l'attente ou un modèle gratuit reste possible.", step: "0.1", }, { key: "monthlyPerUserUSD", label: "Budget mensuel par étudiant·e ($)", description: "Plafond mensuel individuel — protège contre un usage intensif prolongé.", step: "1", }, { key: "monthlyGlobalUSD", label: "Budget global mensuel ($)", description: "Plafond de dépense pour toute la plateforme. Une fois atteint, plus aucun appel n'est effectué pour personne.", step: "10", }, { key: "dailyRequestsPerUser", label: "Requêtes par jour par étudiant·e", description: "Nombre maximal de requêtes quotidiennes par personne, indépendamment du coût.", step: "10", isInt: true, }, ]; export function AdminSettings() { const { data, error, loading, reload } = useFetchJson("/api/admin/settings"); const [budgets, setBudgets] = useState | null>(null); const [integrityEnabled, setIntegrityEnabled] = useState(true); const [examLockdown, setExamLockdown] = useState(false); const [crossCourse, setCrossCourse] = useState(false); const [sharing, setSharing] = useState(true); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [flash, setFlash] = useState(null); useEffect(() => { if (!data) return; setBudgets({ dailyPerUserUSD: String(data.budgets.dailyPerUserUSD), monthlyPerUserUSD: String(data.budgets.monthlyPerUserUSD), monthlyGlobalUSD: String(data.budgets.monthlyGlobalUSD), dailyRequestsPerUser: String(data.budgets.dailyRequestsPerUser), }); setIntegrityEnabled(data.integrityPolicy.enabled); setExamLockdown(data.integrityPolicy.examLockdown); setCrossCourse(data.crossCourseEnabled); setSharing(data.sharingEnabled); }, [data]); async function save() { if (!budgets) return; const parsed: Budgets = { dailyPerUserUSD: parseFloat(budgets.dailyPerUserUSD), monthlyPerUserUSD: parseFloat(budgets.monthlyPerUserUSD), monthlyGlobalUSD: parseFloat(budgets.monthlyGlobalUSD), dailyRequestsPerUser: Math.round(parseFloat(budgets.dailyRequestsPerUser)), }; for (const f of BUDGET_FIELDS) { const v = parsed[f.key]; if (!Number.isFinite(v) || v < 0 || (f.isInt && v < 1)) { setSaveError(`Valeur invalide pour « ${f.label} ».`); return; } } setSaving(true); setSaveError(null); setFlash(null); try { const res = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ budgets: parsed, integrityPolicy: { enabled: integrityEnabled, examLockdown }, crossCourseEnabled: crossCourse, sharingEnabled: sharing, }), }); const j = (await res.json().catch(() => null)) as { error?: string } | null; if (!res.ok) throw new Error(j?.error || `Erreur ${res.status}`); setFlash("Paramètres enregistrés — ils s'appliquent immédiatement."); setTimeout(() => setFlash(null), 4000); await reload(); } catch (e) { setSaveError(e instanceof Error ? e.message : "L'enregistrement a échoué."); } finally { setSaving(false); } } if (loading || !budgets) { return (
{error ? ( ) : (
)}
); } return (
{saving ? : } Enregistrer } />
{saveError && } {flash && } {/* Budgets */} Budgets d'usage
{BUDGET_FIELDS.map((f) => (
setBudgets({ ...budgets, [f.key]: e.target.value })} className="tabular-nums" />

{f.description}

{data && (

Par défaut :{" "} {f.isInt ? data.defaultBudgets[f.key].toLocaleString("fr-CA") : fmtUSD(data.defaultBudgets[f.key])}

)}
))}
{/* Intégrité académique */} Intégrité académique
{/* Options de la plateforme */} Options de la plateforme
); } function SettingRow({ title, description, checked, onChange, disabled, }: { title: string; description: string; checked: boolean; onChange: (v: boolean) => void; disabled?: boolean; }) { return (

{title}

{description}

); }