7.4 KiB
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
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 auchgoToSettings(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)
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 inNetwork.tsxeinNO_EXTERNALSauf 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.