Files
maro 45261a0461 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
2026-08-25 16:40:51 +02:00

119 lines
4.4 KiB
TypeScript

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;
}