diff --git a/docs/02-datenmodell.md b/docs/02-datenmodell.md new file mode 100644 index 0000000..4515225 --- /dev/null +++ b/docs/02-datenmodell.md @@ -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= node src/migrate.js # Probelauf +SUPABASE_URL=https://icondb.perinco.de SUPABASE_KEY= node src/migrate.js --write # schreibt +```