portail-associations/server/db.ts

1649 lines
56 KiB
TypeScript

import { eq, desc, and, sql, count, gte, lte, isNull, isNotNull, or, like, inArray } from "drizzle-orm";
import { drizzle } from "drizzle-orm/mysql2";
import { getAssociationCommuneVariants, normalizeAssociationCommune, type AssociationCommuneFilter } from "@shared/associationCommunes";
import {
matchAssociationDirectoryEntry,
normalizeDirectoryEmail,
normalizeDirectoryRna,
normalizeDirectorySiret,
type AssociationDirectoryMatchInput,
} from "./associationDirectoryMatcher";
import {
InsertUser, users,
associations, InsertAssociation, Association,
associationDirectoryEntries, InsertAssociationDirectoryEntry, AssociationDirectoryEntry,
associationDirectoryReviews, InsertAssociationDirectoryReview, AssociationDirectoryReview,
associationDirectoryUpdateProposals, InsertAssociationDirectoryUpdateProposal, AssociationDirectoryUpdateProposal,
associationInvitations, InsertAssociationInvitation, AssociationInvitation,
documents, InsertDocument, Document,
requests, InsertRequest, Request,
requestTemplates, InsertRequestTemplate, RequestTemplate,
requestHistory, InsertRequestHistory, RequestHistory,
responseTemplates, InsertResponseTemplate, ResponseTemplate,
auditLog, InsertAuditLog, AuditLog,
adminNotifications, InsertAdminNotification, AdminNotification,
portalSettings, InsertPortalSetting, PortalSetting,
operationalRecapServices, InsertOperationalRecapService, OperationalRecapService,
materialReturnFollowups, InsertMaterialReturnFollowup, MaterialReturnFollowup,
emailActionTokens, InsertEmailActionToken, EmailActionToken
} from "../drizzle/schema";
import { ENV } from './_core/env';
let _db: ReturnType<typeof drizzle> | null = null;
export function isDatabaseConfigured() {
return Boolean(process.env.DATABASE_URL);
}
export async function getDb() {
if (!_db && process.env.DATABASE_URL) {
try {
_db = drizzle(process.env.DATABASE_URL);
} catch (error) {
console.warn("[Database] Failed to connect:", error);
_db = null;
}
}
return _db;
}
// ============== USER FUNCTIONS ==============
export async function upsertUser(user: InsertUser): Promise<void> {
if (!user.openId) {
throw new Error("User openId is required for upsert");
}
const db = await getDb();
if (!db) {
console.warn("[Database] Cannot upsert user: database not available");
return;
}
try {
const values: InsertUser = {
openId: user.openId,
};
const updateSet: Record<string, unknown> = {};
const textFields = ["name", "email", "passwordHash", "loginMethod"] as const;
type TextField = (typeof textFields)[number];
const assignNullable = (field: TextField) => {
const value = user[field];
if (value === undefined) return;
const normalized = value ?? null;
values[field] = normalized;
updateSet[field] = normalized;
};
textFields.forEach(assignNullable);
if (user.lastSignedIn !== undefined) {
values.lastSignedIn = user.lastSignedIn;
updateSet.lastSignedIn = user.lastSignedIn;
}
if (user.role !== undefined) {
values.role = user.role;
updateSet.role = user.role;
} else if (user.openId === ENV.ownerOpenId) {
values.role = 'admin';
updateSet.role = 'admin';
}
if (!values.lastSignedIn) {
values.lastSignedIn = new Date();
}
if (Object.keys(updateSet).length === 0) {
updateSet.lastSignedIn = new Date();
}
await db.insert(users).values(values).onDuplicateKeyUpdate({
set: updateSet,
});
} catch (error) {
console.error("[Database] Failed to upsert user:", error);
throw error;
}
}
export async function getUserByOpenId(openId: string) {
const db = await getDb();
if (!db) {
console.warn("[Database] Cannot get user: database not available");
return undefined;
}
const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function getUserByEmail(email: string) {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(users).where(eq(users.email, email)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function getUserById(id: number) {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(users).where(eq(users.id, id)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function getUserByMfaChallengeToken(token: string) {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(users).where(eq(users.mfaChallengeToken, token)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function getUsersScheduledForPurge(referenceDate: Date = new Date()) {
const db = await getDb();
if (!db) return [];
return db.select().from(users).where(
and(
isNotNull(users.purgeScheduledAt),
lte(users.purgeScheduledAt, referenceDate),
isNull(users.purgedAt),
eq(users.legalHold, false)
)
);
}
export async function getAllUsers() {
const db = await getDb();
if (!db) return [];
return db.select().from(users).orderBy(desc(users.createdAt));
}
export async function getAdminUsers() {
const db = await getDb();
if (!db) return [];
return db.select().from(users).where(
or(
eq(users.role, 'accueil'),
eq(users.role, 'service_terrain'),
eq(users.role, 'logistique_controle'),
eq(users.role, 'admin'),
eq(users.role, 'directrice'),
eq(users.role, 'super_admin')
)
).orderBy(desc(users.createdAt));
}
export async function updateUser(id: number, data: Partial<InsertUser>): Promise<void> {
const db = await getDb();
if (!db) return;
await db.update(users).set(data).where(eq(users.id, id));
}
export async function deleteUser(id: number): Promise<void> {
const db = await getDb();
if (!db) return;
await db.delete(users).where(eq(users.id, id));
}
// ============== ASSOCIATION FUNCTIONS ==============
export async function getAssociationByUserId(userId: number): Promise<Association | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(associations).where(eq(associations.userId, userId)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function getAssociationById(id: number): Promise<Association | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(associations).where(eq(associations.id, id)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function getAssociationBySourceDirectoryEntryId(sourceDirectoryEntryId: number): Promise<Association | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select()
.from(associations)
.where(eq(associations.sourceDirectoryEntryId, sourceDirectoryEntryId))
.limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function createAssociation(data: InsertAssociation): Promise<number> {
const db = await getDb();
if (!db) throw new Error("Mode demo actif : la base de donnees n'est pas configuree.");
const result = await db.insert(associations).values(data);
return result[0].insertId;
}
export async function updateAssociation(id: number, data: Partial<InsertAssociation>): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Mode demo actif : la base de donnees n'est pas configuree.");
await db.update(associations).set(data).where(eq(associations.id, id));
}
export async function getAllAssociations(): Promise<Association[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(associations).orderBy(desc(associations.createdAt));
}
export async function searchAssociations(params: {
search?: string;
ville?: string;
profileComplete?: boolean;
isActive?: boolean;
limit?: number;
offset?: number;
}): Promise<{ data: Association[]; total: number }> {
const db = await getDb();
if (!db) return { data: [], total: 0 };
const conditions = [];
if (params.search) {
conditions.push(
or(
like(associations.nomAssociation, `%${params.search}%`),
like(associations.siret, `%${params.search}%`),
like(associations.ville, `%${params.search}%`)
)
);
}
if (params.ville) {
const communeVariants = getAssociationCommuneVariants(params.ville);
if (communeVariants.length > 0) {
conditions.push(or(...communeVariants.map(variant => eq(associations.ville, variant))));
} else {
conditions.push(eq(associations.ville, params.ville));
}
}
if (params.profileComplete !== undefined) {
conditions.push(eq(associations.profileComplete, params.profileComplete));
}
if (params.isActive !== undefined) {
conditions.push(eq(associations.isActive, params.isActive));
}
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
const [data, totalResult] = await Promise.all([
db.select()
.from(associations)
.where(whereClause)
.orderBy(desc(associations.createdAt))
.limit(params.limit || 20)
.offset(params.offset || 0),
db.select({ count: count() })
.from(associations)
.where(whereClause)
]);
return { data, total: totalResult[0]?.count || 0 };
}
export async function toggleAssociationStatus(id: number, isActive: boolean): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(associations).set({ isActive }).where(eq(associations.id, id));
}
export async function getAssociationCommuneCounts(): Promise<Record<AssociationCommuneFilter, number>> {
const db = await getDb();
const counts: Record<AssociationCommuneFilter, number> = {
all: 0,
kourou: 0,
sinnamary: 0,
iracoubo: 0,
saint_elie: 0,
};
if (!db) {
return counts;
}
const rows = await db.select({ ville: associations.ville }).from(associations);
counts.all = rows.length;
rows.forEach((row) => {
const commune = normalizeAssociationCommune(row.ville);
if (commune !== "all") {
counts[commune] += 1;
}
});
return counts;
}
// ============== ASSOCIATION DIRECTORY FUNCTIONS ==============
export async function getAssociationDirectoryEntryByNormalizedEmail(email: string): Promise<AssociationDirectoryEntry | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select()
.from(associationDirectoryEntries)
.where(eq(associationDirectoryEntries.emailOfficielNormalise, email))
.limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function getAssociationDirectoryEntryById(id: number): Promise<AssociationDirectoryEntry | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select()
.from(associationDirectoryEntries)
.where(eq(associationDirectoryEntries.id, id))
.limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function getAssociationDirectoryEntryByFingerprint(sourceFingerprint: string): Promise<AssociationDirectoryEntry | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select()
.from(associationDirectoryEntries)
.where(eq(associationDirectoryEntries.sourceFingerprint, sourceFingerprint))
.limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function getAssociationDirectoryEntriesSummary() {
const db = await getDb();
if (!db) {
return {
total: 0,
withEmail: 0,
withoutEmail: 0,
registered: 0,
unregistered: 0,
lastImportAt: null as Date | null,
};
}
const [totalResult, withEmailResult, withoutEmailResult, registeredResult, latest] = await Promise.all([
db.select({ count: count() }).from(associationDirectoryEntries),
db.select({ count: count() }).from(associationDirectoryEntries).where(sql`${associationDirectoryEntries.emailOfficielNormalise} is not null`),
db.select({ count: count() }).from(associationDirectoryEntries).where(sql`${associationDirectoryEntries.emailOfficielNormalise} is null`),
db.select({ count: count() })
.from(associationDirectoryEntries)
.innerJoin(associations, eq(associations.sourceDirectoryEntryId, associationDirectoryEntries.id)),
db.select().from(associationDirectoryEntries).orderBy(desc(associationDirectoryEntries.importedAt)).limit(1),
]);
const total = totalResult[0]?.count || 0;
const registered = registeredResult[0]?.count || 0;
return {
total,
withEmail: withEmailResult[0]?.count || 0,
withoutEmail: withoutEmailResult[0]?.count || 0,
registered,
unregistered: Math.max(total - registered, 0),
lastImportAt: latest[0]?.importedAt || null,
};
}
export async function listAssociationDirectoryEntries(limit = 50): Promise<AssociationDirectoryEntry[]> {
const db = await getDb();
if (!db) return [];
return db.select()
.from(associationDirectoryEntries)
.orderBy(desc(associationDirectoryEntries.updatedAt))
.limit(limit);
}
export async function listAssociationDirectoryEntriesWithStatus(params?: {
search?: string;
registrationStatus?: "all" | "registered" | "unregistered";
commune?: AssociationCommuneFilter;
thematique?: "all" | (typeof import("@shared/associationThematics").associationThematicValues)[number];
limit?: number;
}) {
const db = await getDb();
if (!db) return { data: [], total: 0 };
const conditions = [];
if (params?.search) {
conditions.push(
or(
like(associationDirectoryEntries.nomAssociation, `%${params.search}%`),
like(associationDirectoryEntries.emailOfficiel, `%${params.search}%`),
like(associationDirectoryEntries.siret, `%${params.search}%`),
like(associationDirectoryEntries.rna, `%${params.search}%`),
like(associationDirectoryEntries.ville, `%${params.search}%`)
)
);
}
if (params?.commune && params.commune !== "all") {
const communeVariants = getAssociationCommuneVariants(params.commune);
if (communeVariants.length > 0) {
conditions.push(or(...communeVariants.map((variant) => eq(associationDirectoryEntries.ville, variant))));
}
}
if (params?.thematique && params.thematique !== "all") {
conditions.push(like(associationDirectoryEntries.thematique, `%\"${params.thematique}\"%`));
}
if (params?.registrationStatus === "registered") {
conditions.push(isNotNull(associations.id));
} else if (params?.registrationStatus === "unregistered") {
conditions.push(isNull(associations.id));
}
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
const [rows, totalResult] = await Promise.all([
db
.select({
id: associationDirectoryEntries.id,
nomAssociation: associationDirectoryEntries.nomAssociation,
emailOfficiel: associationDirectoryEntries.emailOfficiel,
siret: associationDirectoryEntries.siret,
rna: associationDirectoryEntries.rna,
thematique: associationDirectoryEntries.thematique,
adresse: associationDirectoryEntries.adresse,
codePostal: associationDirectoryEntries.codePostal,
ville: associationDirectoryEntries.ville,
latitude: associationDirectoryEntries.latitude,
longitude: associationDirectoryEntries.longitude,
geoSource: associationDirectoryEntries.geoSource,
geoPrecision: associationDirectoryEntries.geoPrecision,
siteWeb: associationDirectoryEntries.siteWeb,
facebookUrl: associationDirectoryEntries.facebookUrl,
instagramUrl: associationDirectoryEntries.instagramUrl,
importedAt: associationDirectoryEntries.importedAt,
updatedAt: associationDirectoryEntries.updatedAt,
sourceFileName: associationDirectoryEntries.sourceFileName,
externalSourceStatus: associationDirectoryEntries.externalSourceStatus,
externalSourceLabel: associationDirectoryEntries.externalSourceLabel,
registeredAssociationId: associations.id,
registeredAssociationName: associations.nomAssociation,
registeredAt: associations.createdAt,
})
.from(associationDirectoryEntries)
.leftJoin(associations, eq(associations.sourceDirectoryEntryId, associationDirectoryEntries.id))
.where(whereClause)
.orderBy(desc(associationDirectoryEntries.updatedAt))
.limit(params?.limit || 50),
db
.select({ count: count() })
.from(associationDirectoryEntries)
.leftJoin(associations, eq(associations.sourceDirectoryEntryId, associationDirectoryEntries.id))
.where(whereClause),
]);
return {
data: rows.map((row) => ({
...row,
registered: Boolean(row.registeredAssociationId),
})),
total: totalResult[0]?.count || 0,
};
}
export async function upsertAssociationDirectoryEntry(data: InsertAssociationDirectoryEntry): Promise<"created" | "updated"> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const existing = data.emailOfficielNormalise
? await getAssociationDirectoryEntryByNormalizedEmail(data.emailOfficielNormalise)
: data.sourceFingerprint
? await getAssociationDirectoryEntryByFingerprint(data.sourceFingerprint)
: undefined;
if (existing) {
await db.update(associationDirectoryEntries)
.set({ ...data, importedAt: new Date() })
.where(eq(associationDirectoryEntries.id, existing.id));
return "updated";
}
await db.insert(associationDirectoryEntries).values(data);
return "created";
}
export async function updateAssociationDirectoryEntry(id: number, data: Partial<InsertAssociationDirectoryEntry>): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(associationDirectoryEntries).set(data).where(eq(associationDirectoryEntries.id, id));
}
export async function createAssociationDirectoryEntry(data: InsertAssociationDirectoryEntry): Promise<number> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(associationDirectoryEntries).values(data);
return result[0].insertId;
}
export async function listAssociationDirectoryEntriesForMatching() {
const db = await getDb();
if (!db) return [];
return db
.select({
id: associationDirectoryEntries.id,
nomAssociation: associationDirectoryEntries.nomAssociation,
emailOfficiel: associationDirectoryEntries.emailOfficiel,
siret: associationDirectoryEntries.siret,
rna: associationDirectoryEntries.rna,
ville: associationDirectoryEntries.ville,
telephone: associationDirectoryEntries.telephone,
nomRepresentant: associationDirectoryEntries.nomRepresentant,
})
.from(associationDirectoryEntries)
.where(eq(associationDirectoryEntries.isActive, true));
}
export async function findAssociationDirectoryMatch(input: AssociationDirectoryMatchInput) {
const entries = await listAssociationDirectoryEntriesForMatching();
return matchAssociationDirectoryEntry(entries, input);
}
export async function createAssociationDirectoryReview(data: InsertAssociationDirectoryReview): Promise<number> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(associationDirectoryReviews).values(data);
return result[0].insertId;
}
export async function getAssociationDirectoryReviewById(id: number): Promise<AssociationDirectoryReview | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(associationDirectoryReviews).where(eq(associationDirectoryReviews.id, id)).limit(1);
return result[0];
}
export async function getPendingAssociationDirectoryReviewByUserId(userId: number): Promise<AssociationDirectoryReview | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(associationDirectoryReviews)
.where(and(eq(associationDirectoryReviews.userId, userId), eq(associationDirectoryReviews.status, "pending")))
.orderBy(desc(associationDirectoryReviews.createdAt))
.limit(1);
return result[0];
}
export async function updateAssociationDirectoryReview(id: number, data: Partial<InsertAssociationDirectoryReview>): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(associationDirectoryReviews).set(data).where(eq(associationDirectoryReviews.id, id));
}
export async function listAssociationDirectoryReviews(params?: {
status?: "pending" | "linked" | "created" | "ignored";
sourceType?: "portal_signup" | "helloasso";
limit?: number;
}) {
const db = await getDb();
if (!db) return [];
const conditions = [];
if (params?.status) {
conditions.push(eq(associationDirectoryReviews.status, params.status));
}
if (params?.sourceType) {
conditions.push(eq(associationDirectoryReviews.sourceType, params.sourceType));
}
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
return db.select().from(associationDirectoryReviews)
.where(whereClause)
.orderBy(desc(associationDirectoryReviews.createdAt))
.limit(params?.limit || 50);
}
export async function createAssociationDirectoryUpdateProposal(data: InsertAssociationDirectoryUpdateProposal): Promise<number> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(associationDirectoryUpdateProposals).values(data);
return result[0].insertId;
}
export async function updateAssociationDirectoryUpdateProposal(
id: number,
data: Partial<InsertAssociationDirectoryUpdateProposal>
): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(associationDirectoryUpdateProposals).set(data).where(eq(associationDirectoryUpdateProposals.id, id));
}
export async function getAssociationDirectoryUpdateProposalById(
id: number
): Promise<AssociationDirectoryUpdateProposal | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(associationDirectoryUpdateProposals)
.where(eq(associationDirectoryUpdateProposals.id, id))
.limit(1);
return result[0];
}
export async function getLatestPendingAssociationDirectoryUpdateProposalByEntryId(
directoryEntryId: number,
sourceType?: "official_registry" | "helloasso"
): Promise<AssociationDirectoryUpdateProposal | undefined> {
const db = await getDb();
if (!db) return undefined;
const conditions = [
eq(associationDirectoryUpdateProposals.directoryEntryId, directoryEntryId),
eq(associationDirectoryUpdateProposals.status, "pending"),
];
if (sourceType) {
conditions.push(eq(associationDirectoryUpdateProposals.sourceType, sourceType));
}
const result = await db.select().from(associationDirectoryUpdateProposals)
.where(and(...conditions))
.orderBy(desc(associationDirectoryUpdateProposals.createdAt))
.limit(1);
return result[0];
}
export async function listAssociationDirectoryUpdateProposals(params?: {
directoryEntryId?: number;
status?: "pending" | "applied" | "dismissed";
sourceType?: "official_registry" | "helloasso";
limit?: number;
}) {
const db = await getDb();
if (!db) return [];
const conditions = [];
if (params?.directoryEntryId) {
conditions.push(eq(associationDirectoryUpdateProposals.directoryEntryId, params.directoryEntryId));
}
if (params?.status) {
conditions.push(eq(associationDirectoryUpdateProposals.status, params.status));
}
if (params?.sourceType) {
conditions.push(eq(associationDirectoryUpdateProposals.sourceType, params.sourceType));
}
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
return db.select().from(associationDirectoryUpdateProposals)
.where(whereClause)
.orderBy(desc(associationDirectoryUpdateProposals.createdAt))
.limit(params?.limit || 50);
}
export async function getAssociationDirectoryEntryDetails(id: number) {
const entry = await getAssociationDirectoryEntryById(id);
if (!entry) return null;
const linkedAssociation = await getAssociationBySourceDirectoryEntryId(id);
if (!linkedAssociation) {
return {
entry,
association: null,
documents: [],
requests: [],
};
}
const [documents, requests] = await Promise.all([
getDocumentsByAssociationId(linkedAssociation.id),
getRequestsByAssociationId(linkedAssociation.id),
]);
return {
entry,
association: linkedAssociation,
documents,
requests,
};
}
export async function listAssociationDirectoryMapEntries(params?: {
search?: string;
commune?: AssociationCommuneFilter;
registrationStatus?: "all" | "registered" | "unregistered";
thematique?: "all" | (typeof import("@shared/associationThematics").associationThematicValues)[number];
limit?: number;
}) {
const db = await getDb();
if (!db) return { data: [], total: 0 };
const conditions: Array<any> = [
isNotNull(associationDirectoryEntries.latitude),
isNotNull(associationDirectoryEntries.longitude),
];
if (params?.search) {
conditions.push(
or(
like(associationDirectoryEntries.nomAssociation, `%${params.search}%`),
like(associationDirectoryEntries.siret, `%${params.search}%`),
like(associationDirectoryEntries.rna, `%${params.search}%`),
like(associationDirectoryEntries.ville, `%${params.search}%`),
like(associationDirectoryEntries.objetAssociation, `%${params.search}%`)
)
);
}
if (params?.commune && params.commune !== "all") {
const communeVariants = getAssociationCommuneVariants(params.commune);
if (communeVariants.length > 0) {
conditions.push(or(...communeVariants.map((variant) => eq(associationDirectoryEntries.ville, variant))));
}
}
if (params?.thematique && params.thematique !== "all") {
conditions.push(like(associationDirectoryEntries.thematique, `%\"${params.thematique}\"%`));
}
if (params?.registrationStatus === "registered") {
conditions.push(isNotNull(associations.id));
} else if (params?.registrationStatus === "unregistered") {
conditions.push(isNull(associations.id));
}
const whereClause = and(...conditions);
const [rows, totalResult] = await Promise.all([
db
.select({
id: associationDirectoryEntries.id,
nomAssociation: associationDirectoryEntries.nomAssociation,
ville: associationDirectoryEntries.ville,
thematique: associationDirectoryEntries.thematique,
objetAssociation: associationDirectoryEntries.objetAssociation,
siteWeb: associationDirectoryEntries.siteWeb,
latitude: associationDirectoryEntries.latitude,
longitude: associationDirectoryEntries.longitude,
geoSource: associationDirectoryEntries.geoSource,
geoPrecision: associationDirectoryEntries.geoPrecision,
externalSourceLabel: associationDirectoryEntries.externalSourceLabel,
registeredAssociationId: associations.id,
registeredAt: associations.createdAt,
})
.from(associationDirectoryEntries)
.leftJoin(associations, eq(associations.sourceDirectoryEntryId, associationDirectoryEntries.id))
.where(whereClause)
.orderBy(desc(associationDirectoryEntries.updatedAt))
.limit(params?.limit || 500),
db
.select({ count: count() })
.from(associationDirectoryEntries)
.leftJoin(associations, eq(associations.sourceDirectoryEntryId, associationDirectoryEntries.id))
.where(whereClause),
]);
return {
data: rows.map((row) => ({
...row,
latitude: row.latitude ? Number(row.latitude) : null,
longitude: row.longitude ? Number(row.longitude) : null,
registered: Boolean(row.registeredAssociationId),
publicUrl: `/associations/${row.id}`,
})),
total: totalResult[0]?.count || 0,
};
}
// ============== DOCUMENT FUNCTIONS ==============
export async function getDocumentsByAssociationId(associationId: number): Promise<Document[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(documents).where(eq(documents.associationId, associationId)).orderBy(desc(documents.uploadedAt));
}
export async function getDocumentById(id: number): Promise<Document | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(documents).where(eq(documents.id, id)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function createDocument(data: InsertDocument): Promise<number> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(documents).values(data);
return result[0].insertId;
}
export async function deleteDocument(id: number): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.delete(documents).where(eq(documents.id, id));
}
// ============== REQUEST FUNCTIONS ==============
export async function getRequestsByAssociationId(associationId: number): Promise<Request[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(requests).where(eq(requests.associationId, associationId)).orderBy(desc(requests.createdAt));
}
export async function getRequestById(id: number): Promise<Request | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(requests).where(eq(requests.id, id)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function createRequest(data: InsertRequest): Promise<number> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(requests).values(data);
return result[0].insertId;
}
export async function updateRequest(id: number, data: Partial<InsertRequest>): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(requests).set(data).where(eq(requests.id, id));
}
export async function getAllRequests(): Promise<Request[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(requests).orderBy(desc(requests.createdAt));
}
export async function getRequestsByStatus(status: string): Promise<Request[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(requests).where(eq(requests.status, status as any)).orderBy(desc(requests.createdAt));
}
export async function searchRequests(params: {
search?: string;
type?: string;
status?: string;
priority?: string;
assigneA?: number;
associationId?: number;
dateFrom?: Date;
dateTo?: Date;
limit?: number;
offset?: number;
}): Promise<{ data: Request[]; total: number }> {
const db = await getDb();
if (!db) return { data: [], total: 0 };
const conditions = [];
if (params.search) {
conditions.push(like(requests.titre, `%${params.search}%`));
}
if (params.type) {
conditions.push(eq(requests.type, params.type as any));
}
if (params.status) {
conditions.push(eq(requests.status, params.status as any));
}
if (params.priority) {
conditions.push(eq(requests.priority, params.priority as any));
}
if (params.assigneA) {
conditions.push(eq(requests.assigneA, params.assigneA));
}
if (params.associationId) {
conditions.push(eq(requests.associationId, params.associationId));
}
if (params.dateFrom) {
conditions.push(gte(requests.createdAt, params.dateFrom));
}
if (params.dateTo) {
conditions.push(lte(requests.createdAt, params.dateTo));
}
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
const [data, totalResult] = await Promise.all([
db.select()
.from(requests)
.where(whereClause)
.orderBy(desc(requests.createdAt))
.limit(params.limit || 20)
.offset(params.offset || 0),
db.select({ count: count() })
.from(requests)
.where(whereClause)
]);
return { data, total: totalResult[0]?.count || 0 };
}
export async function getPendingRequests(): Promise<Request[]> {
const db = await getDb();
if (!db) return [];
return db.select()
.from(requests)
.where(eq(requests.status, 'soumise'))
.orderBy(requests.dateSubmission);
}
export async function getOverdueRequests(): Promise<Request[]> {
const db = await getDb();
if (!db) return [];
const now = new Date();
return db.select()
.from(requests)
.where(
and(
or(eq(requests.status, 'soumise'), eq(requests.status, 'en_cours_traitement')),
lte(requests.dateLimiteTraitement, now)
)
)
.orderBy(requests.dateLimiteTraitement);
}
export async function assignRequest(id: number, adminId: number): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(requests).set({
assigneA: adminId,
status: 'en_cours_traitement'
}).where(eq(requests.id, id));
}
// ============== REQUEST HISTORY FUNCTIONS ==============
export async function createRequestHistory(data: InsertRequestHistory): Promise<number> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(requestHistory).values(data);
return result[0].insertId;
}
export async function getRequestHistoryByRequestId(requestId: number): Promise<RequestHistory[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(requestHistory).where(eq(requestHistory.requestId, requestId)).orderBy(desc(requestHistory.createdAt));
}
// ============== RESPONSE TEMPLATE FUNCTIONS ==============
export async function getAllResponseTemplates(): Promise<ResponseTemplate[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(responseTemplates).where(eq(responseTemplates.actif, true)).orderBy(responseTemplates.nom);
}
export async function getResponseTemplateById(id: number): Promise<ResponseTemplate | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(responseTemplates).where(eq(responseTemplates.id, id)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function createResponseTemplate(data: InsertResponseTemplate): Promise<number> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(responseTemplates).values(data);
return result[0].insertId;
}
export async function updateResponseTemplate(id: number, data: Partial<InsertResponseTemplate>): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(responseTemplates).set(data).where(eq(responseTemplates.id, id));
}
export async function deleteResponseTemplate(id: number): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(responseTemplates).set({ actif: false }).where(eq(responseTemplates.id, id));
}
// ============== REQUEST TEMPLATE FUNCTIONS ==============
export async function getAllRequestTemplates(): Promise<RequestTemplate[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(requestTemplates).where(eq(requestTemplates.actif, true)).orderBy(requestTemplates.nom);
}
export async function getRequestTemplateById(id: number): Promise<RequestTemplate | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(requestTemplates).where(eq(requestTemplates.id, id)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function getRequestTemplateByType(type: string): Promise<RequestTemplate | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(requestTemplates).where(eq(requestTemplates.type, type as any)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function createRequestTemplate(data: InsertRequestTemplate): Promise<number> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(requestTemplates).values(data);
return result[0].insertId;
}
export async function updateRequestTemplate(id: number, data: Partial<InsertRequestTemplate>): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(requestTemplates).set(data).where(eq(requestTemplates.id, id));
}
// ============== AUDIT LOG FUNCTIONS ==============
export async function createAuditLog(data: InsertAuditLog): Promise<number> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(auditLog).values(data);
return result[0].insertId;
}
export async function getAuditLogs(params: {
userId?: number;
entityType?: string;
entityId?: number;
limit?: number;
offset?: number;
}): Promise<{ data: AuditLog[]; total: number }> {
const db = await getDb();
if (!db) return { data: [], total: 0 };
const conditions = [];
if (params.userId) conditions.push(eq(auditLog.userId, params.userId));
if (params.entityType) conditions.push(eq(auditLog.entityType, params.entityType));
if (params.entityId) conditions.push(eq(auditLog.entityId, params.entityId));
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
const [data, totalResult] = await Promise.all([
db.select()
.from(auditLog)
.where(whereClause)
.orderBy(desc(auditLog.createdAt))
.limit(params.limit || 50)
.offset(params.offset || 0),
db.select({ count: count() })
.from(auditLog)
.where(whereClause)
]);
return { data, total: totalResult[0]?.count || 0 };
}
// ============== ADMIN NOTIFICATIONS FUNCTIONS ==============
export async function createAdminNotification(data: InsertAdminNotification): Promise<number> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(adminNotifications).values(data);
return result[0].insertId;
}
export async function getAdminNotifications(userId: number, unreadOnly: boolean = false): Promise<AdminNotification[]> {
const db = await getDb();
if (!db) return [];
const conditions = [
or(eq(adminNotifications.userId, userId), isNull(adminNotifications.userId))
];
if (unreadOnly) {
conditions.push(eq(adminNotifications.lu, false));
}
return db.select()
.from(adminNotifications)
.where(and(...conditions))
.orderBy(desc(adminNotifications.createdAt))
.limit(50);
}
export async function markNotificationAsRead(id: number): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(adminNotifications).set({ lu: true }).where(eq(adminNotifications.id, id));
}
export async function markAllNotificationsAsRead(userId: number): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(adminNotifications)
.set({ lu: true })
.where(
and(
or(eq(adminNotifications.userId, userId), isNull(adminNotifications.userId)),
eq(adminNotifications.lu, false)
)
);
}
export async function getUnreadNotificationCount(userId: number): Promise<number> {
const db = await getDb();
if (!db) return 0;
const result = await db.select({ count: count() })
.from(adminNotifications)
.where(
and(
or(eq(adminNotifications.userId, userId), isNull(adminNotifications.userId)),
eq(adminNotifications.lu, false)
)
);
return result[0]?.count || 0;
}
// ============== PORTAL SETTINGS FUNCTIONS ==============
export async function getPortalSetting(key: string): Promise<string | null> {
const db = await getDb();
if (!db) return null;
const result = await db.select().from(portalSettings).where(eq(portalSettings.cle, key)).limit(1);
return result.length > 0 ? result[0].valeur : null;
}
export async function getPortalSettingRecord(key: string): Promise<PortalSetting | null> {
const db = await getDb();
if (!db) return null;
const result = await db.select().from(portalSettings).where(eq(portalSettings.cle, key)).limit(1);
return result[0] ?? null;
}
export async function setPortalSetting(key: string, value: string, description?: string): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.insert(portalSettings)
.values({ cle: key, valeur: value, description })
.onDuplicateKeyUpdate({ set: { valeur: value, description } });
}
export async function getAllPortalSettings(): Promise<PortalSetting[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(portalSettings).orderBy(portalSettings.cle);
}
// ============== OPERATIONAL RECAP SERVICES FUNCTIONS ==============
export async function getAllOperationalRecapServices(): Promise<OperationalRecapService[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(operationalRecapServices).orderBy(operationalRecapServices.label);
}
export async function getOperationalRecapServiceById(id: number): Promise<OperationalRecapService | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(operationalRecapServices).where(eq(operationalRecapServices.id, id)).limit(1);
return result[0];
}
export async function createOperationalRecapService(data: InsertOperationalRecapService): Promise<number> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(operationalRecapServices).values(data);
return result[0].insertId;
}
export async function updateOperationalRecapService(id: number, data: Partial<InsertOperationalRecapService>): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(operationalRecapServices).set(data).where(eq(operationalRecapServices.id, id));
}
export async function deleteOperationalRecapService(id: number): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.delete(operationalRecapServices).where(eq(operationalRecapServices.id, id));
}
// ============== STATISTICS FUNCTIONS ==============
export async function getDashboardStats() {
const db = await getDb();
if (!db) return null;
const now = new Date();
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const [
totalAssociations,
activeAssociations,
totalRequests,
pendingRequests,
validatedRequests,
rejectedRequests,
newAssociationsThisMonth,
newRequestsThisMonth,
requestsThisWeek,
overdueRequests
] = await Promise.all([
db.select({ count: count() }).from(associations),
db.select({ count: count() }).from(associations).where(eq(associations.isActive, true)),
db.select({ count: count() }).from(requests),
db.select({ count: count() }).from(requests).where(eq(requests.status, 'soumise')),
db.select({ count: count() }).from(requests).where(eq(requests.status, 'validee')),
db.select({ count: count() }).from(requests).where(eq(requests.status, 'refusee')),
db.select({ count: count() }).from(associations).where(gte(associations.createdAt, thirtyDaysAgo)),
db.select({ count: count() }).from(requests).where(gte(requests.createdAt, thirtyDaysAgo)),
db.select({ count: count() }).from(requests).where(gte(requests.createdAt, sevenDaysAgo)),
db.select({ count: count() }).from(requests).where(
and(
or(eq(requests.status, 'soumise'), eq(requests.status, 'en_cours_traitement')),
lte(requests.dateLimiteTraitement, now)
)
)
]);
const totalValidatedAndRejected = (validatedRequests[0]?.count || 0) + (rejectedRequests[0]?.count || 0);
const acceptanceRate = totalValidatedAndRejected > 0
? Math.round((validatedRequests[0]?.count || 0) / totalValidatedAndRejected * 100)
: 0;
return {
totalAssociations: totalAssociations[0]?.count || 0,
activeAssociations: activeAssociations[0]?.count || 0,
totalRequests: totalRequests[0]?.count || 0,
pendingRequests: pendingRequests[0]?.count || 0,
validatedRequests: validatedRequests[0]?.count || 0,
rejectedRequests: rejectedRequests[0]?.count || 0,
newAssociationsThisMonth: newAssociationsThisMonth[0]?.count || 0,
newRequestsThisMonth: newRequestsThisMonth[0]?.count || 0,
requestsThisWeek: requestsThisWeek[0]?.count || 0,
overdueRequests: overdueRequests[0]?.count || 0,
acceptanceRate
};
}
export async function getRequestsPerMonth(months: number = 12) {
const db = await getDb();
if (!db) return [];
const result = await db.execute(sql`
SELECT
DATE_FORMAT(createdAt, '%Y-%m') as month,
COUNT(*) as total,
SUM(CASE WHEN status = 'validee' THEN 1 ELSE 0 END) as validated,
SUM(CASE WHEN status = 'refusee' THEN 1 ELSE 0 END) as rejected,
SUM(CASE WHEN status IN ('soumise', 'en_cours_traitement') THEN 1 ELSE 0 END) as pending
FROM requests
WHERE createdAt >= DATE_SUB(NOW(), INTERVAL ${months} MONTH)
GROUP BY DATE_FORMAT(createdAt, '%Y-%m')
ORDER BY month ASC
`);
return (result as any)[0] as any[];
}
export async function getAssociationsPerMonth(months: number = 12) {
const db = await getDb();
if (!db) return [];
const result = await db.execute(sql`
SELECT
DATE_FORMAT(createdAt, '%Y-%m') as month,
COUNT(*) as total
FROM associations
WHERE createdAt >= DATE_SUB(NOW(), INTERVAL ${months} MONTH)
GROUP BY DATE_FORMAT(createdAt, '%Y-%m')
ORDER BY month ASC
`);
return (result as any)[0] as any[];
}
export async function getRequestsByTypeStats() {
const db = await getDb();
if (!db) return [];
const result = await db.execute(sql`
SELECT
type,
COUNT(*) as total,
SUM(CASE WHEN status = 'validee' THEN 1 ELSE 0 END) as validated,
SUM(CASE WHEN montantAccorde IS NOT NULL THEN montantAccorde ELSE 0 END) as totalMontantAccorde
FROM requests
GROUP BY type
ORDER BY total DESC
`);
return (result as any)[0] as any[];
}
export async function getAverageProcessingTime() {
const db = await getDb();
if (!db) return null;
const result = await db.execute(sql`
SELECT
AVG(TIMESTAMPDIFF(DAY, dateSubmission, dateTraitement)) as avgDays
FROM requests
WHERE dateSubmission IS NOT NULL
AND dateTraitement IS NOT NULL
AND status IN ('validee', 'refusee')
`);
const rows = (result as any)[0] as any[];
return rows[0]?.avgDays || null;
}
export type AssociationInvitationStatusSummary = {
directoryEntryId: number;
status: "never_sent" | "sent" | "expired" | "accepted";
sentAt: Date | null;
expiresAt: Date | null;
usedAt: Date | null;
revokedAt: Date | null;
emailSent: boolean;
deliveryMode: "email" | "manual_link" | null;
};
// ============== ASSOCIATION INVITATION FUNCTIONS ==============
export async function createAssociationInvitation(data: InsertAssociationInvitation): Promise<number> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(associationInvitations).values(data);
return result[0].insertId;
}
export async function getAssociationInvitationByToken(token: string): Promise<AssociationInvitation | null> {
const db = await getDb();
if (!db) return null;
const result = await db.select()
.from(associationInvitations)
.where(eq(associationInvitations.token, token))
.limit(1);
return result[0] ?? null;
}
export async function getActiveAssociationInvitationByDirectoryEntryId(directoryEntryId: number): Promise<AssociationInvitation | null> {
const db = await getDb();
if (!db) return null;
const result = await db.select()
.from(associationInvitations)
.where(and(
eq(associationInvitations.directoryEntryId, directoryEntryId),
isNull(associationInvitations.usedAt),
isNull(associationInvitations.revokedAt),
gte(associationInvitations.expiresAt, new Date())
))
.orderBy(desc(associationInvitations.createdAt))
.limit(1);
return result[0] ?? null;
}
export async function getLatestAssociationInvitationByDirectoryEntryId(directoryEntryId: number): Promise<AssociationInvitation | null> {
const db = await getDb();
if (!db) return null;
const result = await db.select()
.from(associationInvitations)
.where(eq(associationInvitations.directoryEntryId, directoryEntryId))
.orderBy(desc(associationInvitations.createdAt))
.limit(1);
return result[0] ?? null;
}
export async function revokeAssociationInvitationsByDirectoryEntryId(directoryEntryId: number): Promise<void> {
const db = await getDb();
if (!db) return;
await db.update(associationInvitations)
.set({ revokedAt: new Date() })
.where(and(
eq(associationInvitations.directoryEntryId, directoryEntryId),
isNull(associationInvitations.usedAt),
isNull(associationInvitations.revokedAt)
));
}
export async function markAssociationInvitationUsed(token: string, acceptedByUserId: number): Promise<void> {
const db = await getDb();
if (!db) return;
await db.update(associationInvitations)
.set({
usedAt: new Date(),
acceptedByUserId,
})
.where(eq(associationInvitations.token, token));
}
export async function getAssociationInvitationSummariesForDirectoryEntryIds(directoryEntryIds: number[]): Promise<Record<number, AssociationInvitationStatusSummary>> {
if (directoryEntryIds.length === 0) return {};
const db = await getDb();
if (!db) return {};
const rows = await db.select()
.from(associationInvitations)
.where(inArray(associationInvitations.directoryEntryId, directoryEntryIds))
.orderBy(desc(associationInvitations.createdAt));
const now = new Date();
const summaries: Record<number, AssociationInvitationStatusSummary> = {};
rows.forEach((invitation) => {
if (summaries[invitation.directoryEntryId]) return;
let status: AssociationInvitationStatusSummary["status"] = "expired";
if (invitation.usedAt) {
status = "accepted";
} else if (!invitation.revokedAt && invitation.expiresAt >= now) {
status = "sent";
}
summaries[invitation.directoryEntryId] = {
directoryEntryId: invitation.directoryEntryId,
status,
sentAt: invitation.sentAt,
expiresAt: invitation.expiresAt,
usedAt: invitation.usedAt,
revokedAt: invitation.revokedAt,
emailSent: invitation.emailSent,
deliveryMode: invitation.deliveryMode,
};
});
directoryEntryIds.forEach((directoryEntryId) => {
if (!summaries[directoryEntryId]) {
summaries[directoryEntryId] = {
directoryEntryId,
status: "never_sent",
sentAt: null,
expiresAt: null,
usedAt: null,
revokedAt: null,
emailSent: false,
deliveryMode: null,
};
}
});
return summaries;
}
// ============== EMAIL ACTION TOKEN FUNCTIONS ==============
export async function createEmailActionToken(data: {
token: string;
requestId: number;
action: 'validee' | 'refusee' | 'acceptation_devis_salle' | 'refus_devis_salle';
expiresAt: Date;
}) {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.insert(emailActionTokens).values({
token: data.token,
requestId: data.requestId,
action: data.action,
expiresAt: data.expiresAt,
});
return data.token;
}
export async function getEmailActionToken(token: string) {
const db = await getDb();
if (!db) return null;
const result = await db.select().from(emailActionTokens).where(eq(emailActionTokens.token, token)).limit(1);
return result.length > 0 ? result[0] : null;
}
export async function markTokenAsUsed(token: string, userId: number) {
const db = await getDb();
if (!db) return;
await db.update(emailActionTokens)
.set({ used: true, usedAt: new Date(), usedBy: userId })
.where(eq(emailActionTokens.token, token));
}
// ============== RESERVATION CALENDAR FUNCTIONS ==============
export async function getApprovedReservations(): Promise<(Request & { associationName: string; associationEmail: string | null })[]> {
const db = await getDb();
if (!db) return [];
const result = await db
.select({
id: requests.id,
associationId: requests.associationId,
type: requests.type,
titre: requests.titre,
description: requests.description,
formData: requests.formData,
status: requests.status,
priority: requests.priority,
montantDemande: requests.montantDemande,
montantAccorde: requests.montantAccorde,
dateSubmission: requests.dateSubmission,
dateTraitement: requests.dateTraitement,
dateLimiteTraitement: requests.dateLimiteTraitement,
assigneA: requests.assigneA,
traitePar: requests.traitePar,
commentaireAdmin: requests.commentaireAdmin,
documentsJoints: requests.documentsJoints,
createdAt: requests.createdAt,
updatedAt: requests.updatedAt,
associationName: associations.nomAssociation,
associationEmail: associations.emailContact,
})
.from(requests)
.innerJoin(associations, eq(requests.associationId, associations.id))
.where(
and(
eq(requests.type, 'demande_salle'),
eq(requests.status, 'validee')
)
)
.orderBy(desc(requests.dateTraitement));
return result as any;
}
export async function updateRequestSalles(id: number, newFormData: string): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(requests)
.set({ formData: newFormData, updatedAt: new Date() })
.where(eq(requests.id, id));
}
export async function getUserByAssociationId(associationId: number): Promise<{ userId: number } | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select({ userId: associations.userId }).from(associations).where(eq(associations.id, associationId)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function deleteRequest(id: number): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
// Delete associated history first
await db.delete(requestHistory).where(eq(requestHistory.requestId, id));
// Delete the request
await db.delete(requests).where(eq(requests.id, id));
}
export async function getAssociationByRequestId(requestId: number): Promise<Association | undefined> {
const db = await getDb();
if (!db) return undefined;
const request = await getRequestById(requestId);
if (!request) return undefined;
return getAssociationById(request.associationId);
}
// ============== MATERIAL RETURN FOLLOWUPS ==============
export async function getMaterialReturnFollowupByRequestId(requestId: number): Promise<MaterialReturnFollowup | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(materialReturnFollowups).where(eq(materialReturnFollowups.requestId, requestId)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function getMaterialReturnFollowupByToken(token: string): Promise<MaterialReturnFollowup | undefined> {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(materialReturnFollowups).where(eq(materialReturnFollowups.uploadToken, token)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function upsertMaterialReturnFollowup(
requestId: number,
data: Omit<InsertMaterialReturnFollowup, "requestId">
): Promise<number> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const existing = await getMaterialReturnFollowupByRequestId(requestId);
if (existing) {
await db.update(materialReturnFollowups)
.set(data)
.where(eq(materialReturnFollowups.requestId, requestId));
return existing.id;
}
const result = await db.insert(materialReturnFollowups).values({
requestId,
...data,
});
return result[0].insertId;
}
export async function updateMaterialReturnFollowup(id: number, data: Partial<InsertMaterialReturnFollowup>): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(materialReturnFollowups).set(data).where(eq(materialReturnFollowups.id, id));
}
export async function listPendingMaterialReturnFollowups(now: Date): Promise<MaterialReturnFollowup[]> {
const db = await getDb();
if (!db) return [];
return db.select()
.from(materialReturnFollowups)
.where(
and(
eq(materialReturnFollowups.status, "planifie"),
lte(materialReturnFollowups.plannedSendAt, now)
)
)
.orderBy(materialReturnFollowups.plannedSendAt);
}
export async function listMaterialReturnFollowupsToEscalate(): Promise<MaterialReturnFollowup[]> {
const db = await getDb();
if (!db) return [];
return db.select()
.from(materialReturnFollowups)
.where(eq(materialReturnFollowups.status, "en_attente"));
}