From 98fc04c666f8b6346697946c1855caa7ea4f67e2 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Sun, 23 Aug 2026 12:08:57 +0200 Subject: [PATCH] Verwaltung: Projektstand aendern, Wuensche beziffern, im Projekt antworten Drei Server-Funktionen existierten seit Tagen und hatten KEINE Oberflaeche. Erst im Vergleich "was kann der Server" gegen "was ruft die Seite auf" ist es aufgefallen: POST /admin/projekte/:id projektAendern POST /admin/aenderungen/:id/beziffern POST /admin/projekt-nachricht Jede davon schliesst einen Kreis, der bisher offen war. 1. DIE SCHMERZHAFTESTE LUECKE: DER STAND Der Kunde sieht in seinem Portal GANZ OBEN die Phasenleiste und den "naechsten Schritt". Beides liess sich nirgends aendern. Ein Projekt blieb also fuer immer im Briefing stehen, egal wie weit es wirklich war -- und der prominenteste Text im ganzen Kundenportal war dauerhaft leer. Jetzt: sieben Phasen als Knoepfe (plus Pausiert/Abgebrochen daneben, denn das sind keine Phasen, sondern Zustaende), "wer ist am Zug", naechster Schritt, Richttermin, Zahlungsstand, Portfolio-Freigabe. Phase und "wer ist am Zug" speichern SOFORT ohne Speichern-Knopf: Es ist ein Klick auf genau einen Wert, ein zweiter Klick waere Zeremonie. Die Textfelder haben einen Knopf, weil man beim Tippen zwischendurch nicht speichern will. 2. AENDERUNGSWUENSCHE Der Kunde konnte Ideen einreichen, seit es das Portal gibt. Beziffern ging serverseitig auch -- nur gab es keine Oberflaeche. Der Kreis war offen: Er schickt etwas los und hoert nie wieder davon. Jetzt stehen alle Wuensche im Projekt, offene mit Feldern fuer Preis, Dauer und einer kurzen Erklaerung. Ohne Preis geht nichts raus (geprueft) -- ohne Preis kann der Kunde nicht entscheiden, und ein "Angebot" ohne Zahl ist keins. 3. NACHRICHTEN ZUM PROJEKT Getrennt vom allgemeinen Postfach, weil sie zum Projekt gehoeren und im Portal auch dort erscheinen. Sie hier nicht zu haben hiess: Der Kunde schreibt im Projekt, und ich kann ihm nur woanders antworten. AUFBAU Neuer Endpunkt /admin/projekte/:id/alles liefert Projekt, Aufgaben, Fortschritt, Aenderungswuensche und Nachrichten in EINER Antwort. Fuenf Anfragen hintereinander wuerden die Ansicht sichtbar ruckelnd aufbauen. Dieselbe Aufteilung wie in der Anfrageansicht -- links die Arbeit (Aufgaben), rechts das Steuern. Wer zwischen beiden Ansichten wechselt, muss nicht umdenken. GEPRUEFT: 69 Pruefungen, Computer und Handy, alle bestanden. Die neuen Pruefungen schauen nicht darauf, ob sich ein Knopf einfaerbt -- das beweist nichts. Sie schneiden mit, was die Seite TATSAECHLICH an den Server schickt: {"status":"entwicklung"}, {"restBezahlt":true}, {"preisEuro":250,"dauer":"3 Tage"}. Und sie pruefen den Fall, in dem nichts rausgehen darf: Ohne Preis bleibt die Mitschrift leer. Ein Fehler im Test selbst behoben: Die Nachrichtenzaehlung war document-weit und zaehlte die Nachrichten der geschlossenen Projektansicht mit -- geschlossen heisst nicht aus dem Dokument entfernt. Jetzt auf den Postfach-Verlauf eingegrenzt. Versionsstempel und Cache-Name auf v6. Co-Authored-By: Claude Opus 5 --- pruef-verwaltung.mjs | 117 ++++++++- server-internal/index.js | 6 +- server-internal/routes/webdesign-aufgaben.js | 50 ++++ webdesign/ablauf.html | 8 +- webdesign/anfrage.html | 10 +- webdesign/faq.html | 8 +- webdesign/index.html | 8 +- webdesign/leistungen.html | 8 +- webdesign/portal.html | 10 +- webdesign/portfolio.html | 8 +- webdesign/preise.html | 8 +- webdesign/sw.js | 2 +- webdesign/ueber.html | 8 +- webdesign/verwaltung.html | 261 ++++++++++++++++++- webdesign/zugang.html | 8 +- 15 files changed, 470 insertions(+), 50 deletions(-) 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 @@ - - + +