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