mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-23 15:18:04 +02:00
Promotions : publication programmée et affichage limité à la période
Deux comportements demandés : 1. Une promotion n'apparaît sur le site que pendant sa période de validité. Les lectures publiques passent par `VISIBLE_PROMO` (`src/lib/queries.ts`) : statut `live` **et** fenêtre `starts_on` / `ends_on`, journée calculée en Europe/Paris pour qu'un conteneur en UTC ne retire pas une offre deux heures trop tôt. Le statut n'est pas touché : hors période, le backoffice explique pourquoi l'offre n'est pas visible (`visibilityNote`) et elle revient d'elle-même si les dates changent. 2. La publication peut être programmée. `promotions.publish_at` : l'adhérent propose une date, le modérateur la garde, la déplace ou la vide dans le formulaire « Valider ». Avec une échéance future la promotion passe au statut `scheduled` — rien n'est visible, rien n'est diffusé — et `releaseDuePromotions()` fait la bascule à l'heure dite, site et réseaux ensemble. Un bouton « Publier maintenant » court-circuite l'attente. Mécanique : - `src/lib/promo-schedule.ts` (pur, testé) : conversion `datetime-local` ⇄ heure de l'association, changements d'heure compris, et refus d'une saisie illisible plutôt qu'une publication immédiate involontaire. - `src/lib/promo-publish.ts` reprend `publishPromoShares()` : deux appelants désormais, une action serveur et la boucle de libération, qui n'a pas de requête et ne peut donc pas appeler `revalidatePath()`. Idem pour `logActivity()` dans `src/lib/activity-log.ts`. - La bascule `scheduled → live` est un `UPDATE … RETURNING` filtré sur le statut : atomique, donc pas de double publication même à plusieurs instances, en plus de la garde existante sur `social_posts`. - Déclencheur : `src/instrumentation.ts`, boucle d'une minute avec rattrapage au démarrage (`PROMO_SCHEDULER=off` la désactive). Le travail vit dans `instrumentation-node.ts`, écarté du bundle edge par `next.config.mjs` : Next compile aussi l'instrumentation pour le middleware, où `pg` ne peut pas être embarqué. Migration 0016 : `promo_status += 'scheduled'`, `promotions.publish_at`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
This commit is contained in:
20 files changed
+2134
-121
No files matched your search
+25
-4
@@ -1,7 +1,22 @@
|
||||
import { and, asc, count, desc, eq } from "drizzle-orm";
|
||||
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({
|
||||
@@ -32,7 +47,7 @@ export async function getLiveBadgesByMember() {
|
||||
return db
|
||||
.select({ memberId: promotions.memberId, badge: promotions.badge })
|
||||
.from(promotions)
|
||||
.where(eq(promotions.status, "live"));
|
||||
.where(VISIBLE_PROMO);
|
||||
}
|
||||
|
||||
export async function getCategoriesWithCounts(limit = 6) {
|
||||
@@ -75,7 +90,7 @@ export async function getLivePromotions(limit = 6) {
|
||||
})
|
||||
.from(promotions)
|
||||
.leftJoin(members, eq(promotions.memberId, members.id))
|
||||
.where(eq(promotions.status, "live"))
|
||||
.where(VISIBLE_PROMO)
|
||||
.orderBy(desc(promotions.createdAt))
|
||||
.limit(limit);
|
||||
return rows;
|
||||
@@ -125,7 +140,7 @@ export async function getMemberLivePromotions(memberId: number) {
|
||||
endsOn: promotions.endsOn,
|
||||
})
|
||||
.from(promotions)
|
||||
.where(and(eq(promotions.memberId, memberId), eq(promotions.status, "live")))
|
||||
.where(and(eq(promotions.memberId, memberId), VISIBLE_PROMO))
|
||||
.orderBy(desc(promotions.createdAt));
|
||||
}
|
||||
|
||||
@@ -222,3 +237,9 @@ 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<number> {
|
||||
const [row] = await db.select({ n: count() }).from(promotions).where(VISIBLE_PROMO);
|
||||
return row?.n ?? 0;
|
||||
}
|
||||
Reference in new issue
Block a user