Checkpoint: Implémentation des fonctionnalités de communication : formulaire de contact avec choix email/canal, canaux de discussion intégrés (page Mes Échanges), demandes de mise en relation avec gestion admin des canaux, navigation mise à jour dans DashboardLayout, bouton Demande de mise en relation sur fiche établissement. Migration DB : tables canaux_discussion, membres_canal, messages_canal, demandes_mise_en_relation.

This commit is contained in:
Manus
2026-05-13 10:40:13 -04:00
parent 46321a1eaf
commit 0205859880
13 changed files with 2437 additions and 57 deletions

View File

@@ -43,6 +43,19 @@ import {
setReferentForEtablissement,
setAdherentsForEtablissement,
updateLocalPassword,
createCanal,
getCanauxForUser,
getMessagesCanal,
sendMessageCanal,
isMemberOfCanal,
getCanalById,
getMembresCanal,
addMembresCanal,
createDemandeMiseEnRelation,
getAllDemandesMiseEnRelation,
getDemandesMiseEnRelationByUser,
traiterDemandeMiseEnRelation,
getDemandeMiseEnRelationById,
updateUser,
updateUserCgu,
updateUserSonumRole,
@@ -76,10 +89,88 @@ const writeProcedure = protectedProcedure.use(({ ctx, next }) => {
return next({ ctx });
});
// ─── Router principal ─────────────────────────────────────────────────────────
/// ─── Canaux de Discussion ────────────────────────────────────────────────────
const canauxRouter = router({
list: protectedProcedure.query(async ({ ctx }) => {
const isGestionnaire = ctx.user.sonumRole === "gestionnaire" || ctx.user.role === "admin";
return getCanauxForUser(ctx.user.id, isGestionnaire);
}),
messages: protectedProcedure
.input(z.object({ canalId: z.number().int() }))
.query(async ({ input, ctx }) => {
const isGestionnaire = ctx.user.sonumRole === "gestionnaire" || ctx.user.role === "admin";
if (!isGestionnaire) {
const isMember = await isMemberOfCanal(input.canalId, ctx.user.id);
if (!isMember) throw new TRPCError({ code: "FORBIDDEN" });
}
return getMessagesCanal(input.canalId);
}),
membres: protectedProcedure
.input(z.object({ canalId: z.number().int() }))
.query(async ({ input, ctx }) => {
const isGestionnaire = ctx.user.sonumRole === "gestionnaire" || ctx.user.role === "admin";
if (!isGestionnaire) {
const isMember = await isMemberOfCanal(input.canalId, ctx.user.id);
if (!isMember) throw new TRPCError({ code: "FORBIDDEN" });
}
return getMembresCanal(input.canalId);
}),
sendMessage: writeProcedure
.input(z.object({ canalId: z.number().int(), contenu: z.string().min(1).max(5000) }))
.mutation(async ({ input, ctx }) => {
const isGestionnaire = ctx.user.sonumRole === "gestionnaire" || ctx.user.role === "admin";
if (!isGestionnaire) {
const isMember = await isMemberOfCanal(input.canalId, ctx.user.id);
if (!isMember) throw new TRPCError({ code: "FORBIDDEN" });
}
await sendMessageCanal({ canalId: input.canalId, auteurId: ctx.user.id, auteurNom: ctx.user.name ?? "Inconnu", contenu: input.contenu });
return { success: true };
}),
create: gestionnaireProcedure
.input(z.object({
titre: z.string().min(1).max(255),
description: z.string().optional(),
type: z.enum(["contact_referent", "mise_en_relation_public", "mise_en_relation_prive"]),
visibilite: z.enum(["public", "prive"]),
etablissementId: z.number().int().optional(),
demandeMiseEnRelationId: z.number().int().optional(),
membreIds: z.array(z.number().int()),
}))
.mutation(async ({ input, ctx }) => {
const canalId = await createCanal({ ...input, creePar: ctx.user.id });
if (input.demandeMiseEnRelationId) {
await traiterDemandeMiseEnRelation(input.demandeMiseEnRelationId, ctx.user.id, canalId);
}
return { canalId };
}),
addMembres: gestionnaireProcedure
.input(z.object({ canalId: z.number().int(), userIds: z.array(z.number().int()) }))
.mutation(async ({ input }) => { await addMembresCanal(input.canalId, input.userIds); return { success: true }; }),
});
// ─── Demandes de Mise en Relation ─────────────────────────────────────────────
const miseEnRelationRouter = router({
soumettre: writeProcedure
.input(z.object({ sujet: z.string().min(1).max(500), message: z.string().min(1), etablissementDemandeurId: z.number().int().optional() }))
.mutation(async ({ input, ctx }) => {
const id = await createDemandeMiseEnRelation({
demandeurId: ctx.user.id, demandeurNom: ctx.user.name ?? "Inconnu",
demandeurEmail: ctx.user.email ?? undefined,
etablissementDemandeurId: input.etablissementDemandeurId,
sujet: input.sujet, message: input.message,
});
await notifyOwner({ title: `Nouvelle demande de mise en relation — ${input.sujet}`, content: `${ctx.user.name} (${ctx.user.email}) souhaite une mise en relation.\n\nSujet : ${input.sujet}\n\nMessage : ${input.message}` });
return { id };
}),
all: gestionnaireProcedure.query(() => getAllDemandesMiseEnRelation()),
mesDemandes: protectedProcedure.query(({ ctx }) => getDemandesMiseEnRelationByUser(ctx.user.id)),
});
// ─── Router principal ─────────────────────────────────────────────────────────
export const appRouter = router({
system: systemRouter,
canaux: canauxRouter,
miseEnRelation: miseEnRelationRouter,
// ─── Auth ──────────────────────────────────────────────────────────────────
auth: router({