Dokumentation: docs/03-frontend.md
This commit is contained in:
@@ -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<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.
|
||||||
Reference in New Issue
Block a user