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:
@@ -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
|
||||
|
||||
@@ -40,6 +40,13 @@ router.get('/:customerId/portal/password', authenticate, requirePermission('cust
|
||||
router.post('/:customerId/portal/password/generate', authenticate, requirePermission('customers:update'), customerController.generatePortalPassword);
|
||||
router.post('/:customerId/portal/send-credentials', authenticate, requirePermission('customers:update'), customerController.sendPortalCredentials);
|
||||
|
||||
// Salutation-Präferenz pro User (Du/Sie) – 2026-07-04. Nutzt
|
||||
// requireCustomerAccess damit auch Portal-User (falls je nötig) nicht
|
||||
// automatisch scheitern; der Endpoint speichert userId aus dem Token.
|
||||
router.get('/:customerId/salutation-preference', authenticate, requireCustomerAccess, customerController.getSalutationPreference);
|
||||
router.put('/:customerId/salutation-preference', authenticate, requireCustomerAccess, customerController.setSalutationPreference);
|
||||
router.delete('/:customerId/salutation-preference', authenticate, requireCustomerAccess, customerController.clearSalutationPreference);
|
||||
|
||||
// Representatives (Vertreter)
|
||||
router.get('/:customerId/representatives', authenticate, requirePermission('customers:read'), customerController.getRepresentatives);
|
||||
router.post('/:customerId/representatives', authenticate, requirePermission('customers:update'), customerController.addRepresentative);
|
||||
|
||||
@@ -1017,3 +1017,67 @@ export async function searchCustomersForRepresentative(search: string, excludeCu
|
||||
take: 10,
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== SALUTATION PREFERENCE ====================
|
||||
// Per-User "Du"/"Sie" pro Kunde. Fehlender Eintrag ⇒ Kunden-Default
|
||||
// (Customer.useInformalAddress). Nur Mitarbeiter-User, keine Portal-User.
|
||||
|
||||
export type SalutationPreference = 'DU' | 'SIE';
|
||||
|
||||
function isValidPreference(v: unknown): v is SalutationPreference {
|
||||
return v === 'DU' || v === 'SIE';
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die effektive Anrede für einen User/Kunde: eigene Präferenz
|
||||
* bevorzugt, sonst Kunden-Default. `source` sagt der UI, ob's ein eigener
|
||||
* Override ist oder der Fallback greift.
|
||||
*/
|
||||
export async function getSalutationPreference(
|
||||
userId: number,
|
||||
customerId: number,
|
||||
): Promise<{
|
||||
preference: SalutationPreference;
|
||||
source: 'user' | 'customer-default';
|
||||
}> {
|
||||
const own = await prisma.userCustomerSalutation.findUnique({
|
||||
where: { userId_customerId: { userId, customerId } },
|
||||
});
|
||||
if (own && isValidPreference(own.preference)) {
|
||||
return { preference: own.preference, source: 'user' };
|
||||
}
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
select: { useInformalAddress: true },
|
||||
});
|
||||
if (!customer) {
|
||||
throw new Error('Kunde nicht gefunden');
|
||||
}
|
||||
return {
|
||||
preference: customer.useInformalAddress ? 'DU' : 'SIE',
|
||||
source: 'customer-default',
|
||||
};
|
||||
}
|
||||
|
||||
export async function setSalutationPreference(
|
||||
userId: number,
|
||||
customerId: number,
|
||||
preference: SalutationPreference,
|
||||
) {
|
||||
return prisma.userCustomerSalutation.upsert({
|
||||
where: { userId_customerId: { userId, customerId } },
|
||||
create: { userId, customerId, preference },
|
||||
update: { preference },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht den eigenen Override, sodass die UI wieder auf den Kunden-
|
||||
* Default zurückfällt. Kein 404, wenn kein Eintrag da war – das ist
|
||||
* idempotent gedacht ("stelle sicher, dass kein Override existiert").
|
||||
*/
|
||||
export async function clearSalutationPreference(userId: number, customerId: number) {
|
||||
await prisma.userCustomerSalutation.deleteMany({
|
||||
where: { userId, customerId },
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user