Files
ICON-CRM/docs/03-frontend.md
T

171 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 03 Frontend
React 19 mit TypeScript, gebaut von Vite 6, gestaltet mit Tailwind 4. Eine
einzige Seite; Apache liefert sie über `FallbackResource` für jeden Pfad aus.
## Menü
```
Dashboard
Leads Pipeline als Kanban
└ Archiviert alles, was per Aktenschrank weggelegt wurde
Kontakte
Unternehmen
Ausschreibungen Tabelle; nur der laufende Bestand
├ Live! Kanban über die Phasen
└ Archiv archivierte Ausschreibungen, von dort zurückholbar
Laufende Projekte
Historie
Einstellungen
```
Die Unterpunkte klappen auf, sobald ein Punkt des Zweigs benutzt wird. Dauerhaft
ausgeklappt wäre die Leiste doppelt so lang, und Archive schaut man selten an.
Die **Netzwerkansicht ist kein Menüpunkt**, sondern eine Ansicht innerhalb von
Kontakten und Unternehmen – genau wie im perinco CRM.
## Komponentenkarte
```mermaid
flowchart TD
M["main.tsx<br/>AuthProvider, ThemeProvider, ToastProvider,<br/>OptionsProvider, WhisperProvider"] --> A["App.tsx<br/>Ansichtswechsel, gemeinsame Daten"]
A --> SB["Sidebar"]
A --> HD["Header<br/>Suche, Design, hell/dunkel, Konto"]
A --> V{"Ansicht"}
V --> DSH["dashboard/"]
V --> LDS["leads/<br/>Kanban, Karte, ToDos, Erinnerungen"]
V --> KON["contacts/<br/>Liste, Kacheln, Netzwerk, CV-Import"]
V --> UNT["companies/<br/>Liste, Kacheln, Group, Netzwerk"]
V --> AUS["tenders/<br/>Tabelle, Live!, Archiv"]
V --> PRJ["projects/"]
V --> HIS["history/"]
V --> SET["settings/<br/>Nutzer, Stammdaten, Konto, Diktat"]
KON --> NW["network/Network.tsx<br/>d3-force + SVG"]
UNT --> NW
NW --> CT["network/CompanyTree.tsx"]
```
Zwei Dinge liegen bewusst in `App.tsx`:
* Die Listen der Unternehmen und Benutzer werden **einmal** geladen und
weitergereicht – fast jedes Formular braucht sie als Auswahl.
* `open(type, id)` wechselt die Ansicht und reicht die Kennung durch. Damit kann
jede @-Erwähnung und jeder Suchtreffer die passende Karte öffnen, ohne dass
die Ebenen dazwischen davon wissen (`src/lib/navigation.tsx`). Dort hängt auch
`goToSettings(tab)` – der Weg, den der Diktat-Knopf nimmt, wenn das Modell
noch fehlt.
## Zustand
| Was | Wo |
| --- | --- |
| Angemeldeter Benutzer, JWT | `lib/auth.tsx`, Token im `localStorage` |
| Design und hell/dunkel | `lib/theme.tsx` – am Benutzer **und** im `localStorage` |
| Auswahllisten (Branchen, Arten) | `lib/options.tsx` |
| Spracherkennung | `lib/whisper.tsx` |
| Meldungen | `components/Toast.tsx` |
Es gibt keinen globalen Datenspeicher. Jede Ansicht lädt, was sie braucht, und
meldet über `onChanged` zurück, wenn die gemeinsamen Listen neu gelesen werden
müssen. Bei der Größe des Bestands ist das die einfachere Wahl.
## Designs
Rechts oben zwei Umschalter: Farbwelt und hell/dunkel. Beides gilt für den
angemeldeten Benutzer, nicht nur für den Browser.
| Design | Beschreibung |
| --- | --- |
| **ICON** | Gold `#C1862F` auf neutralem Grau, Montserrat – bewusst ohne Grafik |
| **Half-Life** | Black Mesa: HEV-Orange auf Sichtbeton, harte Schatten, Oswald in den Überschriften |
| **Pixel Art** | 8-bit: keine Radien, versetzte Schatten, Arcade-Grün, Silkscreen in den Überschriften |
Half-Life und Pixel Art bringen Grafiken hinter Kopfleiste und Menü mit (SVG
unter `public/themes/`). Die CSS-Variable enthält dabei **zwei** Ebenen: einen
Schleier aus der Flächenfarbe und darunter die Zeichnung – nur so funktioniert
dieselbe Grafik in hell und dunkel, ohne dass die Schrift verliert.
Ein Design tauscht ausschließlich Variablenwerte; im Markup steht nirgends eine
feste Farbe. Auch der Dunkelmodus arbeitet **ohne `dark:`-Varianten**: die
Klasse `dark` auf `<html>` tauscht in `src/index.css` die Werte von `canvas`,
`surface`, `ink-*`, `brand-*` und den getönten Statusfarben. Ein Inline-Skript
in `index.html` setzt die Klasse vor dem ersten Frame, sonst blitzt die helle
Variante auf.
Die Liste der Designs steht an zwei Stellen und muss übereinstimmen:
`THEME_PALETTES` in `src/types.ts` und `THEMES` in `server/src/userFields.js`.
Ein unbekannter Wert wird vom Backend mit 400 abgewiesen und beim Lesen auf
`null` normalisiert – sonst reicht ein entfernter Palettenname, um die Oberfläche
beim Anmelden abstürzen zu lassen.
## Texte: drei Feldarten
| Art | Komponente | Wo |
| --- | --- | --- |
| WYSIWYG (HTML) | `ui/RichTextEditor.tsx` | Ausschreibungstext, Projektnotizen |
| Text mit @-Erwähnungen | `lib/mentions.tsx` (`MentionField`) | Notizen an Kontakt, Unternehmen, Lead |
| einfacher Text | `ui/form.tsx` | alles Übrige |
Der WYSIWYG-Editor kommt **ohne Fremdpaket** aus: `contentEditable` plus
`document.execCommand` für sechs Formate. Quill & Co. bringen ihr eigenes
Farbschema mit, das im Dunkelmodus gegen die Variablen arbeitet, und kosten über
200 kB.
Aus dem Altbestand kommt HTML mit Inline-Farben, geschützten Leerzeichen und
verschachtelten Listen. `src/lib/richText.ts` räumt es beim Laden zurückhaltend
auf (`sanitize()`): Farbe, Hintergrund, Schriftart und -größe fliegen aus den
Stilangaben, Fett und Kursiv bleiben. Zusätzlich hebt `src/index.css` alle
Inline-Farben im Editor aus – sonst stünde im Dunkelmodus schwarze Schrift auf
weißen Kästen.
**@-Erwähnungen** stehen als Token im Text: `@[Anzeigename](contact:12)`, ebenso
für `company` und `user`. Das Feld hat zwei Zustände: *lesen* (Erwähnungen sind
Knöpfe und führen auf das Profil) und *bearbeiten* (der rohe Token – anders
ließe sich eine Erwähnung nicht setzen oder löschen).
## Netzwerkansicht
Kraftgerichtetes Layout (`d3-force`), gerendert als SVG.
* **Rechtecke** = Unternehmen, Größe nach Anzahl Verbindungen, dunkler Rahmen bei Kunden
* **Kreise** = Kontakte
* **Goldene Linie** = arbeitet dort · **graue gestrichelte** = hat dort gearbeitet
* **Feine Linie** = Kontakt kennt Kontakt
* **Grün umrandete Kreise** = Kontakte aus dem perinco CRM (siehe [06 Schnittstellen](06-schnittstellen.md))
Hintergrund ziehen verschiebt, Mausrad zoomt auf den Zeiger, Knoten ziehen
fixiert ihn, Doppelklick löst die Fixierung. Eine Geste gilt erst ab 4 px Weg
als Ziehen, damit Verschieben nicht die Auswahl verwirft.
> **Falle:** Die Liste der externen Knoten muss **stabil** sein. Ein
> `externals = []` als Vorgabewert erzeugt bei jedem Rendern ein neues Array;
> die Simulation startet dann endlos neu und alle Knoten liegen auf einem Punkt.
> Deshalb steht in `Network.tsx` ein `NO_EXTERNALS` auf Modulebene, und die
> aufrufende Ansicht memoisiert ihre Liste.
### Stammbaum eines Unternehmens
Ein Klick auf ein Unternehmen mit Kontakten öffnet dessen Stammbaum: das
Unternehmen oben, die Kontakte darunter.
| Ast | Bedeutung |
| --- | --- |
| **golden** | Berater – Kontaktart „Experte" |
| **hell** | normale Verbindung – Ansprechpartner und übrige |
| **grau, gestrichelt** | Ehemalige – beendete Anstellung; darunter steht, wo die Person heute ist |
Die Art des Astes ergibt sich aus der **Beziehung**, nicht aus der Person: wer
das Unternehmen verlassen hat, hängt am grauen Ast, auch wenn er Berater ist.
## Fotos
Fotos und Logos liegen als Data-URL in der Datenbank. Der Browser verkleinert
jedes Bild **vor** dem Upload (`src/lib/imageCompress.ts`): mittiger Ausschnitt
auf 400 px im Quadrat, WebP mit Qualität 0,82, Rückfall JPEG, dann PNG.
Der Knopf *Von LinkedIn* holt das öffentliche Profilfoto – über das Backend
(`server/src/linkedinPhoto.js`), weil aus dem Browser die Same-Origin-Policy
davorstünde. Gelesen wird ausschließlich die Open-Graph-Vorschau der
öffentlichen Profilseite.