-- ===================================================================== -- 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: | 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);