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