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:
@@ -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;
|
||||
Reference in New Issue
Block a user