Anrede-Verhältnis Du/Sie pro Kunde + Geburtstagsgruß respektiert Anrede
Schema: - Customer.useInformalAddress: Boolean (Default: false = Sie) - Auch bei Firmenkunden verfügbar (Chef kann man auch duzen) Frontend: - Neues Pflichtfeld "Anrede per" (Du/Sie) im Kunden-Formular - Anzeige als Badge in CustomerDetail-Stammdaten Geburtstagsgruß im Portal: - Bei Du: "Herzlichen Glückwunsch, Max! Alles Gute zu deinem 42. Geburtstag!" - Bei Sie: "Herzlichen Glückwunsch, Herr Müller! Alles Gute zu Ihrem 42. Geburtstag!" - Konsistent auch bei nachträglichen Glückwünschen (hattest/hatten, bist/sind etc.) - Backend liefert firstName, lastName, salutation und useInformalAddress Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ba29711ee7
commit
2a3928d0e7
|
|
@ -166,6 +166,9 @@ model Customer {
|
||||||
// Geburtstagsmodal: Jahr in dem dem Kunden der Geburtstagsgruß gezeigt wurde (vermeidet mehrfaches Anzeigen)
|
// Geburtstagsmodal: Jahr in dem dem Kunden der Geburtstagsgruß gezeigt wurde (vermeidet mehrfaches Anzeigen)
|
||||||
lastBirthdayGreetingYear Int?
|
lastBirthdayGreetingYear Int?
|
||||||
|
|
||||||
|
// Anrede-Verhältnis: true = Du (informell), false = Sie (formell, Default)
|
||||||
|
useInformalAddress Boolean @default(false)
|
||||||
|
|
||||||
user User?
|
user User?
|
||||||
addresses Address[]
|
addresses Address[]
|
||||||
bankCards BankCard[]
|
bankCards BankCard[]
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,9 @@ export interface MyBirthdayCheck {
|
||||||
isToday: boolean;
|
isToday: boolean;
|
||||||
daysAgo: number; // 0 = heute, >0 = x Tage her
|
daysAgo: number; // 0 = heute, >0 = x Tage her
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
salutation: string | null;
|
||||||
|
useInformalAddress: boolean;
|
||||||
age: number;
|
age: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,6 +156,9 @@ export async function checkMyBirthday(customerId: number): Promise<MyBirthdayChe
|
||||||
where: { id: customerId },
|
where: { id: customerId },
|
||||||
select: {
|
select: {
|
||||||
firstName: true,
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
salutation: true,
|
||||||
|
useInformalAddress: true,
|
||||||
birthDate: true,
|
birthDate: true,
|
||||||
lastBirthdayGreetingYear: true,
|
lastBirthdayGreetingYear: true,
|
||||||
},
|
},
|
||||||
|
|
@ -160,13 +166,20 @@ export async function checkMyBirthday(customerId: number): Promise<MyBirthdayChe
|
||||||
|
|
||||||
if (!customer?.birthDate) return null;
|
if (!customer?.birthDate) return null;
|
||||||
|
|
||||||
|
const baseInfo = {
|
||||||
|
firstName: customer.firstName,
|
||||||
|
lastName: customer.lastName,
|
||||||
|
salutation: customer.salutation,
|
||||||
|
useInformalAddress: customer.useInformalAddress,
|
||||||
|
};
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
const thisYear = today.getFullYear();
|
const thisYear = today.getFullYear();
|
||||||
|
|
||||||
// Schon dieses Jahr angezeigt?
|
// Schon dieses Jahr angezeigt?
|
||||||
if (customer.lastBirthdayGreetingYear === thisYear) {
|
if (customer.lastBirthdayGreetingYear === thisYear) {
|
||||||
return { show: false, isToday: false, daysAgo: 0, firstName: customer.firstName, age: 0 };
|
return { show: false, isToday: false, daysAgo: 0, ...baseInfo, age: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const birthday = new Date(thisYear, customer.birthDate.getMonth(), customer.birthDate.getDate());
|
const birthday = new Date(thisYear, customer.birthDate.getMonth(), customer.birthDate.getDate());
|
||||||
|
|
@ -177,7 +190,7 @@ export async function checkMyBirthday(customerId: number): Promise<MyBirthdayChe
|
||||||
|
|
||||||
// Nur wenn heute oder in den letzten 7 Tagen (diff: 0–7)
|
// Nur wenn heute oder in den letzten 7 Tagen (diff: 0–7)
|
||||||
if (diff < 0 || diff > 7) {
|
if (diff < 0 || diff > 7) {
|
||||||
return { show: false, isToday: false, daysAgo: 0, firstName: customer.firstName, age: 0 };
|
return { show: false, isToday: false, daysAgo: 0, ...baseInfo, age: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const age = calculateAge(customer.birthDate, today);
|
const age = calculateAge(customer.birthDate, today);
|
||||||
|
|
@ -186,7 +199,7 @@ export async function checkMyBirthday(customerId: number): Promise<MyBirthdayChe
|
||||||
show: true,
|
show: true,
|
||||||
isToday: diff === 0,
|
isToday: diff === 0,
|
||||||
daysAgo: diff,
|
daysAgo: diff,
|
||||||
firstName: customer.firstName,
|
...baseInfo,
|
||||||
age,
|
age,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,12 @@ als Factory-Default beim Initialisieren wieder einspielen lassen.
|
||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [x] **Anrede-Verhältnis Du/Sie pro Kunde**
|
||||||
|
- Neues Feld `useInformalAddress` in Stammdaten (auch bei Firmenkunden)
|
||||||
|
- Default: Sie (formell)
|
||||||
|
- Geburtstagsgruß im Portal nutzt die Anrede: "Du"-Kunden bekommen "Herzlichen Glückwunsch, Max!", "Sie"-Kunden "Herzlichen Glückwunsch, Herr Müller!"
|
||||||
|
- Komplett konsistent auch bei nachträglichen Glückwünschen ("hattest" vs "hatten")
|
||||||
|
|
||||||
- [x] **Geburtsdatum + Geburtsort auch bei Firmenkunden**
|
- [x] **Geburtsdatum + Geburtsort auch bei Firmenkunden**
|
||||||
- Felder werden jetzt unabhängig vom Kundentyp angezeigt
|
- Felder werden jetzt unabhängig vom Kundentyp angezeigt
|
||||||
- Ermöglicht z.B. Geburtstage für Ansprechpartner bei Firmen
|
- Ermöglicht z.B. Geburtstage für Ansprechpartner bei Firmen
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,16 @@ export default function BirthdayModal() {
|
||||||
|
|
||||||
const handleClose = () => ackMutation.mutate();
|
const handleClose = () => ackMutation.mutate();
|
||||||
|
|
||||||
|
// Anrede abhängig vom Du/Sie-Verhältnis zusammenstellen
|
||||||
|
const greetingName = info.useInformalAddress
|
||||||
|
? info.firstName
|
||||||
|
: [info.salutation, info.lastName].filter(Boolean).join(' ') || info.firstName;
|
||||||
|
const greetingPronoun = info.useInformalAddress ? 'dir' : 'Ihnen';
|
||||||
|
const greetingPossessive = info.useInformalAddress ? 'deinem' : 'Ihrem';
|
||||||
|
const greetingYour = info.useInformalAddress ? 'Dein' : 'Ihr';
|
||||||
|
const hadBirthday = info.useInformalAddress ? 'hattest' : 'hatten';
|
||||||
|
const becameOld = info.useInformalAddress ? 'bist' : 'sind';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4">
|
<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">
|
<div className="bg-white rounded-2xl shadow-2xl max-w-md w-full overflow-hidden">
|
||||||
|
|
@ -50,26 +60,26 @@ export default function BirthdayModal() {
|
||||||
{info.isToday ? (
|
{info.isToday ? (
|
||||||
<>
|
<>
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||||
Herzlichen Glückwunsch, {info.firstName}!
|
Herzlichen Glückwunsch, {greetingName}!
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 mb-1">
|
<p className="text-gray-600 mb-1">
|
||||||
Alles Gute zu Ihrem {info.age}. Geburtstag!
|
Alles Gute zu {greetingPossessive} {info.age}. Geburtstag!
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-500 text-sm mb-6">
|
<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. 🌟
|
Wir wünschen {greetingPronoun} einen wunderschönen Tag und alles Gute für {greetingYour.toLowerCase()} neues Lebensjahr. 🌟
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||||
Nachträglich alles Gute, {info.firstName}!
|
Nachträglich alles Gute, {greetingName}!
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 mb-1">
|
<p className="text-gray-600 mb-1">
|
||||||
Sie hatten vor {info.daysAgo} Tag{info.daysAgo === 1 ? '' : 'en'} Geburtstag
|
{greetingYour === 'Ihr' ? 'Sie' : 'Du'} {hadBirthday} vor {info.daysAgo} Tag{info.daysAgo === 1 ? '' : 'en'} Geburtstag
|
||||||
{info.age > 0 && ` und sind ${info.age} Jahre alt geworden`}.
|
{info.age > 0 && ` und ${becameOld} ${info.age} Jahre alt geworden`}.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-500 text-sm mb-6">
|
<p className="text-gray-500 text-sm mb-6">
|
||||||
Wir wünschen Ihnen alles Gute nachträglich und eine tolle Zeit im neuen Lebensjahr. 🌟
|
Wir wünschen {greetingPronoun} alles Gute nachträglich und eine tolle Zeit im neuen Lebensjahr. 🌟
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,14 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-gray-500">Anrede per</dt>
|
||||||
|
<dd>
|
||||||
|
<Badge variant={c.useInformalAddress ? 'info' : 'default'}>
|
||||||
|
{c.useInformalAddress ? 'Du (informell)' : 'Sie (formell)'}
|
||||||
|
</Badge>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-sm text-gray-500">Vorname</dt>
|
<dt className="text-sm text-gray-500">Vorname</dt>
|
||||||
<dd className="flex items-center gap-1">
|
<dd className="flex items-center gap-1">
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,17 @@ export default function CustomerForm() {
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Anrede per *"
|
||||||
|
{...register('useInformalAddress', {
|
||||||
|
setValueAs: (v) => v === 'true' || v === true,
|
||||||
|
})}
|
||||||
|
options={[
|
||||||
|
{ value: 'false', label: 'Sie (formell)' },
|
||||||
|
{ value: 'true', label: 'Du (informell)' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="Vorname"
|
label="Vorname"
|
||||||
{...register('firstName', { required: 'Vorname erforderlich' })}
|
{...register('firstName', { required: 'Vorname erforderlich' })}
|
||||||
|
|
|
||||||
|
|
@ -1599,6 +1599,9 @@ export interface MyBirthdayCheck {
|
||||||
isToday: boolean;
|
isToday: boolean;
|
||||||
daysAgo: number;
|
daysAgo: number;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
salutation: string | null;
|
||||||
|
useInformalAddress: boolean;
|
||||||
age: number;
|
age: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ export interface Customer {
|
||||||
customerNumber: string;
|
customerNumber: string;
|
||||||
type: 'PRIVATE' | 'BUSINESS';
|
type: 'PRIVATE' | 'BUSINESS';
|
||||||
salutation?: string;
|
salutation?: string;
|
||||||
|
useInformalAddress?: boolean;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
companyName?: string;
|
companyName?: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue