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