feat: Interviews, Mail-Papierkorb, Kontaktart

Drei Aenderungen aus dem Alltagsgebrauch:

- Der Kalender heisst jetzt "Interviews" - dafuer wird er benutzt. Der Wert in
  ViewState wandert mit, damit Menue und Kopfzeile nicht auseinanderlaufen.
  Sichtbare Reste ("Kalenderfarbe") heissen jetzt Terminfarbe.

- Mails haben einen Papierkorb, der unter dem Menuepunkt aufklappt. Loeschen
  heisst dabei ausschliesslich: im CRM. Im Postfach crm@perinco.de bleibt die
  Mail unberuehrt - Schreibrechte darauf hat die Anwendung nicht. Damit der
  Delta-Abgleich geloeschte Mails nicht wieder einliest, bleibt nach dem Leeren
  eine leere Zeile als Merker stehen (crm_purged); runMailSync() ueberspringt
  alles, was im CRM geloescht ist. Zurueckholen und Leeren darf jeder
  Angemeldete, Leeren nach einer Warnung.

- Der Kontaktstatus (Neu .. Verloren) wird zur Kontaktart: Recruiter, Partner,
  Endkunde. Die Spalte heisst jetzt kind, weil "Status" das Feld falsch
  benannte. migrateLeadKind() stellt bestehende Datenbanken um und setzt alle
  vorhandenen Kontakte auf Recruiter - welche Art jemand ist, sagt der alte
  Status nicht (so mit dem Nutzer entschieden).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-08-18 11:12:49 +02:00
