Checkpoint: Le panneau d'affectations des établissements dans la page Admin est désormais disponible pour tous les utilisateurs (référents et adhérents), pas uniquement les adhérents. Ajout d'une barre de recherche dans le panneau, affichage du numéro FINESS, et composant AffectationsPanel réutilisable. Zéro erreur TypeScript.
This commit is contained in:
@@ -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() {
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3.5 hidden xl:table-cell">
|
||||
{u.sonumRole === "adherent" ? (
|
||||
{u.sonumRole !== "gestionnaire" ? (
|
||||
<button
|
||||
onClick={() => setExpandedAffectations(isExpanded ? null : u.id)}
|
||||
className="flex items-center gap-1.5 text-xs text-primary hover:text-primary/80 font-medium transition-colors"
|
||||
@@ -409,11 +409,11 @@ function UsersPanel() {
|
||||
>
|
||||
<Key size={14} />
|
||||
</button>
|
||||
{u.sonumRole === "adherent" && (
|
||||
{u.sonumRole !== "gestionnaire" && (
|
||||
<button
|
||||
onClick={() => setExpandedAffectations(isExpanded ? null : u.id)}
|
||||
className="p-1.5 rounded-lg text-muted-foreground hover:bg-emerald-50 hover:text-emerald-600 transition-colors xl:hidden"
|
||||
title="Gérer les affectations"
|
||||
title="Gérer les établissements"
|
||||
>
|
||||
<Building2 size={14} />
|
||||
</button>
|
||||
@@ -435,44 +435,17 @@ function UsersPanel() {
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* Panneau d'affectations (adhérent uniquement) */}
|
||||
{isExpanded && u.sonumRole === "adherent" && (
|
||||
<tr key={`aff-${u.id}`}>
|
||||
<td colSpan={6} className="px-5 py-4 bg-emerald-50/50 border-b border-emerald-100">
|
||||
<div className="max-w-2xl">
|
||||
<p className="text-xs font-semibold text-emerald-800 mb-3 flex items-center gap-1.5">
|
||||
<Building2 size={13} />
|
||||
Établissements affectés à {u.name}
|
||||
</p>
|
||||
{etablissementsQuery.isLoading ? (
|
||||
<div className="text-xs text-muted-foreground">Chargement...</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{etablissementsQuery.data?.map((etab) => {
|
||||
const isAffected = affectedIds.includes(etab.id);
|
||||
return (
|
||||
<button
|
||||
key={etab.id}
|
||||
onClick={() => toggleAffectation(u.id, etab.id, affectedIds)}
|
||||
disabled={setAffectationsMutation.isPending}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg border text-xs font-medium transition-all text-left ${
|
||||
isAffected
|
||||
? "bg-emerald-100 border-emerald-300 text-emerald-800"
|
||||
: "bg-white border-border text-muted-foreground hover:border-emerald-200 hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<div className={`w-4 h-4 rounded flex items-center justify-center flex-shrink-0 ${isAffected ? "bg-emerald-600" : "bg-muted border border-border"}`}>
|
||||
{isAffected && <Check size={10} className="text-white" />}
|
||||
</div>
|
||||
<span className="truncate">{etab.nom}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/* Panneau d'affectations (tous sauf gestionnaire) */}
|
||||
{isExpanded && u.sonumRole !== "gestionnaire" && (
|
||||
<AffectationsPanel
|
||||
key={`aff-${u.id}`}
|
||||
user={u}
|
||||
affectedIds={affectedIds}
|
||||
onToggle={(etabId) => 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 (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-5 py-4 bg-emerald-50/50 border-b border-emerald-100">
|
||||
<div>
|
||||
{/* En-tête */}
|
||||
<div className="flex items-center justify-between mb-3 flex-wrap gap-2">
|
||||
<p className="text-xs font-semibold text-emerald-800 flex items-center gap-1.5">
|
||||
<Building2 size={13} />
|
||||
Établissements de {user.name}
|
||||
<span className="font-normal text-emerald-600">({roleLabel})</span>
|
||||
<span className="ml-1 bg-emerald-200 text-emerald-800 rounded-full px-2 py-0.5">
|
||||
{affectedIds.length} sélectionné{affectedIds.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</p>
|
||||
{/* Barre de recherche */}
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<svg className="absolute left-2 top-1/2 -translate-y-1/2 text-emerald-400" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.35-4.35" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-xs text-muted-foreground">Chargement...</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
{search ? "Aucun établissement ne correspond à la recherche." : "Aucun établissement disponible."}
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
|
||||
{filtered.map((etab) => {
|
||||
const isAffected = affectedIds.includes(etab.id);
|
||||
return (
|
||||
<button
|
||||
key={etab.id}
|
||||
onClick={() => onToggle(etab.id)}
|
||||
disabled={isPending}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg border text-xs font-medium transition-all text-left ${
|
||||
isAffected
|
||||
? "bg-emerald-100 border-emerald-300 text-emerald-800"
|
||||
: "bg-white border-border text-muted-foreground hover:border-emerald-200 hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-4 h-4 rounded flex items-center justify-center flex-shrink-0 ${
|
||||
isAffected ? "bg-emerald-600" : "bg-muted border border-border"
|
||||
}`}
|
||||
>
|
||||
{isAffected && <Check size={10} className="text-white" />}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate">{etab.nom}</div>
|
||||
{etab.finess && (
|
||||
<div className="text-[10px] text-muted-foreground truncate">{etab.finess}</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Panel Établissements ────────────────────────────────────────────────────────────────────────────────
|
||||
function EtablissementsPanel() {
|
||||
const etablissementsQuery = trpc.etablissements.all.useQuery();
|
||||
const usersQuery = trpc.admin.users.useQuery();
|
||||
|
||||
Reference in New Issue
Block a user