diff --git a/pruef-verwaltung.mjs b/pruef-verwaltung.mjs
index 2455054f..3991c162 100644
--- a/pruef-verwaltung.mjs
+++ b/pruef-verwaltung.mjs
@@ -32,8 +32,26 @@ const PROJEKT = {
preis_cent: 180000, waehrung: "EUR", richttermin: null, archiviert: 0,
anzahlung_bezahlt: 1, rest_bezahlt: 0, erstellt_am: new Date().toISOString(),
aufg_gesamt: 6, aufg_erledigt: 2, aufg_kunde: 2, ungelesen: 1,
+ naechster_schritt: "Deine Rueckmeldung zum Entwurf.", wartet_auf: "kunde",
+ kunde_email: "anna@example.com", portfolio_freigabe: 0,
};
+const AENDERUNGEN = [
+ { id: "ae1", titel: "Noch eine Unterseite", beschreibung: "Eine Seite fuer Referenzen waere schoen.",
+ status: "eingereicht", zusatzpreis_cent: null, zusatzdauer_text: null,
+ erstellt_am: new Date(Date.now() - 86400000).toISOString() },
+ { id: "ae2", titel: "Anderes Logo", beschreibung: "Wir haben ein neues.",
+ status: "angenommen", zusatzpreis_cent: 12000, zusatzdauer_text: "2 Tage",
+ erstellt_am: new Date(Date.now() - 172800000).toISOString() },
+];
+
+const PNACHRICHTEN = [
+ { id: "pn1", autor: "kunde", text: "Wann kommt der naechste Entwurf?",
+ erstellt_am: new Date(Date.now() - 7200000).toISOString() },
+ { id: "pn2", autor: "dogfather", text: "Morgen Abend.",
+ erstellt_am: new Date(Date.now() - 3600000).toISOString() },
+];
+
const AUFGABEN = [
{ id: "a1", titel: "Briefing-Gespräch führen", beschreibung: "Ziel und Umfang klären.",
kategorie: "start", status: "erledigt", wer_dran: "dogfather", nicht_enthalten: 0, hinweis: null, reihenfolge: 10 },
@@ -84,6 +102,13 @@ for (const [wo, breite, hoehe] of [["Computer", 1440, 950], ["Handy", 390, 844]]
const fehlerImProtokoll = [];
seite.on("pageerror", (e) => fehlerImProtokoll.push(e.message));
+ /* Mitschrift dessen, was die Seite tatsächlich an den Server schickt.
+ Nur so lässt sich prüfen, ob ein Klick auch WIRKLICH das Richtige
+ speichert — dass sich ein Knopf einfärbt, beweist gar nichts. */
+ const gesendet = [];
+ let beziffert = null;
+ let projektNachricht = null;
+
// NUR die API abfangen. Ein Muster, das den ganzen Ordner /webdesign
// umfasst, trifft auch die Seite SELBST -- dann bekommt der Browser
// statt der HTML-Datei ein JSON serviert. Genau das ist beim ersten
@@ -94,9 +119,25 @@ for (const [wo, breite, hoehe] of [["Computer", 1440, 950], ["Handy", 390, 844]]
// vorzeitig beenden würde -- auch das ist hier schon passiert.)
await seite.route("**/webdesign/admin/**", (route) => {
const u = route.request().url();
+ if (u.includes("/admin/projekte/p1/alles")) {
+ return route.fulfill({ json: { ok: true, projekt: PROJEKT, aufgaben: AUFGABEN,
+ fortschritt: FORTSCHRITT, aenderungen: AENDERUNGEN, nachrichten: PNACHRICHTEN } });
+ }
if (u.includes("/admin/projekte/p1/aufgaben")) {
return route.fulfill({ json: { ok: true, projekt: PROJEKT, aufgaben: AUFGABEN, fortschritt: FORTSCHRITT } });
}
+ if (u.match(/\/admin\/projekte\/p1$/)) {
+ gesendet.push(route.request().postDataJSON());
+ return route.fulfill({ json: { ok: true } });
+ }
+ if (u.includes("/admin/aenderungen/")) {
+ beziffert = route.request().postDataJSON();
+ return route.fulfill({ json: { ok: true, preisText: "250,00 €" } });
+ }
+ if (u.includes("/admin/projekt-nachricht")) {
+ projektNachricht = route.request().postDataJSON();
+ return route.fulfill({ json: { ok: true } });
+ }
if (u.includes("/admin/projekte")) return route.fulfill({ json: { ok: true, projekte: [PROJEKT] } });
if (u.includes("/admin/postfach-zaehler")) return route.fulfill({ json: { ok: true, ungelesen: 1 } });
if (u.includes("/admin/postfach/k1")) {
@@ -194,6 +235,77 @@ for (const [wo, breite, hoehe] of [["Computer", 1440, 950], ["Handy", 390, 844]]
pruefe(wo + ": die Erklärung steht UNTER dem Titel", m.anordnung?.textUnterTitel);
pruefe(wo + ": die Erklärung ist am Titel ausgerichtet, nicht am Rand", m.anordnung?.textNichtUnterKaestchen);
+ /* ---- Stand ändern ----
+ Dafür gab es bis zu dieser Erweiterung KEINE Oberfläche. Der Kunde
+ sah Phase und "nächster Schritt" ganz oben in seinem Portal, und
+ beides liess sich nirgends ändern — ein Projekt blieb für immer im
+ Briefing stehen, egal wie weit es wirklich war. */
+ pruefe(wo + ": alle Phasen sind wählbar",
+ await seite.locator("[data-phase]").count() === 9, "7 Phasen + 2 Sonderfälle");
+ pruefe(wo + ": die aktuelle Phase ist hervorgehoben",
+ await seite.locator('[data-phase="design"].vw-an').count() === 1);
+ pruefe(wo + ": „wer ist am Zug“ zeigt den Kunden",
+ await seite.locator('[data-wartet="kunde"].vw-an').count() === 1);
+
+ gesendet.length = 0;
+ await seite.click('[data-phase="entwicklung"]');
+ await seite.waitForTimeout(400);
+ pruefe(wo + ": Phasenklick speichert sofort",
+ gesendet.some((g) => g && g.status === "entwicklung"), JSON.stringify(gesendet));
+
+ await seite.click('[data-wartet="dogfather"]');
+ await seite.waitForTimeout(400);
+ pruefe(wo + ": „wer ist am Zug“ speichert sofort",
+ gesendet.some((g) => g && g.wartetAuf === "dogfather"));
+
+ await seite.fill("#vw-p-schritt", "Entwurf ansehen und Rückmeldung geben");
+ await seite.fill("#vw-p-termin", "Mitte Oktober");
+ gesendet.length = 0;
+ await seite.click("#vw-p-speichern");
+ await seite.waitForTimeout(500);
+ pruefe(wo + ": „nächster Schritt“ und Termin werden gespeichert",
+ gesendet.some((g) => g && g.naechsterSchritt === "Entwurf ansehen und Rückmeldung geben"
+ && g.richttermin === "Mitte Oktober"), JSON.stringify(gesendet));
+
+ gesendet.length = 0;
+ await seite.check("#vw-p-rest");
+ await seite.waitForTimeout(400);
+ pruefe(wo + ": Zahlungsstand wird sofort gespeichert",
+ gesendet.some((g) => g && g.restBezahlt === true), JSON.stringify(gesendet));
+
+ /* ---- Änderungswunsch beziffern ----
+ Der Kunde konnte seit jeher Ideen einreichen — beziffern ging nur
+ serverseitig. Der Kreis war offen: Er schickt etwas los und hört nie
+ wieder davon. */
+ pruefe(wo + ": beide Änderungswünsche erscheinen", await seite.locator("[data-ae]").count() === 2);
+ pruefe(wo + ": nur der OFFENE hat ein Bezifferungsfeld",
+ await seite.locator("[data-beziffern]").count() === 1);
+
+ // Ohne Preis darf nichts rausgehen — der Kunde könnte sonst nicht entscheiden.
+ beziffert = null;
+ await seite.click("[data-beziffern]");
+ await seite.waitForTimeout(400);
+ pruefe(wo + ": ohne Preis wird NICHT gesendet", beziffert === null, JSON.stringify(beziffert));
+
+ await seite.fill(".vw-ae-preis", "250");
+ await seite.fill(".vw-ae-dauer", "3 Tage");
+ await seite.click("[data-beziffern]");
+ await seite.waitForTimeout(600);
+ pruefe(wo + ": mit Preis geht das Angebot raus",
+ beziffert && beziffert.preisEuro === 250 && beziffert.dauer === "3 Tage", JSON.stringify(beziffert));
+
+ /* ---- Nachricht zum Projekt ----
+ Ohne sie hiess es: Der Kunde schreibt im Projekt, und ich kann ihm
+ nur woanders antworten. */
+ pruefe(wo + ": Projektnachrichten erscheinen",
+ await seite.locator("#vw-p-blasen .vw-blase").count() === 2);
+ await seite.fill("#vw-p-nachricht", "Der Entwurf kommt morgen.");
+ await seite.click("#vw-p-senden");
+ await seite.waitForTimeout(500);
+ pruefe(wo + ": Antwort zum Projekt geht raus",
+ projektNachricht && projektNachricht.text === "Der Entwurf kommt morgen."
+ && projektNachricht.projektId === "p1", JSON.stringify(projektNachricht));
+
// Touch-Ziele auf dem Handy
if (breite < 900) {
const zuKlein = await seite.evaluate(() =>
@@ -220,7 +332,10 @@ for (const [wo, breite, hoehe] of [["Computer", 1440, 950], ["Handy", 390, 844]]
const kunde = document.querySelector(".vw-blase--kunde");
const lauf = document.getElementById("vw-blasen");
return {
- anzahl: document.querySelectorAll(".vw-blase").length,
+ /* Auf den Postfach-Verlauf eingegrenzt. Ein document-weites
+ ".vw-blase" zaehlt auch die Nachrichten der Projektansicht mit --
+ die ist zwar geschlossen, steht aber weiter im Dokument. */
+ anzahl: document.querySelectorAll("#vw-blasen .vw-blase").length,
internDa: !!intern,
internAnders: intern && wir
? getComputedStyle(intern).borderStyle !== getComputedStyle(wir).borderStyle : false,
diff --git a/server-internal/index.js b/server-internal/index.js
index 5988e511..4eb1f2bc 100644
--- a/server-internal/index.js
+++ b/server-internal/index.js
@@ -104,7 +104,7 @@ import {
/* Aufgabenliste je Projekt ("was ist schon gemacht von dem, was im Plan
war") und allgemeines Postfach fuer Nachrichten OHNE Projektbezug. */
import {
- portalAufgaben, portalAufgabeErledigt, adminAufgaben, adminAufgabeAnlegen,
+ portalAufgaben, portalAufgabeErledigt, adminAufgaben, adminAufgabeAnlegen, adminProjektAlles,
adminAufgabeAendern, adminAufgabeLoeschen, adminVorlageAnwenden,
} from "./routes/webdesign-aufgaben.js";
import {
@@ -291,6 +291,10 @@ app.post("/webdesign/admin/projekt-nachricht", wrap(projektNachricht));
app.get("/webdesign/portal/projekt/:id/aufgaben", wrap(portalAufgaben));
app.post("/webdesign/portal/aufgabe/:id/erledigt", wrap(portalAufgabeErledigt));
app.get("/webdesign/admin/projekte/:id/aufgaben", wrap(adminAufgaben));
+/* Das ganze Projekt in EINER Antwort -- Stand, Aufgaben, Aenderungswuensche
+ und Nachrichten. Fuenf Anfragen hintereinander wuerden die Ansicht
+ sichtbar ruckelnd aufbauen. */
+app.get("/webdesign/admin/projekte/:id/alles", wrap(adminProjektAlles));
app.post("/webdesign/admin/projekte/:id/aufgaben", wrap(adminAufgabeAnlegen));
app.post("/webdesign/admin/projekte/:id/aufgaben/vorlage", wrap(adminVorlageAnwenden));
app.post("/webdesign/admin/aufgaben/:id", wrap(adminAufgabeAendern));
diff --git a/server-internal/routes/webdesign-aufgaben.js b/server-internal/routes/webdesign-aufgaben.js
index dee94e05..469b6d67 100644
--- a/server-internal/routes/webdesign-aufgaben.js
+++ b/server-internal/routes/webdesign-aufgaben.js
@@ -178,6 +178,56 @@ export async function adminAufgaben(req, res) {
return json(res, { ok: true, projekt, aufgaben, fortschritt: fortschritt(projektId) });
}
+/* GET /webdesign/admin/projekte/:id/alles — das ganze Projekt
+
+ Warum ein zusätzlicher Endpunkt neben adminAufgaben: Die Verwaltung
+ konnte ein Projekt bisher nur ANLEGEN. Ändern, Änderungswünsche
+ beziffern und im Projekt antworten ging serverseitig längst — es gab
+ nur keine Oberfläche dafür.
+
+ Das war die schmerzhafteste Lücke im ganzen Bereich: Der Kunde sieht
+ in seinem Portal ganz oben die Phase und den "nächsten Schritt". Beide
+ liessen sich nirgends ändern. Ein Projekt blieb also für immer im
+ Briefing stehen, egal wie weit es wirklich war.
+
+ Alles in EINER Antwort statt in fünf: Beim Öffnen eines Projekts
+ braucht man ohnehin alles, und fünf Anfragen hintereinander bauen die
+ Ansicht sichtbar ruckelnd auf. */
+export async function adminProjektAlles(req, res) {
+ const p = await darf(req, "WD_PROJEKTE_MANAGE");
+ if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
+
+ const id = String(req.params.id || "");
+ const projekt = db
+ .prepare(
+ `SELECT p.*, k.name AS kunde_name, k.firma AS kunde_firma, k.email AS kunde_email
+ FROM wd_projekte p JOIN wd_kunden k ON k.id = p.kunde_id WHERE p.id = ?`
+ )
+ .get(id);
+ if (!projekt) return json(res, { ok: false, error: "Projekt nicht gefunden." }, 404);
+
+ const aufgaben = db
+ .prepare(`SELECT * FROM wd_aufgaben WHERE projekt_id = ? ORDER BY reihenfolge, erstellt_am`)
+ .all(id);
+
+ const aenderungen = db
+ .prepare(`SELECT * FROM wd_aenderungen WHERE projekt_id = ? ORDER BY erstellt_am DESC`)
+ .all(id);
+
+ const nachrichten = db
+ .prepare(`SELECT * FROM wd_nachrichten WHERE projekt_id = ? ORDER BY erstellt_am ASC`)
+ .all(id);
+
+ return json(res, {
+ ok: true,
+ projekt,
+ aufgaben,
+ aenderungen,
+ nachrichten,
+ fortschritt: fortschritt(id),
+ });
+}
+
/* POST /webdesign/admin/projekte/:id/aufgaben — neuen Punkt anlegen */
export async function adminAufgabeAnlegen(req, res) {
const p = await darf(req, "WD_PROJEKTE_MANAGE");
diff --git a/webdesign/ablauf.html b/webdesign/ablauf.html
index 02e65249..cbfa1c50 100644
--- a/webdesign/ablauf.html
+++ b/webdesign/ablauf.html
@@ -13,8 +13,8 @@
-
-
+
+