import { and, asc, count, desc, eq, sql } from "drizzle-orm"; import { db } from "@/db"; import { categories, members, promotions } from "@/db/schema"; /** * Promotion réellement visible par le public : validée **et** dans sa période * de validité. * * Les deux bornes sont facultatives — une offre sans date reste affichée sans * limite. La journée de référence est celle de l'association, pas celle du * serveur : un conteneur en UTC retirerait sinon une offre « valable jusqu'au * 30 juin » deux heures trop tôt. */ const VISIBLE_PROMO = and( eq(promotions.status, "live"), sql`(${promotions.startsOn} is null or ${promotions.startsOn} <= (current_timestamp at time zone 'Europe/Paris')::date)`, sql`(${promotions.endsOn} is null or ${promotions.endsOn} >= (current_timestamp at time zone 'Europe/Paris')::date)` ); export async function getActiveMembersWithCategory() { return db .select({ id: members.id, name: members.name, description: members.description, city: members.city, address: members.address, coverUrl: members.coverUrl, logoUrl: members.logoUrl, categoryLabel: categories.label, accent: categories.accent, }) .from(members) .leftJoin(categories, eq(members.categoryId, categories.id)) .where(eq(members.status, "active")) .orderBy(asc(members.name)); } export async function getAllCategories() { return db .select({ id: categories.id, label: categories.label, accent: categories.accent }) .from(categories) .orderBy(asc(categories.sort)); } export async function getLiveBadgesByMember() { return db .select({ memberId: promotions.memberId, badge: promotions.badge }) .from(promotions) .where(VISIBLE_PROMO); } export async function getCategoriesWithCounts(limit = 6) { const rows = await db .select({ id: categories.id, slug: categories.slug, label: categories.label, accent: categories.accent, tint: categories.tint, sort: categories.sort, memberCount: count(members.id), }) .from(categories) .leftJoin( members, and(eq(members.categoryId, categories.id), eq(members.status, "active")) ) .groupBy(categories.id) .orderBy(categories.sort) .limit(limit); return rows; } export async function getLivePromotions(limit = 6) { const rows = await db .select({ id: promotions.id, title: promotions.title, text: promotions.text, category: promotions.category, badge: promotions.badge, imageUrl: promotions.imageUrl, validUntil: promotions.validUntil, startsOn: promotions.startsOn, endsOn: promotions.endsOn, memberId: promotions.memberId, memberName: members.name, memberCity: members.city, memberLogoUrl: members.logoUrl, }) .from(promotions) .leftJoin(members, eq(promotions.memberId, members.id)) .where(VISIBLE_PROMO) .orderBy(desc(promotions.createdAt)) .limit(limit); return rows; } export async function getPublicMember(id: number) { const [m] = await db .select({ id: members.id, name: members.name, email: members.email, contactEmail: members.contactEmail, description: members.description, city: members.city, address: members.address, status: members.status, logoUrl: members.logoUrl, coverUrl: members.coverUrl, phone: members.phone, website: members.website, postalCode: members.postalCode, memberSince: members.memberSince, hours: members.hours, tags: members.tags, categoryLabel: categories.label, categorySlug: categories.slug, accent: categories.accent, tint: categories.tint, }) .from(members) .leftJoin(categories, eq(members.categoryId, categories.id)) .where(eq(members.id, id)); return m ?? null; } export async function getMemberLivePromotions(memberId: number) { return db .select({ id: promotions.id, title: promotions.title, text: promotions.text, category: promotions.category, badge: promotions.badge, imageUrl: promotions.imageUrl, validUntil: promotions.validUntil, startsOn: promotions.startsOn, endsOn: promotions.endsOn, }) .from(promotions) .where(and(eq(promotions.memberId, memberId), VISIBLE_PROMO)) .orderBy(desc(promotions.createdAt)); } // Tous les adhérents actifs, tournés équitablement : l'ordre pivote d'un cran // par heure pour que chacun passe en tête à tour de rôle au fil du temps. export async function getRotatingActiveMembers() { const rows = await db .select({ id: members.id, name: members.name, description: members.description, city: members.city, address: members.address, coverUrl: members.coverUrl, logoUrl: members.logoUrl, categoryLabel: categories.label, accent: categories.accent, }) .from(members) .leftJoin(categories, eq(members.categoryId, categories.id)) .where(eq(members.status, "active")) .orderBy(asc(members.id)); const n = rows.length; if (n <= 1) return rows; const offset = Math.floor(Date.now() / 3_600_000) % n; return [...rows.slice(offset), ...rows.slice(0, offset)]; } export async function getHighlightedMembers(limit = 3) { const rows = await db .select({ id: members.id, name: members.name, description: members.description, city: members.city, address: members.address, categoryLabel: categories.label, accent: categories.accent, }) .from(members) .leftJoin(categories, eq(members.categoryId, categories.id)) .where(and(eq(members.highlighted, true), eq(members.status, "active"))) .limit(limit); return rows; } /** Nombre d'adhérents validés (statut `active`) : affiché sur l'accueil. */ export async function getActiveMemberCount(): Promise { const [row] = await db .select({ total: count(members.id) }) .from(members) .where(eq(members.status, "active")); return Number(row?.total ?? 0); } /** Une catégorie par son slug d'URL, ou `null`. */ export async function getCategoryBySlug(slug: string) { const [row] = await db .select({ id: categories.id, slug: categories.slug, label: categories.label, accent: categories.accent, tint: categories.tint, }) .from(categories) .where(eq(categories.slug, slug)); return row ?? null; } /** Adhérents actifs d'une catégorie, pour la page métier de l'annuaire. */ export async function getActiveMembersByCategory(categoryId: number) { return db .select({ id: members.id, name: members.name, description: members.description, city: members.city, address: members.address, coverUrl: members.coverUrl, logoUrl: members.logoUrl, categoryLabel: categories.label, accent: categories.accent, }) .from(members) .leftJoin(categories, eq(members.categoryId, categories.id)) .where(and(eq(members.status, "active"), eq(members.categoryId, categoryId))) .orderBy(asc(members.name)); } /** Catégories ayant au moins un adhérent actif, avec leur effectif. */ export async function getCategoriesInUse() { const rows = await getCategoriesWithCounts(1000); return rows.filter((c) => Number(c.memberCount) > 0); } /** Promotions réellement affichées aujourd'hui — le chiffre du tableau de bord. */ export async function countVisiblePromotions(): Promise { const [row] = await db.select({ n: count() }).from(promotions).where(VISIBLE_PROMO); return row?.n ?? 0; }