Geburtstagskalender + Geburtstagsgruß-Modal im Kundenportal
Admin (Vertrags-Cockpit): - Neue Section "Geburtstage" zeigt Kunden mit Geburtstag - Fenster: -7 bis +30 Tage um heute - Farbcodierung: heute (pink), vergangen (amber), bevorstehend (grau) - Anzeige: Name, Kundennummer, Geburtsdatum, Alter, "Heute!" / "In X Tagen" / "Vor X Tagen" Portal (Kundenportal): - Modal mit Geburtstagsgruß wenn Geburtstag heute oder in den letzten 7 Tagen war - Unterscheidet zwischen aktuellem Geburtstag und nachträglichen Glückwünschen - Schönes Gradient-Design mit Konfetti-Emojis - Wird pro Jahr nur einmal angezeigt (Customer.lastBirthdayGreetingYear) - Bestätigung speichert das aktuelle Jahr Backend: - Neues Feld Customer.lastBirthdayGreetingYear (Int?) - Service birthday.service.ts mit Fenster-Logik + Alter-Berechnung - Endpoints /api/birthdays/upcoming (Admin), /api/birthdays/my-birthday (Portal GET + POST /acknowledge) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a47dfcd841
commit
9e55e25dc8
|
|
@ -163,6 +163,9 @@ model Customer {
|
|||
portalPasswordEncrypted String? // Verschlüsseltes Passwort (für Anzeige)
|
||||
portalLastLogin DateTime? // Letzte Anmeldung
|
||||
|
||||
// Geburtstagsmodal: Jahr in dem dem Kunden der Geburtstagsgruß gezeigt wurde (vermeidet mehrfaches Anzeigen)
|
||||
lastBirthdayGreetingYear Int?
|
||||
|
||||
user User?
|
||||
addresses Address[]
|
||||
bankCards BankCard[]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
import { Response } from 'express';
|
||||
import { AuthRequest } from '../types/index.js';
|
||||
import * as birthdayService from '../services/birthday.service.js';
|
||||
|
||||
/**
|
||||
* Admin/Mitarbeiter: Kommende und vergangene Geburtstage
|
||||
* Query: ?past=7&future=30 (Default)
|
||||
*/
|
||||
export async function getUpcomingBirthdays(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const past = req.query.past ? parseInt(String(req.query.past)) : 7;
|
||||
const future = req.query.future ? parseInt(String(req.query.future)) : 30;
|
||||
|
||||
const entries = await birthdayService.getUpcomingBirthdays(past, future);
|
||||
res.json({ success: true, data: entries });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Geburtstage:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Abrufen der Geburtstage' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Portal: Eigenen Geburtstags-Check – soll das Modal angezeigt werden?
|
||||
*/
|
||||
export async function getMyBirthday(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const user = req.user as any;
|
||||
if (!user?.isCustomerPortal || !user?.customerId) {
|
||||
return res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
|
||||
}
|
||||
|
||||
const data = await birthdayService.checkMyBirthday(user.customerId);
|
||||
res.json({ success: true, data });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Geburtstags-Check:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Abruf' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Portal: Modal als gesehen markieren
|
||||
*/
|
||||
export async function acknowledgeMyBirthday(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const user = req.user as any;
|
||||
if (!user?.isCustomerPortal || !user?.customerId) {
|
||||
return res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
|
||||
}
|
||||
|
||||
await birthdayService.acknowledgeBirthdayGreeting(user.customerId);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Bestätigen:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Speichern' });
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ import gdprRoutes from './routes/gdpr.routes.js';
|
|||
import consentPublicRoutes from './routes/consent-public.routes.js';
|
||||
import emailLogRoutes from './routes/emailLog.routes.js';
|
||||
import pdfTemplateRoutes from './routes/pdfTemplate.routes.js';
|
||||
import birthdayRoutes from './routes/birthday.routes.js';
|
||||
import { auditContextMiddleware } from './middleware/auditContext.js';
|
||||
import { auditMiddleware } from './middleware/audit.js';
|
||||
|
||||
|
|
@ -81,6 +82,7 @@ app.use('/api/audit-logs', auditLogRoutes);
|
|||
app.use('/api/gdpr', gdprRoutes);
|
||||
app.use('/api/email-logs', emailLogRoutes);
|
||||
app.use('/api/pdf-templates', pdfTemplateRoutes);
|
||||
app.use('/api/birthdays', birthdayRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import { Router } from 'express';
|
||||
import * as birthdayController from '../controllers/birthday.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Admin: Kommende und vergangene Geburtstage
|
||||
router.get('/upcoming', authenticate, requirePermission('customers:read'), birthdayController.getUpcomingBirthdays);
|
||||
|
||||
// Portal: eigener Geburtstag-Check
|
||||
router.get('/my-birthday', authenticate, birthdayController.getMyBirthday);
|
||||
router.post('/my-birthday/acknowledge', authenticate, birthdayController.acknowledgeMyBirthday);
|
||||
|
||||
export default router;
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
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<BirthdayEntry[]> {
|
||||
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;
|
||||
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<MyBirthdayCheck | null> {
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
select: {
|
||||
firstName: true,
|
||||
birthDate: true,
|
||||
lastBirthdayGreetingYear: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!customer?.birthDate) return null;
|
||||
|
||||
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, firstName: customer.firstName, 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, firstName: customer.firstName, age: 0 };
|
||||
}
|
||||
|
||||
const age = calculateAge(customer.birthDate, today);
|
||||
|
||||
return {
|
||||
show: true,
|
||||
isToday: diff === 0,
|
||||
daysAgo: diff,
|
||||
firstName: customer.firstName,
|
||||
age,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Markiert, dass dem Kunden das Geburtstagsmodal dieses Jahr bereits gezeigt wurde.
|
||||
*/
|
||||
export async function acknowledgeBirthdayGreeting(customerId: number): Promise<void> {
|
||||
const thisYear = new Date().getFullYear();
|
||||
await prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: { lastBirthdayGreetingYear: thisYear },
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { birthdayApi } from '../services/api';
|
||||
import { Cake, X } from 'lucide-react';
|
||||
import Button from './ui/Button';
|
||||
|
||||
export default function BirthdayModal() {
|
||||
const { isCustomerPortal } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ['my-birthday'],
|
||||
queryFn: () => birthdayApi.getMyBirthday(),
|
||||
enabled: isCustomerPortal,
|
||||
staleTime: 60 * 60_000, // 1h (reicht völlig)
|
||||
});
|
||||
|
||||
const ackMutation = useMutation({
|
||||
mutationFn: () => birthdayApi.acknowledgeMyBirthday(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['my-birthday'] });
|
||||
},
|
||||
});
|
||||
|
||||
const info = data?.data;
|
||||
if (!info?.show) return null;
|
||||
|
||||
const handleClose = () => ackMutation.mutate();
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4">
|
||||
<div className="bg-white rounded-2xl shadow-2xl max-w-md w-full overflow-hidden">
|
||||
{/* Konfetti-Header */}
|
||||
<div className="bg-gradient-to-br from-pink-500 via-purple-500 to-indigo-500 p-6 text-center relative">
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="absolute top-3 right-3 text-white/80 hover:text-white transition-colors"
|
||||
aria-label="Schließen"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-white/20 backdrop-blur mb-3">
|
||||
<Cake className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
<div className="text-5xl mb-1">🎉🎂🎈</div>
|
||||
</div>
|
||||
|
||||
{/* Nachricht */}
|
||||
<div className="p-6 text-center">
|
||||
{info.isToday ? (
|
||||
<>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
Herzlichen Glückwunsch, {info.firstName}!
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-1">
|
||||
Alles Gute zu Ihrem {info.age}. Geburtstag!
|
||||
</p>
|
||||
<p className="text-gray-500 text-sm mb-6">
|
||||
Wir wünschen Ihnen einen wunderschönen Tag und alles Gute für das neue Lebensjahr. 🌟
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
Nachträglich alles Gute, {info.firstName}!
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-1">
|
||||
Sie hatten vor {info.daysAgo} Tag{info.daysAgo === 1 ? '' : 'en'} Geburtstag
|
||||
{info.age > 0 && ` und sind ${info.age} Jahre alt geworden`}.
|
||||
</p>
|
||||
<p className="text-gray-500 text-sm mb-6">
|
||||
Wir wünschen Ihnen alles Gute nachträglich und eine tolle Zeit im neuen Lebensjahr. 🌟
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button onClick={handleClose} disabled={ackMutation.isPending} className="w-full">
|
||||
Vielen Dank!
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { useAuth } from '../../context/AuthContext';
|
|||
import { gdprApi } from '../../services/api';
|
||||
import Sidebar from './Sidebar';
|
||||
import ScrollToTopButton from '../ScrollToTopButton';
|
||||
import BirthdayModal from '../BirthdayModal';
|
||||
import { AlertTriangle, ArrowRight, Building, Shield } from 'lucide-react';
|
||||
|
||||
function ConsentBanner() {
|
||||
|
|
@ -74,6 +75,7 @@ export default function Layout() {
|
|||
<PortalFooter />
|
||||
</div>
|
||||
<ScrollToTopButton />
|
||||
<BirthdayModal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useState, useMemo, useEffect, useRef } from 'react';
|
|||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { pushHistory } from '../../utils/navigation';
|
||||
import { contractApi, meterApi } from '../../services/api';
|
||||
import { contractApi, meterApi, birthdayApi } from '../../services/api';
|
||||
import Card from '../../components/ui/Card';
|
||||
import Badge from '../../components/ui/Badge';
|
||||
import Select from '../../components/ui/Select';
|
||||
|
|
@ -35,6 +35,7 @@ import {
|
|||
Gauge,
|
||||
ExternalLink,
|
||||
CheckCircle2,
|
||||
Cake,
|
||||
} from 'lucide-react';
|
||||
import { formatDate } from '../../utils/dateFormat';
|
||||
import type { CockpitContract, CockpitUrgencyLevel, ContractType } from '../../types';
|
||||
|
|
@ -137,6 +138,13 @@ export default function ContractCockpit() {
|
|||
staleTime: 0,
|
||||
});
|
||||
|
||||
// Geburtstage (kommende + vergangene der letzten 7 Tage)
|
||||
const { data: birthdaysData } = useQuery({
|
||||
queryKey: ['upcoming-birthdays'],
|
||||
queryFn: () => birthdayApi.getUpcoming(7, 30),
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const [snoozeContractId, setSnoozeContractId] = useState<number | null>(null);
|
||||
const [customDate, setCustomDate] = useState('');
|
||||
|
|
@ -661,6 +669,60 @@ export default function ContractCockpit() {
|
|||
</Card>
|
||||
)}
|
||||
|
||||
{/* Geburtstage */}
|
||||
{birthdaysData?.data && birthdaysData.data.length > 0 && (
|
||||
<Card className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Cake className="w-5 h-5 text-pink-500" />
|
||||
<h3 className="font-medium">Geburtstage</h3>
|
||||
<Badge variant="default">{birthdaysData.data.length}</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mb-3">
|
||||
Kunden mit Geburtstag in den nächsten 30 Tagen oder den letzten 7 Tagen.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{birthdaysData.data.map((b) => (
|
||||
<div
|
||||
key={b.customerId}
|
||||
className={`flex items-center justify-between p-3 rounded-lg border ${
|
||||
b.isToday
|
||||
? 'bg-pink-50 border-pink-300'
|
||||
: b.isPast
|
||||
? 'bg-amber-50 border-amber-200'
|
||||
: 'bg-gray-50 border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Cake className={`w-4 h-4 ${b.isToday ? 'text-pink-500' : b.isPast ? 'text-amber-500' : 'text-gray-400'}`} />
|
||||
<div>
|
||||
<Link
|
||||
to={`/customers/${b.customerId}`}
|
||||
state={pushHistory('/contracts/cockpit')}
|
||||
className="text-sm font-medium hover:underline"
|
||||
>
|
||||
{b.name}
|
||||
</Link>
|
||||
<span className="text-xs text-gray-500 ml-2">({b.customerNumber})</span>
|
||||
<p className="text-xs text-gray-600">
|
||||
{formatDate(b.birthDate)} – wird {b.age} Jahre
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
{b.isToday ? (
|
||||
<Badge variant="warning">🎉 Heute!</Badge>
|
||||
) : b.isPast ? (
|
||||
<Badge variant="default">Vor {Math.abs(b.daysUntil)} Tag{Math.abs(b.daysUntil) === 1 ? '' : 'en'}</Badge>
|
||||
) : (
|
||||
<Badge variant="default">In {b.daysUntil} Tag{b.daysUntil === 1 ? '' : 'en'}</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Filter */}
|
||||
<Card className="mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
|
|
|
|||
|
|
@ -1579,4 +1579,44 @@ export const publicApi = {
|
|||
getConsentPdfUrl: (hash: string) => `/api/public/consent/${hash}/pdf`,
|
||||
};
|
||||
|
||||
// ============ BIRTHDAY API ============
|
||||
export interface BirthdayEntry {
|
||||
customerId: number;
|
||||
customerNumber: string;
|
||||
name: string;
|
||||
birthDate: string;
|
||||
age: number;
|
||||
daysUntil: number;
|
||||
isToday: boolean;
|
||||
isPast: boolean;
|
||||
portalEnabled: boolean;
|
||||
email?: string | null;
|
||||
phone?: string | null;
|
||||
}
|
||||
|
||||
export interface MyBirthdayCheck {
|
||||
show: boolean;
|
||||
isToday: boolean;
|
||||
daysAgo: number;
|
||||
firstName: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
export const birthdayApi = {
|
||||
getUpcoming: async (past: number = 7, future: number = 30) => {
|
||||
const res = await api.get<ApiResponse<BirthdayEntry[]>>('/birthdays/upcoming', {
|
||||
params: { past, future },
|
||||
});
|
||||
return res.data;
|
||||
},
|
||||
getMyBirthday: async () => {
|
||||
const res = await api.get<ApiResponse<MyBirthdayCheck | null>>('/birthdays/my-birthday');
|
||||
return res.data;
|
||||
},
|
||||
acknowledgeMyBirthday: async () => {
|
||||
const res = await api.post<ApiResponse<void>>('/birthdays/my-birthday/acknowledge');
|
||||
return res.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
|
|
|||
Loading…
Reference in New Issue