feat: Projekt Scanner integrieren, Whisper in die Einstellungen

Der eigenstaendige Projekt Scanner zieht komplett ins CRM ein und laeuft
parallel zur bestehenden App weiter - jede geteilte Ressource ist verdoppelt
statt geteilt, damit die alte Installation unberuehrt bleibt:

- eigene Datenbank perinco_scanner, legt sich beim Start selbst an
- eigener SCANNER_ENCRYPTION_KEY, eigenes Upload-Verzeichnis
- eigener Scheduler mit Zeitversatz, damit nicht beide Instanzen gleichzeitig
  mit denselben Portal-Cookies scrapen (Rate-Limits, CAPTCHA, doppelter RAM)
- Sperre gegen ueberlappende Laeufe: Scheduler und "Jetzt scannen" teilen sie,
  ein zweiter Aufruf bekommt 409 statt doppelter OpenAI-Kosten

Backend unter /api/scanner hinter der CRM-Anmeldung; die HTTP-Basic-Auth des
Originals entfaellt. CV-PDFs kommen als Stream mit Token statt aus einem
statischen Verzeichnis. Bestandsdaten holt "npm run scanner:import" aus der
alten Datenbank, ohne sie zu veraendern.

Oberflaeche in Tailwind und CI-Farben neu gebaut: Menuepunkt Scanner mit
aufklappbaren Unterpunkten, Einstellungen als eigener Reiter.

Whisper hat keinen eigenen Menuepunkt mehr, sondern ist ein Reiter in den
Einstellungen. Damit diese Geraeteeinstellung jeder Rolle offensteht, sind die
Einstellungen jetzt fuer alle sichtbar - welche Reiter erscheinen, entscheidet
die Rolle.

