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:
226
server/db.ts
226
server/db.ts
@@ -3,12 +3,17 @@ import { drizzle } from "drizzle-orm/mysql2";
|
||||
import {
|
||||
InsertUser,
|
||||
blocsFonctionnels,
|
||||
canauxDiscussion,
|
||||
consultations,
|
||||
demandesContact,
|
||||
demandesMiseEnRelation,
|
||||
editeurs,
|
||||
etablissements,
|
||||
logicielsEtablissements,
|
||||
membresCanauxDiscussion,
|
||||
messagesCanaux,
|
||||
solutions,
|
||||
userEtablissements,
|
||||
users,
|
||||
} from "../drizzle/schema";
|
||||
import { ENV } from "./_core/env";
|
||||
@@ -430,7 +435,7 @@ export async function getDemandeById(id: number) {
|
||||
|
||||
// ─── Auth locale ──────────────────────────────────────────────────────────────
|
||||
|
||||
import { localCredentials, userEtablissements } from "../drizzle/schema";
|
||||
import { localCredentials } from "../drizzle/schema";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
@@ -1031,3 +1036,222 @@ export async function setAdherentsForEtablissement(etablissementId: number, user
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Canaux de Discussion ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Crée un canal de discussion et ajoute les membres initiaux.
|
||||
*/
|
||||
export async function createCanal(data: {
|
||||
titre: string;
|
||||
description?: string;
|
||||
type: "contact_referent" | "mise_en_relation_public" | "mise_en_relation_prive";
|
||||
visibilite: "public" | "prive";
|
||||
etablissementId?: number;
|
||||
demandeContactId?: number;
|
||||
demandeMiseEnRelationId?: number;
|
||||
creePar: number;
|
||||
membreIds: number[];
|
||||
}) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("DB unavailable");
|
||||
const [result] = await db.insert(canauxDiscussion).values({
|
||||
titre: data.titre,
|
||||
description: data.description,
|
||||
type: data.type,
|
||||
visibilite: data.visibilite,
|
||||
etablissementId: data.etablissementId,
|
||||
demandeContactId: data.demandeContactId,
|
||||
demandeMiseEnRelationId: data.demandeMiseEnRelationId,
|
||||
creePar: data.creePar,
|
||||
});
|
||||
const canalId = (result as any).insertId as number;
|
||||
if (data.membreIds.length > 0) {
|
||||
await db.insert(membresCanauxDiscussion).values(
|
||||
data.membreIds.map((uid) => ({ canalId, userId: uid, role: "membre" as const }))
|
||||
);
|
||||
}
|
||||
return canalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les canaux accessibles à un utilisateur (membre ou gestionnaire).
|
||||
*/
|
||||
export async function getCanauxForUser(userId: number, isGestionnaire: boolean) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
if (isGestionnaire) {
|
||||
return db.select().from(canauxDiscussion).orderBy(desc(canauxDiscussion.updatedAt));
|
||||
}
|
||||
// Canaux dont l'utilisateur est membre
|
||||
const memberships = await db
|
||||
.select({ canalId: membresCanauxDiscussion.canalId })
|
||||
.from(membresCanauxDiscussion)
|
||||
.where(eq(membresCanauxDiscussion.userId, userId));
|
||||
const canalIds = memberships.map((m) => m.canalId);
|
||||
if (canalIds.length === 0) return [];
|
||||
return db
|
||||
.select()
|
||||
.from(canauxDiscussion)
|
||||
.where(inArray(canauxDiscussion.id, canalIds))
|
||||
.orderBy(desc(canauxDiscussion.updatedAt));
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les messages d'un canal.
|
||||
*/
|
||||
export async function getMessagesCanal(canalId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db
|
||||
.select()
|
||||
.from(messagesCanaux)
|
||||
.where(eq(messagesCanaux.canalId, canalId))
|
||||
.orderBy(messagesCanaux.createdAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un message dans un canal.
|
||||
*/
|
||||
export async function sendMessageCanal(data: {
|
||||
canalId: number;
|
||||
auteurId: number;
|
||||
auteurNom: string;
|
||||
contenu: string;
|
||||
}) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("DB unavailable");
|
||||
await db.insert(messagesCanaux).values(data);
|
||||
// Mettre à jour updatedAt du canal
|
||||
await db
|
||||
.update(canauxDiscussion)
|
||||
.set({ updatedAt: new Date() })
|
||||
.where(eq(canauxDiscussion.id, data.canalId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un utilisateur est membre d'un canal.
|
||||
*/
|
||||
export async function isMemberOfCanal(canalId: number, userId: number): Promise<boolean> {
|
||||
const db = await getDb();
|
||||
if (!db) return false;
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(membresCanauxDiscussion)
|
||||
.where(and(eq(membresCanauxDiscussion.canalId, canalId), eq(membresCanauxDiscussion.userId, userId)));
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un canal par son ID.
|
||||
*/
|
||||
export async function getCanalById(id: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return null;
|
||||
const rows = await db.select().from(canauxDiscussion).where(eq(canauxDiscussion.id, id));
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les membres d'un canal.
|
||||
*/
|
||||
export async function getMembresCanal(canalId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db
|
||||
.select({
|
||||
id: users.id,
|
||||
name: users.name,
|
||||
email: users.email,
|
||||
sonumRole: users.sonumRole,
|
||||
role: membresCanauxDiscussion.role,
|
||||
})
|
||||
.from(membresCanauxDiscussion)
|
||||
.innerJoin(users, eq(membresCanauxDiscussion.userId, users.id))
|
||||
.where(eq(membresCanauxDiscussion.canalId, canalId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute des membres à un canal existant.
|
||||
*/
|
||||
export async function addMembresCanal(canalId: number, userIds: number[]) {
|
||||
const db = await getDb();
|
||||
if (!db) return;
|
||||
const existing = await db
|
||||
.select({ userId: membresCanauxDiscussion.userId })
|
||||
.from(membresCanauxDiscussion)
|
||||
.where(eq(membresCanauxDiscussion.canalId, canalId));
|
||||
const existingIds = new Set(existing.map((m) => m.userId));
|
||||
const toAdd = userIds.filter((id) => !existingIds.has(id));
|
||||
if (toAdd.length > 0) {
|
||||
await db.insert(membresCanauxDiscussion).values(
|
||||
toAdd.map((uid) => ({ canalId, userId: uid, role: "membre" as const }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Demandes de Mise en Relation ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Crée une demande de mise en relation.
|
||||
*/
|
||||
export async function createDemandeMiseEnRelation(data: {
|
||||
demandeurId: number;
|
||||
demandeurNom: string;
|
||||
demandeurEmail?: string;
|
||||
etablissementDemandeurId?: number;
|
||||
sujet: string;
|
||||
message: string;
|
||||
}) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("DB unavailable");
|
||||
const [result] = await db.insert(demandesMiseEnRelation).values(data);
|
||||
return (result as any).insertId as number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère toutes les demandes de mise en relation (pour les gestionnaires).
|
||||
*/
|
||||
export async function getAllDemandesMiseEnRelation() {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db
|
||||
.select()
|
||||
.from(demandesMiseEnRelation)
|
||||
.orderBy(desc(demandesMiseEnRelation.createdAt));
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les demandes de mise en relation d'un utilisateur.
|
||||
*/
|
||||
export async function getDemandesMiseEnRelationByUser(userId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db
|
||||
.select()
|
||||
.from(demandesMiseEnRelation)
|
||||
.where(eq(demandesMiseEnRelation.demandeurId, userId))
|
||||
.orderBy(desc(demandesMiseEnRelation.createdAt));
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque une demande de mise en relation comme traitée et lui associe un canal.
|
||||
*/
|
||||
export async function traiterDemandeMiseEnRelation(id: number, traitePar: number, canalId?: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return;
|
||||
await db
|
||||
.update(demandesMiseEnRelation)
|
||||
.set({ statut: "traite", traitePar, traiteAt: new Date(), canalId: canalId ?? null })
|
||||
.where(eq(demandesMiseEnRelation.id, id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une demande de mise en relation par son ID.
|
||||
*/
|
||||
export async function getDemandeMiseEnRelationById(id: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return null;
|
||||
const rows = await db.select().from(demandesMiseEnRelation).where(eq(demandesMiseEnRelation.id, id));
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user