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 @@ - - + +