Files
perinco-CRM/docs/05-frontend.md
T
Sascha WolfandClaude Opus 5 7973dc07b1 docs: echte Umlaute in der Dokumentation
893 Stellen in Fliesstext, Tabellen und Mermaid-Beschriftungen. Dazu die
sichtbaren ss-Fälle (ausschließlich, groß, außen, schließt, heißt,
draußen, weiße, anstoßen, Straße).

Nicht angetastet, damit die Doku weiter beschreibt, was wirklich in der
Datenbank steht:

- Codeblöcke und Code-Spans - dort stehen Bezeichner, SQL, Enum-Werte
  und Dateinamen: `mail_pool`, `'uebernommen'`, `extract.js`
- in Mermaid nur der Text in Anführungszeichen; Knotennamen, Pfeile und
  Werte wie source = 'Mail' bleiben, wie sie sind
- `empfaenger` im ER-Diagramm von crm_mail_contact_candidates: die
  Aufzählung dort nennt die Datenbankwerte, nicht die Anzeige

Die Menü-Skizze in der Benutzeranleitung zeigt jetzt die aufgeklappten
Kontakte mit "Aus Mails" samt Zähler.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 09:45:58 +02:00

6.6 KiB

05 Frontend

Eine einzige React-Anwendung ohne Router: App.tsx hält die aktuelle Ansicht in einem Zustand und rendert sie. Das genügt, weil die App hinter einer Anmeldung sitzt und keine tiefen Links braucht.

Menü 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, Zähler im Menü"]
    K --> M["Mails"]
    M --> M1["Postfach"]
    M --> M2["Kontakt-Vorschläge"]
    K --> SC["Scanner"]
    SC --> SC1["Dashboard"]
    SC --> SC2["Unpassend"]
    SC --> SC3["Suchagenten"]
    SC --> SC4["Börsen-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-Vorschläge sind Reiter innerhalb der Ansicht Mails, keine Menüpunkte - sie gehören zusammen und wechseln oft hin und her.

Die Menüpunkte 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 zurück. Beide Gruppen entstehen aus derselben Struktur MenuGroup in Sidebar.tsx.

Aus Mails trägt einen Zähler 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 für 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"] -->|"lädt 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 blättern, 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 Bestände in einem Aufruf; App.tsx teilt sie in leads (Kontakte) und mailPoolLeads (Vorbereich „Aus Mails") auf. Alle anderen Ansichten bekommen ausschließlich 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 hält die Id in focusLeadId, die Kontaktansicht öffnet den Kontakt und meldet mit onFocusHandled() zurück - der Effekt hängt an leads und wartet damit auf einen gerade erst angelegten Kontakt.

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

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 getönten Statusfarben. Die Komponenten benutzen durchgängig 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 Flächen) und brand-500 (das CI-Grün 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 Fläche. Der Rahmen liest sich damit wie ein Blatt Papier im dunklen Fenster - gewollt.