feat(kontakte): Vorbereich "Aus Mails" mit Dublettenabgleich

Der Postfachabgleich hat gefundene Personen bisher direkt in den
Kontaktbestand geschrieben. Damit wächst eine gepflegte Liste ungefragt
mit jeder Adresse aus jeder Mail - genau daran ist das Altsystem
gescheitert.

Funde landen jetzt in einem Vorbereich (crm_leads.mail_pool = 1), der als
Unterpunkt "Aus Mails" unter dem Menüpunkt Kontakte aufklappt. Der
Kontaktbestand bleibt unberührt, bis dort jemand entscheidet; in
Opportunities, Kalender und Netzwerk tauchen die Funde nicht auf.

Eine Spalte statt einer zweiten Tabelle, weil crm_mail_links und
crm_mail_contact_candidates per Fremdschlüssel auf crm_leads zeigen - so
behält der Vorbereich seine Mailbezüge.

Übernehmen kennt zwei Wege:
- neue Person: Kopie mit mail_pool = 0, die Mailverknüpfungen wandern mit
- Dublette (Adresse, sonst voller Name): gold markiert, abweichende
  Felder mit altem und neuem Wert; die Feldauswahl schreibt sie in den
  vorhandenen Kontakt. Vorausgewählt sind nur Lücken - ein Wert aus einer
  alten Signatur soll nichts Gepflegtes stillschweigend ersetzen.

db.js verschiebt beim ersten Start die vorhandenen Kontakte mit
source = 'Mail' in den Vorbereich, einmalig über den Merker
leads_mail_pool_migrated in crm_settings.