Ausserdem: kein festes Admin-Passwort mehr im Quelltext, playwright exakt auf
1.60.0 gepinnt (nutzt den vorhandenen Chromium-Build des Servers), technische
Dokumentation mit Funktionszeichnungen unter docs/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-08-06 15:08:59 +02:00
co-authored by Claude Opus 5
commit 82d1b7eab4
106 changed files with 24133 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
-- perinco CRM - Schema fuer MariaDB-Datenbank perinco_CRM
-- Idempotent: kann jederzeit erneut ausgefuehrt werden.
CREATE TABLE IF NOT EXISTS crm_users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(64) 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,
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,
PRIMARY KEY (id),
UNIQUE KEY uq_crm_users_username (username),
UNIQUE KEY uq_crm_users_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Persoenliche Farbe fuer die Kalenderansicht. NULL = automatisch aus der
-- Palette (siehe userColor() im Frontend). ADD COLUMN IF NOT EXISTS haelt das
-- Statement idempotent (MariaDB-Erweiterung).
ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS color VARCHAR(9) NULL AFTER role;
-- Benutzerfoto als Data-URL oder externe URL - wie bei crm_leads.avatar.
ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS avatar LONGTEXT NULL AFTER color;
CREATE TABLE IF NOT EXISTS crm_leads (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
salutation VARCHAR(32) NULL,
first_name VARCHAR(120) NOT NULL,
last_name VARCHAR(120) NOT NULL,
company VARCHAR(190) NULL,
position VARCHAR(190) NULL,
email VARCHAR(190) NULL,
phone VARCHAR(64) NULL,
mobile VARCHAR(64) NULL,
linkedin VARCHAR(255) NULL,
website VARCHAR(255) NULL,
street VARCHAR(190) NULL,
zip VARCHAR(24) NULL,
city VARCHAR(120) NULL,
country VARCHAR(120) NULL,
status ENUM('Neu','Kontaktiert','Qualifiziert','Angebot','Gewonnen','Verloren')
NOT NULL DEFAULT 'Neu',
source VARCHAR(120) NULL,
tags LONGTEXT NULL, -- JSON-Array als Text (MariaDB-kompatibel)
notes TEXT NULL,
avatar LONGTEXT NULL, -- Data-URL oder externe URL
owner_id INT UNSIGNED NULL, -- zustaendiger Benutzer
created_by INT UNSIGNED NULL, -- wer den Kontakt angelegt hat
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_crm_leads_name (last_name, first_name),
KEY idx_crm_leads_company (company),
KEY idx_crm_leads_status (status),
KEY idx_crm_leads_owner (owner_id),
KEY idx_crm_leads_creator (created_by),
CONSTRAINT fk_crm_leads_owner FOREIGN KEY (owner_id) REFERENCES crm_users (id) ON DELETE SET NULL,
CONSTRAINT fk_crm_leads_creator FOREIGN KEY (created_by) REFERENCES crm_users (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Wer steht mit wem in Kontakt. Ungerichtet gedacht; Duplikate werden ueber
-- den Unique-Key auf dem sortierten Paar verhindert (siehe Backend).
CREATE TABLE IF NOT EXISTS crm_lead_connections (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
from_lead_id INT UNSIGNED NOT NULL,
to_lead_id INT UNSIGNED NOT NULL,
type ENUM('Kollege','Vorgesetzter','Kunde','Partner','Empfehlung','Netzwerk','Sonstige')
NOT NULL DEFAULT 'Netzwerk',
label VARCHAR(190) NULL,
strength TINYINT UNSIGNED NOT NULL DEFAULT 2, -- 1 = lose, 2 = normal, 3 = eng
created_by INT UNSIGNED NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_crm_conn_pair (from_lead_id, to_lead_id),
KEY idx_crm_conn_to (to_lead_id),
CONSTRAINT fk_crm_conn_from FOREIGN KEY (from_lead_id) REFERENCES crm_leads (id) ON DELETE CASCADE,
CONSTRAINT fk_crm_conn_to FOREIGN KEY (to_lead_id) REFERENCES crm_leads (id) ON DELETE CASCADE,
CONSTRAINT fk_crm_conn_user FOREIGN KEY (created_by) REFERENCES crm_users (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Vertriebschancen. Ansprechpartner haengen als n:m an den Kontakten, damit ein
-- Kontakt in mehreren Opportunities auftauchen kann.
CREATE TABLE IF NOT EXISTS crm_opportunities (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(190) NOT NULL,
company VARCHAR(190) NULL,
description TEXT NULL,
stage ENUM('Neu','Qualifizierung','Angebot','Verhandlung','Gewonnen','Verloren')
NOT NULL DEFAULT 'Neu',
probability TINYINT UNSIGNED NOT NULL DEFAULT 10, -- 0-100 %
daily_rate DECIMAL(10,2) NULL, -- Tagessatz in Euro
duration_days DECIMAL(6,1) NULL, -- Personentage, halbe moeglich
start_date DATE NULL,
end_date DATE NULL,
last_interaction_at DATE NULL,
last_interaction_note VARCHAR(500) NULL,
next_step VARCHAR(500) NULL,
tags LONGTEXT NULL, -- JSON-Array als Text, wie bei crm_leads
notes TEXT 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,
PRIMARY KEY (id),
KEY idx_crm_opp_stage (stage),
KEY idx_crm_opp_owner (owner_id),
KEY idx_crm_opp_interaction (last_interaction_at),
CONSTRAINT fk_crm_opp_owner FOREIGN KEY (owner_id) REFERENCES crm_users (id) ON DELETE SET NULL,
CONSTRAINT fk_crm_opp_creator FOREIGN KEY (created_by) REFERENCES crm_users (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS crm_opportunity_contacts (
opportunity_id INT UNSIGNED NOT NULL,
lead_id INT UNSIGNED NOT NULL,
role VARCHAR(120) NULL, -- z. B. "Entscheider", "Fachbereich"
PRIMARY KEY (opportunity_id, lead_id),
KEY idx_crm_opp_contacts_lead (lead_id),
CONSTRAINT fk_crm_oc_opp FOREIGN KEY (opportunity_id) REFERENCES crm_opportunities (id) ON DELETE CASCADE,
CONSTRAINT fk_crm_oc_lead FOREIGN KEY (lead_id) REFERENCES crm_leads (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Gemeinsamer Teamkalender: alle Benutzer sehen alle Termine.
-- description darf Mention-Tokens der Form @[Anzeigename](lead:12) bzw.
-- @[Anzeigename](user:3) enthalten - das Frontend rendert sie klickbar.
CREATE TABLE IF NOT EXISTS crm_events (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(190) NOT NULL,
description TEXT NULL,
location VARCHAR(190) NULL,
starts_at DATETIME NOT NULL,
ends_at DATETIME NOT NULL,
all_day TINYINT(1) NOT NULL DEFAULT 0,
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,
PRIMARY KEY (id),
KEY idx_crm_events_start (starts_at),
KEY idx_crm_events_creator (created_by),
CONSTRAINT fk_crm_events_creator FOREIGN KEY (created_by) REFERENCES crm_users (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Ein Termin kann mehrere Teilnehmer haben; der Primaerschluessel verhindert
-- Doppeleintraege.
CREATE TABLE IF NOT EXISTS crm_event_participants (
event_id INT UNSIGNED NOT NULL,
user_id INT UNSIGNED NOT NULL,
PRIMARY KEY (event_id, user_id),
KEY idx_crm_event_participants_user (user_id),
CONSTRAINT fk_crm_ep_event FOREIGN KEY (event_id) REFERENCES crm_events (id) ON DELETE CASCADE,
CONSTRAINT fk_crm_ep_user FOREIGN KEY (user_id) REFERENCES crm_users (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;