Initial commit: ANOUMA website with Payload CMS, WebRTC meetings, and booking system
- Next.js 16 App Router site with the ANOUMA design system - Payload CMS (PostgreSQL) for offers, events, posts and page content - WebRTC video-call system with custom signaling server - SMTP email reminders and booking-request notifications - Customer accounts, calendar-based availability, and booking workflow
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { getPayload } from "payload";
|
||||
import config from "@payload-config";
|
||||
import { combineDateAndTime } from "./status";
|
||||
import { sendReminderEmail } from "@/lib/email/sendReminderEmail";
|
||||
|
||||
const REMINDER_TOLERANCE_MS = 10 * 60 * 1000; // cron may run every few minutes
|
||||
|
||||
function isDue(reminderAt: number, now: number): boolean {
|
||||
return now >= reminderAt && now < reminderAt + REMINDER_TOLERANCE_MS;
|
||||
}
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
return date.toLocaleDateString("de-DE", { day: "2-digit", month: "long", year: "numeric" });
|
||||
}
|
||||
|
||||
function formatTime(date: Date): string {
|
||||
return date.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks all upcoming online events for due 60/30-minute reminders and sends
|
||||
* them — to registered, non-cancelled participants and (optionally) to the
|
||||
* admin host. Idempotent: every send is guarded by a "...Sent" flag so a
|
||||
* cron job that runs every few minutes never double-sends.
|
||||
*/
|
||||
export async function runEventReminders() {
|
||||
const payload = await getPayload({ config });
|
||||
const now = new Date();
|
||||
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3000";
|
||||
|
||||
const windowStart = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
|
||||
const windowEnd = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
const { docs: events } = await payload.find({
|
||||
collection: "events",
|
||||
where: {
|
||||
and: [
|
||||
{ isOnline: { equals: true } },
|
||||
{ date: { greater_than_equal: windowStart } },
|
||||
{ date: { less_than_equal: windowEnd } },
|
||||
],
|
||||
},
|
||||
limit: 200,
|
||||
});
|
||||
|
||||
const results = { emailsSent: 0, eventsChecked: events.length };
|
||||
|
||||
for (const event of events) {
|
||||
if (!event.slug) continue;
|
||||
const start = combineDateAndTime(event.date, event.startTime);
|
||||
const dateLabel = formatDate(start);
|
||||
const timeLabel = formatTime(start);
|
||||
const joinUrl = `${serverUrl}/termine/${event.slug}/beitreten`;
|
||||
|
||||
const thresholds: { minutes: 60 | 30; enabled: boolean; participantField: "reminder60Sent" | "reminder30Sent"; hostField: "hostReminder60Sent" | "hostReminder30Sent" }[] = [
|
||||
{ minutes: 60, enabled: Boolean(event.reminder60Enabled), participantField: "reminder60Sent", hostField: "hostReminder60Sent" },
|
||||
{ minutes: 30, enabled: Boolean(event.reminder30Enabled), participantField: "reminder30Sent", hostField: "hostReminder30Sent" },
|
||||
];
|
||||
|
||||
for (const threshold of thresholds) {
|
||||
if (!threshold.enabled) continue;
|
||||
const reminderAt = start.getTime() - threshold.minutes * 60_000;
|
||||
if (!isDue(reminderAt, now.getTime())) continue;
|
||||
|
||||
// Host reminder (per event, not per registration).
|
||||
if (event.hostReminderEnabled && !event[threshold.hostField]) {
|
||||
const { docs: admins } = await payload.find({ collection: "users", limit: 50 });
|
||||
for (const admin of admins) {
|
||||
if (!admin.email) continue;
|
||||
await sendReminderEmail(admin.email, {
|
||||
recipientName: admin.name || "Team ANOUMA",
|
||||
eventTitle: event.title,
|
||||
dateLabel,
|
||||
timeLabel,
|
||||
joinUrl,
|
||||
meetingPassword: event.meetingPassword || "—",
|
||||
minutesBefore: threshold.minutes,
|
||||
});
|
||||
results.emailsSent++;
|
||||
}
|
||||
await payload.update({ collection: "events", id: event.id, data: { [threshold.hostField]: true } });
|
||||
}
|
||||
|
||||
// Participant reminders — only registered (not cancelled), not yet sent.
|
||||
const { docs: registrations } = await payload.find({
|
||||
collection: "event-registrations",
|
||||
where: {
|
||||
and: [
|
||||
{ event: { equals: event.id } },
|
||||
{ status: { equals: "registered" } },
|
||||
{ [threshold.participantField]: { equals: false } },
|
||||
],
|
||||
},
|
||||
limit: 500,
|
||||
});
|
||||
|
||||
for (const registration of registrations) {
|
||||
await sendReminderEmail(registration.email, {
|
||||
recipientName: registration.name,
|
||||
eventTitle: event.title,
|
||||
dateLabel,
|
||||
timeLabel,
|
||||
joinUrl,
|
||||
meetingPassword: event.meetingPassword || "—",
|
||||
minutesBefore: threshold.minutes,
|
||||
});
|
||||
results.emailsSent++;
|
||||
await payload.update({
|
||||
collection: "event-registrations",
|
||||
id: registration.id,
|
||||
data: { [threshold.participantField]: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
Reference in New Issue
Block a user