# 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 `