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>
152 lines
6.7 KiB
Markdown
152 lines
6.7 KiB
Markdown
# 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<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
|
|
|
|
```mermaid
|
|
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.
|
|
|
|
```mermaid
|
|
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.
|