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:
@@ -0,0 +1,20 @@
|
||||
-- Werbe-Beziehung zwischen Kunden ("Geworben / angeworben").
|
||||
-- recruitedId ist UNIQUE → ein Kunde kann nur von EINER Person geworben sein.
|
||||
-- Beide FKs mit ON DELETE CASCADE: wird ein Kunde gelöscht, verschwinden
|
||||
-- seine Werbe-Verknüpfungen mit.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `CustomerReferral` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`recruiterId` INT NOT NULL,
|
||||
`recruitedId` INT NOT NULL,
|
||||
`relationship` VARCHAR(191) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`createdBy` VARCHAR(191) NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE INDEX `CustomerReferral_recruitedId_key` (`recruitedId`),
|
||||
INDEX `CustomerReferral_recruiterId_idx` (`recruiterId`),
|
||||
CONSTRAINT `CustomerReferral_recruiterId_fkey` FOREIGN KEY (`recruiterId`)
|
||||
REFERENCES `Customer` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `CustomerReferral_recruitedId_fkey` FOREIGN KEY (`recruitedId`)
|
||||
REFERENCES `Customer` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
@@ -213,10 +213,34 @@ model Customer {
|
||||
// Persönliche Anrede-Präferenzen aller Mitarbeiter zu diesem Kunden.
|
||||
userSalutations UserCustomerSalutation[]
|
||||
|
||||
// Werbung: wer hat wen an Board geholt.
|
||||
// referralAsRecruited = dieser Kunde WURDE geworben (max. 1 Werber).
|
||||
// referralsAsRecruiter = Kunden, die DIESER Kunde geworben hat (beliebig viele).
|
||||
referralAsRecruited CustomerReferral? @relation("ReferralRecruited")
|
||||
referralsAsRecruiter CustomerReferral[] @relation("ReferralRecruiter")
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// Werbe-Beziehung zwischen zwei Kunden ("Geworben / angeworben").
|
||||
// Ein Datensatz = ein Werber (recruiter) hat einen Kunden (recruited) an
|
||||
// Board geholt, plus die Beziehungsart. `recruitedId` ist @unique, damit ein
|
||||
// Kunde nur von EINER Person geworben sein kann (Abschnitt 1 in der UI).
|
||||
// Ein Werber kann beliebig viele Kunden werben (Abschnitt 2).
|
||||
model CustomerReferral {
|
||||
id Int @id @default(autoincrement())
|
||||
recruiterId Int
|
||||
recruiter Customer @relation("ReferralRecruiter", fields: [recruiterId], references: [id], onDelete: Cascade)
|
||||
recruitedId Int @unique
|
||||
recruited Customer @relation("ReferralRecruited", fields: [recruitedId], references: [id], onDelete: Cascade)
|
||||
relationship String // Ehepartner/in, Kind, Freund/Kumpel, …
|
||||
createdAt DateTime @default(now())
|
||||
createdBy String?
|
||||
|
||||
@@index([recruiterId])
|
||||
}
|
||||
|
||||
// ==================== USER-CUSTOMER SALUTATION ====================
|
||||
// Per-User-Präferenz, ob der eingeloggte Mitarbeiter mit dem jeweiligen
|
||||
// Kunden per Du oder Sie ist. Fehlender Eintrag → fällt auf den
|
||||
|
||||
Reference in New Issue
Block a user