Der Postfachabgleich hat gefundene Personen bisher direkt in den Kontaktbestand geschrieben. Damit wächst eine gepflegte Liste ungefragt mit jeder Adresse aus jeder Mail - genau daran ist das Altsystem gescheitert. Funde landen jetzt in einem Vorbereich (crm_leads.mail_pool = 1), der als Unterpunkt "Aus Mails" unter dem Menüpunkt Kontakte aufklappt. Der Kontaktbestand bleibt unberührt, bis dort jemand entscheidet; in Opportunities, Kalender und Netzwerk tauchen die Funde nicht auf. Eine Spalte statt einer zweiten Tabelle, weil crm_mail_links und crm_mail_contact_candidates per Fremdschlüssel auf crm_leads zeigen - so behält der Vorbereich seine Mailbezüge. Übernehmen kennt zwei Wege: - neue Person: Kopie mit mail_pool = 0, die Mailverknüpfungen wandern mit - Dublette (Adresse, sonst voller Name): gold markiert, abweichende Felder mit altem und neuem Wert; die Feldauswahl schreibt sie in den vorhandenen Kontakt. Vorausgewählt sind nur Lücken - ein Wert aus einer alten Signatur soll nichts Gepflegtes stillschweigend ersetzen. db.js verschiebt beim ersten Start die vorhandenen Kontakte mit source = 'Mail' in den Vorbereich, einmalig über den Merker leads_mail_pool_migrated in crm_settings. Die Sidebar-Gruppe ist dabei generisch geworden (MenuGroup) - Kontakte und Scanner benutzen jetzt denselben Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
152 lines
6.6 KiB
Markdown
152 lines
6.6 KiB
Markdown
# 05 Frontend
|
|
|
|
Eine einzige React-Anwendung ohne Router: `App.tsx` haelt die aktuelle Ansicht in
|
|
einem Zustand und rendert sie. Das genuegt, weil die App hinter einer Anmeldung
|
|
sitzt und keine tiefen Links braucht.
|
|
|
|
## Menue und Ansichten
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
SB["Seitenleiste"] --> K["Kategorien"]
|
|
K --> O["Opportunities<br/>(Startansicht)"]
|
|
K --> C["Kalender"]
|
|
K --> L["Kontakte"]
|
|
L --> L1["Alle Kontakte"]
|
|
L --> L2["Aus Mails<br/>Vorbereich, Zaehler im Menue"]
|
|
K --> M["Mails"]
|
|
M --> M1["Postfach"]
|
|
M --> M2["Kontakt-Vorschlaege"]
|
|
K --> SC["Scanner"]
|
|
SC --> SC1["Dashboard"]
|
|
SC --> SC2["Unpassend"]
|
|
SC --> SC3["Suchagenten"]
|
|
SC --> SC4["Boersen-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-Vorschlaege* sind Reiter **innerhalb** der Ansicht
|
|
*Mails*, keine Menuepunkte - sie gehoeren zusammen und wechseln oft hin und her.
|
|
|
|
Die Menuepunkte *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 zurueck. Beide Gruppen entstehen aus
|
|
derselben Struktur `MenuGroup` in `Sidebar.tsx`.
|
|
|
|
*Aus Mails* traegt einen Zaehler 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["Kalender"]
|
|
DATA --> V3["Kontakte<br/>mail_pool = false"]
|
|
DATA --> V5["Aus Mails<br/>mail_pool = true + Kontakte zum Abgleich"]
|
|
DATA --> V4["Mails<br/>leads + opportunities fuer 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"] -->|"laedt 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 blaettern, 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 Bestaende in einem Aufruf; `App.tsx` teilt sie in
|
|
`leads` (Kontakte) und `mailPoolLeads` (Vorbereich „Aus Mails") auf. Alle
|
|
anderen Ansichten bekommen ausschliesslich `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` haelt die Id in `focusLeadId`, die Kontaktansicht oeffnet den
|
|
Kontakt und meldet mit `onFocusHandled()` zurueck - der Effekt haengt an `leads`
|
|
und wartet damit auf einen gerade erst angelegten Kontakt.
|
|
|
|
`reload={() => loadData(true)}` laedt still nach - ohne Ladeanzeige, damit ein
|
|
Speichern im Dialog nicht die ganze Liste flackern laesst.
|
|
|
|
## 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 fuer 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 Menue 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-Vorschlaege
|
|
scanner/ Dashboard, Unpassend, Suchagenten, Boersen-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 getoenten Statusfarben.
|
|
Die Komponenten benutzen durchgaengig 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 Flaechen) und `brand-500` (das CI-Gruen 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 Flaeche. Der Rahmen liest sich damit
|
|
wie ein Blatt Papier im dunklen Fenster - gewollt.
|