From df8c5ee2c1bbf5552078d3b9fa19bc4abe8c5a3d Mon Sep 17 00:00:00 2001 From: Manus Date: Fri, 17 Apr 2026 08:21:04 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Page=20Solutions=20Logicielles=20?= =?UTF-8?q?:=20toggle=20Accord=C3=A9on=20/=20Liste.=20La=20vue=20Liste=20a?= =?UTF-8?q?ffiche=20toutes=20les=20solutions=20avec=20leurs=20=C3=A9tablis?= =?UTF-8?q?sements=20directement=20dans=20un=20tableau=20(solution,=20?= =?UTF-8?q?=C3=A9diteur,=20bloc=20fonctionnel,=20=C3=A9tablissement,=20r?= =?UTF-8?q?=C3=A9gion,=20=C3=A9tat)=20sans=20aucune=20interaction.=20Les?= =?UTF-8?q?=20rowSpan=20groupent=20les=20lignes=20par=20solution.=200=20er?= =?UTF-8?q?reur=20TypeScript.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/SolutionsLogicielles.tsx | 224 ++++++++++++++++++---- todo.md | 4 + 2 files changed, 190 insertions(+), 38 deletions(-) diff --git a/client/src/pages/SolutionsLogicielles.tsx b/client/src/pages/SolutionsLogicielles.tsx index 893eb4a..e13adda 100644 --- a/client/src/pages/SolutionsLogicielles.tsx +++ b/client/src/pages/SolutionsLogicielles.tsx @@ -4,15 +4,21 @@ import SonumLayout from "@/components/SonumLayout"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; -import { ChevronDown, ChevronRight, Building2, Package, Search, Filter, BarChart2, Plus, Pencil, Trash2 } from "lucide-react"; +import { + ChevronDown, ChevronRight, Building2, Package, Search, Filter, + BarChart2, Plus, Pencil, Trash2, LayoutList, Rows3 +} from "lucide-react"; import { useState, useMemo } from "react"; import { EtatBadge } from "@/components/EtatBadge"; import { toast } from "sonner"; +type ViewMode = "accordion" | "flat"; + export default function SolutionsLogicielles() { const { user, isAuthenticated } = useAuth(); const isGestionnaire = user?.sonumRole === "gestionnaire" || user?.role === "admin"; + const [viewMode, setViewMode] = useState("accordion"); const [openIds, setOpenIds] = useState>(new Set()); const [search, setSearch] = useState(""); const [filterBloc, setFilterBloc] = useState(""); @@ -43,7 +49,13 @@ export default function SolutionsLogicielles() { const toggle = (id: number) => setOpenIds((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; }); const openAdd = () => { setFormNom(""); setFormEditeurId(""); setFormBlocId(""); setEditTarget(null); setDialogMode("add"); }; - const openEdit = (sol: any) => { setFormNom(sol.solutionNom); setFormEditeurId(sol.editeurId ?? ""); setFormBlocId(sol.blocFonctionnelId ?? ""); setEditTarget(sol); setDialogMode("edit"); }; + const openEdit = (sol: any) => { + setFormNom(sol.solutionNom); + setFormEditeurId(sol.editeurId ?? ""); + setFormBlocId(sol.blocFonctionnelId ?? ""); + setEditTarget(sol); + setDialogMode("edit"); + }; const handleSubmit = () => { if (!formNom.trim() || !formEditeurId) { toast.error("Nom et éditeur obligatoires"); return; } @@ -77,7 +89,8 @@ export default function SolutionsLogicielles() { return ( -
+
+ {/* En-tête */}
@@ -97,7 +110,7 @@ export default function SolutionsLogicielles() {
{solutions && ( -
+
{solutions.length}
Solutions référencées
@@ -106,7 +119,7 @@ export default function SolutionsLogicielles() {
{totalEtablissements}
Établissements équipés
-
+
{blocsUniques.length}
Blocs fonctionnels
@@ -114,21 +127,53 @@ export default function SolutionsLogicielles() { )}
+ {/* Barre de filtres + toggle vue */}
- setSearch(e.target.value)} className="w-full pl-9 pr-4 py-2.5 text-sm bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary" /> + setSearch(e.target.value)} + className="w-full pl-9 pr-4 py-2.5 text-sm bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary" + />
- setFilterBloc(e.target.value)} + className="pl-9 pr-8 py-2.5 text-sm bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 appearance-none min-w-[200px]" + > {blocsUniques.map((b) => )}
+ {/* Toggle vue */} +
+ + +
- {(search || filterBloc) &&

{filtered.length} solution{filtered.length > 1 ? "s" : ""} correspondant aux critères

} + {(search || filterBloc) && ( +

+ {filtered.length} solution{filtered.length > 1 ? "s" : ""} correspondant aux critères +

+ )} {isLoading ? (
{[...Array(6)].map((_, i) =>
)}
@@ -137,14 +182,18 @@ export default function SolutionsLogicielles() {

Aucune solution trouvée

- ) : ( + ) : viewMode === "accordion" ? ( + /* ── VUE ACCORDÉON ─────────────────────────────────────────── */
{filtered.map((sol) => { const isOpen = openIds.has(sol.solutionId); return (
- -
)}
- {isOpen && ( -
-
- {sol.etablissements.length === 0 ? ( -

Aucun établissement n'utilise encore cette solution.

- ) : ( - <> -
Établissements équipés
-
- {sol.etablissements.map((etab) => ( -
-
- - {etab.nom} - {etab.region && — {etab.region}} -
- +
+ {sol.etablissements.length === 0 ? ( +

Aucun établissement n'utilise encore cette solution.

+ ) : ( + <> +
Établissements équipés
+
+ {sol.etablissements.map((etab) => ( +
+
+ + {etab.nom} + {etab.region && — {etab.region}}
- ))} -
- - )} -
+ +
+ ))} +
+ + )}
)}
); })}
+ ) : ( + /* ── VUE LISTE À PLAT ──────────────────────────────────────── */ +
+ + + + + + + + + + {isGestionnaire && } + + + + {filtered.map((sol) => { + if (sol.etablissements.length === 0) { + return ( + + + + + + + {isGestionnaire && ( + + )} + + ); + } + return sol.etablissements.map((etab, idx) => ( + + {idx === 0 ? ( + <> + + + + + ) : null} + + + + {isGestionnaire && idx === 0 ? ( + + ) : isGestionnaire ? : null} + + )); + })} + +
SolutionÉditeurBloc fonctionnelÉtablissementRégionÉtat
{sol.solutionNom}{sol.editeurNom} + {sol.blocFonctionnelNom && ( + {sol.blocFonctionnelNom} + )} + Non déployée +
+ + +
+
+
+ + {sol.solutionNom} +
+
{sol.editeurNom} + {sol.blocFonctionnelNom && ( + {sol.blocFonctionnelNom} + )} + +
+ + {etab.nom} +
+
{etab.region ?? "—"} +
+ + +
+
+
)}
+ {/* Dialog ajout / modification */} !o && setDialogMode(null)}> @@ -218,18 +353,31 @@ export default function SolutionsLogicielles() {
- setFormNom(e.target.value)} placeholder="Ex : DPI, Logiciel RH…" className="w-full border border-border rounded-lg px-3 py-2 text-sm bg-background focus:outline-none focus:ring-2 focus:ring-primary/30" /> + setFormNom(e.target.value)} + placeholder="Ex : DPI, Logiciel RH…" + className="w-full border border-border rounded-lg px-3 py-2 text-sm bg-background focus:outline-none focus:ring-2 focus:ring-primary/30" + />
- setFormEditeurId(e.target.value ? Number(e.target.value) : "")} + className="w-full border border-border rounded-lg px-3 py-2 text-sm bg-background" + > {editeursList?.map((e: any) => )}
- setFormBlocId(e.target.value ? Number(e.target.value) : "")} + className="w-full border border-border rounded-lg px-3 py-2 text-sm bg-background" + > {blocsList?.map((b: any) => )} diff --git a/todo.md b/todo.md index d2a9587..8d0b3cc 100644 --- a/todo.md +++ b/todo.md @@ -70,3 +70,7 @@ - [x] Page "Solutions Logicielles" : liste globale des solutions avec établissements équipés (accordéon), accessible depuis la sidebar - [x] Admin : onglet import établissements (CSV/Excel) - [x] Admin : onglet import contacts (CSV/Excel) + +## Évolution v5 + +- [x] Solutions Logicielles : toggle vue accordéon / vue liste à plat (solutions + établissements visibles sans clic)