From b39105ac273c300fc3eb340967e8c448a1704a75 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 6 Sep 2026 06:04:53 +0200 Subject: [PATCH] feat(web): page admin de CRUD des didacticiels (#44) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'UI par-dessus l'API `/admin/api/tours*`. Nouvelle entrée `/admin/didacticiels` dans `AdminLayout` (i18n `admin.nav.tours`), route sous `RequireAdmin`+`AdminLayout` dans `App.tsx`. `ToursPage.tsx` : liste des scénarios à gauche (nom, état activé/désactivé, nombre d'étapes) ; à droite l'éditeur du scénario sélectionné — - renommer, activer/désactiver ; - éditer la liste d'étapes (route, cible via ` setKey(event.target.value)} + placeholder="onboardingHouse" + /> + + + {error &&

{error}

} +
+ + +
+ + ); +} + +function ScenarioEditor({ + scenario, + targets, + onSaved, + onDeleted, +}: { + scenario: AdminTourScenarioView; + targets: TourTargetView[]; + onSaved: () => void; + onDeleted: () => void; +}) { + const [name, setName] = useState(scenario.name); + const [enabled, setEnabled] = useState(scenario.enabled); + const [steps, setSteps] = useState( + scenario.steps.map((step) => ({ + route: step.route ?? "", + targetKey: step.targetKey, + title: step.title, + content: step.content, + })), + ); + const [error, setError] = useState(null); + const [status, setStatus] = useState<"idle" | "saving" | "saved">("idle"); + + function patchStep(index: number, patch: Partial) { + setSteps((prev) => prev.map((step, i) => (i === index ? { ...step, ...patch } : step))); + } + + function moveStep(index: number, delta: number) { + setSteps((prev) => { + const target = index + delta; + if (target < 0 || target >= prev.length) return prev; + const next = [...prev]; + const moved = next[index]; + const displaced = next[target]; + if (moved === undefined || displaced === undefined) return prev; + next[index] = displaced; + next[target] = moved; + return next; + }); + } + + function addStep() { + setSteps((prev) => [ + ...prev, + { route: "", targetKey: targets[0]?.key ?? "", title: "", content: "" }, + ]); + } + + function save() { + setError(null); + setStatus("saving"); + adminApiClient + .updateTourScenario(scenario.id, { + name: name.trim(), + enabled, + steps: steps.map((step) => ({ + route: step.route.trim() === "" ? null : step.route.trim(), + targetKey: step.targetKey, + title: step.title.trim(), + content: step.content.trim(), + })), + }) + .then(() => { + setStatus("saved"); + onSaved(); + }) + .catch((err: unknown) => { + setStatus("idle"); + setError(err instanceof AdminApiError ? err.message : "Enregistrement impossible"); + }); + } + + function remove() { + if (!window.confirm(`Supprimer le scénario « ${scenario.name} » ?`)) return; + adminApiClient + .deleteTourScenario(scenario.id) + .then(onDeleted) + .catch(() => onDeleted()); + } + + return ( +
+

+ {scenario.name} {scenario.key} +

+ + + + +

Étapes

+
    + {steps.map((step, index) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: steps are positional; reordering swaps them in place. +
  1. +
    + + +
    + +