Files
perinco-CRM/docs/05-frontend.md
T
Sascha WolfandClaude Opus 5 8741a9ae0c feat: Interviews, Mail-Papierkorb, Kontaktart
Drei Aenderungen aus dem Alltagsgebrauch:

- Der Kalender heisst jetzt "Interviews" - dafuer wird er benutzt. Der Wert in
  ViewState wandert mit, damit Menue und Kopfzeile nicht auseinanderlaufen.
  Sichtbare Reste ("Kalenderfarbe") heissen jetzt Terminfarbe.

- Mails haben einen Papierkorb, der unter dem Menuepunkt aufklappt. Loeschen
  heisst dabei ausschliesslich: im CRM. Im Postfach crm@perinco.de bleibt die
  Mail unberuehrt - Schreibrechte darauf hat die Anwendung nicht. Damit der
  Delta-Abgleich geloeschte Mails nicht wieder einliest, bleibt nach dem Leeren
  eine leere Zeile als Merker stehen (crm_purged); runMailSync() ueberspringt
  alles, was im CRM geloescht ist. Zurueckholen und Leeren darf jeder
  Angemeldete, Leeren nach einer Warnung.

- Der Kontaktstatus (Neu .. Verloren) wird zur Kontaktart: Recruiter, Partner,
  Endkunde. Die Spalte heisst jetzt kind, weil "Status" das Feld falsch
  benannte. migrateLeadKind() stellt bestehende Datenbanken um und setzt alle
  vorhandenen Kontakte auf Recruiter - welche Art jemand ist, sagt der alte
  Status nicht (so mit dem Nutzer entschieden).

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

6.7 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["Interviews"]
    K --> L["Kontakte"]
    L --> L1["Alle Kontakte"]
    L --> L2["Aus Mails<br/>Vorbereich, Zähler im Menü"]
    K --> M["Mails"]
    M --> M1["Postfach<br/>Reiter: Postfach, Kontakt-Vorschläge"]
    M --> M2["Papierkorb<br/>im CRM gelöschte Mails"]
    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["Interviews"]
    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 für 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               Menü 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-Vorschläge
    scanner/                  Dashboard, Unpassend, Suchagenten, Börsen-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.