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:
co-authored by
Claude Opus 5
parent
b8a78f609c
commit
05cc261900
+15
-1
@@ -25,6 +25,7 @@ erDiagram
|
||||
crm_opportunities ||--o{ crm_opportunity_contacts : hat
|
||||
crm_users ||--o{ crm_acquisitions : created_by
|
||||
crm_acquisitions |o--o| crm_opportunities : "wird zu"
|
||||
crm_acquisition_options ||--o{ crm_acquisitions : "fuellt Von, Anbieter, Kanal, Berater"
|
||||
|
||||
crm_users {
|
||||
int id PK
|
||||
@@ -88,6 +89,11 @@ erDiagram
|
||||
datetime converted_at "uebernommen -> raus aus der Liste"
|
||||
int opportunity_id FK
|
||||
}
|
||||
crm_acquisition_options {
|
||||
int id PK
|
||||
enum field "sender | provider | channel | consultant"
|
||||
varchar value "Inhalt des Dropdowns"
|
||||
}
|
||||
crm_events {
|
||||
int id PK
|
||||
varchar title
|
||||
@@ -103,7 +109,7 @@ erDiagram
|
||||
}
|
||||
```
|
||||
|
||||
Fünf Eigenheiten, die man kennen muss:
|
||||
Sechs Eigenheiten, die man kennen muss:
|
||||
|
||||
- **`crm_leads` führt zwei Bestände.** `mail_pool = 0` ist der gepflegte
|
||||
Kontaktbestand, `mail_pool = 1` der Vorbereich „Aus Mails" mit den Funden des
|
||||
@@ -120,6 +126,14 @@ Fünf Eigenheiten, die man kennen muss:
|
||||
- **`avatar` ist eine Data-URL in der Datenbank.** Der Browser verkleinert jedes
|
||||
Bild vor dem Upload auf 256 px (`src/lib/imageCompress.ts`), aus 2,9 MB werden
|
||||
wenige Kilobyte.
|
||||
- **Die Dropdowns der Akquise stehen in einer Tabelle, nicht in vier.**
|
||||
`crm_acquisition_options` unterscheidet die Listen über die Spalte `field`. Die
|
||||
vier Felder verhalten sich gleich, also ist jede weitere Liste ein Wert im
|
||||
ENUM statt einer neuen Tabelle samt Route und Oberfläche. Der Unique-Schlüssel
|
||||
auf `(field, value)` greift über die Kollation `utf8mb4_unicode_ci` auch bei
|
||||
abweichender Groß-/Kleinschreibung - „hays" ist neben „Hays" kein neuer Wert.
|
||||
Die Akquise-Einträge halten den **Text**, keinen Fremdschlüssel: wird ein Wert
|
||||
aus der Liste entfernt, bleiben die Einträge lesbar und änderbar.
|
||||
- **`crm_acquisitions` hat keine Zustandsspalte.** Eingang, Archiv und Löschung
|
||||
ergeben sich aus `found_at` (14 bzw. 90 Tage) und `converted_at`. Eine Spalte,
|
||||
die ein Job umsetzen müsste, kann von der Wirklichkeit abweichen - eine
|
||||
|
||||
@@ -88,16 +88,19 @@ weg.
|
||||
| Feld | wofür |
|
||||
| --- | --- |
|
||||
| **Datum** | wann gefunden. Vorbelegt mit heute; entscheidet später über Archiv und Löschung |
|
||||
| **Von** | wer es geschickt hat |
|
||||
| **Anbieter** | Vermittler oder Portal, z. B. Hays |
|
||||
| **Kanal** | Mail, Telefon, Portal, LinkedIn, XING, Empfehlung, Sonstiges |
|
||||
| **Von** | Auswahl: wer es geschickt hat |
|
||||
| **Anbieter** | Auswahl: Vermittler oder Portal, z. B. Hays |
|
||||
| **Kanal** | Auswahl: auf welchem Weg es kam |
|
||||
| **Projekttitel** | das einzige Pflichtfeld |
|
||||
| **Berater** | für wen das Projekt passt |
|
||||
| **Berater** | Auswahl: für wen das Projekt passt |
|
||||
| **Link** | Adresse der Ausschreibung. Ist einer hinterlegt, wird der Titel in der Liste anklickbar und öffnet ihn in einem neuen Tab |
|
||||
|
||||
*Von*, *Anbieter* und *Berater* schlagen vor, was schon einmal eingetragen wurde -
|
||||
antippen statt tippen. Beim Link genügt `hays.de/projekt/1`, das `https://` wird
|
||||
ergänzt.
|
||||
*Von*, *Anbieter*, *Kanal* und *Berater* sind **Auswahllisten**. Ihre Inhalte
|
||||
stehen unter *Einstellungen → Akquise* und lassen sich dort von jedem Benutzer
|
||||
pflegen - Freitext hätte aus „Hays", „hays" und „Hays AG" drei Anbieter gemacht.
|
||||
Ist eine Liste noch leer, steht unter dem Feld ein Verweis dorthin.
|
||||
|
||||
Beim Link genügt `hays.de/projekt/1`, das `https://` wird ergänzt.
|
||||
|
||||
Unter **Projektdaten für die Opportunity** klappt auf, was in der Liste keine
|
||||
Spalte hat, später aber in der Opportunity stehen soll: Endkunde, Projektstart
|
||||
@@ -561,10 +564,21 @@ Datenbank.
|
||||
| --- | --- | --- |
|
||||
| **Nutzerverwaltung** | Admin | Benutzer anlegen, bearbeiten, deaktivieren, löschen |
|
||||
| **Mein Konto** | alle | Foto, Terminfarbe, Passwort ändern |
|
||||
| **Akquise** | alle | Inhalte der Auswahllisten *Von*, *Anbieter*, *Kanal*, *Berater* |
|
||||
| **Postfach** | Admin | Verbindung zu Microsoft 365, Abgleich, Kontaktübernahme |
|
||||
| **Scanner** | Admin | Suchintervall, Schwellenwerte, KI-Schlüssel, Ton, E-Mail |
|
||||
| **Whisper** | alle | Spracherkennung für das Diktat einrichten |
|
||||
|
||||
Im Reiter **Akquise** stehen die vier Auswahllisten des Akquise-Formulars, jede
|
||||
mit einem Feld zum Hinzufügen und je Eintrag einem Stift und einem Papierkorb.
|
||||
Zwei Dinge sind dabei unterschiedlich:
|
||||
|
||||
* **Umbenennen** ändert die vorhandenen Akquise-Einträge mit. Aus „Hays" wird in
|
||||
allen Einträgen „Hays AG"; die Meldung sagt, wie viele betroffen waren.
|
||||
* **Entfernen** nimmt den Wert nur aus der Auswahl. Einträge, die ihn tragen,
|
||||
behalten ihn und zeigen ihn im Formular als „nicht in der Liste" - so ändert
|
||||
das Aufräumen einer Liste keine Daten.
|
||||
|
||||
Im Reiter **Scanner** steht das **Suchintervall (Minuten)** - also wie oft der
|
||||
Hintergrund-Scanner die Börsen durchsucht.
|
||||
|
||||
|
||||
+10
-2
@@ -47,6 +47,11 @@ Mal etwas:
|
||||
* `migrateMailPool()` schiebt die vom Postfachabgleich angelegten Kontakte in den
|
||||
Vorbereich. Sie merkt sich das per Schalter in `crm_settings`, weil an den
|
||||
Daten selbst nicht zu erkennen ist, ob sie schon gelaufen ist.
|
||||
* `seedAcquisitionOptions()` legt den Startbestand der Akquise-Dropdowns an: die
|
||||
Kanäle, die vorher im Quelltext standen, plus alles, was in vorhandenen
|
||||
Einträgen als Freitext steht. Auch hier ein Schalter in `crm_settings` - ohne
|
||||
ihn wäre jeder Neustart ein Wiederherstellen von Werten, die jemand bewusst
|
||||
entfernt hat.
|
||||
|
||||
`ensureAdminUser()` legt nur an, wenn `crm_users` **leer** ist. Ohne
|
||||
`ADMIN_PASSWORD` erzeugt es ein Zufallspasswort und schreibt es einmalig ins Log -
|
||||
@@ -83,7 +88,7 @@ Alle unter `router.use(requireAuth)`.
|
||||
| `connections.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | jeder Angemeldete |
|
||||
| `events.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | anlegen jeder; ändern/löschen nur Ersteller oder Admin |
|
||||
| `opportunities.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | ändern/löschen nur Ersteller, Zuständiger oder Admin |
|
||||
| `acquisitions.js` | `GET /?view=eingang\|archiv\|alle`, `POST /`, `PATCH /:id`, `DELETE /:id`, `POST /:id/opportunity` | ändern/löschen nur Ersteller oder Admin |
|
||||
| `acquisitions.js` | `GET /?view=eingang\|archiv\|alle`, `POST /`, `PATCH /:id`, `DELETE /:id`, `POST /:id/opportunity`; Auswahllisten: `GET/POST /options`, `PATCH/DELETE /options/:id` | Einträge ändern/löschen nur Ersteller oder Admin; Auswahllisten jeder Angemeldete |
|
||||
| `mail.js` | `GET /status`, `/messages`, `/messages/:id`, `/candidates`; `POST /messages/:id/links`, `/messages/:id/restore`, `/candidates/:id/adopt`, `/candidates/:id/reject`, `/trash/empty`; `DELETE /links/:id`, `/messages/:id`; `requireAdmin`: `GET/POST /settings`, `POST /test`, `POST /sync` | lesen, zuordnen und löschen jeder; einrichten Admin |
|
||||
|
||||
Wiederkehrende Muster in diesen Routern:
|
||||
@@ -259,6 +264,8 @@ Warum genau diese Konstanten festgelegt sind, steht in
|
||||
| `components/acquisition/Acquisitions.tsx` | Akquise und Archiv (`view`); Suche, Sortierung, Übernahme, Löschen |
|
||||
| `components/acquisition/AcquisitionForm.tsx` | Eingabe über der Liste; behält Datum, Anbieter, Kanal und Berater für den nächsten Eintrag |
|
||||
| `components/acquisition/AcquisitionTable.tsx` | die Liste; `safeLink()` lässt nur http(s) zum Link werden |
|
||||
| `components/acquisition/acquisitionUi.tsx` | `fieldClass` (abgesetzte Felder) und `OptionSelect` - zeigt einen Wert, der nicht mehr in der Liste steht, als solchen |
|
||||
| `components/settings/AcquisitionSettings.tsx` | Reiter *Einstellungen → Akquise*: die vier Auswahllisten pflegen |
|
||||
| `components/leads/leadUi.tsx` | `inputClass`, `labelClass`, `KindBadge`, `Avatar`, Farben der Kontaktart |
|
||||
| `components/leads/mailPool.ts` | `findMailPoolMatch()`, `diffFields()`, `isConflict()`, `MAIL_POOL_FIELDS` - Dublettenabgleich des Vorbereichs |
|
||||
| `components/leads/MailPoolLeads.tsx` | Ansicht „Aus Mails": Goldmarkierung, Abweichungen, Übernehmen, Sammelübernahme |
|
||||
@@ -283,7 +290,8 @@ nicht auseinanderlaufen:
|
||||
| `MAIL_LEAD_SOURCE`, `LeadOriginFilter` | Herkunft „Mail" in `Lead.source` und ihr Filter |
|
||||
| `USER_COLORS`, `userColor()` | Farbe gesetzt, sonst stabil aus der Benutzer-Id |
|
||||
| `opportunityVolume()`, `weightedVolume()`, `formatEuro()` | Festpreis, sonst Tagessatz x Personentage; gewichtet; formatiert |
|
||||
| `OPPORTUNITY_STAGES`, `OPEN_STAGES`, `LEAD_KINDS`, `ACQUISITION_CHANNELS`, `CONNECTION_TYPES` | die Aufzählungen der Datenbank |
|
||||
| `OPPORTUNITY_STAGES`, `OPEN_STAGES`, `LEAD_KINDS`, `CONNECTION_TYPES` | die Aufzählungen der Datenbank |
|
||||
| `ACQUISITION_OPTION_FIELDS`, `ACQUISITION_FIELD_LABELS`, `ACQUISITION_FIELD_HINTS` | die vier Auswahllisten der Akquise samt Beschriftung |
|
||||
| `eventColor()`, `leadFullName()`, `leadInitials()`, `contactFullName()` | Anzeigelogik |
|
||||
|
||||
## Datenfluss im Frontend
|
||||
|
||||
@@ -22,6 +22,7 @@ Quelltext.
|
||||
| **Match** / Treffer | Die Bewertung **eines** Projekts für **einen** Agenten: Prozentwert, Pro-Liste, Contra-Liste, Status. Tabelle `matches`. Ein Projekt erzeugt so viele Treffer, wie es aktive Agenten gibt. |
|
||||
| **Opportunity** | Geschäft in Arbeit: Phase, Tagessatz und Laufzeit oder Projektpreis, Wahrscheinlichkeit, Ansprechpartner. Tabelle `crm_opportunities`. |
|
||||
| **Akquise** | Eingang für gefundene Projekte, bevor daraus eine Opportunity wird. Tabelle `crm_acquisitions`. Nach 14 Tagen im **Archiv**, nach 90 Tagen gelöscht. |
|
||||
| **Auswahllisten** (Akquise) | Inhalte der Dropdowns *Von*, *Anbieter*, *Kanal*, *Berater*. Tabelle `crm_acquisition_options`, gepflegt unter *Einstellungen → Akquise*. |
|
||||
| **Art** (eines Kontakts) | Recruiter, Partner oder Endkunde. Spalte `crm_leads.kind`; hieß bis 18.08.2026 „Status" mit den Werten Neu .. Verloren. |
|
||||
| **Papierkorb** | Im CRM gelöschte Mails. Umkehrbar, bis er geleert wird. Im Postfach crm@perinco.de bleiben die Mails in jedem Fall; der Abgleich holt gelöschte nicht wieder herein. |
|
||||
| **Phase** / Stage | Fortschritt einer Opportunity: Neu, Qualifizierung, Angebot, Verhandlung, Gewonnen, Verloren. |
|
||||
|
||||
Reference in New Issue
Block a user