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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user