Kundenakte: Tab "Geworben / angeworben" (Kundenempfehlungen)

Neuer Tab vor "Datenschutz", nur Mitarbeiter/Admin (nicht Portal),
ohne Consent-Pflicht. Zwei Abschnitte:
1. "<Kunde> wurde an Board geholt durch:" – max. 1 Werber
   (DB-Unique auf recruitedId).
2. "<Kunde> hat folgende Kunden an Board geholt:" – beliebig viele.

Jede Zeile: Kunde per Lupe-Such-Modal (breite Suche über Name/
Kundennr./Firma/E-Mail/Telefon) + Beziehungs-Dropdown. Löschen +
Externtab-Link zur Kundenakte pro Zeile.

Bidirektional aus EINEM Datensatz: "A geworben durch B" erscheint
automatisch bei B unter "hat geworben"; von beiden Akten
hinzufügbar/löschbar.

Backend: neues Model CustomerReferral (recruiter/recruited FKs,
recruitedId @unique, relationship) + Migration. Beziehungs-Whitelist
serverseitig; Self-Werbung + Doppel-Werber (409) abgefangen.
Portal-Token wird explizit geblockt (Defense-in-Depth, nicht nur
UI-Ausblendung). CREATE/DELETE auditiert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 17:50:15 +02:00
co-authored by Claude Opus 4.7
parent 6b08762ff9
commit cda6d2814e
10 changed files with 744 additions and 1 deletions
+24 -1
View File
@@ -1,5 +1,5 @@
import axios from 'axios';
import type { ApiResponse, Customer, Contract, ContractTask, ContractTaskSubtask, ContractTaskStatus, SalesPlatform, CancellationPeriod, ContractDuration, ContractCategory, Provider, Tariff, User, Address, BankCard, IdentityDocument, Meter, MeterReading, Invoice, Role, PortalSettings, CustomerRepresentative, CustomerSummary, ContractHistoryEntry, AuditLog, AuditSensitivity, AuditRetentionPolicy, CustomerConsent, ConsentType, ConsentStatus, DataDeletionRequest, DeletionRequestStatus, GDPRDashboardStats, RepresentativeAuthorization } from '../types';
import type { ApiResponse, Customer, Contract, ContractTask, ContractTaskSubtask, ContractTaskStatus, SalesPlatform, CancellationPeriod, ContractDuration, ContractCategory, Provider, Tariff, User, Address, BankCard, IdentityDocument, Meter, MeterReading, Invoice, Role, PortalSettings, CustomerRepresentative, CustomerSummary, CustomerReferrals, ContractHistoryEntry, AuditLog, AuditSensitivity, AuditRetentionPolicy, CustomerConsent, ConsentType, ConsentStatus, DataDeletionRequest, DeletionRequestStatus, GDPRDashboardStats, RepresentativeAuthorization } from '../types';
// ============================================================================
// In-Memory-Token-Store
@@ -232,6 +232,29 @@ export const customerApi = {
},
};
// Werbung ("Geworben / angeworben")
export const referralApi = {
get: async (customerId: number) => {
const res = await api.get<ApiResponse<CustomerReferrals>>(`/customers/${customerId}/referrals`);
return res.data;
},
search: async (customerId: number, search: string) => {
const res = await api.get<ApiResponse<CustomerSummary[]>>(`/customers/${customerId}/referrals/search`, { params: { search } });
return res.data;
},
create: async (
customerId: number,
payload: { direction: 'recruitedBy' | 'recruited'; otherCustomerId: number; relationship: string },
) => {
const res = await api.post<ApiResponse<unknown>>(`/customers/${customerId}/referrals`, payload);
return res.data;
},
remove: async (customerId: number, referralId: number) => {
const res = await api.delete<ApiResponse<void>>(`/customers/${customerId}/referrals/${referralId}`);
return res.data;
},
};
// Addresses
export const addressApi = {
getByCustomer: async (customerId: number) => {