Files
perinco-CRM/docs/05-frontend.md
T
Sascha WolfandClaude Opus 5 ea55acdb5c 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>
2026-08-11 08:59:14 +02:00

6.6 KiB

05 Frontend

Eine einzige React-Anwendung ohne Router: App.tsx haelt die aktuelle Ansicht in einem Zustand und rendert sie. Das genuegt, weil die App hinter einer Anmeldung sitzt und keine tiefen Links braucht.

Menue und Ansichten

flowchart TD
    SB["Seitenleiste"] --> K["Kategorien"]
    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"]
    K --> SC["Scanner"]
    SC --> SC1["Dashboard"]
    SC --> SC2["Unpassend"]
    SC --> SC3["Suchagenten"]
    SC --> SC4["Boersen-Konten"]
    SB --> E["Einstellungen"]
    E --> E1["Nutzerverwaltung (admin)"]
    E --> E2["Mein Konto"]
    E --> E3["Postfach (admin)"]
    E --> E4["Scanner (admin)"]
    E --> E5["Whisper"]

Postfach und Kontakt-Vorschlaege sind Reiter innerhalb der Ansicht Mails, keine Menuepunkte - sie gehoeren zusammen und wechseln oft hin und her.

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

flowchart TB
    subgraph Kontexte["Kontexte in main.tsx"]
        TH["ThemeProvider<br/>hell/dunkel/System"]
        TO["ToastProvider"]
        AU["AuthProvider<br/>JWT, Benutzer"]
        WH["WhisperProvider<br/>Modell, Worker"]
    end

    APP["App.tsx<br/>currentView, settingsTab, focusLeadId"]
    NAV["NavigationProvider<br/>goTo(view, {tab, leadId})"]
    DATA["loadData()<br/>leads, connections, events, users, opportunities"]

    Kontexte --> APP
    APP --> NAV
    APP --> DATA
    DATA --> V1["Opportunities"]
    DATA --> V2["Kalender"]
    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"]

    SCV["Scanner-Ansichten"] -->|"laden selbst"| API["/api/scanner"]
    MV["Mailansicht"] -->|"laedt selbst"| MAPI["/api/mail"]

Die CRM-Daten werden einmal zentral geladen und als Props durchgereicht; die Scanner- und Mailansichten laden ihre eigenen Daten selbst - der Scanner, weil er eine andere Datenbank und eine Auffrischung im 30-Sekunden-Takt hat; die Mails, weil sie blaettern, suchen und filtern und dabei nicht die ganze Liste im Speicher 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 und wartet damit auf einen gerade erst angelegten Kontakt.

reload={() => loadData(true)} laedt still nach - ohne Ladeanzeige, damit ein Speichern im Dialog nicht die ganze Liste flackern laesst.

Komponentenkarte

src/
  App.tsx                     Layout, Datenladen, Ansichtswechsel
  types.ts                    Gemeinsame Typen, Farbpalette, Rechenhilfen
  lib/
    api.ts                    Fetch-Wrapper (JWT, 401-Behandlung) + CRM-Endpunkte
    scannerApi.ts             Typisierte Endpunkte unter /api/scanner
    mailApi.ts                Endpunkte unter /api/mail, UTC-Datumshelfer
    auth.tsx                  Anmeldung, Benutzer, Rolle
    theme.tsx                 Hell/Dunkel/System
    navigation.tsx            Ansichtswechsel fuer tiefe Komponenten
    whisper.tsx               Modell, Worker-Lebenszyklus
    dictation.tsx             Aufnahme-Hook
    audio.ts                  Aufnahme zu 16-kHz-Mono-PCM
    imageCompress.ts          Bilder vor dem Upload verkleinern
  components/
    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),
                              Vorbereich "Aus Mails" mit Dublettenabgleich
    mail/                     Postfachliste, Detail, Zuordnung, Kontakt-Vorschlaege
    scanner/                  Dashboard, Unpassend, Suchagenten, Boersen-Konten
    settings/                 Reiter: Nutzerverwaltung, Konto, Postfach, Scanner, Whisper
    whisper/                  Einrichtung der Spracherkennung
    ui/                       Modal, Avatare, Diktatfeld

Dark Mode ohne dark:-Varianten

Die Klasse dark auf <html> tauscht in src/index.css nur die Werte der Variablen canvas, surface, ink-*, brand-* und der getoenten Statusfarben. Die Komponenten benutzen durchgaengig diese Skalen und folgen damit automatisch - kein dark: im Markup.

flowchart LR
    T["Umschalter im Header"] --> P["localStorage<br/>perinco_crm_theme"]
    P --> H["class 'dark' auf html"]
    H --> V["Variablen in index.css<br/>canvas, surface, ink-*, brand-*"]
    V --> U["Utility-Klassen der Komponenten"]
    SYS["Systemeinstellung"] -.->|"ohne eigene Wahl"| H

Ein Inline-Skript in index.html setzt die Klasse vor dem ersten Frame, sonst blitzt beim Laden die helle Variante auf. Drei Werte bleiben in beiden Modi gleich: ink-950 (liegt nur unter Dialog-Overlays), text-white (steht immer auf farbigen Flaechen) und brand-500 (das CI-Gruen der Buttons).

Eine bewusste Ausnahme: der Mailinhalt bleibt auch im Dunkelmodus auf weissem Grund. Fremdes Mail-HTML bringt seine eigenen Farben mit; ein dunkler Hintergrund darunter macht schwarze Schrift auf schwarzer Flaeche. Der Rahmen liest sich damit wie ein Blatt Papier im dunklen Fenster - gewollt.