Kunde: persönliche Du/Sie-Präferenz pro Mitarbeiter

Neue Tabelle UserCustomerSalutation (PK userId+customerId,
preference 'DU'|'SIE'). Fehlender Eintrag → Fallback auf
Customer.useInformalAddress (Kunden-Default).

Backend:
- customerService: get/set/clearSalutationPreference mit Fallback-
  Logik. Response enthält immer effektive Präferenz + `source`
  ('user' | 'customer-default'), damit die UI den Standard-Text
  anzeigen kann.
- customerController: 3 Endpunkte GET/PUT/DELETE
  /:customerId/salutation-preference. userId aus dem JWT, canAccessCustomer
  greift.

Frontend:
- customerApi: 3 neue Methoden.
- CustomerDetail: neues Feld "Anrede für mich" mit Du/Sie-Toggle
  und Zurücksetzen-Link. Wird nur Mitarbeitern angezeigt, nicht
  Portal-Usern.
- Bestehendes Feld "Anrede per" bekommt Hinweis "Standard für alle
  Mitarbeiter", damit die Semantik der beiden Felder klar ist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 00:34:51 +02:00
parent d14db3de2f
commit 57959cd782
7 changed files with 311 additions and 1 deletions
@@ -1274,6 +1274,88 @@ export async function removeRepresentative(req: AuthRequest, res: Response): Pro
}
}
// ==================== SALUTATION PREFERENCE ====================
// Per-User "Du"/"Sie" pro Kunde User-Wunsch 2026-07-04.
export async function getSalutationPreference(req: AuthRequest, res: Response): Promise<void> {
try {
const customerId = parseInt(req.params.customerId);
if (!Number.isFinite(customerId) || customerId < 1) {
res.status(400).json({ success: false, error: 'Ungültige Kunden-ID' } as ApiResponse);
return;
}
if (!(await canAccessCustomer(req, res, customerId))) return;
const userId = req.user?.userId;
if (!userId) {
res.status(401).json({ success: false, error: 'Nicht authentifiziert' } as ApiResponse);
return;
}
const result = await customerService.getSalutationPreference(userId, customerId);
res.json({ success: true, data: result } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Laden der Anrede-Präferenz',
} as ApiResponse);
}
}
export async function setSalutationPreference(req: AuthRequest, res: Response): Promise<void> {
try {
const customerId = parseInt(req.params.customerId);
if (!Number.isFinite(customerId) || customerId < 1) {
res.status(400).json({ success: false, error: 'Ungültige Kunden-ID' } as ApiResponse);
return;
}
if (!(await canAccessCustomer(req, res, customerId))) return;
const userId = req.user?.userId;
if (!userId) {
res.status(401).json({ success: false, error: 'Nicht authentifiziert' } as ApiResponse);
return;
}
const preference = req.body?.preference;
if (preference !== 'DU' && preference !== 'SIE') {
res.status(400).json({
success: false,
error: 'preference muss "DU" oder "SIE" sein',
} as ApiResponse);
return;
}
await customerService.setSalutationPreference(userId, customerId, preference);
const result = await customerService.getSalutationPreference(userId, customerId);
res.json({ success: true, data: result } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Speichern der Anrede-Präferenz',
} as ApiResponse);
}
}
export async function clearSalutationPreference(req: AuthRequest, res: Response): Promise<void> {
try {
const customerId = parseInt(req.params.customerId);
if (!Number.isFinite(customerId) || customerId < 1) {
res.status(400).json({ success: false, error: 'Ungültige Kunden-ID' } as ApiResponse);
return;
}
if (!(await canAccessCustomer(req, res, customerId))) return;
const userId = req.user?.userId;
if (!userId) {
res.status(401).json({ success: false, error: 'Nicht authentifiziert' } as ApiResponse);
return;
}
await customerService.clearSalutationPreference(userId, customerId);
const result = await customerService.getSalutationPreference(userId, customerId);
res.json({ success: true, data: result } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Zurücksetzen der Anrede-Präferenz',
} as ApiResponse);
}
}
export async function searchForRepresentative(req: AuthRequest, res: Response): Promise<void> {
try {
// KRITISCH (Pentest Runde 6): ohne canAccessCustomer kann ein Portal-User