diff --git a/README.md b/README.md
index 13d5662..f1bb41e 100644
--- a/README.md
+++ b/README.md
@@ -74,7 +74,7 @@ den Apache-Proxy erreichbar.
| Tabelle | Inhalt |
| --- | --- |
| `crm_users` | Benutzer, Rolle `admin` oder `user`, bcrypt-Hash, `color` (Kalenderfarbe), `avatar` (Foto) |
-| `crm_leads` | Kontakte inkl. `created_by` (Ersteller) und `owner_id` (zustaendig) |
+| `crm_leads` | Kontakte inkl. `created_by` (Ersteller) und `owner_id` (zustaendig); `mail_pool = 1` ist der Vorbereich „Aus Mails" aus dem Postfachabgleich |
| `crm_opportunities` | Vertriebschancen: Phase, Tagessatz, Laufzeit, letzte Interaktion, Wahrscheinlichkeit |
| `crm_opportunity_contacts` | Ansprechpartner je Opportunity (n:m auf `crm_leads`) mit Rolle |
| `crm_lead_connections` | Wer steht mit wem in Kontakt; Paar aufsteigend gespeichert, Unique-Key verhindert Duplikate |
diff --git a/docs/02-datenmodell.md b/docs/02-datenmodell.md
index 8c23c83..a52c6ef 100644
--- a/docs/02-datenmodell.md
+++ b/docs/02-datenmodell.md
@@ -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
diff --git a/docs/05-frontend.md b/docs/05-frontend.md
index b952fb1..cd60a48 100644
--- a/docs/05-frontend.md
+++ b/docs/05-frontend.md
@@ -12,6 +12,8 @@ flowchart TD
K --> O["Opportunities (Startansicht)"]
K --> C["Kalender"]
K --> L["Kontakte"]
+ L --> L1["Alle Kontakte"]
+ L --> L2["Aus Mails 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 mail_pool = false"]
+ DATA --> V5["Aus Mails mail_pool = true + Kontakte zum Abgleich"]
DATA --> V4["Mails leads + opportunities fuer den Zuordnungsdialog"]
NAV --> DF["DictationField springt nach Whisper"]
NAV --> ML["Mails 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
diff --git a/docs/07-anleitung-benutzer.md b/docs/07-anleitung-benutzer.md
index d2513ee..ae173bc 100644
--- a/docs/07-anleitung-benutzer.md
+++ b/docs/07-anleitung-benutzer.md
@@ -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 → 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 |
diff --git a/docs/09-quellcode.md b/docs/09-quellcode.md
index bba7d92..39b085d 100644
--- a/docs/09-quellcode.md
+++ b/docs/09-quellcode.md
@@ -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 `