From 43a85f6150b0ab475fb25194384b9d376c7283ed Mon Sep 17 00:00:00 2001 From: Manus Deploy Date: Fri, 24 Apr 2026 07:55:53 -0400 Subject: [PATCH] =?UTF-8?q?v11:=20affectation=20=C3=A9tablissements=20depu?= =?UTF-8?q?is=20Admin=20+=20filtrage=20par=20r=C3=B4le=20+=20ic=C3=B4nes?= =?UTF-8?q?=20solutions=20vertes=20[1777031753]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION | 1 + client/src/pages/Admin.tsx | 294 ++++++++++++++++++++++++++++++++----- server/db.ts | 82 +++++++++++ server/routers.ts | 30 ++++ 4 files changed, 371 insertions(+), 36 deletions(-) create mode 100644 VERSION diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..13d71a7 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +v11-1777031516 diff --git a/client/src/pages/Admin.tsx b/client/src/pages/Admin.tsx index 5d239f6..bfc9ce7 100644 --- a/client/src/pages/Admin.tsx +++ b/client/src/pages/Admin.tsx @@ -675,11 +675,200 @@ function AffectationsPanel({ ); } +// ─── Sous-panneau d'affectation d'un établissement ─────────────────────────── +function EtabAffectationPanel({ + etab, + allUsers, + onClose, +}: { + etab: { id: number; nom: string; referentId?: number | null }; + allUsers: Array<{ id: number; name: string | null; email: string | null; firstName: string | null; lastName: string | null; sonumRole: string }>; + onClose: () => void; +}) { + const utils = trpc.useUtils(); + + const usersQuery = trpc.admin.getUsersForEtablissement.useQuery({ etablissementId: etab.id }); + + const [selectedReferentId, setSelectedReferentId] = useState(etab.referentId ?? null); + const [selectedAdherentIds, setSelectedAdherentIds] = useState([]); + const [search, setSearch] = useState(""); + const [initialized, setInitialized] = useState(false); + + // Initialiser les sélections depuis les données chargées + if (usersQuery.data && !initialized) { + setSelectedReferentId(usersQuery.data.referentId ?? null); + setSelectedAdherentIds(usersQuery.data.adherents.map((a) => a.id)); + setInitialized(true); + } + + const setReferentMutation = trpc.admin.setReferentForEtablissement.useMutation({ + onSuccess: () => { + utils.etablissements.all.invalidate(); + utils.admin.getUsersForEtablissement.invalidate({ etablissementId: etab.id }); + }, + onError: (err) => toast.error(err.message), + }); + + const setAdherentsMutation = trpc.admin.setAdherentsForEtablissement.useMutation({ + onSuccess: () => { + utils.admin.users.invalidate(); + utils.admin.getUsersForEtablissement.invalidate({ etablissementId: etab.id }); + }, + onError: (err) => toast.error(err.message), + }); + + const handleSave = async () => { + await setReferentMutation.mutateAsync({ etablissementId: etab.id, referentId: selectedReferentId }); + await setAdherentsMutation.mutateAsync({ etablissementId: etab.id, userIds: selectedAdherentIds }); + toast.success("Affectations enregistrées"); + onClose(); + }; + + const referents = allUsers.filter((u) => u.sonumRole === "referent" || u.sonumRole === "gestionnaire"); + const adherents = allUsers.filter((u) => u.sonumRole === "adherent"); + const filteredAdherents = search + ? adherents.filter((u) => + (u.name ?? u.email ?? "").toLowerCase().includes(search.toLowerCase()) + ) + : adherents; + + const toggleAdherent = (id: number) => { + setSelectedAdherentIds((prev) => + prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id] + ); + }; + + const isPending = setReferentMutation.isPending || setAdherentsMutation.isPending; + + return ( + + +
+ {/* En-tête */} +
+

+ + Affecter des utilisateurs à {etab.nom} +

+ +
+ + {usersQuery.isLoading ? ( +
Chargement...
+ ) : ( +
+ {/* Référent numérique */} +
+

+ + Référent numérique +

+ +

+ Le référent numérique peut saisir et modifier les logiciels de cet établissement. +

+
+ + {/* Adhérents FEHAP */} +
+
+

+ + Adhérents FEHAP + {selectedAdherentIds.length > 0 && ( + + {selectedAdherentIds.length} + + )} +

+ setSearch(e.target.value)} + placeholder="Rechercher..." + className="pl-2 pr-2 py-1 text-xs bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-emerald-400 w-32" + /> +
+ {adherents.length === 0 ? ( +

Aucun adhérent créé.

+ ) : filteredAdherents.length === 0 ? ( +

Aucun résultat.

+ ) : ( +
+ {filteredAdherents.map((u) => { + const isSelected = selectedAdherentIds.includes(u.id); + return ( + + ); + })} +
+ )} +

+ Les adhérents sélectionnés pourront consulter la fiche de cet établissement. +

+
+
+ )} + + {/* Actions */} +
+ + +
+
+ + + ); +} + // ─── Panel Établissements ──────────────────────────────────────────────────────────────────────────────── function EtablissementsPanel() { const etablissementsQuery = trpc.etablissements.all.useQuery(); const usersQuery = trpc.admin.users.useQuery(); const [showCreate, setShowCreate] = useState(false); + const [expandedEtabId, setExpandedEtabId] = useState(null); const [form, setForm] = useState({ nom: "", finess: "", @@ -700,6 +889,7 @@ function EtablissementsPanel() { }); const referents = usersQuery.data?.filter((u) => u.sonumRole === "referent" || u.sonumRole === "gestionnaire") ?? []; + const allUsers = usersQuery.data ?? []; return (
@@ -760,7 +950,7 @@ function EtablissementsPanel() { />
- +