Aufgabenlisten und allgemeines Postfach: Datenbank und Vorlagen

Erster von vier Schritten fuer zwei Wuensche vom 22.08.2026:
"wenn sie drauf druecken sollen sie auch sehen was ich schon gemacht habe
von dem was im plan war" und "die kunden sollen mir auch nachrichten
hinterlassen koennen".

DATENBANK (3 neue Tabellen, jetzt 18 insgesamt, Fremdschluessel geprueft)

wd_aufgaben -- die Punkte je Projekt. Drei Entscheidungen darin:
  * "wer_dran" ist eine eigene Spalte, kein Text. Manche Punkte warten
    auf den KUNDEN ("Texte liefern"), und genau die muessen ihm ins Auge
    springen. Ohne diese Unterscheidung liest er die Liste als reinen
    Fortschrittsbericht und uebersieht seinen eigenen Teil -- der
    haeufigste Grund fuer Verzoegerungen ueberhaupt.
  * "nicht_enthalten" bildet ab, was NICHT zum Umfang gehoert. Das ist
    die haeufigste Streitfrage in jedem Projekt ("ich dachte, das ist
    dabei"). Vorher sichtbar aufgeschrieben kostet es nichts, hinterher
    kostet es Geld oder den Kunden.
  * "kategorie" nutzt dieselben vier Abschnitte wie die Farben im Portal.
    Damit bedeutet eine Farbe ueberall dasselbe statt nur huebsch zu sein.

wd_postfach -- Nachrichten OHNE Projektbezug. Bewusst eine EIGENE Tabelle:
wd_nachrichten hat projekt_id als NOT NULL, und SQLite kann das nicht
lockern, ohne die ganze Tabelle zu kopieren -- ein unnoetiges Risiko bei
echten Kundennachrichten. Die neue Tabelle hat ohnehin andere
Anforderungen (Anhaenge, Lesestatus in BEIDE Richtungen, optionaler Bezug
auf eine Aufgabe). Zwei klar getrennte Tabellen sind ehrlicher als eine,
die beides halb kann.

VORLAGEN (56 Punkte ueber 5 Pakete, davon 20 beim Kunden)

Bei jedem Projekt fuenfzehn Punkte von Hand einzutippen fuehrt
zuverlaessig dazu, dass es irgendwann niemand mehr macht -- und dann
steht der Kunde wieder vor der leeren Liste, die der Ausloeser war.

Die Punkte sind in der Sprache formuliert, in der ein KUNDE denkt:
"Aufbau der Seiten festlegen" statt "Informationsarchitektur", "Auf dem
Handy durchtesten" statt "Responsive QA". Er liest diese Liste -- sie
muss ihm etwas sagen, nicht mir.

Die Vorlagen wandern beim ersten Start in die Datenbank und sind ab dann
dort aenderbar. Bereits vorhandene werden NIE ueberschrieben: sonst waeren
von Hand angepasste Vorlagen nach dem naechsten Neustart weg, und niemand
kaeme auf die Idee, dass der Neustart schuld war.

Naechste Schritte: Server-Routen, Verwaltung, Portal.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-22 22:48:39 +02:00
co-authored by Claude Opus 5
parent 71b9fc09c1
commit 548ec6bc00
3 changed files with 340 additions and 0 deletions
@@ -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);
+5
View File
@@ -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) {
+177
View File
@@ -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;
}