From 77839a08fffbb1fce3d13bd7b1eead9e2fa24508 Mon Sep 17 00:00:00 2001 From: Manus Date: Thu, 16 Apr 2026 11:07:29 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Le=20panneau=20d'affectations=20d?= =?UTF-8?q?es=20=C3=A9tablissements=20dans=20la=20page=20Admin=20est=20d?= =?UTF-8?q?=C3=A9sormais=20disponible=20pour=20tous=20les=20utilisateurs?= =?UTF-8?q?=20(r=C3=A9f=C3=A9rents=20et=20adh=C3=A9rents),=20pas=20uniquem?= =?UTF-8?q?ent=20les=20adh=C3=A9rents.=20Ajout=20d'une=20barre=20de=20rech?= =?UTF-8?q?erche=20dans=20le=20panneau,=20affichage=20du=20num=C3=A9ro=20F?= =?UTF-8?q?INESS,=20et=20composant=20AffectationsPanel=20r=C3=A9utilisable?= =?UTF-8?q?.=20Z=C3=A9ro=20erreur=20TypeScript.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/Admin.tsx | 165 ++++++++++++++++++++++++--------- scripts/create-admin.mjs | 93 +++++++++++++++++++ scripts/set-local-password.mjs | 73 +++++++++++++++ todo.md | 5 + 4 files changed, 293 insertions(+), 43 deletions(-) create mode 100644 scripts/create-admin.mjs create mode 100644 scripts/set-local-password.mjs diff --git a/client/src/pages/Admin.tsx b/client/src/pages/Admin.tsx index 5da65e7..3af3e4d 100644 --- a/client/src/pages/Admin.tsx +++ b/client/src/pages/Admin.tsx @@ -17,7 +17,7 @@ import { Users, X, } from "lucide-react"; -import { useState } from "react"; +import { useState, useMemo } from "react"; import { toast } from "sonner"; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -360,7 +360,7 @@ function UsersPanel() { - {u.sonumRole === "adherent" ? ( + {u.sonumRole !== "gestionnaire" ? ( - {u.sonumRole === "adherent" && ( + {u.sonumRole !== "gestionnaire" && ( @@ -435,44 +435,17 @@ function UsersPanel() { - {/* Panneau d'affectations (adhérent uniquement) */} - {isExpanded && u.sonumRole === "adherent" && ( - - -
-

- - Établissements affectés à {u.name} -

- {etablissementsQuery.isLoading ? ( -
Chargement...
- ) : ( -
- {etablissementsQuery.data?.map((etab) => { - const isAffected = affectedIds.includes(etab.id); - return ( - - ); - })} -
- )} -
- - + {/* Panneau d'affectations (tous sauf gestionnaire) */} + {isExpanded && u.sonumRole !== "gestionnaire" && ( + toggleAffectation(u.id, etabId, affectedIds)} + isPending={setAffectationsMutation.isPending} + etablissements={etablissementsQuery.data ?? []} + isLoading={etablissementsQuery.isLoading} + /> )} ); @@ -532,8 +505,114 @@ function UsersPanel() { ); } -// ─── Panel Établissements ───────────────────────────────────────────────────── +// ─── Panneau d'affectations (composant réutilisable) ──────────────────────────────────────────────────────────────────────────────── +function AffectationsPanel({ + user, + affectedIds, + onToggle, + isPending, + etablissements, + isLoading, +}: { + user: any; + affectedIds: number[]; + onToggle: (etabId: number) => void; + isPending: boolean; + etablissements: any[]; + isLoading: boolean; +}) { + const [search, setSearch] = useState(""); + + const filtered = useMemo(() => { + const q = search.toLowerCase().trim(); + if (!q) return etablissements; + return etablissements.filter( + (e) => + e.nom?.toLowerCase().includes(q) || + e.finess?.toLowerCase().includes(q) || + e.region?.toLowerCase().includes(q) + ); + }, [etablissements, search]); + + const roleLabel = + user.sonumRole === "adherent" ? "Adhérent FEHAP" : "Référent numérique"; + + return ( + + +
+ {/* En-tête */} +
+

+ + Établissements de {user.name} + ({roleLabel}) + + {affectedIds.length} sélectionné{affectedIds.length !== 1 ? "s" : ""} + +

+ {/* Barre de recherche */} +
+ setSearch(e.target.value)} + placeholder="Rechercher un établissement..." + className="pl-7 pr-3 py-1.5 text-xs bg-white border border-emerald-200 rounded-lg focus:outline-none focus:ring-1 focus:ring-emerald-400 w-56" + /> + + + +
+
+ + {isLoading ? ( +
Chargement...
+ ) : filtered.length === 0 ? ( +

+ {search ? "Aucun établissement ne correspond à la recherche." : "Aucun établissement disponible."} +

+ ) : ( +
+ {filtered.map((etab) => { + const isAffected = affectedIds.includes(etab.id); + return ( + + ); + })} +
+ )} +
+ + + ); +} + +// ─── Panel Établissements ──────────────────────────────────────────────────────────────────────────────── function EtablissementsPanel() { const etablissementsQuery = trpc.etablissements.all.useQuery(); const usersQuery = trpc.admin.users.useQuery(); diff --git a/scripts/create-admin.mjs b/scripts/create-admin.mjs new file mode 100644 index 0000000..84003d4 --- /dev/null +++ b/scripts/create-admin.mjs @@ -0,0 +1,93 @@ +import { drizzle } from "drizzle-orm/mysql2"; +import mysql from "mysql2/promise"; +import bcrypt from "bcryptjs"; +import { eq } from "drizzle-orm"; +import * as schema from "../drizzle/schema.ts"; +import dotenv from "dotenv"; + +dotenv.config(); + +const { users, localCredentials } = schema; + +const EMAIL = "adminItinova"; // login (utilisé comme email) +const PASSWORD = "Itinova69!"; +const NAME = "Gestionnaire SONUM (Itinova)"; +const SONUM_ROLE = "gestionnaire"; +const OPEN_ID = "local-admin-itinova"; // openId unique pour compte local + +async function main() { + if (!process.env.DATABASE_URL) { + console.error("DATABASE_URL manquant"); + process.exit(1); + } + + const connection = await mysql.createConnection(process.env.DATABASE_URL); + const db = drizzle(connection); + + // Vérifier si l'utilisateur existe déjà + const existing = await db + .select() + .from(users) + .where(eq(users.openId, OPEN_ID)) + .limit(1); + + let userId; + + if (existing.length > 0) { + userId = existing[0].id; + console.log(`Utilisateur existant trouvé (id=${userId}), mise à jour du mot de passe...`); + // Mettre à jour le rôle au cas où + await db + .update(users) + .set({ sonumRole: SONUM_ROLE, name: NAME, email: EMAIL, updatedAt: new Date() }) + .where(eq(users.id, userId)); + } else { + // Créer l'utilisateur + const result = await db.insert(users).values({ + openId: OPEN_ID, + name: NAME, + email: EMAIL, + loginMethod: "local", + role: "user", + sonumRole: SONUM_ROLE, + cguAccepted: true, + cguAcceptedAt: new Date(), + lastSignedIn: new Date(), + }); + userId = Number(result[0].insertId); + console.log(`Utilisateur créé (id=${userId})`); + } + + // Hasher le mot de passe + const passwordHash = await bcrypt.hash(PASSWORD, 12); + + // Insérer ou mettre à jour les credentials locaux + const existingCreds = await db + .select() + .from(localCredentials) + .where(eq(localCredentials.userId, userId)) + .limit(1); + + if (existingCreds.length > 0) { + await db + .update(localCredentials) + .set({ passwordHash, updatedAt: new Date() }) + .where(eq(localCredentials.userId, userId)); + console.log("Mot de passe mis à jour."); + } else { + await db.insert(localCredentials).values({ userId, passwordHash }); + console.log("Credentials locaux créés."); + } + + console.log("\n✅ Compte générique gestionnaire SONUM prêt :"); + console.log(` Login : ${EMAIL}`); + console.log(` Rôle : ${SONUM_ROLE}`); + console.log(` Nom : ${NAME}`); + + await connection.end(); +} + +main().catch((err) => { + console.error("Erreur :", err); + process.exit(1); +}); diff --git a/scripts/set-local-password.mjs b/scripts/set-local-password.mjs new file mode 100644 index 0000000..914f4ff --- /dev/null +++ b/scripts/set-local-password.mjs @@ -0,0 +1,73 @@ +import { drizzle } from "drizzle-orm/mysql2"; +import mysql from "mysql2/promise"; +import bcrypt from "bcryptjs"; +import { eq } from "drizzle-orm"; +import * as schema from "../drizzle/schema.ts"; +import dotenv from "dotenv"; + +dotenv.config(); + +const { users, localCredentials } = schema; + +// ─── Paramètres ─────────────────────────────────────────────────────────────── +const TARGET_EMAIL = "o.pareige@itinova.org"; +const NEW_PASSWORD = "Itinova69!"; +// ───────────────────────────────────────────────────────────────────────────── + +async function main() { + if (!process.env.DATABASE_URL) { + console.error("DATABASE_URL manquant"); + process.exit(1); + } + + const connection = await mysql.createConnection(process.env.DATABASE_URL); + const db = drizzle(connection); + + // Trouver l'utilisateur par email + const result = await db + .select() + .from(users) + .where(eq(users.email, TARGET_EMAIL)) + .limit(1); + + if (!result.length) { + console.error(`Aucun utilisateur trouvé avec l'email : ${TARGET_EMAIL}`); + await connection.end(); + process.exit(1); + } + + const user = result[0]; + console.log(`Utilisateur trouvé : ${user.name} (id=${user.id}, rôle=${user.sonumRole})`); + + // Hasher le mot de passe + const passwordHash = await bcrypt.hash(NEW_PASSWORD, 12); + + // Insérer ou mettre à jour les credentials locaux + const existingCreds = await db + .select() + .from(localCredentials) + .where(eq(localCredentials.userId, user.id)) + .limit(1); + + if (existingCreds.length > 0) { + await db + .update(localCredentials) + .set({ passwordHash, updatedAt: new Date() }) + .where(eq(localCredentials.userId, user.id)); + console.log("Mot de passe mis à jour."); + } else { + await db.insert(localCredentials).values({ userId: user.id, passwordHash }); + console.log("Credentials locaux créés."); + } + + console.log(`\n✅ Connexion locale activée pour ${TARGET_EMAIL}`); + console.log(` Login : ${TARGET_EMAIL}`); + console.log(` Rôle : ${user.sonumRole}`); + + await connection.end(); +} + +main().catch((err) => { + console.error("Erreur :", err); + process.exit(1); +}); diff --git a/todo.md b/todo.md index b3e1f9b..3d782b9 100644 --- a/todo.md +++ b/todo.md @@ -56,3 +56,8 @@ - [x] Page admin : affectations établissements inline dans le tableau utilisateurs - [x] Badge "Adhérent FEHAP" dans le header et la sidebar - [x] Vue "Mes Établissements" filtrée pour les adhérents (uniquement les établissements affectés) + +## Évolution v3 — Gestion des établissements par utilisateur +- [x] Étendre admin.setAffectations pour accepter tous les rôles (référent + adhérent) +- [x] Interface Admin : panneau dédié "Établissements" par utilisateur avec liste complète, barre de recherche et cases à cocher +- [x] Afficher le nombre d'établissements affectés dans le tableau utilisateurs