co-authored by Claude Opus 5
parent 025390e18a
commit 8741a9ae0c
39 changed files with 719 additions and 199 deletions
+3 -3
View File
@@ -90,8 +90,8 @@ sofort draußen, ohne auf den Ablauf des Tokens zu warten.
```mermaid
flowchart TD
L["Anmeldung"] --> R{"Rolle"}
R -->|admin| A["Opportunities, Kalender, Kontakte, Mails, Scanner<br/>Einstellungen: Nutzerverwaltung, Mein Konto, Postfach, Scanner, Whisper"]
R -->|user| U["Opportunities, Kalender, Kontakte, Mails, Scanner<br/>Einstellungen: Mein Konto, Whisper"]
R -->|admin| A["Opportunities, Interviews, Kontakte, Mails, Scanner<br/>Einstellungen: Nutzerverwaltung, Mein Konto, Postfach, Scanner, Whisper"]
R -->|user| U["Opportunities, Interviews, Kontakte, Mails, Scanner<br/>Einstellungen: Mein Konto, Whisper"]
```
Bei den Mails darf jede Rolle lesen und zuordnen - `crm@perinco.de` ist ein
@@ -102,7 +102,7 @@ Die Rollenprüfung sitzt **doppelt**: das Menü blendet aus, was nicht erlaubt
ist, und das Backend weist Zugriffe zusätzlich mit `403` ab (`requireAdmin`).
Ausnahme ist `GET /api/users/directory` - die Benutzerliste brauchen alle Rollen
für Netzwerkdiagramm, Zuständigkeiten, @-Mentions, Teilnehmerauswahl und
Kalenderfarben. Passwort-Hashes liefert der Endpunkt nie.
Terminfarben. Passwort-Hashes liefert der Endpunkt nie.
Ein Sicherheitsnetz im Backend verhindert, dass der letzte aktive Administrator
degradiert, deaktiviert oder gelöscht wird.
+3 -3
View File
@@ -30,7 +30,7 @@ erDiagram
varchar email UK
varchar name
enum role "admin | user"
varchar color "Kalenderfarbe, NULL = aus Id abgeleitet"
varchar color "Terminfarbe, NULL = aus Id abgeleitet"
longtext avatar "Data-URL"
varchar password_hash "bcrypt"
tinyint is_active
@@ -40,7 +40,7 @@ erDiagram
varchar first_name
varchar last_name
varchar company
enum status "Neu .. Verloren"
enum kind "Recruiter | Partner | Endkunde"
varchar source "z.B. 'Mail' bei Herkunft Postfach"
tinyint mail_pool "1 = Vorbereich 'Aus Mails', noch kein Kontakt"
tinyint mail_consent "Einverständnis für Werbemails"
@@ -94,7 +94,7 @@ Vier 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
Postfachabgleichs. Nur der Kontaktbestand erscheint in Kontaktliste,
Opportunities, Kalender und Netzwerk; der Vorbereich hat seine eigene Ansicht.
Opportunities, Interviews und Netzwerk; der Vorbereich hat seine eigene Ansicht.
Eine Spalte statt einer zweiten Tabelle, damit `crm_mail_links` und
`crm_mail_contact_candidates` für beide Bestände gelten
([11 Postfachanbindung](11-postfach.md)).
+4 -4
View File
@@ -10,13 +10,13 @@ sitzt und keine tiefen Links braucht.
flowchart TD
SB["Seitenleiste"] --> K["Kategorien"]
K --> O["Opportunities<br/>(Startansicht)"]
K --> C["Kalender"]
K --> C["Interviews"]
K --> L["Kontakte"]
L --> L1["Alle Kontakte"]
L --> L2["Aus Mails<br/>Vorbereich, Zähler im Menü"]
K --> M["Mails"]
M --> M1["Postfach"]
M --> M2["Kontakt-Vorschläge"]
M --> M1["Postfach<br/>Reiter: Postfach, Kontakt-Vorschläge"]
M --> M2["Papierkorb<br/>im CRM gelöschte Mails"]
K --> SC["Scanner"]
SC --> SC1["Dashboard"]
SC --> SC2["Unpassend"]
@@ -61,7 +61,7 @@ flowchart TB
APP --> NAV
APP --> DATA
DATA --> V1["Opportunities"]
DATA --> V2["Kalender"]
DATA --> V2["Interviews"]
DATA --> V3["Kontakte<br/>mail_pool = false"]
DATA --> V5["Aus Mails<br/>mail_pool = true + Kontakte zum Abgleich"]
DATA --> V4["Mails<br/>leads + opportunities für den Zuordnungsdialog"]
+62 -21
View File
@@ -10,7 +10,7 @@ Ein CRM für die Projektvermittlung. Es beantwortet vier Fragen:
| Frage | Bereich |
| --- | --- |
| Welche Geschäfte sind in Arbeit und was sind sie wert? | **Opportunities** |
| Wer ist wann wo, und wer ist beteiligt? | **Kalender** |
| Wer ist wann wo, und wer ist beteiligt? | **Interviews** |
| Wen kennen wir, und wer kennt wen? | **Kontakte** |
| Welche Projekte auf den Börsen passen zu unseren Beratern? | **Scanner** |
@@ -32,11 +32,13 @@ die Scanner-Einstellungen. Alles andere ist für beide Rollen gleich.
┌──────────────┬───────────────────────────────────────────┐
│ Kategorien │ Titel der Ansicht [Hell/Dunkel] │
│ Opportunities├──────────────────────────────────────────┤
│ Kalender │ │
│ Interviews │ │
│ Kontakte ▾ │ Inhalt │
│ Alle Kont. │ │
│ Aus Mails 5│ │
│ Mails │ │
│ Mails ▾ │ │
│ Postfach │ │
│ Papierkorb │ │
│ Scanner ▾ │ │
│ Dashboard │ │
│ Unpassend │ │
@@ -112,12 +114,13 @@ der Server, nicht nur die Oberfläche.
Auf dem Tablet oder ohne Lust auf Ziehen: die Phase lässt sich auch über das
Auswahlfeld direkt auf der Karte ändern.
## Kalender
## Interviews
Ein **gemeinsamer** Teamkalender - alle sehen dieselben Termine, jeder darf
eintragen.
Der gemeinsame Terminkalender des Teams - alle sehen dieselben Termine, jeder
darf eintragen. Genutzt wird er vor allem für Gesprächstermine, deshalb heißt
der Menüpunkt **Interviews** und nicht „Kalender".
![Monatsansicht des Kalenders](bilder/kalender-monat.png)
![Monatsansicht der Termine](bilder/kalender-monat.png)
* **Monat, Woche, Tag** oben umschalten, `Heute` und die Pfeile blättern.
* Klick auf einen Monatstag oder eine Stundenzelle legt sofort einen Termin an.
@@ -129,20 +132,20 @@ eintragen.
gleich, und der Punkt, an dem man den Termin angefasst hat, wandert mit. Ziehen
darf nur, wer den Termin auch ändern darf.
* **Farben** gehören zum Ersteller. Die eigene Farbe ändert man unter
*Einstellungen → Mein Konto*; die Legende unter dem Kalender zeigt, welche
Farbe zu wem gehört.
*Einstellungen → Mein Konto* („Meine Terminfarbe"); die Legende unter dem
Raster zeigt, welche Farbe zu wem gehört.
* **Teilnehmer** erscheinen mit Foto oder Initiale und sind anklickbar.
* **@-Erwähnungen**: In der Beschreibung `@` tippen, dann Kontakte *und*
Benutzer aus der Vorschlagsliste wählen (Pfeiltasten, Enter). In der
Leseansicht sind sie klickbar - Kontakte öffnen die Kontaktkarte, Benutzer ein
Detailfenster mit nächsten Terminen.
* Rechts stehen die **nächsten Termine**, nach Tagen gruppiert. Vergangenes
fliegt aus der Liste, bleibt im Kalender aber erhalten.
fliegt aus der Liste, bleibt im Raster aber erhalten.
![Wochenansicht des Kalenders mit Terminen dreier Benutzer](bilder/kalender-woche.png)
![Wochenansicht mit Terminen dreier Benutzer](bilder/kalender-woche.png)
Die Farbe eines Termins gehört zu seinem Ersteller - die Legende unter dem
Kalender löst auf, welche Farbe zu wem gehört. Rechts stehen die nächsten
Raster löst auf, welche Farbe zu wem gehört. Rechts stehen die nächsten
Termine mit Beschreibung, Teilnehmern und den markierten Personen.
Ändern und löschen darf nur der **Ersteller** oder ein **Administrator**.
@@ -154,18 +157,25 @@ Drei Ansichten, davon eine besondere:
| Ansicht | wofür |
| --- | --- |
| **Kachel** | Überblick mit Foto, Firma, Status |
| **Kachel** | Überblick mit Foto, Firma, Art |
| **Liste** | sortierbar, viele Einträge |
| **Netzwerk** | wer steht mit wem in Kontakt |
![Kontakte in der Kachelansicht](bilder/kontakte-kacheln.png)
Ein Klick auf eine Karte öffnet den Kontakt zum Bearbeiten - mit Foto, Anschrift,
Status, Zuständigkeit, Tags, Notizen und den Verbindungen zu anderen Kontakten.
Art, Zuständigkeit, Tags, Notizen und den Verbindungen zu anderen Kontakten.
Die **Art** sagt, mit wem man es zu tun hat: **Recruiter**, **Partner** oder
**Endkunde**. Früher stand an dieser Stelle ein Vertriebsstatus (Neu,
Kontaktiert, …); der sagte über den Kontakt selbst nichts. Bei der Umstellung am
18.08.2026 wurden alle vorhandenen Kontakte auf *Recruiter* gesetzt - welche Art
jemand wirklich ist, war aus dem alten Status nicht ableitbar. Wer die Angabe
braucht, pflegt sie im Kontakt nach.
![Dialog "Neuer Kontakt" mit allen Feldern](bilder/kontakt-dialog.png)
Im **Netzwerk** sind Kreise Kontakte (Farbe = Status, Größe = Anzahl
Im **Netzwerk** sind Kreise Kontakte (Farbe = Art, Größe = Anzahl
Verbindungen) und Quadrate Benutzer, die Kontakte angelegt haben. Grüne Linien
verbinden Kontakte untereinander, graue gestrichelte den Ersteller mit seinem
Kontakt.
@@ -175,10 +185,10 @@ Kontakt.
Bedienung: Hintergrund ziehen verschiebt, Mausrad zoomt, einen Knoten ziehen
setzt ihn fest (grüner Punkt), Doppelklick löst ihn wieder. Über *Verbindung
erstellen* zwei Kontakte anklicken, um eine Verbindung anzulegen; `Esc` bricht ab.
Suche und Statusfilter wirken auch hier.
Suche und Artfilter wirken auch hier.
Sobald Kontakte aus dem Postfach übernommen wurden, erscheint neben dem
Statusfilter ein **Herkunftsfilter**: *Aus Mails übernommen* zeigt nur die aus
Artfilter ein **Herkunftsfilter**: *Aus Mails übernommen* zeigt nur die aus
dem Postfach stammenden, *Selbst angelegt* nur die von Hand erfassten.
### Einverständnis für Mails
@@ -240,7 +250,7 @@ Der Menüpunkt *Kontakte* klappt zwei Unterpunkte auf: *Alle Kontakte* und
In *Aus Mails* stehen die Personen, die der Postfachabgleich in Mails gefunden
hat. **Sie sind noch keine Kontakte**: sie erscheinen nicht in der Kontaktliste,
nicht in Opportunities und nicht im Kalender. Jede Karte zeigt alle gefundenen
nicht in Opportunities und nicht unter Interviews. Jede Karte zeigt alle gefundenen
Angaben und darunter, aus welcher Mail sie stammen.
![Vorbereich "Aus Mails" mit zwei neuen Personen und einer gold markierten Dublette](bilder/aus-mails.png)
@@ -278,7 +288,8 @@ Das gemeinsame Postfach `crm@perinco.de` liegt hier im CRM. Der Zugriff ist
markiert, nichts gelöscht. Antworten geht weiter in Outlook; der Link *In Outlook
öffnen* springt direkt zur Mail.
Zwei Reiter oben:
Der Menüpunkt **Mails** klappt zwei Unterpunkte auf: *Postfach* und
*Papierkorb*. Im Postfach selbst gibt es oben zwei Reiter:
### Postfach
@@ -310,6 +321,36 @@ zeigt die Rohfassung ohne Formatierung.
Steht an einer Mail **Im Postfach gelöscht**, wurde sie in Outlook entfernt. Im
CRM bleibt sie erhalten, damit Zuordnungen nicht verloren gehen.
**Löschen** legt die Mail in den Papierkorb des CRM. Im Postfach
`crm@perinco.de` bleibt sie unangetastet - Schreibzugriff darauf hat das CRM
nicht.
### Papierkorb
Der Unterpunkt **Papierkorb** unter *Mails* zeigt alles, was im CRM gelöscht
wurde. Aufbau und Suche sind dieselben wie im Postfach, nur die Knöpfe wechseln:
* **Wiederherstellen** holt die Mail zurück in das Postfach des CRM - samt ihrer
Zuordnungen und Kontakt-Vorschlägen.
* **Papierkorb leeren** entfernt alle Mails darin **endgültig**, zusammen mit
ihren Zuordnungen zu Kontakten und Opportunities und ihren noch offenen
Kontakt-Vorschlägen. Bereits übernommene Vorschläge bleiben als Nachweis
erhalten, woher ein Kontakt stammt. Vorher fragt eine Warnung nach; danach ist
nichts mehr zurückzuholen. Das darf jeder angemeldete Benutzer.
Zwei Dinge sind wichtig zu wissen:
1. Gelöscht heißt **nur im CRM gelöscht**. In `crm@perinco.de` liegt die Mail
weiter, dort ändert das CRM nichts.
2. Der Abgleich holt gelöschte Mails **nicht wieder herein** - weder die im
Papierkorb noch die endgültig gelöschten. Dafür bleibt zu jeder endgültig
gelöschten Mail ein leerer Merker in der Datenbank stehen: ohne ihn würde der
nächste Abgleich sie im Postfach wiederfinden und erneut einlesen.
Offene Kontakt-Vorschläge einer Mail im Papierkorb verlangen keine Entscheidung
mehr - sie verschwinden aus dem Reiter *Kontakt-Vorschläge* und sind nach dem
Wiederherstellen wieder da.
### Kontakt-Vorschläge
Aus Absender, Empfängerliste und Signatur jeder Mail liest die Anwendung
@@ -442,7 +483,7 @@ Datenbank.
| Reiter | Wer | Inhalt |
| --- | --- | --- |
| **Nutzerverwaltung** | Admin | Benutzer anlegen, bearbeiten, deaktivieren, löschen |
| **Mein Konto** | alle | Foto, Kalenderfarbe, Passwort ändern |
| **Mein Konto** | alle | Foto, Terminfarbe, Passwort ändern |
| **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 |
@@ -467,7 +508,7 @@ Die Zugangsdaten selbst stehen **nicht** in der Oberfläche, sondern in der
Serverkonfiguration - sie werden beim Start gelesen. Fehlt einer, sagt die Seite
welcher.
Foto und Kalenderfarbe werden **sofort** gespeichert, ohne Speichern-Knopf. Der
Foto und Terminfarbe werden **sofort** gespeichert, ohne Speichern-Knopf. Der
letzte aktive Administrator kann sich nicht selbst degradieren oder löschen -
das verhindert der Server.
+7 -2
View File
@@ -128,12 +128,17 @@ danach nichts nachladen.
### Neuer Menüpunkt
1. `src/types.ts`: Wert in `ViewState` ergänzen.
2. `src/components/Sidebar.tsx`: Eintrag in `CATEGORY_ITEMS` (Name + SVG-Pfad).
2. `src/components/Sidebar.tsx`: Eintrag in `CATEGORY_ENTRIES` (Name + SVG-Pfad).
3. `src/App.tsx`: Komponente rendern und einen Untertitel setzen.
Der Name in `ViewState` ist gleichzeitig die Beschriftung und der Titel in der
Kopfzeile. Wer eine abweichende Beschriftung braucht, setzt `label` am `MenuItem`
(so macht es der Unterpunkt *Dashboard*, der auf die Ansicht `Scanner` zeigt).
(so macht es der Unterpunkt *Postfach*, der auf die Ansicht `Mails` zeigt).
Soll der Punkt aufklappen, kommt statt eines `MenuItem` eine `MenuGroup` in
`CATEGORY_ENTRIES` - mit einer Liste ihrer Ansichten in `types.ts`
(`CONTACT_VIEWS`, `MAIL_VIEWS`, `SCANNER_VIEWS`), damit die Gruppe offen bleibt,
solange eine ihrer Ansichten aktiv ist.
### Neuer Reiter in den Einstellungen
+19 -6
View File
@@ -28,13 +28,26 @@ Beim Start: `initDatabase()` (CRM-Schema, Admin-Seed) → `initScannerDatabase()
| Export | Zweck |
| --- | --- |
| `pool` | mysql2-Pool, `connectionLimit: 8`, `dateStrings: true` |
| `initDatabase()` | führt `schema.sql` Statement für Statement aus, dann `ensureAdminUser()` |
| `initDatabase()` | führt `schema.sql` Statement für Statement aus, dann `migrateLeadKind()`, `migrateMailPool()`, `ensureAdminUser()` |
`dateStrings: true` ist eine bewusste Entscheidung: Termine kommen als
`YYYY-MM-DD HH:MM:SS` **ohne** Zeitzonenumrechnung an. Der Kalender rechnet mit
`YYYY-MM-DD HH:MM:SS` **ohne** Zeitzonenumrechnung an. Die Terminansicht rechnet mit
Wandzeit; ein `Date`-Objekt aus dem Treiber würde beim Serialisieren nach UTC
verschieben.
Die beiden Einmal-Umstellungen laufen bei jedem Start, tun aber nur beim ersten
Mal etwas:
* `migrateLeadKind()` macht aus dem Vertriebsstatus eines Kontakts seine Art.
Erkannt wird der Altstand an der Spalte `crm_leads.status` selbst - liegt schon
`kind` vor, passiert nichts. Die Auswahl wird erst auf alte plus neue Werte
erweitert, dann werden alle Altwerte auf `Recruiter` gesetzt, dann wird
umbenannt und auf die drei neuen Werte verengt. Direkt zu verengen würde jeden
Altwert zu `''` machen.
* `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.
`ensureAdminUser()` legt nur an, wenn `crm_users` **leer** ist. Ohne
`ADMIN_PASSWORD` erzeugt es ein Zufallspasswort und schreibt es einmalig ins Log -
im Quelltext steht kein Passwort.
@@ -70,7 +83,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 |
| `mail.js` | `GET /status`, `/messages`, `/messages/:id`, `/candidates`; `POST /messages/:id/links`, `/candidates/:id/adopt`, `/candidates/:id/reject`; `DELETE /links/:id`; `requireAdmin`: `GET/POST /settings`, `POST /test`, `POST /sync` | lesen und zuordnen jeder; einrichten Admin |
| `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:
@@ -213,7 +226,7 @@ Objekt `mailApi`, `MAIL_SETTINGS_DEFAULTS` und die Datumshelfer.
`parseMailDate()` hängt ein `Z` an den MariaDB-Zeitstempel, bevor daraus ein
`Date` wird. Ohne das liest der Browser den Wert als Ortszeit, und die Anzeige
wäre je Zeitzone um Stunden verschoben - die Mailzeiten stehen als UTC in der
Datenbank, anders als die Kalendertermine. `formatMailDate()` zeigt heute nur die
Datenbank, anders als die Termine unter Interviews. `formatMailDate()` zeigt heute nur die
Uhrzeit, dieses Jahr Tag und Monat, sonst mit Jahr.
### Kontexte
@@ -242,7 +255,7 @@ Warum genau diese Konstanten festgelegt sind, steht in
| Datei | Inhalt |
| --- | --- |
| `components/leads/leadUi.tsx` | `inputClass`, `labelClass`, `StatusBadge`, `Avatar`, Statusfarben |
| `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 |
| `components/leads/MailPoolMergeDialog.tsx` | Feldauswahl beim Ergänzen eines vorhandenen Kontakts |
@@ -266,7 +279,7 @@ 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_STATUSES`, `CONNECTION_TYPES` | die Aufzählungen der Datenbank |
| `OPPORTUNITY_STAGES`, `OPEN_STAGES`, `LEAD_KINDS`, `CONNECTION_TYPES` | die Aufzählungen der Datenbank |
| `eventColor()`, `leadFullName()`, `leadInitials()`, `contactFullName()` | Anzeigelogik |
## Datenfluss im Frontend
+2
View File
@@ -21,6 +21,8 @@ Quelltext.
| **Kontakt-Vorschlag** | Aus einer Mail gelesene Person, die noch kein Kontakt ist. Tabelle `crm_mail_contact_candidates`. Wird übernommen, ergänzt oder verworfen. |
| **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`. |
| **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. |
| **Pipeline** | Zwei Bedeutungen, je nach Zusammenhang: die Kanban-Ansicht der Opportunities **oder** der Suchlauf des Scanners (`services/pipeline.js`). |
| **Projekt** | Ein von einer Börse gescrapter Auftrag. Tabelle `projects`, Aufbewahrung 7 Tage. |
+44 -3
View File
@@ -124,7 +124,8 @@ sequenceDiagram
end
G-->>Y: Seite mit Metadaten + nextLink/deltaLink
loop je Nachricht
Y->>D: bekannt? Text vorhanden?
Y->>D: bekannt? Text vorhanden? im CRM gelöscht?
Note over Y: im Papierkorb oder endgültig gelöscht? dann weiter
Note over Y: unbekannt und älter als die Grenze? dann weiter
opt Text fehlt
Y->>G: /messages/{id}?$select=body
@@ -224,7 +225,7 @@ nur `mail_pool = 0`, der Vorbereich steht als Unterpunkt *Aus Mails* unter dem
Menüpunkt *Kontakte*.
Der Grund ist Pflegbarkeit: die Kontaktliste ist eine gepflegte Liste, an der
Opportunities, Kalender und Netzwerk hängen. Ein Abgleich, der ungefragt jede
Opportunities, Interviews und Netzwerk hängen. Ein Abgleich, der ungefragt jede
Adresse aus jeder Mail hineinschreibt, macht sie unbrauchbar - genau daran ist
das Altsystem gescheitert. Deshalb sind Funde aus Mails Rohmaterial, bis jemand
entscheidet.
@@ -288,13 +289,16 @@ Alle unter `/api/mail`, alle hinter `requireAuth`.
| Methode | Pfad | Rolle | Zweck |
| --- | --- | --- | --- |
| GET | `/status` | alle | Betriebszustand, Zähler, letzter Fehler |
| GET | `/messages` | alle | Liste; `search`, `filter`, `limit`, `offset` |
| GET | `/messages` | alle | Liste; `search`, `filter`, `limit`, `offset`, `trash` |
| GET | `/messages/:id` | alle | Mail mit Text, Zuordnungen, Vorschlägen |
| POST | `/messages/:id/links` | alle | an Kontakt und/oder Opportunity hängen |
| DELETE | `/links/:id` | alle | Zuordnung lösen |
| GET | `/candidates` | alle | Kontakt-Vorschläge, `status` wählbar |
| POST | `/candidates/:id/adopt` | alle | Kontakt anlegen bzw. ergänzen; `target: 'pool'` legt in „Aus Mails" ab |
| POST | `/candidates/:id/reject` | alle | Vorschlag verwerfen |
| DELETE | `/messages/:id` | alle | Mail in den Papierkorb legen |
| POST | `/messages/:id/restore` | alle | Mail aus dem Papierkorb zurückholen |
| POST | `/trash/empty` | alle | Papierkorb endgültig leeren |
Der Vorbereich hängt an den Kontakten und hat seine Endpunkte deshalb unter
`/api/leads`:
@@ -319,6 +323,43 @@ vorbehalten.
`filter` in der Liste: `unlinked` (noch nirgends zugeordnet - die Arbeitsliste),
`candidates` (offene Kontakt-Vorschläge), `linked` (bereits zugeordnet).
`trash=true` zeigt genau den Papierkorb; ohne den Parameter ist er ausgeblendet.
## Papierkorb
Löschen heißt im CRM: aus dem CRM verschwinden lassen. Das Postfach bleibt
unberührt - Schreibrechte darauf hat die Anwendung nicht und soll sie nicht
haben. Zwei Spalten in `crm_mail_messages` tragen das:
| Spalte | Bedeutung |
| --- | --- |
| `crm_deleted_at` (+ `crm_deleted_by`) | liegt im Papierkorb, lässt sich zurückholen |
| `crm_purged` | Papierkorb geleert: Zeile bleibt als leerer Merker stehen |
Der Merker ist der Kern der Sache. Im Postfach liegt die Mail weiter, der
Delta-Abgleich liefert sie also erneut aus. `runMailSync()` prüft deshalb bei
jeder Nachricht zuerst, ob sie im CRM gelöscht ist, und lässt sie dann in Ruhe -
kein Text wird nachgeladen, keine Zeile aktualisiert. Ohne den Merker käme jede
endgültig gelöschte Mail beim nächsten Lauf zurück. Der Lauf zählt die
übersprungenen Mails in `skippedTrashed` und schreibt sie ins Protokoll.
Beim Leeren fällt zusätzlich weg:
* alle `crm_mail_links` der betroffenen Mails - was es nicht mehr gibt, kann an
keiner Opportunity und keinem Kontakt hängen,
* deren **offene** Kontakt-Vorschläge. Entschiedene bleiben: sie sind der
Nachweis, woher ein Kontakt stammt, und tragen die Herkunftsanzeige in
„Aus Mails".
Vom Inhalt bleibt nichts: Betreff, Absender, Empfänger, Vorschautext und Text
werden geleert. In der Zeile stehen danach nur noch die Graph-Kennung, ihr Hash,
der Empfangszeitpunkt und der Löschvermerk.
In den Papierkorb legen und leeren darf **jeder angemeldete Benutzer** - beim
Leeren nach einer Warnung. Der erste Schritt ist umkehrbar, der zweite nicht.
`crm_deleted_by` trägt bewusst keinen Fremdschlüssel auf `crm_users`: Spalten
lassen sich mit `ADD COLUMN IF NOT EXISTS` idempotent nachziehen, ein
Fremdschlüssel nicht.
## Was anders ist als im Altsystem