Die Sidebar-Gruppe ist dabei generisch geworden (MenuGroup) - Kontakte
und Scanner benutzen jetzt denselben Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-08-11 08:59:14 +02:00
co-authored by Claude Opus 5
parent 8090d0aae1
commit ea55acdb5c
22 changed files with 1524 additions and 115 deletions
+10 -1
View File
@@ -41,6 +41,8 @@ erDiagram
varchar last_name
varchar company
enum status "Neu .. Verloren"
varchar source "z.B. 'Mail' bei Herkunft Postfach"
tinyint mail_pool "1 = Vorbereich 'Aus Mails', noch kein Kontakt"
longtext tags "JSON-Array als Text"
longtext avatar
int owner_id FK
@@ -84,8 +86,15 @@ erDiagram
}
```
Drei Eigenheiten, die man kennen muss:
Vier Eigenheiten, die man kennen muss:
- **`crm_leads` fuehrt zwei Bestaende.** `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.
Eine Spalte statt einer zweiten Tabelle, damit `crm_mail_links` und
`crm_mail_contact_candidates` fuer beide Bestaende gelten
([11 Postfachanbindung](11-postfach.md)).
- **`tags` ist Text, kein JSON-Typ.** Bewusst, damit es auf aelteren
MariaDB-Versionen ohne JSON-Funktionen laeuft. Das Backend serialisiert.
- **`crm_lead_connections` speichert das Paar sortiert** (kleinere Id in
+21 -6
View File
@@ -12,6 +12,8 @@ flowchart TD
K --> O["Opportunities<br/>(Startansicht)"]
K --> C["Kalender"]
K --> L["Kontakte"]
L --> L1["Alle Kontakte"]
L --> L2["Aus Mails<br/>Vorbereich, Zaehler im Menue"]
K --> M["Mails"]
M --> M1["Postfach"]
M --> M2["Kontakt-Vorschlaege"]
@@ -31,9 +33,14 @@ flowchart TD
*Postfach* und *Kontakt-Vorschlaege* sind Reiter **innerhalb** der Ansicht
*Mails*, keine Menuepunkte - sie gehoeren zusammen und wechseln oft hin und her.
Der Menuepunkt *Scanner* klappt seine Unterpunkte auf; *Scanner* selbst ist das
Dashboard. Bei eingeklappter Leiste (Symbolbreite) erscheinen nur die Symbole,
die Unterpunkte kommen mit den Beschriftungen zurueck.
Die Menuepunkte *Kontakte* und *Scanner* klappen ihre Unterpunkte auf; der
Kopfeintrag ist dabei selbst eine Ansicht (Kontaktliste bzw. Scanner-Dashboard).
Bei eingeklappter Leiste (Symbolbreite) erscheinen nur die Symbole, die
Unterpunkte kommen mit den Beschriftungen zurueck. Beide Gruppen entstehen aus
derselben Struktur `MenuGroup` in `Sidebar.tsx`.
*Aus Mails* traegt einen Zaehler mit der Anzahl der offenen Funde - er kommt aus
`App.tsx`, das die Kontakte ohnehin geladen hat.
## Zustand und Datenfluss
@@ -55,7 +62,8 @@ flowchart TB
APP --> DATA
DATA --> V1["Opportunities"]
DATA --> V2["Kalender"]
DATA --> V3["Kontakte"]
DATA --> V3["Kontakte<br/>mail_pool = false"]
DATA --> V5["Aus Mails<br/>mail_pool = true + Kontakte zum Abgleich"]
DATA --> V4["Mails<br/>leads + opportunities fuer den Zuordnungsdialog"]
NAV --> DF["DictationField<br/>springt nach Whisper"]
NAV --> ML["Mails<br/>springt zum Kontakt einer Zuordnung"]
@@ -71,6 +79,12 @@ weil sie blaettern, suchen und filtern und dabei nicht die ganze Liste im Speich
halten sollen. Kontakte und Opportunities bekommt die Mailansicht dagegen als
Props: der Zuordnungsdialog braucht sie beide zum Vorschlagen.
`/api/leads` liefert beide Bestaende in einem Aufruf; `App.tsx` teilt sie in
`leads` (Kontakte) und `mailPoolLeads` (Vorbereich „Aus Mails") auf. Alle
anderen Ansichten bekommen ausschliesslich `leads` - ein Fund aus einer Mail soll
in keiner Auswahlliste auftauchen. Die Ansicht *Aus Mails* braucht beides: sie
vergleicht jeden Fund gegen den Kontaktbestand und markiert Dubletten.
`goTo('Kontakte', { leadId })` ist der Sprung aus einer Mailzuordnung zum
Kontakt. `App.tsx` haelt die Id in `focusLeadId`, die Kontaktansicht oeffnet den
Kontakt und meldet mit `onFocusHandled()` zurueck - der Effekt haengt an `leads`
@@ -97,11 +111,12 @@ src/
audio.ts Aufnahme zu 16-kHz-Mono-PCM
imageCompress.ts Bilder vor dem Upload verkleinern
components/
Sidebar.tsx Menue inkl. aufklappbarer Scanner-Gruppe
Sidebar.tsx Menue inkl. aufklappbarer Gruppen (Kontakte, Scanner)
Header.tsx Titel, Untertitel, Dark-Mode-Schalter
opportunities/ Kachel, Liste, Pipeline (Drag & Drop), Formular
calendar/ Monat/Woche/Tag, Termine (Drag & Drop), @-Mentions
leads/ Kachel, Liste, Netzwerkdiagramm (d3-force)
leads/ Kachel, Liste, Netzwerkdiagramm (d3-force),
Vorbereich "Aus Mails" mit Dublettenabgleich
mail/ Postfachliste, Detail, Zuordnung, Kontakt-Vorschlaege
scanner/ Dashboard, Unpassend, Suchagenten, Boersen-Konten
settings/ Reiter: Nutzerverwaltung, Konto, Postfach, Scanner, Whisper
+42 -5
View File
@@ -128,8 +128,42 @@ erstellen* zwei Kontakte anklicken, um eine Verbindung anzulegen; `Esc` bricht a
Suche und Statusfilter wirken auch hier.
Sobald Kontakte aus dem Postfach uebernommen wurden, erscheint neben dem
Statusfilter ein **Herkunftsfilter**: *Aus Mails* zeigt nur die automatisch
entstandenen, *Selbst angelegt* nur die von Hand erfassten.
Statusfilter ein **Herkunftsfilter**: *Aus Mails uebernommen* zeigt nur die aus
dem Postfach stammenden, *Selbst angelegt* nur die von Hand erfassten.
### Aus Mails
Der Menuepunkt *Kontakte* klappt zwei Unterpunkte auf: *Alle Kontakte* und
*Aus Mails*. Der Zaehler am Menuepunkt sagt, wie viele Funde warten.
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
Angaben und darunter, aus welcher Mail sie stammen.
Zwei Faelle:
* **Weisse Karte** - die Person ist neu. *In Kontakte übernehmen* legt sie als
Kontakt an, mit Herkunft „Mail".
* **Gold markierte Karte** - es gibt sie in den Kontakten schon, erkannt an der
E-Mail-Adresse oder am Namen. Gold hinterlegte Felder weichen ab; darunter steht
durchgestrichen, was bisher im Kontakt stand. *Kontakt aktualisieren* oeffnet
die Feldauswahl: angehakt sind zunaechst nur die Luecken, also Felder, die im
Kontakt leer sind. Ein Wert, der etwas Gepflegtes ersetzen wuerde, ist als
*ueberschreibt* gekennzeichnet und muss bewusst angehakt werden - die Angabe
kann aus einer alten Signatur stammen. *Neu anlegen* erzeugt trotz Dublette
einen zweiten Kontakt, etwa wenn es zwei Personen mit demselben Namen gibt.
Weitere Bedienelemente:
* **Alle neuen übernehmen** legt alle weissen Karten auf einmal als Kontakte an;
gold markierte bleiben unberuehrt.
* **Eintrag hier behalten** - normalerweise verschwindet ein Eintrag aus *Aus
Mails*, sobald er als Kontakt gefuehrt wird. Mit dem Haken bleibt er stehen.
* Das **Papierkorbsymbol** entfernt einen Fund. Die Mails bleiben erhalten; taucht
die Person in einer neuen Mail auf, erscheint sie hier wieder.
* Der Filter waehlt zwischen allen Eintraegen, nur neuen Personen und nur
Dubletten.
## Mails
@@ -172,7 +206,8 @@ CRM bleibt sie erhalten, damit Zuordnungen nicht verloren gehen.
Aus Absender, Empfaengerliste und Signatur jeder Mail liest die Anwendung
Kontaktdaten. Wer eine echte Mailadresse hat und aus Absender oder
Empfaengerliste stammt, wird **automatisch** Kontakt. Alles Unsichere landet
Empfaengerliste stammt, landet **automatisch** im Vorbereich
[Aus Mails](#aus-mails) - nicht direkt in den Kontakten. Alles Unsichere landet
hier - typischerweise Personen, die nur mit Telefonnummer im Text genannt werden.
Der Balken rechts ist die Zuversicht: je voller und gruener, je verlaesslicher die
@@ -181,7 +216,9 @@ Angaben. Das Kennzeichen darunter sagt, woher sie stammen (*Absender*,
Zwei Knoepfe:
* **Als Kontakt anlegen** - es entsteht ein neuer Kontakt mit Herkunft „Mail".
* **Als Kontakt anlegen** - es entsteht sofort ein richtiger Kontakt mit Herkunft
„Mail", nicht nur ein Eintrag in *Aus Mails*: hier hat ja schon jemand
hingesehen und entschieden.
* **Ergaenzen** (statt „anlegen", wenn **Bereits vorhanden** dransteht) - der
vorhandene Kontakt wird um die fehlenden Angaben erweitert. Was schon gepflegt
ist, bleibt unangetastet.
@@ -342,4 +379,4 @@ nicht zum Benutzerkonto.
| Keine Mails sichtbar | steht oben ein Hinweisbalken? Sonst Admin bitten, in *Einstellungen &rarr; Postfach* die Verbindung zu testen |
| „Erstabgleich laeuft noch" | aeltere Mails werden in mehreren Durchlaeufen nachgeholt, die Liste waechst noch |
| Bilder in einer Mail fehlen | *Externe Bilder laden* klicken - sie sind absichtlich gesperrt |
| Kontakt-Vorschlaege bleiben leer | Normalfall: sichere Vorschlaege werden automatisch uebernommen und tauchen hier nicht auf |
| Kontakt-Vorschlaege bleiben leer | Normalfall: sichere Vorschlaege wandern automatisch nach *Kontakte › Aus Mails* und tauchen hier nicht auf |
+7 -3
View File
@@ -66,7 +66,7 @@ Alle unter `router.use(requireAuth)`.
| --- | --- | --- |
| `auth.js` | `POST /login`, `GET /me`, `PATCH /me`, `POST /change-password` | `/login` offen, Rest angemeldet |
| `users.js` | `GET /directory`; danach `requireAdmin`: `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | `/directory` fuer alle, Rest Admin |
| `leads.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | jeder Angemeldete |
| `leads.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id`; Vorbereich: `GET /mail-pool/sources`, `POST /mail-pool/promote`, `POST /:id/promote` | jeder Angemeldete |
| `connections.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | jeder Angemeldete |
| `events.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | anlegen jeder; aendern/loeschen nur Ersteller oder Admin |
| `opportunities.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | aendern/loeschen nur Ersteller, Zustaendiger oder Admin |
@@ -107,8 +107,8 @@ Postfachanbindung, fachlich in [11 Postfachanbindung](11-postfach.md).
| `graph.js` | Token mit Zwischenspeicher, `graphRequest()` mit Wiederholung bei 429/5xx und Beachtung von `Retry-After`, `testConnection()` |
| `html.js` | `htmlToText()` (Zeilen bleiben erhalten, Linkziele werden hinter den Linktext gesetzt), `sanitizeHtml()` |
| `extract.js` | `extractCandidates()`, `parsePersonName()`, `parseSignature()`, `AUTO_ADOPT_MIN_CONFIDENCE` |
| `store.js` | Zustandszeile, `upsertMessage()`, `insertCandidates()`, `linkMessage()` |
| `contacts.js` | `adoptCandidate()`: anlegen oder vorhandenen Kontakt ergaenzen, `markCandidate()` |
| `store.js` | Zustandszeile, `upsertMessage()`, `insertCandidates()`, `linkMessage()`, `findLeadByEmail()` je Bestand |
| `contacts.js` | `adoptCandidate()`: Eintrag in „Aus Mails" (`target: 'pool'`) oder richtiger Kontakt (`'contacts'`), sonst ergaenzen; `markCandidate()` |
| `sync.js` | ein Abgleichslauf, Delta-Seiten, Einzelsperre, Fehlerbehandlung |
| `scheduler.js` | Minutentakt, Faelligkeit aus `last_run_at`, kein Zeitfenster |
@@ -184,6 +184,7 @@ Zusagen, auf die sich der Rest verlaesst:
| `setUnauthorizedHandler` | wird bei 401 gerufen; `AuthProvider` haengt sich hier ein |
| `ApiError` | Fehler mit `status` |
| `authApi`, `usersApi`, `leadsApi`, `opportunitiesApi`, `eventsApi`, `connectionsApi` | typisierte Endpunkte |
| `leadsApi.mailPoolSources` / `.promote` / `.promoteMany` | Vorbereich „Aus Mails": Herkunft lesen, einzeln bzw. gesammelt uebernehmen |
Zwei Feinheiten: Der `Content-Type` wird **nicht** gesetzt, wenn der Body
`FormData` ist. Und ein Netzwerkfehler wird zu `ApiError(status 0)` mit
@@ -240,6 +241,9 @@ Warum genau diese Konstanten festgelegt sind, steht in
| Datei | Inhalt |
| --- | --- |
| `components/leads/leadUi.tsx` | `inputClass`, `labelClass`, `StatusBadge`, `Avatar`, Statusfarben |
| `components/leads/mailPool.ts` | `findMailPoolMatch()`, `diffFields()`, `isConflict()`, `MAIL_POOL_FIELDS` - Dublettenabgleich des Vorbereichs |
| `components/leads/MailPoolLeads.tsx` | Ansicht „Aus Mails": Goldmarkierung, Abweichungen, Uebernehmen, Sammeluebernahme |
| `components/leads/MailPoolMergeDialog.tsx` | Feldauswahl beim Ergaenzen eines vorhandenen Kontakts |
| `components/scanner/scannerUi.tsx` | `cardClass`, Buttonklassen, `MatchBadge`, `MatchStatusBadge`, `PortalStatusPill`, `formatDateTime`, `projectTimestamp` |
| `components/mail/mailUi.tsx` | `Badge`, `ConfidenceBar`, Herkunfts- und Statusbeschriftungen, `formatAddresses`, `avatarColor` |
| `components/mail/MailBody.tsx` | Mailinhalt im `<iframe sandbox="">`, externe Bilder per CSP gesperrt |
+6 -4
View File
@@ -13,7 +13,9 @@ Quelltext.
| **App-only** | Zugriff auf Microsoft 365 im Namen der Anwendung, nicht eines Benutzers (OAuth-Ablauf *Client Credentials*). Braucht Anwendungsberechtigungen und Administratorzustimmung, aber keinen Login. |
| **Cookie-Import** | Uebertragen einer angemeldeten Browsersitzung auf den Scraper, per JSON-Export der Erweiterung *Cookie-Editor*. Landet als `session_state` am Portal. |
| **Delta Query** | Graph-Abfrage, die beim naechsten Aufruf nur noch die Aenderungen liefert. Der Fortschritt steckt in einem Verweis (`delta_link`), der in `crm_mail_sync_state` liegt. |
| **Kontakt** / Lead | Person im Netzwerk: Firma, Position, Status, Verbindungen. Tabelle `crm_leads`. Im Code durchgaengig `lead`. |
| **Kontakt** / Lead | Person im Netzwerk: Firma, Position, Status, Verbindungen. Tabelle `crm_leads` mit `mail_pool = 0`. Im Code durchgaengig `lead`. |
| **Aus Mails** / Vorbereich | Zeilen in `crm_leads` mit `mail_pool = 1`: Personen, die der Postfachabgleich gefunden hat. Eigener Menuepunkt unter *Kontakte*, taucht in keiner Auswahlliste auf. Erst das Uebernehmen macht daraus einen Kontakt. |
| **Dublette** (Aus Mails) | Ein Fund, zu dem es in den Kontakten schon eine Person gibt - erkannt an der E-Mail-Adresse, sonst am vollen Namen. In der Liste **gold** markiert, abweichende Felder hervorgehoben. |
| **Kontakt-Vorschlag** | Aus einer Mail gelesene Person, die noch kein Kontakt ist. Tabelle `crm_mail_contact_candidates`. Wird uebernommen, ergaenzt oder verworfen. |
| **Match** / Treffer | Die Bewertung **eines** Projekts fuer **einen** Agenten: Prozentwert, Pro-Liste, Contra-Liste, Status. Tabelle `matches`. Ein Projekt erzeugt so viele Treffer, wie es aktive Agenten gibt. |
| **Opportunity** | Geschaeft in Arbeit: Phase, Tagessatz, Laufzeit, Wahrscheinlichkeit, Ansprechpartner. Tabelle `crm_opportunities`. |
@@ -31,7 +33,7 @@ Quelltext.
| **Whisper** | Das Spracherkennungsmodell fuer das Diktat. Laeuft im Browser, nicht auf dem Server. |
| **Zugriffsrichtlinie** | `ApplicationAccessPolicy` in Exchange Online. Begrenzt eine App-only-Berechtigung auf einzelne Postfaecher - ohne sie gilt sie tenant-weit. |
| **Zustaendiger** / Owner | Benutzer, dem ein Kontakt oder eine Opportunity zugeordnet ist. Darf sie aendern, auch wenn er sie nicht angelegt hat. |
| **Zuversicht** / Confidence | Wert von 0 bis 100 an einem Kontakt-Vorschlag. Ab 70 und mit echter Mailadresse wird automatisch uebernommen. |
| **Zuversicht** / Confidence | Wert von 0 bis 100 an einem Kontakt-Vorschlag. Ab 70 und mit echter Mailadresse landet er automatisch im Vorbereich *Aus Mails*. |
## Herkunft eines Kontakt-Vorschlags
@@ -47,8 +49,8 @@ Quelltext.
| Wert | in der Oberflaeche | Bedeutung |
| --- | --- | --- |
| `offen` | Offen | wartet auf eine Entscheidung |
| `uebernommen` | Uebernommen | es wurde ein Kontakt angelegt |
| `dublette` | Ergaenzt | Adresse war vorhanden, der Kontakt wurde erweitert |
| `uebernommen` | Uebernommen | es entstand ein Eintrag - im Automatiklauf in *Aus Mails*, von Hand ein Kontakt |
| `dublette` | Ergaenzt | die Adresse war im Zielbestand vorhanden, der Eintrag wurde erweitert |
| `verworfen` | Verworfen | von Hand abgelehnt |
## Statuswerte im Scanner
+61 -6
View File
@@ -192,8 +192,8 @@ flowchart TD
F -->|"Person"| C["Vorschlag mit Zuversicht"]
C --> Z{"E-Mail vorhanden<br/>und Zuversicht &ge; 70?"}
Z -->|ja, Automatik an| N["Kontakt anlegen<br/>source = 'Mail'"]
Z -->|Adresse existiert| G["vorhandenen Kontakt ergaenzen"]
Z -->|ja, Automatik an| N["Eintrag in 'Aus Mails'<br/>mail_pool = 1"]
Z -->|"Adresse dort schon"| G["Eintrag ergaenzen"]
Z -->|nein| R["bleibt zur Durchsicht"]
```
@@ -216,9 +216,51 @@ Zwei Entscheidungen, die die Trefferqualitaet tragen:
`k.mueller@…` wird der Vorname aus der Adresse genommen, der Nachname aber aus
dem Anzeigenamen - dort stehen die Umlaute richtig.
Uebernommene Kontakte tragen `crm_leads.source = 'Mail'`. Das ist die
„Unterkategorie": die Kontaktliste hat dafuer einen Herkunftsfilter, ein eigenes
Feld war nicht noetig.
## Vorbereich „Aus Mails"
Der Hintergrundlauf schreibt **nicht** in den Kontaktbestand. Was er findet,
landet in einem Vorbereich: `crm_leads.mail_pool = 1`. Die Kontaktansicht zeigt
nur `mail_pool = 0`, der Vorbereich steht als Unterpunkt *Aus Mails* unter dem
Menuepunkt *Kontakte*.
Der Grund ist Pflegbarkeit: die Kontaktliste ist eine gepflegte Liste, an der
Opportunities, Kalender und Netzwerk haengen. 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.
Eine Spalte statt einer zweiten Tabelle, weil `crm_mail_links` und
`crm_mail_contact_candidates` per Fremdschluessel auf `crm_leads` zeigen: der
Vorbereich behaelt damit seine Mailbezuege, ohne dass es zwei Wege gibt, eine
Person mit einer Mail zu verbinden.
```mermaid
flowchart LR
P["Aus Mails<br/>mail_pool = 1"] --> D{"Person schon<br/>in den Kontakten?"}
D -->|nein| NEU["In Kontakte uebernehmen<br/>Kopie mit mail_pool = 0"]
D -->|"ja - Adresse oder Name"| GOLD["gold markiert<br/>Abweichungen hervorgehoben"]
GOLD --> M["Felder einzeln in den<br/>vorhandenen Kontakt uebernehmen"]
GOLD --> N2["trotzdem neu anlegen"]
P --> W["entfernen"]
```
Der Abgleich gegen die Kontakte laeuft im Browser (`mailPool.ts`): erst ueber die
E-Mail-Adresse, sonst ueber den vollen Namen. Telefonnummern werden dabei ueber
ihre Ziffern verglichen, damit „0221 123-4" und „02211234" nicht als Abweichung
erscheinen.
Beim Uebernehmen sind nur die **Luecken** vorausgewaehlt - Felder, in denen der
Kontakt noch nichts hat. Werte, die einen gepflegten Eintrag ersetzen wuerden,
sind als *ueberschreibt* gekennzeichnet und muessen bewusst angehakt werden: der
Mailfund kann aus einer alten Signatur stammen.
Uebernommene Kontakte tragen weiterhin `crm_leads.source = 'Mail'` - die
Kontaktliste hat dafuer den Herkunftsfilter *Aus Mails uebernommen*.
Bestandsumstellung: `db.js` verschiebt beim ersten Start alle vorhandenen
Kontakte mit `source = 'Mail'` in den Vorbereich. Das laeuft genau einmal,
festgehalten im Merker `leads_mail_pool_migrated` in `crm_settings` - sonst
wanderten uebernommene Kontakte bei jedem Neustart zurueck.
## Anzeige des Mailtexts
@@ -251,8 +293,21 @@ Alle unter `/api/mail`, alle hinter `requireAuth`.
| POST | `/messages/:id/links` | alle | an Kontakt und/oder Opportunity haengen |
| DELETE | `/links/:id` | alle | Zuordnung loesen |
| GET | `/candidates` | alle | Kontakt-Vorschlaege, `status` waehlbar |
| POST | `/candidates/:id/adopt` | alle | Kontakt anlegen bzw. ergaenzen |
| POST | `/candidates/:id/adopt` | alle | Kontakt anlegen bzw. ergaenzen; `target: 'pool'` legt in „Aus Mails" ab |
| POST | `/candidates/:id/reject` | alle | Vorschlag verwerfen |
Der Vorbereich haengt an den Kontakten und hat seine Endpunkte deshalb unter
`/api/leads`:
| Methode | Pfad | Zweck |
| --- | --- | --- |
| GET | `/leads/mail-pool/sources` | Herkunft je Eintrag: Mailanzahl, letzter Betreff, Zuversicht |
| POST | `/leads/mail-pool/promote` | mehrere Eintraege als neue Kontakte anlegen |
| POST | `/leads/:id/promote` | einen Eintrag uebernehmen; mit `target_lead_id` und `fields` in einen vorhandenen Kontakt |
`keep_pool: true` behaelt den Eintrag in „Aus Mails"; ohne das verschwindet er
dort, sobald er als Kontakt gefuehrt wird. In beiden Faellen wandern die
Mailverknuepfungen mit an den Zielkontakt.
| GET | `/settings` | admin | verstellbare Einstellungen |
| POST | `/settings` | admin | Einstellungen speichern |
| POST | `/test` | admin | Anmeldung **und** Postfachzugriff pruefen |