import prisma from '../lib/prisma.js'; export interface BirthdayEntry { customerId: number; customerNumber: string; name: string; birthDate: Date; age: number; // Alter das der Kunde wird bzw. geworden ist daysUntil: number; // 0 = heute, -3 = vor 3 Tagen, +5 = in 5 Tagen isToday: boolean; isPast: boolean; // Geburtstag war in den letzten Tagen portalEnabled: boolean; email?: string | null; phone?: string | null; } /** * Berechnet die Anzahl Tage von "heute" bis zum nächsten Vorkommen eines Geburtstags. * Negative Werte = Geburtstag war kürzlich, positive = Geburtstag steht bevor. * * Für den Zeitraum `[-pastDays, +futureDays]` suchen wir den relevantesten Wert: * - ist der Geburtstag dieses Jahr innerhalb des Fensters → dieser * - sonst: nichts (nicht relevant) */ function daysDifferenceInWindow( birthDate: Date, today: Date, pastDays: number, futureDays: number, ): number | null { const month = birthDate.getMonth(); const day = birthDate.getDate(); // Geburtstag dieses Jahr const thisYear = new Date(today.getFullYear(), month, day); thisYear.setHours(0, 0, 0, 0); const todayNormalized = new Date(today); todayNormalized.setHours(0, 0, 0, 0); const msPerDay = 1000 * 60 * 60 * 24; const diffThisYear = Math.round((thisYear.getTime() - todayNormalized.getTime()) / msPerDay); // Falls im Fenster → nutzen if (diffThisYear >= -pastDays && diffThisYear <= futureDays) { return diffThisYear; } // Falls Geburtstag schon lange vorbei (>pastDays her), ist nächstes Jahr relevant if (diffThisYear < -pastDays) { const nextYear = new Date(today.getFullYear() + 1, month, day); nextYear.setHours(0, 0, 0, 0); const diffNextYear = Math.round((nextYear.getTime() - todayNormalized.getTime()) / msPerDay); if (diffNextYear <= futureDays) { return diffNextYear; } } return null; } function calculateAge(birthDate: Date, refDate: Date): number { let age = refDate.getFullYear() - birthDate.getFullYear(); const m = refDate.getMonth() - birthDate.getMonth(); if (m < 0 || (m === 0 && refDate.getDate() < birthDate.getDate())) { age--; } return age; } /** * Kommende und vergangene Geburtstage in einem Fenster von [pastDays, futureDays]. * Default: 7 Tage zurück, 30 Tage voraus. */ export async function getUpcomingBirthdays( pastDays: number = 7, futureDays: number = 30, ): Promise { const customers = await prisma.customer.findMany({ where: { birthDate: { not: null }, }, select: { id: true, customerNumber: true, firstName: true, lastName: true, companyName: true, birthDate: true, portalEnabled: true, email: true, phone: true, }, }); const today = new Date(); today.setHours(0, 0, 0, 0); const entries: BirthdayEntry[] = []; for (const c of customers) { if (!c.birthDate) continue; const daysUntil = daysDifferenceInWindow(c.birthDate, today, pastDays, futureDays); if (daysUntil === null) continue; // Alter am nächsten Geburtstag (bei daysUntil >= 0) bzw. aktuelles Alter (daysUntil < 0) const refDate = new Date(today); refDate.setDate(refDate.getDate() + daysUntil); const age = calculateAge(c.birthDate, refDate); entries.push({ customerId: c.id, customerNumber: c.customerNumber, name: c.companyName || `${c.firstName} ${c.lastName}`.trim(), birthDate: c.birthDate, age, daysUntil, isToday: daysUntil === 0, isPast: daysUntil < 0, portalEnabled: c.portalEnabled, email: c.email, phone: c.phone, }); } // Sortiert: zuerst heute, dann kommende aufsteigend, dann vergangene absteigend entries.sort((a, b) => { if (a.daysUntil === 0 && b.daysUntil !== 0) return -1; if (b.daysUntil === 0 && a.daysUntil !== 0) return 1; if (a.daysUntil >= 0 && b.daysUntil >= 0) return a.daysUntil - b.daysUntil; if (a.daysUntil < 0 && b.daysUntil < 0) return b.daysUntil - a.daysUntil; // kommende vor vergangenen return a.daysUntil >= 0 ? -1 : 1; }); return entries; } export interface MyBirthdayCheck { show: boolean; // Modal anzeigen? isToday: boolean; daysAgo: number; // 0 = heute, >0 = x Tage her firstName: string; lastName: string; salutation: string | null; useInformalAddress: boolean; age: number; } /** * Portal-Check: Soll dem Kunden ein Geburtstagsmodal angezeigt werden? * - Heute oder in den letzten 7 Tagen * - Modal dieses Jahr noch nicht gezeigt (lastBirthdayGreetingYear != aktuelles Jahr) */ export async function checkMyBirthday(customerId: number): Promise { const customer = await prisma.customer.findUnique({ where: { id: customerId }, select: { firstName: true, lastName: true, salutation: true, useInformalAddress: true, birthDate: true, lastBirthdayGreetingYear: true, }, }); if (!customer?.birthDate) return null; const baseInfo = { firstName: customer.firstName, lastName: customer.lastName, salutation: customer.salutation, useInformalAddress: customer.useInformalAddress, }; const today = new Date(); today.setHours(0, 0, 0, 0); const thisYear = today.getFullYear(); // Schon dieses Jahr angezeigt? if (customer.lastBirthdayGreetingYear === thisYear) { return { show: false, isToday: false, daysAgo: 0, ...baseInfo, age: 0 }; } const birthday = new Date(thisYear, customer.birthDate.getMonth(), customer.birthDate.getDate()); birthday.setHours(0, 0, 0, 0); const msPerDay = 1000 * 60 * 60 * 24; const diff = Math.round((today.getTime() - birthday.getTime()) / msPerDay); // Nur wenn heute oder in den letzten 7 Tagen (diff: 0–7) if (diff < 0 || diff > 7) { return { show: false, isToday: false, daysAgo: 0, ...baseInfo, age: 0 }; } const age = calculateAge(customer.birthDate, today); return { show: true, isToday: diff === 0, daysAgo: diff, ...baseInfo, age, }; } /** * Markiert, dass dem Kunden das Geburtstagsmodal dieses Jahr bereits gezeigt wurde. */ export async function acknowledgeBirthdayGreeting(customerId: number): Promise { const thisYear = new Date().getFullYear(); await prisma.customer.update({ where: { id: customerId }, data: { lastBirthdayGreetingYear: thisYear }, }); } /** * Setzt den Gruß-Marker zurück, damit das Modal beim nächsten Login wieder erscheint. * (Für Mitarbeiter – nützlich zum Debuggen und als Fallback wenn etwas schief ging.) */ export async function resetBirthdayGreeting(customerId: number): Promise { await prisma.customer.update({ where: { id: customerId }, data: { lastBirthdayGreetingYear: null }, }); } /** * Generiert den persönlichen Geburtstagsgruß-Text (Du/Sie-abhängig). */ export function buildBirthdayGreetingText( customer: { firstName: string; lastName: string; salutation: string | null; useInformalAddress: boolean; }, age: number, ): { subject: string; plain: string; html: string } { const name = customer.useInformalAddress ? customer.firstName : [customer.salutation, customer.lastName].filter(Boolean).join(' ') || customer.firstName; const du = customer.useInformalAddress; const pronoun = du ? 'dir' : 'Ihnen'; const possessive = du ? 'deinem' : 'Ihrem'; const yourLower = du ? 'dein' : 'Ihr'; const subject = du ? `Alles Gute zum Geburtstag, ${customer.firstName}! 🎉` : 'Herzlichen Glückwunsch zum Geburtstag 🎉'; // Plain-Text ohne Emojis, damit WhatsApp/Telegram/Signal-URL-Handler nicht stolpern const plain = [ `Herzlichen Glückwunsch, ${name}!`, '', age > 0 ? `Alles Gute zu ${possessive} ${age}. Geburtstag!` : `Alles Gute zu ${possessive} Geburtstag!`, '', `Wir wünschen ${pronoun} einen wunderschönen Tag und alles Gute für ${yourLower} neues Lebensjahr.`, '', 'Herzliche Grüße', 'Hacker-Net Telekommunikation', ].join('\n'); const html = `
🎉🎂🎈

Herzlichen Glückwunsch, ${name}!

${age > 0 ? `Alles Gute zu ${possessive} ${age}. Geburtstag!` : `Alles Gute zu ${possessive} Geburtstag!`}

Wir wünschen ${pronoun} einen wunderschönen Tag und alles Gute für ${yourLower} neues Lebensjahr. 🌟


Herzliche Grüße
Hacker-Net Telekommunikation
Am Wunderburgpark 5b, 26135 Oldenburg
info@hacker-net.de

`; return { subject, plain, html }; } /** * Lädt die für den Gruß benötigten Kundendaten inkl. aktuellem Alter heute. */ export async function getBirthdayGreetingData(customerId: number): Promise<{ firstName: string; lastName: string; salutation: string | null; useInformalAddress: boolean; email: string | null; phone: string | null; mobile: string | null; age: number; } | null> { const c = await prisma.customer.findUnique({ where: { id: customerId }, select: { firstName: true, lastName: true, salutation: true, useInformalAddress: true, email: true, phone: true, mobile: true, birthDate: true, }, }); if (!c?.birthDate) return null; const today = new Date(); today.setHours(0, 0, 0, 0); const age = calculateAge(c.birthDate, today); return { firstName: c.firstName, lastName: c.lastName, salutation: c.salutation, useInformalAddress: c.useInformalAddress, email: c.email, phone: c.phone, mobile: c.mobile, age, }; }