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:
co-authored by
Claude Opus 5
parent
025390e18a
commit
8741a9ae0c
@@ -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.
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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".
|
||||
|
||||

|
||||

|
||||
|
||||
* **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.
|
||||
|
||||

|
||||

|
||||
|
||||
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 |
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user