Files
ICON-CRM/docs/02-datenmodell.md
T

210 lines
8.9 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.
# 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; daneben
lassen sich Stationen in der Kontaktkarte von Hand pflegen.
Zwei Regeln halten Station und Kontakt zusammen (`server/src/routes/contacts.js`):
* Genau **eine** Station kann `is_current = 1` sein. Wird eine dazu erklärt,
bekommen die übrigen ein Enddatum.
* Die laufende Station **ist** der Arbeitgeber: `icon_contacts.company_id` wird
mitgeführt – wird sie beendet oder gelöscht, steht dort `NULL`. Beides
auseinanderlaufen zu lassen wäre die Quelle für „im Kontakt steht A, im
Werdegang B".
### 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), `photo`, `error`, `contact_id` (nach der Übernahme), `uploaded_by`.
`photo` ist das im Lebenslauf gefundene Bewerbungsfoto, unverändert als
Data-URL. Es ist eine **Zwischenablage**: beim Anlegen des Kontakts geht das
Bild verkleinert an `icon_contacts.avatar` und wird hier geleert.
### 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
```