v11: affectation établissements depuis Admin + filtrage par rôle + icônes solutions vertes [1777031753]
This commit is contained in:
@@ -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<number | null>(etab.referentId ?? null);
|
||||||
|
const [selectedAdherentIds, setSelectedAdherentIds] = useState<number[]>([]);
|
||||||
|
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 (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5} className="px-5 py-4 bg-blue-50/60 border-b border-blue-100">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* En-tête */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-xs font-semibold text-blue-800 flex items-center gap-1.5">
|
||||||
|
<UserCheck size={14} />
|
||||||
|
Affecter des utilisateurs à <span className="font-bold">{etab.nom}</span>
|
||||||
|
</p>
|
||||||
|
<button onClick={onClose} className="text-muted-foreground hover:text-foreground">
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{usersQuery.isLoading ? (
|
||||||
|
<div className="text-xs text-muted-foreground">Chargement...</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Référent numérique */}
|
||||||
|
<div className="bg-white rounded-lg border border-blue-200 p-3">
|
||||||
|
<p className="text-xs font-semibold text-foreground mb-2 flex items-center gap-1.5">
|
||||||
|
<Shield size={12} className="text-blue-600" />
|
||||||
|
Référent numérique
|
||||||
|
</p>
|
||||||
|
<select
|
||||||
|
value={selectedReferentId ?? ""}
|
||||||
|
onChange={(e) => setSelectedReferentId(e.target.value ? Number(e.target.value) : null)}
|
||||||
|
className="w-full px-3 py-2 text-sm bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
>
|
||||||
|
<option value="">— Non assigné —</option>
|
||||||
|
{referents.map((r) => (
|
||||||
|
<option key={r.id} value={r.id}>
|
||||||
|
{r.name ?? r.email}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<p className="text-[10px] text-muted-foreground mt-1.5">
|
||||||
|
Le référent numérique peut saisir et modifier les logiciels de cet établissement.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Adhérents FEHAP */}
|
||||||
|
<div className="bg-white rounded-lg border border-emerald-200 p-3">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<p className="text-xs font-semibold text-foreground flex items-center gap-1.5">
|
||||||
|
<Users size={12} className="text-emerald-600" />
|
||||||
|
Adhérents FEHAP
|
||||||
|
{selectedAdherentIds.length > 0 && (
|
||||||
|
<span className="bg-emerald-100 text-emerald-700 rounded-full px-1.5 py-0.5 text-[10px]">
|
||||||
|
{selectedAdherentIds.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{adherents.length === 0 ? (
|
||||||
|
<p className="text-xs text-muted-foreground italic">Aucun adhérent créé.</p>
|
||||||
|
) : filteredAdherents.length === 0 ? (
|
||||||
|
<p className="text-xs text-muted-foreground italic">Aucun résultat.</p>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-1 max-h-40 overflow-y-auto pr-1">
|
||||||
|
{filteredAdherents.map((u) => {
|
||||||
|
const isSelected = selectedAdherentIds.includes(u.id);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={u.id}
|
||||||
|
onClick={() => toggleAdherent(u.id)}
|
||||||
|
className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg border text-xs font-medium transition-all text-left ${
|
||||||
|
isSelected
|
||||||
|
? "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-3.5 h-3.5 rounded flex items-center justify-center flex-shrink-0 ${
|
||||||
|
isSelected ? "bg-emerald-600" : "bg-muted border border-border"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isSelected && <Check size={9} className="text-white" />}
|
||||||
|
</div>
|
||||||
|
<span className="truncate">{u.name ?? u.email}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="text-[10px] text-muted-foreground mt-1.5">
|
||||||
|
Les adhérents sélectionnés pourront consulter la fiche de cet établissement.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-3 py-1.5 text-xs font-medium border border-border rounded-lg text-foreground hover:bg-muted"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isPending || usersQuery.isLoading}
|
||||||
|
className="flex items-center gap-1.5 px-4 py-1.5 text-xs font-medium bg-primary text-white rounded-lg hover:bg-primary/90 disabled:opacity-50 shadow-sm"
|
||||||
|
>
|
||||||
|
<Check size={13} />
|
||||||
|
{isPending ? "Enregistrement..." : "Enregistrer les affectations"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Panel Établissements ────────────────────────────────────────────────────────────────────────────────
|
// ─── Panel Établissements ────────────────────────────────────────────────────────────────────────────────
|
||||||
function EtablissementsPanel() {
|
function EtablissementsPanel() {
|
||||||
const etablissementsQuery = trpc.etablissements.all.useQuery();
|
const etablissementsQuery = trpc.etablissements.all.useQuery();
|
||||||
const usersQuery = trpc.admin.users.useQuery();
|
const usersQuery = trpc.admin.users.useQuery();
|
||||||
const [showCreate, setShowCreate] = useState(false);
|
const [showCreate, setShowCreate] = useState(false);
|
||||||
|
const [expandedEtabId, setExpandedEtabId] = useState<number | null>(null);
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
nom: "",
|
nom: "",
|
||||||
finess: "",
|
finess: "",
|
||||||
@@ -700,6 +889,7 @@ function EtablissementsPanel() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const referents = usersQuery.data?.filter((u) => u.sonumRole === "referent" || u.sonumRole === "gestionnaire") ?? [];
|
const referents = usersQuery.data?.filter((u) => u.sonumRole === "referent" || u.sonumRole === "gestionnaire") ?? [];
|
||||||
|
const allUsers = usersQuery.data ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-card rounded-xl border border-border shadow-sm overflow-hidden">
|
<div className="bg-card rounded-xl border border-border shadow-sm overflow-hidden">
|
||||||
@@ -760,7 +950,7 @@ function EtablissementsPanel() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-foreground mb-1">Référent numérique</label>
|
<label className="block text-xs font-medium text-foreground mb-1">Référent numérique (optionnel)</label>
|
||||||
<select
|
<select
|
||||||
value={form.referentId ?? ""}
|
value={form.referentId ?? ""}
|
||||||
onChange={(e) => setForm((f) => ({ ...f, referentId: e.target.value ? Number(e.target.value) : undefined }))}
|
onChange={(e) => setForm((f) => ({ ...f, referentId: e.target.value ? Number(e.target.value) : undefined }))}
|
||||||
@@ -797,15 +987,15 @@ function EtablissementsPanel() {
|
|||||||
<div className="animate-spin rounded-full h-6 w-6 border-2 border-primary border-t-transparent" />
|
<div className="animate-spin rounded-full h-6 w-6 border-2 border-primary border-t-transparent" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto max-h-[500px] overflow-y-auto">
|
<div className="overflow-x-auto max-h-[600px] overflow-y-auto">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead className="sticky top-0 z-10">
|
<thead className="sticky top-0 z-10">
|
||||||
<tr className="border-b border-border bg-muted/30">
|
<tr className="border-b border-border bg-muted/30">
|
||||||
<th className="text-left px-5 py-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider">Établissement</th>
|
<th className="text-left px-5 py-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider">Établissement</th>
|
||||||
<th className="text-left px-4 py-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider hidden md:table-cell">Région</th>
|
<th className="text-left px-4 py-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider hidden md:table-cell">Région</th>
|
||||||
<th className="text-left px-4 py-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider hidden lg:table-cell">Type</th>
|
<th className="text-left px-4 py-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider hidden lg:table-cell">Type</th>
|
||||||
<th className="text-left px-4 py-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider hidden xl:table-cell">Référent</th>
|
<th className="text-left px-4 py-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider">Référent / Adhérents</th>
|
||||||
<th className="text-left px-4 py-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider hidden xl:table-cell">Adhérents affectés</th>
|
<th className="text-right px-4 py-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-border">
|
<tbody className="divide-y divide-border">
|
||||||
@@ -814,9 +1004,11 @@ function EtablissementsPanel() {
|
|||||||
(u) => u.sonumRole === "adherent" && (u.etablissements ?? []).some((e: any) => e.id === etab.id)
|
(u) => u.sonumRole === "adherent" && (u.etablissements ?? []).some((e: any) => e.id === etab.id)
|
||||||
) ?? [];
|
) ?? [];
|
||||||
const referent = usersQuery.data?.find((u) => u.id === etab.referentId);
|
const referent = usersQuery.data?.find((u) => u.id === etab.referentId);
|
||||||
|
const isExpanded = expandedEtabId === etab.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={etab.id} className="hover:bg-muted/20 transition-colors">
|
<Fragment key={etab.id}>
|
||||||
|
<tr className={`transition-colors ${isExpanded ? "bg-blue-50/40" : "hover:bg-muted/20"}`}>
|
||||||
<td className="px-5 py-3">
|
<td className="px-5 py-3">
|
||||||
<div className="font-medium text-foreground text-sm">{etab.nom}</div>
|
<div className="font-medium text-foreground text-sm">{etab.nom}</div>
|
||||||
{etab.finess && <div className="text-xs text-muted-foreground">FINESS : {etab.finess}</div>}
|
{etab.finess && <div className="text-xs text-muted-foreground">FINESS : {etab.finess}</div>}
|
||||||
@@ -829,27 +1021,57 @@ function EtablissementsPanel() {
|
|||||||
</span>
|
</span>
|
||||||
) : <span className="text-muted-foreground text-xs">—</span>}
|
) : <span className="text-muted-foreground text-xs">—</span>}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 hidden xl:table-cell text-muted-foreground text-xs">
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
{referent ? (
|
{referent ? (
|
||||||
|
<span className="text-xs flex items-center gap-1">
|
||||||
|
<Shield size={10} className="text-blue-500 flex-shrink-0" />
|
||||||
<span className="font-medium text-foreground">{referent.name ?? referent.email}</span>
|
<span className="font-medium text-foreground">{referent.name ?? referent.email}</span>
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="italic">Non assigné</span>
|
<span className="text-xs text-muted-foreground italic flex items-center gap-1">
|
||||||
|
<Shield size={10} className="flex-shrink-0" /> Pas de référent
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 hidden xl:table-cell">
|
|
||||||
{adherentsAffectes.length > 0 ? (
|
{adherentsAffectes.length > 0 ? (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{adherentsAffectes.map((a) => (
|
{adherentsAffectes.slice(0, 3).map((a) => (
|
||||||
<span key={a.id} className="text-xs bg-emerald-50 text-emerald-700 border border-emerald-200 px-2 py-0.5 rounded-full">
|
<span key={a.id} className="text-[10px] bg-emerald-50 text-emerald-700 border border-emerald-200 px-1.5 py-0.5 rounded-full">
|
||||||
{a.name ?? a.email}
|
{a.name ?? a.email}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
{adherentsAffectes.length > 3 && (
|
||||||
|
<span className="text-[10px] text-muted-foreground">+{adherentsAffectes.length - 3}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs text-muted-foreground italic">Aucun</span>
|
<span className="text-[10px] text-muted-foreground italic">Aucun adhérent</span>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-right">
|
||||||
|
<button
|
||||||
|
onClick={() => setExpandedEtabId(isExpanded ? null : etab.id)}
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border transition-colors ml-auto ${
|
||||||
|
isExpanded
|
||||||
|
? "bg-blue-100 border-blue-300 text-blue-700"
|
||||||
|
: "border-border text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<UserCheck size={12} />
|
||||||
|
{isExpanded ? "Fermer" : "Affecter"}
|
||||||
|
{isExpanded ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{isExpanded && (
|
||||||
|
<EtabAffectationPanel
|
||||||
|
etab={etab}
|
||||||
|
allUsers={allUsers}
|
||||||
|
onClose={() => setExpandedEtabId(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
82
server/db.ts
82
server/db.ts
@@ -183,6 +183,21 @@ export async function searchEtablissements(filters: {
|
|||||||
if (filters.sonumRole !== "gestionnaire") {
|
if (filters.sonumRole !== "gestionnaire") {
|
||||||
conditions.push(eq(etablissements.visibilite, "tous"));
|
conditions.push(eq(etablissements.visibilite, "tous"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filtrage par affectations : les adhérents ne voient que leurs établissements affectés
|
||||||
|
if (filters.sonumRole === "adherent" && filters.userId) {
|
||||||
|
const affectations = await getAffectationsByUser(filters.userId);
|
||||||
|
if (affectations.length === 0) {
|
||||||
|
return []; // Aucun établissement affecté
|
||||||
|
}
|
||||||
|
conditions.push(inArray(etablissements.id, affectations));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrage par référent : les référents ne voient que leurs établissements
|
||||||
|
if (filters.sonumRole === "referent" && filters.userId) {
|
||||||
|
conditions.push(eq(etablissements.referentId, filters.userId));
|
||||||
|
}
|
||||||
|
|
||||||
if (filters.region) conditions.push(eq(etablissements.region, filters.region));
|
if (filters.region) conditions.push(eq(etablissements.region, filters.region));
|
||||||
if (filters.typeActivite) conditions.push(eq(etablissements.typeActivite, filters.typeActivite));
|
if (filters.typeActivite) conditions.push(eq(etablissements.typeActivite, filters.typeActivite));
|
||||||
if (filters.tailleEffectifs) conditions.push(eq(etablissements.tailleEffectifs, filters.tailleEffectifs));
|
if (filters.tailleEffectifs) conditions.push(eq(etablissements.tailleEffectifs, filters.tailleEffectifs));
|
||||||
@@ -949,3 +964,70 @@ export async function getStatistiques() {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Affectations depuis l'établissement ─────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne les utilisateurs (adhérents + référent) associés à un établissement.
|
||||||
|
*/
|
||||||
|
export async function getUsersForEtablissement(etablissementId: number) {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) return { adherents: [], referentId: null as number | null };
|
||||||
|
|
||||||
|
// Adhérents affectés via user_etablissements
|
||||||
|
const adherentsRows = await db
|
||||||
|
.select({
|
||||||
|
id: users.id,
|
||||||
|
name: users.name,
|
||||||
|
firstName: users.firstName,
|
||||||
|
lastName: users.lastName,
|
||||||
|
email: users.email,
|
||||||
|
sonumRole: users.sonumRole,
|
||||||
|
})
|
||||||
|
.from(userEtablissements)
|
||||||
|
.innerJoin(users, eq(userEtablissements.userId, users.id))
|
||||||
|
.where(eq(userEtablissements.etablissementId, etablissementId));
|
||||||
|
|
||||||
|
// Référent via etablissements.referentId
|
||||||
|
const etabRow = await db
|
||||||
|
.select({ referentId: etablissements.referentId })
|
||||||
|
.from(etablissements)
|
||||||
|
.where(eq(etablissements.id, etablissementId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
adherents: adherentsRows,
|
||||||
|
referentId: etabRow[0]?.referentId ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Définit le référent numérique d'un établissement.
|
||||||
|
*/
|
||||||
|
export async function setReferentForEtablissement(etablissementId: number, referentId: number | null) {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) return;
|
||||||
|
await db
|
||||||
|
.update(etablissements)
|
||||||
|
.set({ referentId: referentId ?? undefined })
|
||||||
|
.where(eq(etablissements.id, etablissementId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remplace tous les adhérents affectés à un établissement.
|
||||||
|
*/
|
||||||
|
export async function setAdherentsForEtablissement(etablissementId: number, userIds: number[]) {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) return;
|
||||||
|
// Supprimer les affectations existantes pour cet établissement (seulement les adhérents)
|
||||||
|
// On supprime toutes les lignes user_etablissements pour cet établissement
|
||||||
|
await db
|
||||||
|
.delete(userEtablissements)
|
||||||
|
.where(eq(userEtablissements.etablissementId, etablissementId));
|
||||||
|
// Réinsérer
|
||||||
|
if (userIds.length > 0) {
|
||||||
|
await db.insert(userEtablissements).values(
|
||||||
|
userIds.map((uid) => ({ userId: uid, etablissementId }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ import {
|
|||||||
removeEtablissementFromUser,
|
removeEtablissementFromUser,
|
||||||
repondreDemandeContact,
|
repondreDemandeContact,
|
||||||
setAffectationsForUser,
|
setAffectationsForUser,
|
||||||
|
getUsersForEtablissement,
|
||||||
|
setReferentForEtablissement,
|
||||||
|
setAdherentsForEtablissement,
|
||||||
updateLocalPassword,
|
updateLocalPassword,
|
||||||
updateUser,
|
updateUser,
|
||||||
updateUserCgu,
|
updateUserCgu,
|
||||||
@@ -564,6 +567,33 @@ export const appRouter = router({
|
|||||||
await removeEtablissementFromUser(input.userId, input.etablissementId);
|
await removeEtablissementFromUser(input.userId, input.etablissementId);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/** Retourne les utilisateurs (adhérents + référent) d'un établissement */
|
||||||
|
getUsersForEtablissement: gestionnaireProcedure
|
||||||
|
.input(z.object({ etablissementId: z.number().int() }))
|
||||||
|
.query(({ input }) => getUsersForEtablissement(input.etablissementId)),
|
||||||
|
|
||||||
|
/** Définit le référent numérique d'un établissement */
|
||||||
|
setReferentForEtablissement: gestionnaireProcedure
|
||||||
|
.input(z.object({
|
||||||
|
etablissementId: z.number().int(),
|
||||||
|
referentId: z.number().int().nullable(),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
await setReferentForEtablissement(input.etablissementId, input.referentId);
|
||||||
|
return { success: true };
|
||||||
|
}),
|
||||||
|
|
||||||
|
/** Remplace tous les adhérents affectés à un établissement */
|
||||||
|
setAdherentsForEtablissement: gestionnaireProcedure
|
||||||
|
.input(z.object({
|
||||||
|
etablissementId: z.number().int(),
|
||||||
|
userIds: z.array(z.number().int()),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
await setAdherentsForEtablissement(input.etablissementId, input.userIds);
|
||||||
|
return { success: true };
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user