Neuer Menuepunkt ueber den Opportunities, mit dem Unterpunkt "Archiv". Die Liste traegt genau die Spalten, die beim Durchgehen zaehlen: Datum, Von, Anbieter, Kanal, Projekttitel, Berater, ein Knopf fuer die Uebernahme und die Symbole fuer Bearbeiten und Loeschen. Eingabe steht ueber der Liste und ist bewusst kein Dialog - Projekte kommen in Serie herein. Nach dem Eintragen bleiben Datum, Anbieter, Kanal und Berater stehen, damit drei Projekte aus einer Mail nicht dreimal dieselben Angaben kosten. Von, Anbieter und Berater schlagen frueher benutzte Werte vor. Was in der Liste keine Spalte hat, aber in der Opportunity vorkommt (Endkunde, Beschreibung, Tagessatz, Laufzeit, Projektpreis, Zeitraum, naechster Schritt, Notizen), klappt darunter auf. Eingang, Archiv und Loeschung ergeben sich aus dem Funddatum - 14 bzw. 90 Tage - und aus converted_at. Es gibt keine Zustandsspalte, die ein Job pflegen muesste und die von der Wirklichkeit abweichen kann. Der Loeschlauf raeumt beim Abruf der Liste, hoechstens einmal pro Stunde; die Abfragen filtern Faelliges ohnehin heraus. Die Uebernahme legt Opportunity und Merker in einer Transaktion an. Anbieter, Von, Kanal, Berater und Link haben in der Opportunity kein Feld und wandern als Herkunftsvermerk in die Notizen. Der Titel wird nur dann zum Link, wenn eine http(s)-Adresse hinterlegt ist: "hays.de/projekt/1" ergaenzt das Backend zu https://, und die Anzeige laesst nichts anderes durch als http und https. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6.9 KiB
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
flowchart TD
SB["Seitenleiste"] --> K["Kategorien"]
K --> AQ["Akquise"]
AQ --> AQ1["Eingang<br/>Funde der letzten 14 Tage"]
AQ --> AQ2["Archiv<br/>älter; nach 90 Tagen gelöscht"]
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
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"]
NOTE["Akquise lädt selbst<br/>acquisitionsApi, nicht über loadData()"]
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.
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.