From dcaf34d14b9627a6f87381b8401074722b17e5e8 Mon Sep 17 00:00:00 2001 From: Sascha Date: Mon, 14 Sep 2026 15:34:10 +0200 Subject: [PATCH] Dokumentation: docs/03-frontend.md --- docs/03-frontend.md | 170 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 docs/03-frontend.md diff --git a/docs/03-frontend.md b/docs/03-frontend.md new file mode 100644 index 0000000..2f74b39 --- /dev/null +++ b/docs/03-frontend.md @@ -0,0 +1,170 @@ +# 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
AuthProvider, ThemeProvider, ToastProvider,
OptionsProvider, WhisperProvider"] --> A["App.tsx
Ansichtswechsel, gemeinsame Daten"] + A --> SB["Sidebar"] + A --> HD["Header
Suche, Design, hell/dunkel, Konto"] + A --> V{"Ansicht"} + V --> DSH["dashboard/"] + V --> LDS["leads/
Kanban, Karte, ToDos, Erinnerungen"] + V --> KON["contacts/
Liste, Kacheln, Netzwerk, CV-Import"] + V --> UNT["companies/
Liste, Kacheln, Group, Netzwerk"] + V --> AUS["tenders/
Tabelle, Live!, Archiv"] + V --> PRJ["projects/"] + V --> HIS["history/"] + V --> SET["settings/
Nutzer, Stammdaten, Konto, Diktat"] + KON --> NW["network/Network.tsx
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 `` 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.