feat(akquise): Auswahllisten statt Freitext, Felder abgesetzt

"Von", "Anbieter", "Kanal" und "Berater" sind Dropdowns. Ihre Inhalte stehen in
crm_acquisition_options und werden im neuen Reiter Einstellungen -> Akquise
gepflegt. Freitext hat aus "Hays", "hays" und "Hays AG" drei Anbieter gemacht;
der Unique-Schluessel greift ueber die Kollation auch bei abweichender
Gross-/Kleinschreibung.

Eine Tabelle mit Feldkennzeichen statt vier Tabellen: die Felder verhalten sich
gleich, also ist jede weitere Liste ein Wert im ENUM statt einer neuen Route und
einer neuen Oberflaeche.

Zwei Entscheidungen dabei:

- Pflegen darf jeder Angemeldete, nicht nur ein Administrator. Eine Liste, die
  nur ein Admin ergaenzen kann, blockiert die Eingabe genau dann, wenn ein neuer
  Anbieter auftaucht.
- Die Eintraege halten den Text, keinen Fremdschluessel. Umbenennen zieht sie
  darum ausdruecklich mit (in einer Transaktion), Loeschen nicht: eine Liste
  aufzuraeumen darf keine Daten aendern. Ein Wert, der nicht mehr in der Liste
  steht, wird im Dropdown weiter angeboten und als solcher gekennzeichnet -
  sonst wuerde das Bearbeiten eines alten Eintrags ihn stillschweigend
  ueberschreiben.

Das Backend prueft beim Speichern eines Eintrags nur noch die Laenge und nicht
mehr die Zugehoerigkeit zur Liste - aus demselben Grund.

Dazu: die Felder der Akquise-Eingabe sind abgesetzt (getoente Flaeche,
deutlicherer Rahmen, beim Hineinklicken weiss). Auf der weissen Karte gingen sie
unter, und im Formular stehen bis zu fuenfzehn davon nebeneinander.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-08-18 12:13:21 +02:00
co-authored by Claude Opus 5
parent b8a78f609c
commit 05cc261900
14 changed files with 762 additions and 137 deletions
+39 -16
View File
@@ -21,7 +21,7 @@ export type ViewState =
* `account` und `whisper` stehen jedem offen - jeder pflegt sein eigenes Konto
* und richtet sein eigenes Diktat ein.
*/
export type SettingsTab = 'users' | 'account' | 'mail' | 'scanner' | 'whisper';
export type SettingsTab = 'users' | 'account' | 'akquise' | 'mail' | 'scanner' | 'whisper';
export const ADMIN_SETTINGS_TABS: SettingsTab[] = ['users', 'mail', 'scanner'];
@@ -251,20 +251,43 @@ export type OpportunityViewMode = 'grid' | 'list' | 'pipeline';
// ---------- Akquise ----------
/**
* Wie das Projekt zu uns kam. Feste Auswahl, damit sich die Liste nach dem Kanal
* gruppieren laesst - als Freitext waeren "Mail", "E-Mail" und "mail" drei Kanaele.
* Felder der Akquise, die aus einer Auswahlliste kommen. Die Werte stehen in der
* Datenbank (crm_acquisition_options) und werden unter Einstellungen -> Akquise
* gepflegt - als Freitext waeren "Hays", "hays" und "Hays AG" drei Anbieter.
*/
export const ACQUISITION_CHANNELS = [
'Mail',
'Telefon',
'Portal',
'LinkedIn',
'XING',
'Empfehlung',
'Sonstiges',
] as const;
export const ACQUISITION_OPTION_FIELDS = ['sender', 'provider', 'channel', 'consultant'] as const;
export type AcquisitionChannel = (typeof ACQUISITION_CHANNELS)[number];
export type AcquisitionOptionField = (typeof ACQUISITION_OPTION_FIELDS)[number];
/** Beschriftung der Felder - dieselbe in der Liste, im Formular und in den Einstellungen. */
export const ACQUISITION_FIELD_LABELS: Record<AcquisitionOptionField, string> = {
sender: 'Von',
provider: 'Anbieter',
channel: 'Kanal',
consultant: 'Berater',
};
/** Was in der jeweiligen Liste steht - als Hilfe in den Einstellungen. */
export const ACQUISITION_FIELD_HINTS: Record<AcquisitionOptionField, string> = {
sender: 'Personen, von denen Projekte kommen',
provider: 'Vermittler und Portale, z. B. Hays',
channel: 'Auf welchem Weg das Projekt kam',
consultant: 'Eigene Berater, für die Projekte gesucht werden',
};
export interface AcquisitionOption {
id: number;
value: string;
}
export type AcquisitionOptions = Record<AcquisitionOptionField, AcquisitionOption[]>;
export const EMPTY_ACQUISITION_OPTIONS: AcquisitionOptions = {
sender: [],
provider: [],
channel: [],
consultant: [],
};
/**
* Ein gefundenes Projekt, bevor daraus eine Opportunity wird.
@@ -280,7 +303,7 @@ export interface Acquisition {
sender: string | null;
/** "Anbieter": Vermittler oder Portal. */
provider: string | null;
channel: AcquisitionChannel | null;
channel: string | null;
title: string;
/** "Berater", fuer den es passt. */
consultant: string | null;
@@ -312,8 +335,8 @@ export interface AcquisitionList {
archive_after_days: number;
delete_after_days: number;
counts: { eingang: number; archiv: number };
/** Bisher benutzte Werte aus dem gesamten Bestand, fuer die Eingabehilfen. */
suggestions: { providers: string[]; consultants: string[]; senders: string[] };
/** Inhalte der Dropdowns; kommen mit, damit die Eingabe sie sofort hat. */
options: AcquisitionOptions;
entries: Acquisition[];
}