# 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 ```mermaid flowchart TD SB["Seitenleiste"] --> K["Kategorien"] K --> D["Dashboard
(Startansicht, auch über das Logo)"] K --> AQ["Akquise"] AQ --> AQ1["Eingang
Funde der letzten 14 Tage"] AQ --> AQ2["Archiv
älter; nach 90 Tagen gelöscht"] AQ --> AQ3["Papierkorb
von Hand gelöscht; 90 Tage"] AQ2 -. "Zurück in den Eingang
(Funddatum = heute)" .-> AQ1 AQ3 -. "Wiederherstellen
(Funddatum bleibt)" .-> AQ2 K --> O["Opportunities"] K --> C["Interviews"] K --> L["Kontakte"] L --> L1["Alle Kontakte"] L --> L2["Aus Mails
Vorbereich, Zähler im Menü"] K --> M["Mails"] M --> M1["Postfach
Reiter: Postfach, Kontakt-Vorschläge"] M --> M2["Papierkorb
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 ```mermaid flowchart TB subgraph Kontexte["Kontexte in main.tsx"] TH["ThemeProvider
hell/dunkel/System"] TO["ToastProvider"] AU["AuthProvider
JWT, Benutzer"] WH["WhisperProvider
Modell, Worker"] end APP["App.tsx
currentView, settingsTab, focusLeadId"] NAV["NavigationProvider
goTo(view, {tab, leadId})"] DATA["loadData()
leads, connections, events, users, opportunities"] Kontexte --> APP APP --> NAV APP --> DATA DATA --> V1["Opportunities"] NOTE["Akquise lädt selbst
acquisitionsApi, nicht über loadData()"] DATA --> V2["Interviews"] DATA --> V3["Kontakte
mail_pool = false"] DATA --> V5["Aus Mails
mail_pool = true + Kontakte zum Abgleich"] DATA --> V4["Mails
leads + opportunities für den Zuordnungsdialog"] NAV --> DF["DictationField
springt nach Whisper"] NAV --> ML["Mails
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 (auch das LinkedIn-Foto) components/ Sidebar.tsx Menü inkl. aufklappbarer Gruppen (Kontakte, Scanner) Header.tsx Titel, Untertitel, Design-Auswahl, Hell/Dunkel-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 ``` ## Themes ohne `dark:`-Varianten Die Darstellung hat **zwei unabhängige Achsen**, beide als Klasse am ``: | Achse | Klasse | Werte | | --- | --- | --- | | Farbwelt | `theme-perinco`, `theme-pixel`, `theme-outdoor`, `theme-cyberpunk` | perinco ist der Standard | | Modus | `dark` bzw. keine | hell, dunkel, oder der Systemeinstellung folgen | `src/index.css` tauscht darin nur Variablenwerte: `canvas`, `surface`, `ink-*`, `brand-*`, die getönten Statusfarben, dazu `--radius-*`, `--shadow-*` und die Schriften. Die Komponenten benutzen durchgängig diese Skalen und folgen damit automatisch - **kein `dark:` und keine themespezifische Klasse im Markup**. ```mermaid flowchart LR U["Benutzer wählt
im Header"] --> DB["crm_users.theme
crm_users.theme_mode"] U --> LS["localStorage
nur Zwischenspeicher"] DB -->|"beim Anmelden"| H["Klassen am html
theme-*, dark"] LS -->|"vor dem ersten Frame"| H H --> V["Variablen in index.css
Farben, Radien, Schatten, Schrift"] V --> W["Utility-Klassen der Komponenten"] SYS["Systemeinstellung"] -.->|"ohne eigene Wahl"| H ``` Die Wahl gehört dem **Benutzer**, nicht dem Browser: sie steht in `crm_users` und kommt mit `/api/auth/me`. Wer sich an einem anderen Rechner anmeldet, findet sein Design wieder, und zwei Benutzer an derselben Maschine kommen sich nicht in die Quere. Der `localStorage` bleibt daneben als Zwischenspeicher liegen, damit das Inline-Skript in `index.html` die Klassen vor dem ersten Frame setzen kann - sonst blitzt beim Laden das Standarddesign auf. Das hat genau eine sichtbare Folge: melden sich zwei Leute an demselben Rechner an, sieht der zweite für einen Augenblick das Design des ersten. Der `ThemeProvider` steht deshalb in `main.tsx` **unter** dem `AuthProvider` - er liest den Benutzer. ### Hintergrundbilder in Leiste und Seitenleiste Alle Themes außer perinco legen eine Grafik hinter die obere Leiste und die Seitenleiste: 8-bit-Himmel und Steinmauer, blauer Himmel und Wiese, Neonröhren und regnerische Gasse. Die Dateien liegen als **SVG** in `public/themes/` und sind von Hand gezeichnet - stilisierte Grafik statt Foto, damit sie in jeder Auflösung scharf bleibt und wenige Kilobyte wiegt. Zwei Anker im Markup: `app-header` an `
` und `app-sidebar` an `