From ea55acdb5c254ea7811e17118178d30c2075c80a Mon Sep 17 00:00:00 2001 From: Sascha Wolf Date: Tue, 11 Aug 2026 08:58:55 +0200 Subject: [PATCH] feat(kontakte): Vorbereich "Aus Mails" mit Dublettenabgleich MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- README.md | 2 +- docs/02-datenmodell.md | 11 +- docs/05-frontend.md | 27 +- docs/07-anleitung-benutzer.md | 47 +- docs/09-quellcode.md | 10 +- docs/10-glossar.md | 10 +- docs/11-postfach.md | 67 ++- server/src/db.js | 30 ++ server/src/mail/contacts.js | 36 +- server/src/mail/store.js | 17 +- server/src/mail/sync.js | 9 +- server/src/routes/leads.js | 268 +++++++++- server/src/routes/mail.js | 23 +- server/src/schema.sql | 14 + src/App.tsx | 32 +- src/components/Sidebar.tsx | 198 +++++--- src/components/leads/Leads.tsx | 10 +- src/components/leads/MailPoolLeads.tsx | 495 +++++++++++++++++++ src/components/leads/MailPoolMergeDialog.tsx | 162 ++++++ src/components/leads/mailPool.ts | 115 +++++ src/lib/api.ts | 42 ++ src/types.ts | 14 + 22 files changed, 1524 insertions(+), 115 deletions(-) create mode 100644 src/components/leads/MailPoolLeads.tsx create mode 100644 src/components/leads/MailPoolMergeDialog.tsx create mode 100644 src/components/leads/mailPool.ts 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 `