diff --git a/cloudflare-worker/migrations/0013_webdesign_aufgaben.sql b/cloudflare-worker/migrations/0013_webdesign_aufgaben.sql new file mode 100644 index 00000000..6f24e732 --- /dev/null +++ b/cloudflare-worker/migrations/0013_webdesign_aufgaben.sql @@ -0,0 +1,158 @@ +-- ===================================================================== +-- 0013_webdesign_aufgaben.sql +-- +-- Zwei Erweiterungen, bewusst in EINER Migration — beide brauchen +-- denselben Neustart, und zwei Deploy-Runden für zusammengehörende +-- Funktionen sind vermeidbare Arbeit für Filipe. +-- +-- 1. Aufgabenliste je Projekt ("was ist schon gemacht von dem, was im +-- Plan war") +-- 2. Nachrichten OHNE Projektbezug ("Kunden sollen mir auch +-- Kleinigkeiten schreiben können") +-- +-- Wunsch vom 22.08.2026. +-- ===================================================================== + +-- --------------------------------------------------------------------- +-- 1. AUFGABEN JE PROJEKT +-- +-- Heute sieht der Kunde nur die Phase ("Design · 3/7"). Er sieht NICHT, +-- was innerhalb dieser Phase schon erledigt ist — und genau daraus +-- entstehen die meisten Rückfragen ("wie weit seid ihr denn?"). +-- +-- Drei Entscheidungen, die hier drinstecken: +-- +-- * "wer_dran" ist eine eigene Spalte, kein Textfeld. Manche Punkte +-- warten auf den KUNDEN ("Texte liefern"), und genau die müssen ihm +-- ins Auge springen. Ohne diese Unterscheidung liest er die Liste +-- als reinen Fortschrittsbericht und übersieht seinen eigenen Teil. +-- +-- * "nicht_enthalten" bildet ab, was NICHT zum Umfang gehört. Das ist +-- die häufigste Streitfrage in jedem Projekt ("ich dachte, das ist +-- dabei"). Sichtbar aufgeschrieben, bevor es strittig wird, kostet +-- es nichts — hinterher kostet es Geld oder den Kunden. +-- +-- * "kategorie" nutzt dieselben vier Abschnitte wie die Farben im +-- Portal (Start / Gestaltung / Umsetzung / Abschluss). Damit +-- bedeutet eine Farbe überall dasselbe, statt nur hübsch zu sein. +-- --------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS wd_aufgaben ( + id TEXT PRIMARY KEY, + projekt_id TEXT NOT NULL, + kunde_id TEXT NOT NULL, + + titel TEXT NOT NULL, + beschreibung TEXT, + + -- start | gestaltung | umsetzung | abschluss (die vier Prozessfarben) + kategorie TEXT NOT NULL DEFAULT 'umsetzung', + + -- offen | in_arbeit | erledigt | entfaellt + status TEXT NOT NULL DEFAULT 'offen', + + -- dogfather | kunde — wer diesen Punkt erledigen muss + wer_dran TEXT NOT NULL DEFAULT 'dogfather', + + /* Punkte, die ausdrücklich NICHT zum Umfang gehören. Sie stehen + trotzdem in der Liste, aber deutlich als "nicht enthalten" + gekennzeichnet — mit einem Hinweis, was es kosten würde. */ + nicht_enthalten INTEGER NOT NULL DEFAULT 0, + hinweis TEXT, + + -- Reihenfolge in der Anzeige. Lücken (10, 20, 30) lassen Platz zum + -- Einschieben, ohne alle folgenden Zeilen neu zu nummerieren. + reihenfolge INTEGER NOT NULL DEFAULT 100, + + erledigt_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_aufgaben_projekt ON wd_aufgaben(projekt_id, reihenfolge); +CREATE INDEX IF NOT EXISTS idx_wd_aufgaben_status ON wd_aufgaben(projekt_id, status); +CREATE INDEX IF NOT EXISTS idx_wd_aufgaben_kunde ON wd_aufgaben(kunde_id, wer_dran, status); + +-- --------------------------------------------------------------------- +-- 2. NACHRICHTEN OHNE PROJEKTBEZUG +-- +-- wd_nachrichten hat projekt_id als NOT NULL. Das war richtig, solange +-- jede Nachricht zu einem Projekt gehörte — es verhindert verwaiste +-- Einträge. Es bedeutet aber auch: Wer noch kein Projekt hat, kann gar +-- nicht schreiben. Genau das ist aufgefallen. +-- +-- SQLite kann eine NOT-NULL-Beschränkung nicht einfach lockern; dafür +-- müsste die ganze Tabelle kopiert werden. Das wäre bei einer Tabelle +-- mit echten Kundennachrichten ein unnötiges Risiko. +-- +-- Deshalb eine EIGENE Tabelle für den allgemeinen Kanal. Sie hat +-- ohnehin andere Anforderungen: Anhänge, Lesestatus in beide Richtungen, +-- und optional ein Bezug auf eine Aufgabe ("zu diesem Punkt habe ich +-- eine Frage"). Zwei klar getrennte Tabellen sind hier ehrlicher als +-- eine, die beides halb kann. +-- --------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS wd_postfach ( + id TEXT PRIMARY KEY, + kunde_id TEXT NOT NULL, + + -- kunde | dogfather | vanvan + autor TEXT NOT NULL, + text TEXT NOT NULL, + + /* Optionaler Bezug. Eine Nachricht kann sich auf ein Projekt ODER auf + einen einzelnen Aufgabenpunkt beziehen — muss aber nicht. Beides + darf leer sein, das ist der ganze Zweck dieser Tabelle. */ + projekt_id TEXT, + aufgabe_id TEXT, + + /* Anhang: "Kleinigkeiten" sind fast immer Screenshots. Die Datei + selbst liegt neben der Datenbank, hier steht nur der Verweis — + gleiches Muster wie bei wd_dateien. */ + datei_id TEXT, + + /* Lesestatus getrennt für beide Seiten. Eine einzige "gelesen"-Spalte + könnte nicht beantworten, ob der KUNDE die Antwort schon gesehen + hat — und genau das will man wissen, bevor man nachhakt. */ + gelesen_kunde INTEGER NOT NULL DEFAULT 0, + gelesen_admin INTEGER NOT NULL DEFAULT 0, + + /* Interne Bemerkungen im selben Verlauf. Die Trennung ist heikel: + eine versehentlich sichtbare interne Notiz ist der peinlichste + Fehler in so einem System. Deshalb Vorgabe 0 (= sichtbar) und die + Abfragen im Portal filtern immer ausdrücklich auf intern = 0. */ + intern INTEGER NOT NULL DEFAULT 0, + + erstellt_am TEXT NOT NULL, + + FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE, + FOREIGN KEY (projekt_id) REFERENCES wd_projekte(id) ON DELETE SET NULL, + FOREIGN KEY (aufgabe_id) REFERENCES wd_aufgaben(id) ON DELETE SET NULL, + FOREIGN KEY (datei_id) REFERENCES wd_dateien(id) ON DELETE SET NULL +); +CREATE INDEX IF NOT EXISTS idx_wd_postfach_kunde ON wd_postfach(kunde_id, erstellt_am DESC); +CREATE INDEX IF NOT EXISTS idx_wd_postfach_offen ON wd_postfach(gelesen_admin, erstellt_am DESC); + +-- --------------------------------------------------------------------- +-- 3. VORLAGEN FÜR AUFGABENLISTEN +-- +-- Bei jedem neuen Projekt dieselben fünfzehn Punkte von Hand einzutippen +-- führt zuverlässig dazu, dass es irgendwann niemand mehr macht — und +-- dann steht der Kunde wieder vor einer leeren Liste. +-- +-- Deshalb: Vorlagen je Paket, die beim Anlegen eines Projekts +-- automatisch übernommen werden. Sie sind änderbar, damit sie mit der +-- Erfahrung wachsen können, statt im Code zu versteinern. +-- --------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS wd_aufgaben_vorlagen ( + id TEXT PRIMARY KEY, + paket TEXT NOT NULL, -- onepager | website | shop | verwaltung | betreuung + titel TEXT NOT NULL, + beschreibung TEXT, + kategorie TEXT NOT NULL DEFAULT 'umsetzung', + wer_dran TEXT NOT NULL DEFAULT 'dogfather', + reihenfolge INTEGER NOT NULL DEFAULT 100, + aktiv INTEGER NOT NULL DEFAULT 1, + erstellt_am TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_wd_vorlagen_paket ON wd_aufgaben_vorlagen(paket, reihenfolge); diff --git a/server-internal/db.js b/server-internal/db.js index ec721c00..2370740c 100644 --- a/server-internal/db.js +++ b/server-internal/db.js @@ -43,6 +43,11 @@ const MIGRATION_FILES = [ // fuer Anzahlung/Restbetrag/Zusatzangebote, PayPal-Abo fuer die // monatliche Betreuung, Webhook-Merkliste gegen Doppelverbuchung. "0012_webdesign_zahlungen.sql", + // Aufgabenliste je Projekt ("was ist schon gemacht von dem, was im Plan + // war") und ein allgemeines Postfach fuer Nachrichten OHNE Projektbezug + // (22.08.2026). Begruendung fuer die getrennte Postfach-Tabelle steht + // im Kopf der Migration. + "0013_webdesign_aufgaben.sql", ]; function alreadyApplied(name) { diff --git a/server-internal/lib/webdesign-vorlagen.js b/server-internal/lib/webdesign-vorlagen.js new file mode 100644 index 00000000..18f5bc82 --- /dev/null +++ b/server-internal/lib/webdesign-vorlagen.js @@ -0,0 +1,177 @@ +/* ===================================================================== + webdesign-vorlagen.js — Standard-Aufgabenlisten je Paket + + Warum es diese Datei gibt: Bei jedem neuen Projekt fünfzehn Punkte von + Hand einzutippen führt zuverlässig dazu, dass es irgendwann niemand + mehr macht — und dann steht der Kunde wieder vor einer leeren Liste. + Genau die Leere war ja der Auslöser ("wenn sie drauf drücken sollen + sie auch sehen was ich schon gemacht habe von dem was im plan war"). + + Die Listen stehen hier im Code, wandern beim ersten Start aber in die + Datenbank (wd_aufgaben_vorlagen). Ab dann sind sie dort änderbar. Der + Code ist also nur die Erstbefüllung, nicht die dauerhafte Wahrheit — + sonst müsste man für jede neue Erfahrung einen Deploy machen. + + Die Punkte sind bewusst in der Sprache formuliert, in der ein KUNDE + denkt, nicht in Fachsprache. "Aufbau der Seiten festlegen" statt + "Informationsarchitektur", "Auf dem Handy durchtesten" statt + "Responsive QA". Der Kunde liest diese Liste — sie muss ihm etwas + sagen, nicht mir. + + wer_dran ist der wichtigste Wert in jeder Zeile: Punkte, die auf den + Kunden warten, springen ihm im Portal farblich ins Auge. Ohne diese + Markierung liest er die Liste als reinen Fortschrittsbericht und + übersieht seinen eigenen Teil — der häufigste Grund für Verzögerungen. + ===================================================================== */ + +import { db } from "../db.js"; +import { jetzt, neueId } from "./webdesign-helfer.js"; + +/* Die vier Kategorien entsprechen exakt den vier Prozessfarben im + Portal: start (blau), gestaltung (lila), umsetzung (grün), + abschluss (gold). Eine Farbe bedeutet damit überall dasselbe. */ + +const GEMEINSAM_START = [ + ["Briefing-Gespräch führen", "Ziel, Zielgruppe und Umfang gemeinsam klären.", "start", "dogfather", 10], + ["Texte und Bilder zusammenstellen", "Alles, was auf die Seite soll. Lieber früh als vollständig — Lücken klären wir gemeinsam.", "start", "kunde", 20], + ["Logo und Farbwünsche liefern", "Falls vorhanden. Wenn nicht, entwerfen wir eine passende Richtung.", "start", "kunde", 30], + ["Angebot prüfen und annehmen", "Preis, Umfang und Zeitrahmen schriftlich.", "start", "kunde", 40], + ["Anzahlung 30 % überweisen", "Erst danach wird dein Zeitfenster fest reserviert.", "start", "kunde", 50], +]; + +const GEMEINSAM_ABSCHLUSS = [ + ["Alles auf Handy und Computer durchtesten", "Jedes Formular, jeder Link, jeder Fehlerfall.", "abschluss", "dogfather", 810], + ["Seite selbst durchklicken", "Am besten auf deinem eigenen Handy — du fällst über Dinge, die mir nicht auffallen.", "abschluss", "kunde", 820], + ["Rückmeldungen einarbeiten", "Gesammelt, nicht einzeln — das spart beiden Zeit.", "abschluss", "dogfather", 830], + ["Freigabe zur Veröffentlichung erteilen", "Ohne dein ausdrückliches Ja geht nichts online.", "abschluss", "kunde", 840], + ["Restbetrag begleichen", "Fällig nach der Abnahme, vor der Veröffentlichung.", "abschluss", "kunde", 850], + ["Seite live schalten", "Domain, Zertifikat, Suchmaschinen-Grundeinstellungen.", "abschluss", "dogfather", 860], + ["Einweisung: Inhalte selbst pflegen", "Damit du nicht für jede Kleinigkeit fragen musst.", "abschluss", "dogfather", 870], +]; + +export const VORLAGEN = { + onepager: [ + ...GEMEINSAM_START, + ["Aufbau der Seite festlegen", "Welche Abschnitte in welcher Reihenfolge.", "gestaltung", "dogfather", 110], + ["Entwurf gestalten", "Farben, Schriften, Bildsprache — du siehst ihn auf Handy und Computer.", "gestaltung", "dogfather", 120], + ["Entwurf ansehen und Rückmeldung geben", "Erste von üblicherweise zwei Runden.", "gestaltung", "kunde", 130], + ["Entwurf überarbeiten", "", "gestaltung", "dogfather", 140], + ["Seite bauen", "Mit deinen echten Inhalten, nicht mit Blindtext.", "umsetzung", "dogfather", 210], + ["Kontaktmöglichkeit einrichten", "Formular oder direkte Verlinkung.", "umsetzung", "dogfather", 220], + ["Für Suchmaschinen vorbereiten", "Titel, Beschreibung, saubere Überschriften.", "umsetzung", "dogfather", 230], + ...GEMEINSAM_ABSCHLUSS, + ], + + website: [ + ...GEMEINSAM_START, + ["Seitenstruktur festlegen", "Welche Unterseiten es gibt und wie man dorthin kommt.", "gestaltung", "dogfather", 110], + ["Entwurf der Startseite", "Die Startseite gibt den Ton für alle anderen vor.", "gestaltung", "dogfather", 120], + ["Entwurf ansehen und Rückmeldung geben", "Erste von üblicherweise zwei Runden.", "gestaltung", "kunde", 130], + ["Entwurf auf alle Seiten übertragen", "", "gestaltung", "dogfather", 140], + ["Zweite Rückmeldung", "Danach wird gebaut.", "gestaltung", "kunde", 150], + ["Alle Unterseiten bauen", "", "umsetzung", "dogfather", 210], + ["Navigation und Verlinkung", "Kein Weg soll in einer Sackgasse enden.", "umsetzung", "dogfather", 220], + ["Formulare einrichten und prüfen", "Anfragen müssen zuverlässig ankommen.", "umsetzung", "dogfather", 230], + ["Bilder optimieren", "Damit die Seite auch im Mobilfunk schnell lädt.", "umsetzung", "dogfather", 240], + ["Für Suchmaschinen vorbereiten", "Titel, Beschreibungen, Sitemap.", "umsetzung", "dogfather", 250], + ["Rechtstexte prüfen lassen", "Impressum und Datenschutz — die endgültige Prüfung gehört zu einer Fachperson.", "umsetzung", "kunde", 260], + ...GEMEINSAM_ABSCHLUSS, + ], + + shop: [ + ...GEMEINSAM_START, + ["Produktdaten liefern", "Bilder, Beschreibungen, Preise, Varianten.", "start", "kunde", 60], + ["Zahlungskonto einrichten", "Läuft auf deinen Namen — du bekommst das Geld direkt.", "start", "kunde", 70], + ["Aufbau von Shop und Kasse festlegen", "Wie ein Kunde vom Produkt zur Bestellung kommt.", "gestaltung", "dogfather", 110], + ["Entwurf gestalten", "Startseite, Produktansicht, Warenkorb.", "gestaltung", "dogfather", 120], + ["Entwurf ansehen und Rückmeldung geben", "", "gestaltung", "kunde", 130], + ["Shop bauen", "Kategorien, Produkte, Varianten.", "umsetzung", "dogfather", 210], + ["Warenkorb und Kasse einrichten", "", "umsetzung", "dogfather", 220], + ["Zahlungsarten anbinden und testen", "Mit echten Testbestellungen, nicht nur theoretisch.", "umsetzung", "dogfather", 230], + ["Versandregeln festlegen", "Kosten, Zonen, Versandkostenfrei-Grenze.", "umsetzung", "kunde", 240], + ["Adminbereich einrichten", "Damit du Artikel selbst anlegen kannst.", "umsetzung", "dogfather", 250], + ["Testbestellung durchführen", "Einmal komplett bis zur Bestätigungsmail.", "abschluss", "kunde", 805], + ...GEMEINSAM_ABSCHLUSS, + ["Einweisung in die Produktpflege", "Artikel anlegen, ändern, ausverkauft setzen.", "abschluss", "dogfather", 880], + ], + + verwaltung: [ + ...GEMEINSAM_START, + ["Benötigte Auswertungen festlegen", "Was muss am Jahresende herauskommen?", "start", "kunde", 60], + ["Rollen und Zugriffe festlegen", "Wer darf was sehen und ändern.", "gestaltung", "dogfather", 110], + ["Aufbau der Masken entwerfen", "Belege erfassen soll schnell gehen, nicht schön aussehen.", "gestaltung", "dogfather", 120], + ["Entwurf ansehen und Rückmeldung geben", "", "gestaltung", "kunde", 130], + ["Anmeldung und Rollen einrichten", "", "umsetzung", "dogfather", 210], + ["Erfassung von Belegen bauen", "Mit Foto-Upload vom Handy.", "umsetzung", "dogfather", 220], + ["Filter, Suche und Export bauen", "", "umsetzung", "dogfather", 230], + ["Sicherungen einrichten und zurückspielen testen", "Eine Sicherung, die nie getestet wurde, ist keine.", "umsetzung", "dogfather", 240], + ["Mit echten Belegen testen", "Ein paar Wochen aus der Praxis sagen mehr als jede Theorie.", "abschluss", "kunde", 805], + ...GEMEINSAM_ABSCHLUSS, + ], + + betreuung: [ + ["Zugänge übergeben und prüfen", "Domain, Hosting, Adminbereich.", "start", "dogfather", 10], + ["Sicherungen einrichten", "Regelmäßig und mit geprüfter Wiederherstellung.", "start", "dogfather", 20], + ["Überwachung der Erreichbarkeit einrichten", "Damit ein Ausfall auffällt, bevor du ihn bemerkst.", "start", "dogfather", 30], + ["Ansprechweg festlegen", "Wie du Änderungswünsche meldest.", "start", "kunde", 40], + ["Laufende Sicherheitsaktualisierungen", "Dauerhaft, ohne dass du etwas tun musst.", "umsetzung", "dogfather", 210], + ], +}; + +/* Vorlagen beim ersten Start in die Datenbank schreiben. + Läuft bei jedem Start, tut aber nur beim ersten Mal etwas — sind + bereits Vorlagen für ein Paket vorhanden, wird es übersprungen. + Wichtig: NICHT überschreiben. Sonst wären von Hand angepasste + Vorlagen nach dem nächsten Neustart wieder weg, und niemand käme auf + die Idee, dass der Neustart daran schuld war. */ +export function vorlagenSicherstellen() { + const vorhanden = db.prepare(`SELECT COUNT(*) AS n FROM wd_aufgaben_vorlagen`).get().n; + if (vorhanden > 0) return { angelegt: 0, uebersprungen: true }; + + const einfuegen = db.prepare( + `INSERT INTO wd_aufgaben_vorlagen + (id, paket, titel, beschreibung, kategorie, wer_dran, reihenfolge, aktiv, erstellt_am) + VALUES (?,?,?,?,?,?,?,1,?)` + ); + + let anzahl = 0; + db.transaction(() => { + for (const [paket, punkte] of Object.entries(VORLAGEN)) { + for (const [titel, beschreibung, kategorie, werDran, reihenfolge] of punkte) { + einfuegen.run(neueId(), paket, titel, beschreibung || null, kategorie, werDran, reihenfolge, jetzt()); + anzahl++; + } + } + })(); + + console.log(`[webdesign] ${anzahl} Aufgaben-Vorlagen angelegt.`); + return { angelegt: anzahl, uebersprungen: false }; +} + +/* Beim Anlegen eines Projekts die passende Vorlage übernehmen. + Gibt es für das Paket keine, bleibt die Liste leer — das ist besser + als falsche Punkte, die der Kunde dann nicht zuordnen kann. */ +export function aufgabenAusVorlage(projektId, kundeId, paket) { + const punkte = db + .prepare( + `SELECT titel, beschreibung, kategorie, wer_dran, reihenfolge + FROM wd_aufgaben_vorlagen WHERE paket = ? AND aktiv = 1 ORDER BY reihenfolge` + ) + .all(paket); + if (!punkte.length) return 0; + + const einfuegen = db.prepare( + `INSERT INTO wd_aufgaben + (id, projekt_id, kunde_id, titel, beschreibung, kategorie, status, wer_dran, reihenfolge, erstellt_am) + VALUES (?,?,?,?,?,?,'offen',?,?,?)` + ); + + db.transaction(() => { + for (const p of punkte) { + einfuegen.run(neueId(), projektId, kundeId, p.titel, p.beschreibung, + p.kategorie, p.wer_dran, p.reihenfolge, jetzt()); + } + })(); + + return punkte.length; +}