Files
ICON-CRM/server/src/schema.sql
T
schakal00783andClaude Opus 5.5 1ad774fdd2 Scanner: ohne Altersgrenze suchen, Filter Anzeigenalter, Alarm-Schlagwörter
- Suche ohne 21-Tage-Grenze (BA ohne veroeffentlichtseit, Adzuna ohne
  max_days_old); Einstellung Höchstalter entfällt
- Dashboard: Filter Anzeigenalter (bis/älter als 21 Tage), Sortierung
  älteste Anzeige, Filter nur Alarm, Zeitraum heißt jetzt Gefunden
- Suchagent: Schlagwörter mit Alarm; Treffer mit alarm = 1 melden sich
  über den AlarmWatcher (App-weit, jede Minute) mit Ton und Pop-up,
  Quittieren gilt für alle

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:28:29 +02:00

544 lines
27 KiB
SQL

-- ============================================================================
-- ICON CRM - Datenbankschema (MariaDB, Datenbank icon_CRM)
--
-- Idempotent: die Datei laeuft bei jedem Backend-Start durch. Jedes Statement
-- muss deshalb ein IF NOT EXISTS tragen oder sonst wiederholbar sein.
-- Spaltenaenderungen an bestehenden Tabellen gehoeren nicht hierher, sondern in
-- die Migrationen in db.js - CREATE TABLE IF NOT EXISTS fasst eine vorhandene
-- Tabelle nicht an.
--
-- Grundgedanke des Modells (aus der Featureliste vom 14.09.2026):
-- * "Experten" heissen jetzt Kontakte und umfassen auch die Ansprechpartner
-- der Kunden - eine Tabelle, unterschieden ueber `kind`.
-- * "Kunden" heissen jetzt Unternehmen. Ein Kunde ist ein Unternehmen mit
-- is_customer = 1; der Umschalter in der Oberflaeche filtert nur darauf.
-- * Jede Aenderung landet in icon_history. Wer wann was getan hat, steht
-- damit an genau einer Stelle - je Kontakt, je Unternehmen und global.
-- ============================================================================
-- ---------------------------------------------------------------------------
-- Benutzer
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS icon_users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(60) NOT NULL,
email VARCHAR(190) NULL,
name VARCHAR(190) NOT NULL,
role ENUM('admin','user') NOT NULL DEFAULT 'user',
password_hash VARCHAR(255) NOT NULL,
-- Persoenliche Farbe: traegt Initialen-Avatare, Historie und Netzwerkknoten.
color VARCHAR(9) NULL,
-- Foto als Data-URL, im Browser auf 256 px vorkomprimiert.
avatar MEDIUMTEXT NULL,
theme VARCHAR(40) NULL,
theme_mode ENUM('light','dark') NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_icon_users_username (username),
UNIQUE KEY uq_icon_users_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- Unternehmen (fruehere "Kunden")
--
-- `is_customer` unterscheidet unsere Kunden von allen uebrigen bekannten
-- Unternehmen; `customer_state` trennt darin aktiv von alumni. Ein Unternehmen
-- ohne Kundenhaken ist trotzdem vollwertig - dort haengen Werdegaenge, Leads
-- und die Unternehmenskarte dran.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS icon_companies (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(190) NOT NULL,
-- Dachgesellschaft, z. B. "Finanz Informatik" ueber FI Hamburg/Frankfurt.
group_name VARCHAR(190) NULL,
industry VARCHAR(190) NULL,
employees INT UNSIGNED NULL,
website VARCHAR(300) NULL,
address TEXT NULL,
logo MEDIUMTEXT NULL,
-- 0 = nicht bewertet, 1..5 Sterne: wie interessant ist das Unternehmen.
rating TINYINT UNSIGNED NOT NULL DEFAULT 0,
is_customer TINYINT(1) NOT NULL DEFAULT 0,
customer_state ENUM('aktiv','alumni') NOT NULL DEFAULT 'aktiv',
freelancers ENUM('unbekannt','ja','nein') NOT NULL DEFAULT 'unbekannt',
notes MEDIUMTEXT NULL,
created_by INT UNSIGNED NULL,
owner_id INT UNSIGNED NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_icon_companies_name (name),
KEY idx_icon_companies_group (group_name),
KEY idx_icon_companies_customer (is_customer, customer_state),
CONSTRAINT fk_icon_companies_creator FOREIGN KEY (created_by)
REFERENCES icon_users (id) ON DELETE SET NULL,
CONSTRAINT fk_icon_companies_owner FOREIGN KEY (owner_id)
REFERENCES icon_users (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- Kontakte (fruehere "Experten" plus Ansprechpartner der Unternehmen)
--
-- Der Verfuegbarkeitsstatus ist bewusst entfallen (Featureliste 14.09.2026).
-- `status` beschreibt nur noch den Stand der Zusammenarbeit.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS icon_contacts (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
-- Akademischer Titel als Freitext ("Dr.", "Prof. Dr."). Bewusst keine
-- Auswahlliste: die Schreibweisen sind zu vielfaeltig, um sie zu pflegen.
title VARCHAR(60) NULL,
first_name VARCHAR(120) NULL,
last_name VARCHAR(120) NOT NULL,
position VARCHAR(190) NULL,
email VARCHAR(190) NULL,
phone VARCHAR(80) NULL,
linkedin VARCHAR(300) NULL,
avatar MEDIUMTEXT NULL,
-- Aktueller Arbeitgeber. Die Historie der frueheren steht in
-- icon_employments - daraus speist sich der Zweig "Verlassen".
company_id INT UNSIGNED NULL,
kind ENUM('Experte','Ansprechpartner (Kunde)','Lead') NOT NULL DEFAULT 'Experte',
status ENUM('Interessiert','Interview','Gebucht','Inaktiv') NOT NULL DEFAULT 'Interessiert',
-- Woher kommt der Kontakt (LinkedIn, Empfehlung, Portal, ...).
lead_source VARCHAR(190) NULL,
-- Featureliste: "persoenlicher Kontakt" oder "noch kein Kontakt gehabt".
contact_state ENUM('unbekannt','persoenlich','kein Kontakt') NOT NULL DEFAULT 'unbekannt',
freelancers ENUM('unbekannt','ja','nein') NOT NULL DEFAULT 'unbekannt',
bio MEDIUMTEXT NULL,
-- Kommagetrennte Schlagworte; die Oberflaeche zerlegt sie in Chips.
tags VARCHAR(1000) NULL,
rate_onsite DECIMAL(8,2) NULL,
rate_remote DECIMAL(8,2) NULL,
cv_url VARCHAR(500) NULL,
cv_file VARCHAR(255) NULL,
created_by INT UNSIGNED NULL,
owner_id INT UNSIGNED NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_icon_contacts_name (last_name, first_name),
KEY idx_icon_contacts_company (company_id),
KEY idx_icon_contacts_kind (kind),
KEY idx_icon_contacts_status (status),
CONSTRAINT fk_icon_contacts_company FOREIGN KEY (company_id)
REFERENCES icon_companies (id) ON DELETE SET NULL,
CONSTRAINT fk_icon_contacts_creator FOREIGN KEY (created_by)
REFERENCES icon_users (id) ON DELETE SET NULL,
CONSTRAINT fk_icon_contacts_owner FOREIGN KEY (owner_id)
REFERENCES icon_users (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- Werdegang: welcher Kontakt war wann bei welchem Unternehmen
--
-- Traegt zwei Dinge aus der Featureliste: die automatische Liste "letzter
-- Arbeitgeber (Branche & Unternehmen)" und den Zweig "Verlassen" im Stammbaum
-- eines Unternehmens (is_current = 0).
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS icon_employments (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
contact_id INT UNSIGNED NOT NULL,
company_id INT UNSIGNED NOT NULL,
position VARCHAR(190) NULL,
started_on DATE NULL,
ended_on DATE NULL,
is_current TINYINT(1) NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_icon_employments_contact (contact_id),
KEY idx_icon_employments_company (company_id, is_current),
CONSTRAINT fk_icon_employments_contact FOREIGN KEY (contact_id)
REFERENCES icon_contacts (id) ON DELETE CASCADE,
CONSTRAINT fk_icon_employments_company FOREIGN KEY (company_id)
REFERENCES icon_companies (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- Interviews: "Hatten wir schon einmal ein Interview, und falls ja - wann,
-- mit wem?" Eigene Tabelle statt Freitext, damit danach gefiltert werden kann.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS icon_interviews (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
contact_id INT UNSIGNED NULL,
company_id INT UNSIGNED NULL,
-- Wer von uns das Gespraech gefuehrt hat.
user_id INT UNSIGNED NULL,
held_on DATE NOT NULL,
notes MEDIUMTEXT NULL,
created_by INT UNSIGNED NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_icon_interviews_contact (contact_id, held_on),
KEY idx_icon_interviews_company (company_id, held_on),
CONSTRAINT fk_icon_interviews_contact FOREIGN KEY (contact_id)
REFERENCES icon_contacts (id) ON DELETE CASCADE,
CONSTRAINT fk_icon_interviews_company FOREIGN KEY (company_id)
REFERENCES icon_companies (id) ON DELETE CASCADE,
CONSTRAINT fk_icon_interviews_user FOREIGN KEY (user_id)
REFERENCES icon_users (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- Historie / Aktivitaetsprotokoll
--
-- Eine Tabelle fuer alles: Systemereignisse (angelegt, geaendert, geloescht)
-- und von Hand erfasste Notizen. `entity_type`/`entity_id` sagen, an welcher
-- Karte der Eintrag haengt; `user_id` immer, wer es war.
--
-- Absichtlich keine Fremdschluessel auf die Ziel-Tabellen: die Historie soll
-- einen geloeschten Kontakt ueberleben. Deshalb steht mit `entity_label` auch
-- der Name zum Zeitpunkt des Eintrags mit drin.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS icon_history (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
entity_type ENUM('contact','company','tender','project','lead','user','system') NOT NULL,
entity_id INT UNSIGNED NULL,
entity_label VARCHAR(300) NULL,
-- created | updated | deleted | note | mention | interview | stage | ...
action VARCHAR(40) NOT NULL DEFAULT 'note',
-- Anzeigetext, darf Mention-Token @[Name](contact:12) enthalten.
text MEDIUMTEXT NULL,
-- Feldaenderungen als JSON: { feld: [vorher, nachher], ... }
changes MEDIUMTEXT NULL,
user_id INT UNSIGNED NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_icon_history_entity (entity_type, entity_id, created_at),
KEY idx_icon_history_created (created_at),
CONSTRAINT fk_icon_history_user FOREIGN KEY (user_id)
REFERENCES icon_users (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- Erwaehnungen (@-Mentions)
--
-- Der Token steht im Text; diese Tabelle ist der Index dazu. Damit beantwortet
-- eine Abfrage "wo wurde dieses Unternehmen ueberall genannt?", ohne jeden
-- Freitext der Datenbank durchsuchen zu muessen.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS icon_mentions (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
-- Wo die Erwaehnung steht.
source_type ENUM('history','contact','company','tender','project','lead') NOT NULL,
source_id BIGINT UNSIGNED NOT NULL,
-- Wer bzw. was erwaehnt wurde.
target_type ENUM('contact','company','user') NOT NULL,
target_id INT UNSIGNED NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_icon_mentions (source_type, source_id, target_type, target_id),
KEY idx_icon_mentions_target (target_type, target_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- Netzwerk: wer kennt wen
--
-- Das Paar wird aufsteigend gespeichert (a < b), der Unique-Key verhindert
-- damit Duplikate in beide Richtungen.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS icon_connections (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
contact_a INT UNSIGNED NOT NULL,
contact_b INT UNSIGNED NOT NULL,
kind VARCHAR(60) NOT NULL DEFAULT 'Kontakt',
strength TINYINT UNSIGNED NOT NULL DEFAULT 1,
created_by INT UNSIGNED NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_icon_connections (contact_a, contact_b),
CONSTRAINT fk_icon_connections_a FOREIGN KEY (contact_a)
REFERENCES icon_contacts (id) ON DELETE CASCADE,
CONSTRAINT fk_icon_connections_b FOREIGN KEY (contact_b)
REFERENCES icon_contacts (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- Ausschreibungen
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS icon_tenders (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(300) NOT NULL,
source VARCHAR(190) NULL,
link VARCHAR(600) NULL,
company_id INT UNSIGNED NULL,
status ENUM('Aktiv','Inaktiv','Ausgelaufen','Archiviert') NOT NULL DEFAULT 'Aktiv',
stage ENUM('Ausschreibung','Kontakte sichten','Interview','Kunden Interview',
'Vertragsgestaltung','Abschluss','Geschlossen') NOT NULL DEFAULT 'Ausschreibung',
online TINYINT(1) NOT NULL DEFAULT 1,
details MEDIUMTEXT NULL,
notes MEDIUMTEXT NULL,
deadline DATE NULL,
created_by INT UNSIGNED NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_icon_tenders_stage (stage),
KEY idx_icon_tenders_status (status),
CONSTRAINT fk_icon_tenders_company FOREIGN KEY (company_id)
REFERENCES icon_companies (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS icon_tender_contacts (
tender_id INT UNSIGNED NOT NULL,
contact_id INT UNSIGNED NOT NULL,
role VARCHAR(120) NULL,
PRIMARY KEY (tender_id, contact_id),
CONSTRAINT fk_icon_tc_tender FOREIGN KEY (tender_id)
REFERENCES icon_tenders (id) ON DELETE CASCADE,
CONSTRAINT fk_icon_tc_contact FOREIGN KEY (contact_id)
REFERENCES icon_contacts (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- Laufende Projekte
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS icon_projects (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(300) NOT NULL,
company_id INT UNSIGNED NULL,
contact_id INT UNSIGNED NULL,
started_on DATE NULL,
ended_on DATE NULL,
status ENUM('Aktiv','Nicht aktiv','Abgeschlossen') NOT NULL DEFAULT 'Aktiv',
rate DECIMAL(8,2) NULL,
notes MEDIUMTEXT NULL,
created_by INT UNSIGNED NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_icon_projects_status (status),
CONSTRAINT fk_icon_projects_company FOREIGN KEY (company_id)
REFERENCES icon_companies (id) ON DELETE SET NULL,
CONSTRAINT fk_icon_projects_contact FOREIGN KEY (contact_id)
REFERENCES icon_contacts (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- Lead-Pipeline (Kanban)
--
-- Schema steht schon; die Oberflaeche dazu kommt in der zweiten Ausbaustufe.
-- `remind_at` traegt die Erinnerungsfunktion, `reminder_days` das Intervall.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS icon_leads (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(300) NOT NULL,
company_id INT UNSIGNED NULL,
contact_id INT UNSIGNED NULL,
stage ENUM('Fuer Spaeter','Neuer Lead','Erstkontakt','1. Gespraech',
'Followup geplant','Followup Gespraech','Closed','Absage')
NOT NULL DEFAULT 'Neuer Lead',
contact_state ENUM('unbekannt','persoenlich','kein Kontakt') NOT NULL DEFAULT 'unbekannt',
archived TINYINT(1) NOT NULL DEFAULT 0,
position INT NOT NULL DEFAULT 0,
notes MEDIUMTEXT NULL,
-- Zeitpunkt der naechsten Erinnerung; NULL = keine.
remind_at DATETIME NULL,
reminder_days INT UNSIGNED NULL,
last_contact_at DATETIME NULL,
owner_id INT UNSIGNED NULL,
created_by INT UNSIGNED NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_icon_leads_stage (stage, archived, position),
KEY idx_icon_leads_remind (remind_at),
CONSTRAINT fk_icon_leads_company FOREIGN KEY (company_id)
REFERENCES icon_companies (id) ON DELETE SET NULL,
CONSTRAINT fk_icon_leads_contact FOREIGN KEY (contact_id)
REFERENCES icon_contacts (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- Suspects - die Stufe vor dem Lead (29.09.2026)
--
-- Personen, die interessant sein koennten, als schlichte Tabelle: Name,
-- Funktion und Unternehmen sind bewusst Freitext, nicht Verweise auf Kontakte
-- und Unternehmen - ein Suspect soll in Sekunden erfasst sein. Erst beim
-- Schritt zum Lead wird gegen die Stammdaten abgeglichen.
--
-- `remind_at` ist der "naechste Impuls": dann erinnert das CRM daran, die
-- Person erneut anzusprechen. `lead_id` zeigt nach der Uebergabe auf den Lead;
-- der Suspect selbst wandert dann ins Archiv.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS icon_suspects (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(190) NOT NULL,
role VARCHAR(190) NULL,
company VARCHAR(190) NULL,
email VARCHAR(190) NULL,
linkedin VARCHAR(500) NULL,
website VARCHAR(500) NULL,
notes TEXT NULL,
last_contact_on DATE NULL,
remind_at DATETIME NULL,
archived TINYINT(1) NOT NULL DEFAULT 0,
lead_id INT UNSIGNED NULL,
created_by INT UNSIGNED NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_icon_suspects_archived (archived, id),
KEY idx_icon_suspects_remind (remind_at),
CONSTRAINT fk_icon_suspects_lead FOREIGN KEY (lead_id)
REFERENCES icon_leads (id) ON DELETE SET NULL,
CONSTRAINT fk_icon_suspects_creator FOREIGN KEY (created_by)
REFERENCES icon_users (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS icon_lead_todos (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
lead_id INT UNSIGNED NOT NULL,
text VARCHAR(500) NOT NULL,
done TINYINT(1) NOT NULL DEFAULT 0,
due_on DATE NULL,
position INT NOT NULL DEFAULT 0,
created_by INT UNSIGNED NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_icon_lead_todos_lead (lead_id, position),
CONSTRAINT fk_icon_lead_todos_lead FOREIGN KEY (lead_id)
REFERENCES icon_leads (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- CV-Import
--
-- Der Ablauf aus der Featureliste als drei Ordner in einer Tabelle:
-- eingang hochgeladen, noch nicht ausgelesen
-- screening von der KI ausgelesen, wartet auf Ja oder Nein
-- auswahl uebernommen - der Kontakt ist angelegt, der CV haengt dran
-- verworfen abgelehnt; die Datei bleibt, bis sie jemand loescht
--
-- `extracted` haelt die Ausgabe des Modells als JSON. Sie bleibt auch nach der
-- Uebernahme stehen: so laesst sich spaeter nachvollziehen, was die KI gelesen
-- hat und was ein Mensch daran geaendert hat.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS icon_cv_imports (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(255) NOT NULL,
mime VARCHAR(120) NULL,
size_bytes INT UNSIGNED NOT NULL DEFAULT 0,
status ENUM('eingang','screening','auswahl','verworfen') NOT NULL DEFAULT 'eingang',
extracted MEDIUMTEXT NULL,
-- Das im Lebenslauf gefundene Portrait, unveraendert als Data-URL. Es steht
-- hier nur zwischen Auslesen und Uebernehmen; beim Anlegen des Kontakts geht
-- es verkleinert an icon_contacts.avatar und wird hier geleert.
photo MEDIUMTEXT NULL,
error VARCHAR(500) NULL,
contact_id INT UNSIGNED NULL,
uploaded_by INT UNSIGNED NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP NULL DEFAULT NULL,
KEY idx_icon_cv_status (status, created_at),
CONSTRAINT fk_icon_cv_contact FOREIGN KEY (contact_id)
REFERENCES icon_contacts (id) ON DELETE SET NULL,
CONSTRAINT fk_icon_cv_user FOREIGN KEY (uploaded_by)
REFERENCES icon_users (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- Stammdaten: gepflegte Auswahllisten
--
-- Branche und Kontaktart waren Freitext bzw. ein festes ENUM. Beides wird in
-- der Praxis erweitert - eine neue Branche, eine neue Art von Kontakt - und
-- beides soll ohne Codeaenderung gehen. Die Werte stehen deshalb hier und
-- speisen die Auswahlfelder der Oberflaeche.
--
-- `field` ist bewusst kein ENUM: eine weitere Liste soll keine Migration
-- kosten.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS icon_options (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
field VARCHAR(40) NOT NULL,
value VARCHAR(190) NOT NULL,
position INT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_icon_options (field, value),
KEY idx_icon_options_field (field, position)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- Einstellungen (Schluessel/Wert) - auch fuer einmalige Migrationsmerker
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS icon_settings (
key_name VARCHAR(100) NOT NULL PRIMARY KEY,
value MEDIUMTEXT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- Scanner: Stellenanzeigen aus Jobbörsen (DACH, jedes Alter)
--
-- Ein Suchagent ist eine Liste von Schlagwörtern plus der Börsen, in denen
-- gesucht wird, plus Ausschlussbegriffen. Jede Anzeige liegt genau einmal in
-- icon_scanner_jobs (eindeutig je Börse und deren Kennung); welcher Agent sie
-- mit welcher Güte gefunden hat, steht in icon_scanner_matches.
--
-- Bewertet wird nicht per KI, sondern per Zählung: je mehr Schlagwörter eines
-- Agenten in Titel und Text vorkommen, desto höher der Treffer.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS icon_scanner_agents (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(190) NOT NULL,
-- JSON-Arrays: ["SAP FI", "Interim CFO"], ["ba", "stepstone"], ["Praktikum"]
keywords TEXT NOT NULL,
sources TEXT NOT NULL,
excludes TEXT NULL,
-- Schlagwoerter mit Alarm: werden wie `keywords` gesucht und gezaehlt,
-- ein Fund meldet sich zusaetzlich mit Ton und Pop-up.
alarm_keywords TEXT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_by INT UNSIGNED NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_icon_scanner_agents_creator FOREIGN KEY (created_by)
REFERENCES icon_users (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS icon_scanner_jobs (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
source VARCHAR(30) NOT NULL,
external_id VARCHAR(190) NOT NULL,
title VARCHAR(500) NOT NULL,
company VARCHAR(300) NULL,
-- Treffer im CRM: Unternehmen mit gleichem (normalisiertem) Namen.
company_id INT UNSIGNED NULL,
location VARCHAR(300) NULL,
country CHAR(2) NULL,
employment_type VARCHAR(60) NULL,
url VARCHAR(1000) NOT NULL,
description MEDIUMTEXT NULL,
published_at DATETIME NULL,
first_seen_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_seen_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_icon_scanner_jobs (source, external_id),
KEY idx_icon_scanner_jobs_published (published_at),
KEY idx_icon_scanner_jobs_company (company_id),
CONSTRAINT fk_icon_scanner_jobs_company FOREIGN KEY (company_id)
REFERENCES icon_companies (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS icon_scanner_matches (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
agent_id INT UNSIGNED NOT NULL,
job_id INT UNSIGNED NOT NULL,
score TINYINT UNSIGNED NOT NULL DEFAULT 0,
-- JSON-Array der gefundenen Schlagwörter
hits TEXT NULL,
status ENUM('neu','gesehen','in_bearbeitung','verworfen') NOT NULL DEFAULT 'neu',
-- Alarm-Schlagwort getroffen; alarm_ack = jemand hat das Pop-up quittiert.
alarm TINYINT(1) NOT NULL DEFAULT 0,
alarm_ack TINYINT(1) NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_icon_scanner_matches (agent_id, job_id),
KEY idx_icon_scanner_matches_created (created_at),
CONSTRAINT fk_icon_scanner_matches_agent FOREIGN KEY (agent_id)
REFERENCES icon_scanner_agents (id) ON DELETE CASCADE,
CONSTRAINT fk_icon_scanner_matches_job FOREIGN KEY (job_id)
REFERENCES icon_scanner_jobs (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Protokoll der Läufe: wann, wodurch ausgelöst, was je Börse herauskam.
CREATE TABLE IF NOT EXISTS icon_scanner_runs (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
trigger_kind ENUM('auto','manual') NOT NULL,
started_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
finished_at TIMESTAMP NULL,
jobs_found INT UNSIGNED NOT NULL DEFAULT 0,
matches_new INT UNSIGNED NOT NULL DEFAULT 0,
-- JSON: { ba: { ok: true, count: 12 }, stepstone: { ok: false, error: "..." } }
per_source TEXT NULL,
KEY idx_icon_scanner_runs_started (started_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;