feat(akquise): mehrere Berater je Eintrag, je Berater eine Opportunity
Ein gefundenes Projekt passt oft zu mehr als einem Berater, und fuer jeden geht eine eigene Bewerbung heraus. Das Feld "Berater" liess bisher nur einen Namen zu. Gespeichert wird eine kommagetrennte Liste in crm_acquisitions.consultant (auf VARCHAR(500) erweitert, Migration in db.js) und keine eigene Zuordnungstabelle: die Namen sind reiner Text, die Auswahlliste in crm_acquisition_options ist eine Pflegehilfe und kein Fremdschluessel. Suche, Sortierung und Anzeige der Liste arbeiten damit weiter auf einem Feld. Die Funktionen zum Zerlegen stehen in acquisitionRules.js, das Gegenstueck fuer das Frontend in src/types.ts, geprueft von server/test/acquisition.test.mjs. Zerlegt wird an vier Stellen: - Uebernahme: je Berater eine Opportunity, alle in einer Transaktion. Ihre Titel tragen den Namen in Klammern, weil die Opportunity kein eigenes Feld fuer den Berater hat und in Pipeline und Liste sonst identische Zeilen stehen; bei einem einzigen Berater bleibt der Titel unveraendert. In den Notizen steht neben dem eigenen Berater auch, mit wem er sich das Projekt teilt. Der Eintrag merkt sich die erste Opportunity - sie entscheidet nur darueber, ob er wieder im Eingang auftaucht. - Verlauf (crm_acquisition_history): ein Berater je Zeile. Ein Eintrag mit drei Beratern sind drei Bewerbungen, "Nico, Lisa" waere sonst ein eigener Schluessel neben "Nico" und "Lisa". - Dashboard: dieselbe Zaehlung. Die Summe der Balken liegt damit ueber der Gesamtzahl "Bewerbungen", die weiter die Eintraege zaehlt. - Auswahlliste pflegen: Umbenennen ersetzt den Namen innerhalb der Liste, Loeschen zaehlt auch, wer nur einer von mehreren ist. Ein Gleichheitsvergleich uebersah beides. In der Oberflaeche haengt das Auswahlfeld jeweils einen Namen an, die Gewaehlten stehen als Plaketten darunter und lassen sich einzeln entfernen; ein bereits gewaehlter Berater verschwindet aus der Liste. Bewusst kein <select multiple>: Strg-Klick ist mit der Maus unauffindbar und mit einer Beruehrung gar nicht bedienbar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a866cd9a52
commit
62dce64739
+14
-1
@@ -86,7 +86,7 @@ erDiagram
|
||||
varchar provider "Anbieter"
|
||||
varchar channel "Mail | Telefon | Portal | ..."
|
||||
varchar title "Projekttitel"
|
||||
varchar consultant "Berater"
|
||||
varchar consultant "Berater, mehrere kommagetrennt"
|
||||
varchar link "macht den Titel klickbar"
|
||||
datetime converted_at "uebernommen -> raus aus der Liste"
|
||||
int opportunity_id FK
|
||||
@@ -133,6 +133,19 @@ Acht 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.
|
||||
- **Mehrere Berater stehen in einem Feld.** `crm_acquisitions.consultant`
|
||||
nimmt eine kommagetrennte Liste auf (`VARCHAR(500)`), keine eigene
|
||||
Zuordnungstabelle: die Namen sind reiner Text, die Auswahlliste in
|
||||
`crm_acquisition_options` ist eine Pflegehilfe und kein Fremdschlüssel. Suche,
|
||||
Sortierung und Anzeige der Liste arbeiten damit weiter auf einem Feld.
|
||||
Zerlegt wird an drei Stellen - im Verlauf, in der Auswertung und bei der
|
||||
Übernahme; die Funktionen dafür stehen in `server/src/acquisitionRules.js`,
|
||||
das Gegenstück fürs Frontend in `src/types.ts`.
|
||||
- **Der Verlauf zählt je Berater.** `crm_acquisition_history` hält einen Berater
|
||||
pro Zeile (Primärschlüssel `found_on, consultant`), auch wenn der Eintrag drei
|
||||
hatte: ein Fund für drei Berater sind drei versendete Profile. Die Summe im
|
||||
Balkendiagramm liegt deshalb über der Gesamtzahl *Bewerbungen*, die weiter die
|
||||
Einträge zählt.
|
||||
- **Das Dashboard rechnet aus zwei Quellen.** Jeder Eintrag in
|
||||
`crm_acquisitions` ist eine versendete Bewerbung. Weil die Akquise nach 90
|
||||
Tagen löscht, sichert der Aufräumlauf vorher Tag, Berater und Anzahl in
|
||||
|
||||
@@ -112,7 +112,10 @@ zeigt das Diagramm das **Ende** des Zeitraums und lässt sich nach links scrolle
|
||||
die aktuellen Tage stehen rechts.
|
||||
|
||||
Rechts daneben steht **Bewerbungen je Berater** für denselben Zeitraum. Einträge
|
||||
ohne hinterlegten Berater erscheinen als *Ohne Berater*.
|
||||
ohne hinterlegten Berater erscheinen als *Ohne Berater*. Ein Eintrag mit mehreren
|
||||
Beratern zählt bei **jedem** von ihnen: für jeden ist ein Profil herausgegangen.
|
||||
Die Summe der Balken liegt damit über der Gesamtzahl *Bewerbungen* oben, die die
|
||||
Einträge zählt - beides ist so gewollt.
|
||||
|
||||
### Anstehende Interviews und „Beim Kunden"
|
||||
|
||||
@@ -147,13 +150,26 @@ weg.
|
||||
| **Anbieter** | Freitext: Vermittler oder Portal, z. B. Hays. Vorschläge aus den bisherigen Einträgen |
|
||||
| **Kanal** | Auswahl: auf welchem Weg es kam |
|
||||
| **Projekttitel** | das einzige Pflichtfeld |
|
||||
| **Berater** | Auswahl: für wen das Projekt passt |
|
||||
| **Berater** | Auswahl: für wen das Projekt passt - **mehrere möglich** |
|
||||
| **Link** | Adresse der Ausschreibung. Ist einer hinterlegt, wird der Titel in der Liste anklickbar und öffnet ihn in einem neuen Tab |
|
||||
|
||||
*Von*, *Kanal* und *Berater* sind **Auswahllisten**. Ihre Inhalte stehen unter
|
||||
*Einstellungen → Akquise* und lassen sich dort von jedem Benutzer pflegen. Ist
|
||||
eine Liste noch leer, steht unter dem Feld ein Verweis dorthin.
|
||||
|
||||
**Berater** nimmt **mehrere Namen** auf - ein Projekt passt oft zu mehr als
|
||||
einem. Jede Auswahl hängt einen Namen an und erscheint darunter als Plakette;
|
||||
das Kreuz daran nimmt ihn wieder weg. Ein bereits gewählter Berater steht nicht
|
||||
mehr im Auswahlfeld, es zeigt also immer nur, wer noch zu haben ist.
|
||||
|
||||
> **Was mehrere Berater bedeuten:** Beim Übernehmen entsteht **je Berater eine
|
||||
> Opportunity** - jeder durchläuft seinen eigenen Vorgang, einer kann gewonnen
|
||||
> werden und ein anderer verloren. Ihre Titel bekommen den Namen in Klammern
|
||||
> angehängt, damit sie sich in Pipeline und Liste auseinanderhalten lassen; bei
|
||||
> einem einzigen Berater bleibt der Titel unverändert. Im Dashboard zählt der
|
||||
> Eintrag bei **jedem** der Berater - drei Berater sind drei versendete Profile,
|
||||
> also drei Bewerbungen.
|
||||
|
||||
**Von** ist mit dem **angemeldeten Benutzer** vorbelegt - in der Regel trägt
|
||||
jeder seine eigenen Funde ein. Trägt Markus etwas für Artur ein, wählt er Artur
|
||||
aus der Liste. Der eigene Name steht dabei immer zur Auswahl, auch wenn ihn
|
||||
@@ -183,7 +199,7 @@ wird geleert - für das nächste Projekt aus derselben Quelle.
|
||||
| **Von** | wer den Fund hereingebracht hat |
|
||||
| **Anbieter**, **Kanal** | woher es kam |
|
||||
| **Projekttitel** | mit Link anklickbar (neuer Tab); darunter klein Endkunde, Tagessatz bzw. Festpreis und Personentage |
|
||||
| **Berater** | für wen |
|
||||
| **Berater** | für wen; bei mehreren stehen sie als einzelne Plaketten |
|
||||
| **Opportunity** | ein Klick macht daraus eine Opportunity in der Phase *Erstgespräch Berater* |
|
||||
| Pfeil / Stift / Papierkorb | zurück in die Akquise (nur im Archiv), bearbeiten (füllt das Formular oben) und löschen (legt den Eintrag in den **Papierkorb**) |
|
||||
|
||||
@@ -268,6 +284,11 @@ im Archiv.
|
||||
Der Knopf **Opportunity** legt sie sofort an: Phase *Erstgespräch Berater*, 10 % Wahrscheinlichkeit,
|
||||
zuständig ist der angemeldete Benutzer. Mit übernommen werden Titel, Endkunde,
|
||||
Beschreibung, Tagessatz, Laufzeit, Projektpreis, Zeitraum und nächster Schritt.
|
||||
Sind **mehrere Berater** hinterlegt, entsteht **je Berater eine Opportunity**
|
||||
mit denselben Projektdaten; ihr Titel trägt den Namen in Klammern. In den Notizen
|
||||
steht neben dem eigenen Berater auch, mit wem er sich das Projekt teilt
|
||||
(*Auch beworben*).
|
||||
|
||||
Anbieter, Von, Kanal, Berater und Link haben in der Opportunity kein Feld - sie
|
||||
landen als Herkunftsvermerk in den internen Notizen, damit nachvollziehbar
|
||||
bleibt, woher das Projekt kam.
|
||||
|
||||
@@ -53,15 +53,21 @@ ansehen - siehe *Oberfläche prüfen ohne Datenbank* weiter unten.
|
||||
```bash
|
||||
npm run lint # = tsc --noEmit, das ist die Wahrheit
|
||||
npm run build # muss ohne Fehler durchlaufen
|
||||
cd server && npm test # Prüfungen der Postfachanbindung
|
||||
cd server && npm test # Postfachanbindung und Berater-Liste
|
||||
```
|
||||
|
||||
Automatische Tests gibt es nur für **einen** Bereich: `server/test/mail.test.mjs`
|
||||
prüft die Kontaktextraktion aus Mails und den Graph-Client. Beides läuft
|
||||
unbeaufsichtigt im Hintergrund und lässt sich von Hand kaum nachstellen - ein
|
||||
falsch erkannter Name landet still in der Kontaktliste, und ein 429 vom echten
|
||||
Dienst kommt genau dann, wenn niemand hinsieht. Kein Test-Framework, reines Node,
|
||||
keine Datenbank.
|
||||
Automatische Tests gibt es für zwei Bereiche, beide ohne Test-Framework, reines
|
||||
Node und ohne Datenbank:
|
||||
|
||||
* `server/test/mail.test.mjs` prüft die Kontaktextraktion aus Mails und den
|
||||
Graph-Client. Beides läuft unbeaufsichtigt im Hintergrund und lässt sich von
|
||||
Hand kaum nachstellen - ein falsch erkannter Name landet still in der
|
||||
Kontaktliste, und ein 429 vom echten Dienst kommt genau dann, wenn niemand
|
||||
hinsieht.
|
||||
* `server/test/acquisition.test.mjs` prüft das Zerlegen des Feldes *Berater*,
|
||||
das mehrere Namen kommagetrennt hält. Es steckt im Speichern, im Verlauf, in
|
||||
der Auswertung und in der Übernahme; ein Fehler darin fällt nicht auf, weil
|
||||
die Liste weiter Namen zeigt und nur die Zahlen falsch wären.
|
||||
|
||||
Für alles andere gilt: `tsc`, ein Build und ein Blick in die Oberfläche - notfalls
|
||||
mit dem Stub aus [Oberfläche prüfen ohne Datenbank](#oberflaeche-pruefen-ohne-datenbank).
|
||||
|
||||
@@ -94,7 +94,7 @@ Alle unter `router.use(requireAuth)`.
|
||||
| `opportunities.js` | `GET /?trash=true`, `POST /`, `PATCH /:id`, `DELETE /:id` (Papierkorb), `POST /:id/restore`, `POST /trash/empty` | ändern, löschen und wiederherstellen nur Ersteller, Zuständiger oder Admin; Leeren jeder Angemeldete |
|
||||
| `acquisitions.js` | `GET /?view=eingang\|archiv\|papierkorb\|alle`, `POST /`, `PATCH /:id`, `DELETE /:id` (Papierkorb), `DELETE /:id/permanent`, `POST /trash/empty`, `POST /:id/opportunity`, `POST /:id/reopen`, `POST /:id/restore`; Auswahllisten: `GET/POST /options`, `PATCH/DELETE /options/:id` | Einträge ändern, löschen und zurückholen nur Ersteller oder Admin; Auswahllisten und Leeren jeder Angemeldete. Zwei verschiedene Rückwege, nicht zu verwechseln: `/reopen` holt aus dem **Archiv** in den Eingang (setzt `found_at` auf heute und leert `converted_at`/`opportunity_id` - sonst hält die Übernahme den Eintrag im Archiv fest), `/restore` holt aus dem **Papierkorb** zurück (leert nur `deleted_at`/`deleted_by`, das Funddatum bleibt). Beide antworten 409, wenn der Eintrag im falschen Zustand ist |
|
||||
| `badges.js` | `GET /` | Zahlen für die Seitenleiste: Akquise-Archiv und dessen Papierkorb, Papierkorb der Opportunities und der Mails, verworfene Scanner-Treffer. Der Scanner-Zähler ist `null`, wenn dessen Datenbank fehlt |
|
||||
| `acquisitionRules.js` (kein Router) | Fristen und die SQL-Bedingungen `IS_OPEN`, `IN_ARCHIVE`, `NOT_DUE`, `OPPORTUNITY_GONE`, `NOT_DELETED`, `IN_TRASH`. Geteilt von Liste, Zählern, Aufräumlauf und Menüzahl - zwei Kopien derselben Regel sind einmal auseinandergelaufen |
|
||||
| `acquisitionRules.js` (kein Router) | Fristen und die SQL-Bedingungen `IS_OPEN`, `IN_ARCHIVE`, `NOT_DUE`, `OPPORTUNITY_GONE`, `NOT_DELETED`, `IN_TRASH`. Geteilt von Liste, Zählern, Aufräumlauf und Menüzahl - zwei Kopien derselben Regel sind einmal auseinandergelaufen. Dazu `splitConsultants` / `joinConsultants` / `normalizeConsultants` für das Feld *Berater*, das mehrere Namen kommagetrennt hält (Gegenstück im Frontend: `src/types.ts`) |
|
||||
| `dashboard.js` | `GET /?from=&to=&bucket=tag\|monat\|quartal\|jahr` | drei Reihen über der Zeit plus Bewerbungen je Berater. Liest Akquise **und** `crm_acquisition_history`; Termine und Phasenlisten stehen bewusst nicht drin - die lädt die App ohnehin |
|
||||
| `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 |
|
||||
|
||||
@@ -276,7 +276,7 @@ Warum genau diese Konstanten festgelegt sind, steht in
|
||||
| `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. Der Pfeil zurück in den Eingang erscheint nur, wenn `onReopen` gesetzt ist - im Eingang gibt es nichts zurückzuholen |
|
||||
| `components/acquisition/AcquisitionTrash.tsx` | Papierkorb der Akquise: zurückholen, endgültig löschen, leeren. Die Meldung nach dem Zurückholen nennt das Ziel (Eingang oder Archiv) - das Funddatum bleibt, also landet ein alter Fund im Archiv |
|
||||
| `components/acquisition/acquisitionUi.tsx` | `fieldClass` (abgesetzte Felder) und `OptionSelect` - zeigt einen Wert, der nicht mehr in der Liste steht, als solchen; `alwaysOffer` hält den eigenen Namen für *Von* immer wählbar. *Anbieter* nutzt es nicht: Freitext mit `<datalist>` |
|
||||
| `components/acquisition/acquisitionUi.tsx` | `fieldClass` (abgesetzte Felder) und `OptionSelect` - zeigt einen Wert, der nicht mehr in der Liste steht, als solchen; `alwaysOffer` hält den eigenen Namen für *Von* immer wählbar. *Anbieter* nutzt es nicht: Freitext mit `<datalist>`. Dazu `ConsultantSelect` (mehrere Berater: Auswahlfeld hängt an, Plaketten nehmen weg) und `ConsultantList` für die Anzeige in der Liste |
|
||||
| `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 |
|
||||
|
||||
Reference in New Issue
Block a user