Umsetzung des "Website Masterplan" (16 Seiten) unter /webdesign. Zugangsschutz mit EIGENER Schranke (server/webdesign-gate.js) statt gate.js: gate.js laesst seit dem oeffentlichen Start am 21.08.2026 jeden durch, weil die Pruefung auf SITE_PUBLIC_LAUNCH_AT als allererste Zeile steht. Haette man /webdesign dahintergehaengt, waere der ausdruecklich nicht-oeffentliche Bereich inklusive Preisen und spaeteren Kundendaten ab der ersten Sekunde fuer jeden lesbar gewesen. Eigenes Sitzungs-Cookie, bereich="webdesign" im Token, damit ein gueltiges Universe-Cookie hier NICHT gilt. 37/37 Tests. Sieben oeffentliche Seiten in fuenf Sprachen (de, de-CH mit echtem Dialekt, en, fr, pt). Preise, Zeitrahmen, Paketnamen und die 30-%-Regel stehen an genau EINER Stelle in wd-core.js -- der Masterplan verlangt "ueberall widerspruchsfrei", und vier Kopien laufen bei der ersten Preisaenderung auseinander. Als App installierbar auf Handy und PC. Der Service Worker speichert bewusst KEINE HTML-Seite zwischen: nach dem Abmelden wuerden sonst geschuetzte Seiten weiter ausgeliefert, ohne dass der Server je gefragt wird. 18/18 Tests. Handy-Abnahme ueber alle Seiten in fuenf Breiten (320-1440) und fuenf Sprachen: 40/40. Der Test fand 35 echte Fehler (Touch-Ziele unter 44px), behoben im Designsystem statt einzeln pro Seite. PayPal (Wunsch 22.08.2026 "sofort auf meinem paypal"): Orders API mit intent=CAPTURE, also sofortiger Einzug statt blosser Reservierung. Gebuehr und Nettobetrag getrennt gespeichert. Betraege durchgehend als Ganzzahl in Cent. Gefaelschte Webhooks werden abgewiesen. Fail closed solange Zugangsdaten fehlen. 28/28 Tests gegen einen nachgebauten PayPal-Server. Datenbank: 15 Tabellen mit Praefix wd_, fachlich vollstaendig vom Universe getrennt. Co-Authored-By: Claude Opus 5 <[email protected]>
322 lines
15 KiB
SQL
322 lines
15 KiB
SQL
-- =====================================================================
|
|
-- 0011_webdesign.sql — Datenbank für den Bereich /webdesign
|
|
-- (Masterplan S.10 Lead-System, S.12 Kundenportal, S.13 Verwaltung)
|
|
--
|
|
-- Warum alles mit dem Präfix "wd_":
|
|
-- Diese Datenbank trägt bereits das Universe (Team, Bewerbungen,
|
|
-- Supporter, Abstimmungen, Testimonials). Der Webdesign-Bereich ist ein
|
|
-- eigenes Geschäft mit eigenen Kunden — ohne Präfix wäre in einem Jahr
|
|
-- nicht mehr erkennbar, welche "users"-Tabelle wozu gehört, und ein
|
|
-- versehentlicher JOIN über zwei fachlich getrennte Welten ist der
|
|
-- Anfang jedes Datenlecks.
|
|
--
|
|
-- Grundsätze:
|
|
-- - Löschen wird vermieden. Kritische Datensätze werden archiviert
|
|
-- (Masterplan S.13: "Kritische Löschungen werden zunächst archiviert").
|
|
-- - Jede Tabelle mit Kundenbezug hat eine kunde_id, über die serverseitig
|
|
-- gefiltert wird. Ohne diese Spalte gibt es keine saubere Trennung.
|
|
-- - Zeitstempel immer als ISO-8601-Text in UTC (wie im restlichen
|
|
-- Projekt), damit Sortierung und Vergleich rein textuell funktionieren.
|
|
-- =====================================================================
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- ANFRAGEN (Masterplan S.10)
|
|
-- Entsteht durch das öffentliche Formular. Wird NICHT gelöscht, sondern
|
|
-- archiviert — eine Anfrage ist auch dann noch belegrelevant, wenn daraus
|
|
-- nichts wurde.
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS wd_anfragen (
|
|
id TEXT PRIMARY KEY,
|
|
-- Kurze, sprechende Nummer für die Kommunikation ("A-2608-0001").
|
|
nummer TEXT NOT NULL UNIQUE,
|
|
|
|
-- Kontakt
|
|
name TEXT NOT NULL,
|
|
email TEXT NOT NULL,
|
|
telefon TEXT,
|
|
kontaktweg TEXT NOT NULL DEFAULT 'email', -- email | telefon
|
|
|
|
-- Projekt
|
|
paket TEXT NOT NULL, -- onepager | website | shop | verwaltung | betreuung | unklar
|
|
ziel TEXT, -- Was soll die Seite erreichen
|
|
zielgruppe TEXT,
|
|
wirkung TEXT,
|
|
seiten TEXT, -- gewünschte Seiten/Bereiche
|
|
funktionen TEXT, -- JSON-Array angekreuzter Funktionen
|
|
-- Kategoriespezifische Folgefragen als JSON, damit neue Fragen keine
|
|
-- Schemaänderung brauchen (Masterplan S.10 "Intelligente Felder").
|
|
details TEXT,
|
|
|
|
-- Vorhandene Inhalte
|
|
hat_texte TEXT, -- ja | teilweise | nein
|
|
hat_bilder TEXT,
|
|
hat_logo TEXT,
|
|
hat_domain TEXT,
|
|
|
|
-- Rahmen
|
|
budget TEXT,
|
|
termin TEXT,
|
|
bemerkung TEXT,
|
|
|
|
-- Verwaltung
|
|
status TEXT NOT NULL DEFAULT 'neu', -- neu | pruefung | rueckfrage | angebot | angenommen | abgeschlossen | abgelehnt
|
|
archiviert INTEGER NOT NULL DEFAULT 0,
|
|
kunde_id TEXT, -- gesetzt, sobald daraus ein Kunde wurde
|
|
sprache TEXT NOT NULL DEFAULT 'de', -- in welcher Sprache angefragt wurde -> so wird geantwortet
|
|
|
|
-- Nachvollziehbarkeit
|
|
erstellt_am TEXT NOT NULL,
|
|
aktualisiert_am TEXT,
|
|
ip_hash TEXT, -- nur Hash, nie die IP selbst (Datensparsamkeit)
|
|
|
|
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE SET NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_anfragen_status ON wd_anfragen(status, archiviert);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_anfragen_erstellt ON wd_anfragen(erstellt_am DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_anfragen_email ON wd_anfragen(email);
|
|
|
|
-- Interne Notizen und Antworten an der Anfrage (Masterplan S.10).
|
|
CREATE TABLE IF NOT EXISTS wd_anfrage_notizen (
|
|
id TEXT PRIMARY KEY,
|
|
anfrage_id TEXT NOT NULL,
|
|
autor TEXT NOT NULL, -- dogfather | vanvan
|
|
text TEXT NOT NULL,
|
|
intern INTEGER NOT NULL DEFAULT 1, -- 1 = nur intern sichtbar
|
|
erstellt_am TEXT NOT NULL,
|
|
FOREIGN KEY (anfrage_id) REFERENCES wd_anfragen(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_anfrage_notizen ON wd_anfrage_notizen(anfrage_id, erstellt_am);
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- KUNDEN (Masterplan S.12)
|
|
-- Ein Kunde entsteht erst, wenn ein Projekt gekauft oder eine Betreuung
|
|
-- vereinbart wurde. Registrierung allein reicht ausdrücklich nicht:
|
|
-- "Projektkauf oder laufende Betreuung wird vor Freischaltung geprüft".
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS wd_kunden (
|
|
id TEXT PRIMARY KEY,
|
|
email TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
firma TEXT,
|
|
telefon TEXT,
|
|
sprache TEXT NOT NULL DEFAULT 'de',
|
|
|
|
-- Anmeldung
|
|
passwort_hash TEXT, -- NULL = noch kein Passwort gesetzt
|
|
email_bestaetigt INTEGER NOT NULL DEFAULT 0,
|
|
-- Freischaltung ist bewusst getrennt von der E-Mail-Bestätigung:
|
|
-- bestätigte E-Mail heißt nur "die Adresse gehört dir", nicht
|
|
-- "du darfst hier hinein".
|
|
freigeschaltet INTEGER NOT NULL DEFAULT 0,
|
|
gesperrt INTEGER NOT NULL DEFAULT 0,
|
|
gesperrt_grund TEXT,
|
|
|
|
-- Betreuung
|
|
betreuung_aktiv INTEGER NOT NULL DEFAULT 0,
|
|
betreuung_paket TEXT,
|
|
betreuung_start TEXT,
|
|
betreuung_ende TEXT,
|
|
|
|
benachrichtigungen INTEGER NOT NULL DEFAULT 1,
|
|
erstellt_am TEXT NOT NULL,
|
|
aktualisiert_am TEXT,
|
|
letzter_login TEXT,
|
|
archiviert INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_kunden_email ON wd_kunden(email);
|
|
|
|
-- Einmal-Kennungen für E-Mail-Bestätigung, Einladung und Passwort-Reset.
|
|
-- Getrennt von der Kundentabelle, damit ein Kunde mehrere offene Vorgänge
|
|
-- haben kann und abgelaufene Kennungen einfach wegzuräumen sind.
|
|
CREATE TABLE IF NOT EXISTS wd_kunden_token (
|
|
token_hash TEXT PRIMARY KEY, -- nur der Hash, nie das Token selbst
|
|
kunde_id TEXT NOT NULL,
|
|
zweck TEXT NOT NULL, -- bestaetigung | einladung | passwort
|
|
laeuft_ab TEXT NOT NULL,
|
|
benutzt_am TEXT,
|
|
erstellt_am TEXT NOT NULL,
|
|
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_kunden_token_kunde ON wd_kunden_token(kunde_id, zweck);
|
|
|
|
-- Aktive Anmeldungen. Serverseitig gespeichert, damit sich Sitzungen im
|
|
-- Verwaltungsbereich gezielt beenden lassen (Masterplan S.13:
|
|
-- "Aktive Sitzungen abmelden").
|
|
CREATE TABLE IF NOT EXISTS wd_kunden_sitzungen (
|
|
token_hash TEXT PRIMARY KEY,
|
|
kunde_id TEXT NOT NULL,
|
|
erstellt_am TEXT NOT NULL,
|
|
laeuft_ab TEXT NOT NULL,
|
|
letzte_aktion TEXT,
|
|
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_sitzungen_kunde ON wd_kunden_sitzungen(kunde_id);
|
|
|
|
-- Fehlversuche bei der Kundenanmeldung (Sperre nach zu vielen Versuchen).
|
|
CREATE TABLE IF NOT EXISTS wd_login_versuche (
|
|
schluessel TEXT PRIMARY KEY, -- E-Mail in Kleinschreibung
|
|
anzahl INTEGER NOT NULL DEFAULT 0,
|
|
gesperrt_bis TEXT,
|
|
zuletzt TEXT NOT NULL
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- PROJEKTE (Masterplan S.12/S.13)
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS wd_projekte (
|
|
id TEXT PRIMARY KEY,
|
|
nummer TEXT NOT NULL UNIQUE, -- "P-2608-0001"
|
|
kunde_id TEXT NOT NULL,
|
|
anfrage_id TEXT,
|
|
|
|
titel TEXT NOT NULL,
|
|
paket TEXT NOT NULL,
|
|
beschreibung TEXT,
|
|
|
|
status TEXT NOT NULL DEFAULT 'briefing', -- briefing | angebot | design | entwicklung | tests | abnahme | uebergeben | pausiert | abgebrochen
|
|
naechster_schritt TEXT,
|
|
wartet_auf TEXT, -- kunde | dogfather -> wer gerade am Zug ist
|
|
|
|
-- Geld. In CENT als Ganzzahl, nie als Kommazahl: 0.1 + 0.2 ergibt in
|
|
-- Gleitkomma nicht 0.3, und bei Rechnungsbeträgen ist das inakzeptabel.
|
|
preis_cent INTEGER,
|
|
anzahlung_cent INTEGER,
|
|
anzahlung_bezahlt INTEGER NOT NULL DEFAULT 0,
|
|
rest_bezahlt INTEGER NOT NULL DEFAULT 0,
|
|
waehrung TEXT NOT NULL DEFAULT 'EUR',
|
|
|
|
start_am TEXT,
|
|
richttermin TEXT,
|
|
uebergabe_am TEXT,
|
|
|
|
-- Portfoliofreigabe ist bewusst ein EIGENES Feld, getrennt von der
|
|
-- Veröffentlichung der Kundenseite (Masterplan S.13: "Portfoliofreigabe
|
|
-- separat dokumentieren"). Beides darf nie aneinander hängen.
|
|
portfolio_freigabe INTEGER NOT NULL DEFAULT 0,
|
|
portfolio_freigabe_am TEXT,
|
|
|
|
archiviert INTEGER NOT NULL DEFAULT 0,
|
|
erstellt_am TEXT NOT NULL,
|
|
aktualisiert_am TEXT,
|
|
|
|
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (anfrage_id) REFERENCES wd_anfragen(id) ON DELETE SET NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_projekte_kunde ON wd_projekte(kunde_id, archiviert);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_projekte_status ON wd_projekte(status);
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- DATEIEN (Masterplan S.12: Versionen und Zeitstempel)
|
|
-- Die Datei selbst liegt im Dateisystem neben der Datenbank, hier steht
|
|
-- nur der Verweis. Der gespeicherte Name ist eine zufällige Kennung —
|
|
-- ein hochgeladener Originalname darf nie in einen Pfad wandern.
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS wd_dateien (
|
|
id TEXT PRIMARY KEY,
|
|
projekt_id TEXT,
|
|
kunde_id TEXT NOT NULL,
|
|
anfrage_id TEXT,
|
|
|
|
anzeigename TEXT NOT NULL, -- was der Mensch sieht
|
|
speichername TEXT NOT NULL, -- zufällig, im Dateisystem
|
|
typ TEXT NOT NULL, -- MIME-Typ
|
|
groesse INTEGER NOT NULL,
|
|
|
|
kategorie TEXT NOT NULL DEFAULT 'sonstiges', -- logo | bild | text | angebot | freigabe | sonstiges
|
|
version INTEGER NOT NULL DEFAULT 1,
|
|
ersetzt_id TEXT, -- Vorgängerversion
|
|
hochgeladen_von TEXT NOT NULL, -- kunde | dogfather | vanvan
|
|
-- Kunden dürfen nur sehen, was für sie bestimmt ist. Interne Arbeitsdateien
|
|
-- bleiben damit unsichtbar, ohne sie in einer zweiten Tabelle zu führen.
|
|
fuer_kunde INTEGER NOT NULL DEFAULT 1,
|
|
|
|
erstellt_am TEXT NOT NULL,
|
|
geloescht INTEGER NOT NULL DEFAULT 0,
|
|
|
|
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (projekt_id) REFERENCES wd_projekte(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_dateien_projekt ON wd_dateien(projekt_id, geloescht);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_dateien_kunde ON wd_dateien(kunde_id, geloescht);
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- NACHRICHTEN am Projekt (Masterplan S.12: "projektbezogene Kommunikation
|
|
-- bleibt vollständig nachvollziehbar")
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS wd_nachrichten (
|
|
id TEXT PRIMARY KEY,
|
|
projekt_id TEXT NOT NULL,
|
|
kunde_id TEXT NOT NULL,
|
|
autor TEXT NOT NULL, -- kunde | dogfather | vanvan
|
|
text TEXT NOT NULL,
|
|
intern INTEGER NOT NULL DEFAULT 0, -- 1 = Kunde sieht es NICHT
|
|
gelesen_kunde INTEGER NOT NULL DEFAULT 0,
|
|
gelesen_admin INTEGER NOT NULL DEFAULT 0,
|
|
erstellt_am TEXT NOT NULL,
|
|
FOREIGN KEY (projekt_id) REFERENCES wd_projekte(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_nachrichten_projekt ON wd_nachrichten(projekt_id, erstellt_am);
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- ÄNDERUNGSWÜNSCHE / ZUSATZANGEBOTE (Masterplan S.12)
|
|
-- Der Kunde meldet eine Idee an, Dogfather beziffert Aufwand und Preis,
|
|
-- der Kunde nimmt an oder lehnt ab. Ein eigener Datensatz statt einer
|
|
-- Nachricht, damit der Zustand eindeutig ist und nichts im Verlauf untergeht.
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS wd_aenderungen (
|
|
id TEXT PRIMARY KEY,
|
|
projekt_id TEXT NOT NULL,
|
|
kunde_id TEXT NOT NULL,
|
|
|
|
titel TEXT NOT NULL,
|
|
beschreibung TEXT NOT NULL,
|
|
|
|
status TEXT NOT NULL DEFAULT 'eingereicht', -- eingereicht | geprueft | angeboten | angenommen | abgelehnt | umgesetzt
|
|
aufwand_text TEXT,
|
|
zusatzpreis_cent INTEGER,
|
|
zusatzdauer_text TEXT,
|
|
antwort TEXT,
|
|
|
|
entschieden_am TEXT,
|
|
erstellt_am TEXT NOT NULL,
|
|
aktualisiert_am TEXT,
|
|
|
|
FOREIGN KEY (projekt_id) REFERENCES wd_projekte(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_aenderungen_projekt ON wd_aenderungen(projekt_id, status);
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- INHALTE, DIE IN DER VERWALTUNG PFLEGBAR SIND (Masterplan S.13)
|
|
-- Ein einfacher Schlüssel-Wert-Speicher mit mehrsprachigem JSON-Wert.
|
|
-- Bewusst kein eigenes Feld je Text: sonst braucht jeder neue Satz auf der
|
|
-- Seite eine Datenbankänderung.
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS wd_inhalte (
|
|
schluessel TEXT PRIMARY KEY,
|
|
wert_json TEXT NOT NULL, -- {"de":"...","de-CH":"...","en":"...","fr":"...","pt":"..."}
|
|
bereich TEXT, -- zur Gruppierung in der Oberfläche
|
|
aktualisiert_am TEXT NOT NULL,
|
|
aktualisiert_von TEXT
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- ÄNDERUNGSVERLAUF (Masterplan S.13: "Änderungsverlauf und verantwortliche
|
|
-- Person protokollieren")
|
|
-- Absichtlich eine eigene Tabelle statt Spalten in jeder Tabelle: so ist
|
|
-- der Verlauf an einer Stelle lesbar und überlebt auch das Archivieren
|
|
-- des Datensatzes, auf den er sich bezieht.
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS wd_verlauf (
|
|
id TEXT PRIMARY KEY,
|
|
bereich TEXT NOT NULL, -- anfrage | kunde | projekt | inhalt | zugang | datei
|
|
objekt_id TEXT,
|
|
aktion TEXT NOT NULL, -- angelegt | geaendert | archiviert | freigegeben | ...
|
|
wer TEXT NOT NULL, -- dogfather | vanvan | kunde:<id> | system
|
|
details TEXT, -- JSON: was genau sich geändert hat
|
|
erstellt_am TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_verlauf_objekt ON wd_verlauf(bereich, objekt_id, erstellt_am DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_verlauf_zeit ON wd_verlauf(erstellt_am DESC);
|