# 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 --> O["Opportunities
(Startansicht)"]
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"]
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
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 `` 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.
```mermaid
flowchart LR
T["Umschalter im Header"] --> P["localStorage
perinco_crm_theme"]
P --> H["class 'dark' auf html"]
H --> V["Variablen in index.css
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.