Dokumentation: docs/02-datenmodell.md
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
# 02 Datenmodell
|
||||
|
||||
Eine Datenbank, `icon_CRM`, alle Tabellen mit dem Präfix `icon_`. Das Schema
|
||||
steht als idempotentes SQL in `server/src/schema.sql` und wird beim Start des
|
||||
Backends angewandt; spätere Feldänderungen liegen als Migrationen in
|
||||
`server/src/db.js`.
|
||||
|
||||
## Überblick
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
icon_users ||--o{ icon_companies : "erstellt / zustaendig"
|
||||
icon_users ||--o{ icon_contacts : "erstellt / zustaendig"
|
||||
icon_users ||--o{ icon_history : "hat getan"
|
||||
icon_companies ||--o{ icon_contacts : "beschaeftigt"
|
||||
icon_companies ||--o{ icon_employments : "Stationen"
|
||||
icon_contacts ||--o{ icon_employments : "Werdegang"
|
||||
icon_contacts ||--o{ icon_interviews : "Gespraeche"
|
||||
icon_contacts ||--o{ icon_connections : "kennt"
|
||||
icon_companies ||--o{ icon_tenders : "Kunde"
|
||||
icon_tenders ||--o{ icon_tender_contacts : "zugewiesen"
|
||||
icon_contacts ||--o{ icon_tender_contacts : "zugewiesen"
|
||||
icon_companies ||--o{ icon_projects : "Kunde"
|
||||
icon_contacts ||--o{ icon_projects : "Experte"
|
||||
icon_companies ||--o{ icon_leads : "Ziel"
|
||||
icon_contacts ||--o{ icon_leads : "Ansprechpartner"
|
||||
icon_leads ||--o{ icon_lead_todos : "Checkliste"
|
||||
icon_contacts ||--o{ icon_cv_imports : "wurde daraus"
|
||||
```
|
||||
|
||||
`icon_mentions`, `icon_options` und `icon_settings` hängen an keiner
|
||||
Fremdschlüsselbeziehung und stehen deshalb nicht im Bild.
|
||||
|
||||
## Die Tabellen im Einzelnen
|
||||
|
||||
### icon_users
|
||||
|
||||
| Feld | Typ | Bedeutung |
|
||||
| --- | --- | --- |
|
||||
| `username`, `email` | VARCHAR, je eindeutig | Anmeldung geht mit beidem |
|
||||
| `name` | VARCHAR | Anzeigename |
|
||||
| `role` | ENUM `admin`,`user` | siehe [01 Architektur](01-architektur.md) |
|
||||
| `password_hash` | VARCHAR(255) | bcrypt; Altbestand SHA-256, wird beim Login umgeschrieben |
|
||||
| `color` | VARCHAR(9) | persönliche Farbe für Avatar und Historie |
|
||||
| `avatar` | MEDIUMTEXT | Foto als Data-URL |
|
||||
| `theme`, `theme_mode` | VARCHAR / ENUM | Design und hell/dunkel, gilt geräteübergreifend |
|
||||
| `is_active` | TINYINT | deaktiviert = sofort ausgesperrt |
|
||||
|
||||
### icon_companies
|
||||
|
||||
`name`, `group_name` (Konzern), `industry` (aus den Stammdaten), `employees`,
|
||||
`website`, `address`, `logo` (Data-URL), `rating` (0–5 Sterne), `is_customer`,
|
||||
`customer_state` (`aktiv`/`alumni`), `freelancers`
|
||||
(`unbekannt`/`ja`/`nein`), `notes`, `created_by`, `owner_id`.
|
||||
|
||||
**Ein Kunde ist kein eigener Datentyp**, sondern ein Unternehmen mit
|
||||
`is_customer = 1`. Der Umschalter in der Oberfläche filtert nur darauf – es gibt
|
||||
keine zweite Liste, die auseinanderlaufen könnte.
|
||||
|
||||
### icon_contacts
|
||||
|
||||
| Feld | Bedeutung |
|
||||
| --- | --- |
|
||||
| `first_name`, `last_name`, `position` | Person |
|
||||
| `email`, `phone`, `linkedin`, `avatar` | Kontaktdaten und Foto |
|
||||
| `company_id` | aktueller Arbeitgeber |
|
||||
| `kind` | Art: Experte, Ansprechpartner, Lead – seit einer Migration VARCHAR statt ENUM, weil die Liste in den Stammdaten wächst |
|
||||
| `status` | Interessiert, Interview, Gebucht, Inaktiv |
|
||||
| `lead_source` | woher der Kontakt kam |
|
||||
| `contact_state` | unbekannt / persönlich / kein Kontakt |
|
||||
| `freelancers` | arbeitet freiberuflich |
|
||||
| `bio` | Notiz, darf @-Erwähnungen enthalten |
|
||||
| `tags` | Schlagworte, kommagetrennt |
|
||||
| `rate_onsite`, `rate_remote` | **Stundensätze** in Euro, `NULL` = nicht hinterlegt |
|
||||
| `cv_url`, `cv_file` | Link bzw. hochgelegter Lebenslauf |
|
||||
|
||||
**Experten und Ansprechpartner liegen in einer Tabelle.** Ein Ansprechpartner,
|
||||
der die Firma wechselt und plötzlich als Experte interessant wird, ist derselbe
|
||||
Mensch und behält seine Historie.
|
||||
|
||||
> `rate_onsite`/`rate_remote` stammen aus `hourlyRateOnsite`/`hourlyRateRemote`
|
||||
> des alten CRM und sind **Stundensätze**. Die Oberfläche schreibt die Einheit
|
||||
> überall mit (`125,00 €/h`), und die erweiterte Suche weiß es ebenfalls.
|
||||
|
||||
### icon_employments
|
||||
|
||||
Werdegang: `contact_id`, `company_id`, `position`, `started_on`, `ended_on`,
|
||||
`is_current`. Daraus speist sich der Zweig **Verlassen** im Stammbaum eines
|
||||
Unternehmens. Ein Arbeitgeberwechsel schreibt hier automatisch fort.
|
||||
|
||||
### icon_interviews
|
||||
|
||||
`contact_id` oder `company_id`, `user_id`, `held_on`, `notes` – wann wurde mit
|
||||
wem gesprochen.
|
||||
|
||||
### icon_history
|
||||
|
||||
Jede Tätigkeit: `entity_type` (contact, company, tender, project, lead, user,
|
||||
system), `entity_id`, `entity_label`, `action`, `text`, `changes` (JSON mit
|
||||
Vorher/Nachher je Feld), `user_id`, `created_at`.
|
||||
|
||||
Geschrieben wird **ausschließlich** über `logHistory()` in
|
||||
`server/src/history.js` – nicht von den Routen selbst. Dadurch gilt an einer
|
||||
Stelle, was überall gelten soll: in jeder Historie steht, welcher Benutzer was
|
||||
geändert hat.
|
||||
|
||||
### icon_mentions
|
||||
|
||||
Index der `@`-Erwähnungen: `source_type`/`source_id` → `target_type`/`target_id`,
|
||||
eindeutig je Paar. Ohne diesen Index müsste eine Karte jeden Freitext der
|
||||
Datenbank durchsuchen, um zu zeigen, wo sie genannt wurde.
|
||||
|
||||
### icon_connections
|
||||
|
||||
Wer kennt wen: `contact_a`, `contact_b`, `kind`, `strength`. Das Paar wird
|
||||
aufsteigend gespeichert, ein Unique-Key verhindert, dass dieselbe Verbindung
|
||||
zweimal entsteht.
|
||||
|
||||
### icon_tenders, icon_tender_contacts
|
||||
|
||||
| Feld | Bedeutung |
|
||||
| --- | --- |
|
||||
| `title`, `source`, `link` | Ausschreibung und Herkunft |
|
||||
| `company_id` | Kunde |
|
||||
| `status` | Aktiv, Inaktiv, Ausgelaufen, **Archiviert** |
|
||||
| `stage` | Phase: Ausschreibung → Kontakte sichten → Interview → Kunden Interview → Vertragsgestaltung → Abschluss → Geschlossen |
|
||||
| `online` | erscheint auf der ICON Website |
|
||||
| `notes` | **der veröffentlichte Ausschreibungstext**, als HTML |
|
||||
| `details` | im gesamten Bestand leer, ungenutzt |
|
||||
| `deadline` | Frist |
|
||||
|
||||
> **Die wichtigste Zeile dieses Kapitels:** der Text der Website steht in
|
||||
> `notes`, nicht in `details`. Wer den Editor auf `details` legt, zeigt ein
|
||||
> leeres Feld und schreibt am veröffentlichten Text vorbei.
|
||||
|
||||
`icon_tender_contacts` verbindet Ausschreibung und Kontakt (`role` = Rolle im
|
||||
Projekt).
|
||||
|
||||
### icon_projects
|
||||
|
||||
`title`, `company_id`, `contact_id`, `started_on`, `ended_on`, `status`
|
||||
(Aktiv / Nicht aktiv / Abgeschlossen), `rate`, `notes` (HTML). Im Menü heißt die
|
||||
Ansicht **Laufende Projekte**.
|
||||
|
||||
Im übernommenen Bestand steht im Kopf von `notes` ein Textblock aus dem alten
|
||||
CRM (`KUNDE:`, `KONTAKT:`, `EXPERTE:`, `START:`, `ENDE:`), darunter der
|
||||
übernommene Ausschreibungstext als HTML. `src/lib/richText.ts` macht daraus im
|
||||
Editor Absätze plus HTML – deshalb der Sonderfall „Text, dann HTML".
|
||||
|
||||
### icon_leads, icon_lead_todos
|
||||
|
||||
`title`, `company_id`, `contact_id`, `stage` (acht Phasen, erste = `Fuer
|
||||
Spaeter`, in der Oberfläche **Backlog**), `contact_state`, `archived`,
|
||||
`position` (Reihenfolge in der Spalte), `notes`, `remind_at`, `reminder_days`,
|
||||
`last_contact_at`, `owner_id`.
|
||||
|
||||
`icon_lead_todos`: `lead_id`, `text`, `done`, `due_on`, `position` – die
|
||||
Checkliste, deren Fortschritt auf der Kachel als Prozentwert steht.
|
||||
|
||||
### icon_cv_imports
|
||||
|
||||
`filename`, `original_name`, `mime`, `size_bytes`, `status`
|
||||
(`eingang` → `screening` → `auswahl`, oder `verworfen`), `extracted` (das JSON
|
||||
der KI), `error`, `contact_id` (nach der Übernahme), `uploaded_by`.
|
||||
|
||||
### icon_options
|
||||
|
||||
Die gepflegten Auswahllisten: `field` (`industry`, `contact_kind`), `value`,
|
||||
`position`. Umbenennen zieht den Bestand mit; Entfernen geht nur, solange ein
|
||||
Wert nirgends benutzt wird.
|
||||
|
||||
### icon_settings
|
||||
|
||||
Schlüssel/Wert, unter anderem für Migrationsmerker – damit eine einmalige
|
||||
Umstellung nicht bei jedem Start erneut läuft.
|
||||
|
||||
## Migration aus Supabase
|
||||
|
||||
`server/src/migrate.js` übernimmt den Altbestand einmalig und wiederholbar
|
||||
(Unternehmen und Kontakte werden über ihren Namen wiedererkannt):
|
||||
|
||||
| alt | neu |
|
||||
| --- | --- |
|
||||
| `icon_customers` | `icon_companies` mit `is_customer = 1` |
|
||||
| `icon_contacts` | `icon_contacts`, `kind = 'Ansprechpartner'` |
|
||||
| `icon_experts` | `icon_contacts`, `kind = 'Experte'` |
|
||||
| `hourlyRateOnsite`, `hourlyRateRemote` | `rate_onsite`, `rate_remote` |
|
||||
| Status `Verfügbar` | `Interessiert` |
|
||||
| `kanbanStatus: Experten sichten` | Phase `Kontakte sichten` |
|
||||
|
||||
```bash
|
||||
cd /var/www/icon_CRM/server
|
||||
SUPABASE_URL=https://icondb.perinco.de SUPABASE_KEY=<anon-key> node src/migrate.js # Probelauf
|
||||
SUPABASE_URL=https://icondb.perinco.de SUPABASE_KEY=<anon-key> node src/migrate.js --write # schreibt
|
||||
```
|
||||
Reference in New Issue
Block a user