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 <[email protected]>
This commit is contained in:
2026-08-23 12:08:57 +02:00
co-authored by Claude Opus 5
parent 469a6c195f
commit 98fc04c666
15 changed files with 470 additions and 50 deletions
+116 -1
View File
@@ -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: "[email protected]", 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,
+5 -1
View File
@@ -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));
@@ -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");
+4 -4
View File
@@ -13,8 +13,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd6" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd6" />
<style>
/* Zeitstrahl: die Linie verbindet die Nummernkreise. Sie liegt hinter
den Kreisen (z-index) und endet beim letzten Schritt, damit sie nicht
@@ -183,7 +183,7 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-ablauf.js?v=20260823wd5"></script>
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
<script src="/assets/js/i18n-wd-ablauf.js?v=20260823wd6"></script>
<script src="/assets/js/wd-core.js?v=20260823wd6"></script>
</body>
</html>
+5 -5
View File
@@ -13,8 +13,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd6" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd6" />
<style>
/* ---------- Schrittanzeige ----------
Zeigt alle vier Schritte gleichzeitig statt nur "3 von 4". Man sieht
@@ -418,8 +418,8 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-anfrage.js?v=20260823wd5"></script>
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
<script src="/assets/js/wd-anfrage.js?v=20260823wd5"></script>
<script src="/assets/js/i18n-wd-anfrage.js?v=20260823wd6"></script>
<script src="/assets/js/wd-core.js?v=20260823wd6"></script>
<script src="/assets/js/wd-anfrage.js?v=20260823wd6"></script>
</body>
</html>
+4 -4
View File
@@ -13,8 +13,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd6" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd6" />
<style>
.fq-thema { scroll-margin-top: 90px; }
.fq-thema + .fq-thema { margin-top: clamp(2rem, 4vw, 3rem); }
@@ -195,7 +195,7 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-faq.js?v=20260823wd5"></script>
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
<script src="/assets/js/i18n-wd-faq.js?v=20260823wd6"></script>
<script src="/assets/js/wd-core.js?v=20260823wd6"></script>
</body>
</html>
+4 -4
View File
@@ -16,8 +16,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd6" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd6" />
<style>
/* ---------- Nur auf der Startseite gebraucht ---------- */
@@ -407,8 +407,8 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-index.js?v=20260823wd5"></script>
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
<script src="/assets/js/i18n-wd-index.js?v=20260823wd6"></script>
<script src="/assets/js/wd-core.js?v=20260823wd6"></script>
<script>
/* ---------------------------------------------------------------------
Marken-Intro (Masterplan S.3, Schritt 02: "Nach erfolgreichem Code
+4 -4
View File
@@ -13,8 +13,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd6" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd6" />
<style>
/* Sprungmarken.
Frueher eine waagerecht wischbare Leiste. Rueckmeldung 22.08.2026:
@@ -331,7 +331,7 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-leistungen.js?v=20260823wd5"></script>
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
<script src="/assets/js/i18n-wd-leistungen.js?v=20260823wd6"></script>
<script src="/assets/js/wd-core.js?v=20260823wd6"></script>
</body>
</html>
+5 -5
View File
@@ -12,8 +12,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd6" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd6" />
<style>
.po-login { max-width: 440px; margin: clamp(1.5rem,6vh,4rem) auto; }
@@ -134,8 +134,8 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-portal.js?v=20260823wd5"></script>
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
<script src="/assets/js/wd-portal.js?v=20260823wd5"></script>
<script src="/assets/js/i18n-wd-portal.js?v=20260823wd6"></script>
<script src="/assets/js/wd-core.js?v=20260823wd6"></script>
<script src="/assets/js/wd-portal.js?v=20260823wd6"></script>
</body>
</html>
+4 -4
View File
@@ -13,8 +13,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd6" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd6" />
<style>
.pfo-projekt { overflow: hidden; padding: 0; margin-bottom: clamp(1.4rem, 3vw, 2.2rem); }
.pfo-bild {
@@ -192,7 +192,7 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-portfolio.js?v=20260823wd5"></script>
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
<script src="/assets/js/i18n-wd-portfolio.js?v=20260823wd6"></script>
<script src="/assets/js/wd-core.js?v=20260823wd6"></script>
</body>
</html>
+4 -4
View File
@@ -13,8 +13,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd6" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd6" />
<style>
.pr-preis-gross { font-family: var(--font-head); font-weight: 800; color: var(--wd-blau); white-space: nowrap; }
@@ -196,7 +196,7 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-preise.js?v=20260823wd5"></script>
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
<script src="/assets/js/i18n-wd-preise.js?v=20260823wd6"></script>
<script src="/assets/js/wd-core.js?v=20260823wd6"></script>
</body>
</html>
+1 -1
View File
@@ -34,7 +34,7 @@
Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den
Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten
Stand fest. */
const CACHE_NAME = "dogfather-webdesign-v5";
const CACHE_NAME = "dogfather-webdesign-v6";
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
HTML-Datei in dieser Liste. */
+4 -4
View File
@@ -13,8 +13,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd6" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd6" />
<style>
.ue-kopf { display: grid; grid-template-columns: minmax(0, .85fr) minmax(0, 1.15fr); gap: clamp(1.5rem, 4vw, 3rem); align-items: center; }
/* Auf dem Handy zuerst der Text, dann das Bild: sonst schiebt ein
@@ -127,7 +127,7 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-ueber.js?v=20260823wd5"></script>
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
<script src="/assets/js/i18n-wd-ueber.js?v=20260823wd6"></script>
<script src="/assets/js/wd-core.js?v=20260823wd6"></script>
</body>
</html>
+256 -5
View File
@@ -11,8 +11,8 @@
<link rel="apple-touch-icon" href="/assets/img/wd-app-180.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="DF Verwaltung" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd6" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd6" />
<style>
/* =====================================================================
Verwaltungsbereich — bewusst NUR AUF DEUTSCH.
@@ -753,7 +753,7 @@
<div class="vw-detail-blatt" id="vw-detail-blatt"></div>
</div>
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
<script src="/assets/js/wd-core.js?v=20260823wd6"></script>
<script>
/* =====================================================================
Verwaltung: Anfragen ansehen, Status ändern, Notizen, Archivieren.
@@ -1462,6 +1462,22 @@
};
var offenesProjekt = null;
/* Die Phasen des Projekts — dieselbe Reihenfolge wie die Leiste, die
der Kunde in seinem Portal sieht. "pausiert" und "abgebrochen"
stehen bewusst am Ende und ausserhalb der Reihe: Sie sind keine
Phase, sondern ein Zustand daneben. */
var PHASEN = ["briefing", "angebot", "design", "entwicklung", "tests", "abnahme", "uebergeben"];
var PHASEN_SONDER = ["pausiert", "abgebrochen"];
var PHASE_TEXT = {
briefing: "Briefing", angebot: "Angebot", design: "Design",
entwicklung: "Entwicklung", tests: "Tests", abnahme: "Abnahme",
uebergeben: "Übergeben", pausiert: "Pausiert", abgebrochen: "Abgebrochen",
};
var AE_TEXT = {
eingereicht: "Eingereicht", geprueft: "Wird geprüft", angeboten: "Angebot liegt vor",
angenommen: "Angenommen", abgelehnt: "Abgelehnt", umgesetzt: "Umgesetzt",
};
/* Der Balken in vier Abschnitten. Ein einzelner Balken sagt "60 %",
vier Abschnitte sagen zusaetzlich WO diese 60 % liegen. */
function balken(f) {
@@ -1567,7 +1583,7 @@
blatt.innerHTML = '<div class="vw-laedt">Wird geladen…</div>';
try {
var d = await api("/webdesign/admin/projekte/" + encodeURIComponent(projektId) + "/aufgaben");
var d = await api("/webdesign/admin/projekte/" + encodeURIComponent(projektId) + "/alles");
zeichneAufgaben(d);
} catch (fehler) {
if (fehler.status === 401) { schliesseDetail(); token = null; tokenSchreiben(null); zeigeAnmeldung(); return; }
@@ -1617,6 +1633,12 @@
"</div>" +
balken(f) +
/* Gleiche Aufteilung wie in der Anfrageansicht: links die Arbeit,
rechts das Steuern. Wer zwischen beiden Ansichten wechselt, muss
nicht umdenken. */
'<div class="vw-detail-spalten">' +
'<div class="vw-dt-links">' +
(d.aufgaben.length ? gruppen :
'<div class="vw-leer-hinweis">Für dieses Projekt gibt es noch keine Aufgabenliste.' +
'<div style="margin-top:1rem"><button type="button" class="wd-btn wd-btn--haupt wd-btn--klein" ' +
@@ -1628,7 +1650,136 @@
"Der Kunde sieht jede Änderung sofort in seinem Portal. " +
'<button type="button" class="vw-mini" id="vw-vorlage" style="min-width:auto;padding:.25rem .6rem">' +
"Fehlende Standardpunkte ergänzen</button></p>"
: "");
: "") +
"</div>" +
'<div class="vw-dt-rechts">' +
/* ----- STAND -----
Das Wichtigste an dieser ganzen Erweiterung. Phase und "nächster
Schritt" stehen im Portal des Kunden GANZ OBEN — und liessen
sich bis hierher nirgends ändern. Ein Projekt blieb dadurch für
immer im Briefing stehen, egal wie weit es wirklich war. */
'<div class="vw-abschnitt"><h3>Stand</h3>' +
'<p class="wd-klein wd-leise" style="margin-top:-.2rem">' +
"Das sieht der Kunde ganz oben in seinem Portal." +
"</p>" +
'<div class="vw-status-wahl" style="margin-bottom:.7rem">' +
PHASEN.map(function (k, i) {
return '<button type="button" class="vw-filter-knopf' + (p.status === k ? " vw-an" : "") +
'" data-phase="' + k + '">' + (i + 1) + ". " + s(PHASE_TEXT[k]) + "</button>";
}).join("") +
"</div>" +
'<div class="vw-status-wahl" style="margin-bottom:.9rem">' +
PHASEN_SONDER.map(function (k) {
return '<button type="button" class="vw-filter-knopf' + (p.status === k ? " vw-an" : "") +
'" data-phase="' + k + '">' + s(PHASE_TEXT[k]) + "</button>";
}).join("") +
"</div>" +
'<div class="wd-feld" style="margin-bottom:.6rem">' +
'<label for="vw-p-schritt">Nächster Schritt</label>' +
'<input type="text" id="vw-p-schritt" maxlength="300" value="' + s(p.naechster_schritt || "") +
'" placeholder="z. B. Entwurf ansehen und Rückmeldung geben" />' +
'<span class="wd-feld-hilfe">Ein Satz in der Sprache des Kunden — nicht „QA-Runde 2“.</span>' +
"</div>" +
'<div class="wd-feld" style="margin-bottom:.6rem">' +
"<label>Wer ist am Zug?</label>" +
'<div class="vw-status-wahl">' +
'<button type="button" class="vw-filter-knopf' + (p.wartet_auf === "dogfather" ? " vw-an" : "") +
'" data-wartet="dogfather">Ich</button>' +
'<button type="button" class="vw-filter-knopf' + (p.wartet_auf === "kunde" ? " vw-an" : "") +
'" data-wartet="kunde">Der Kunde</button>' +
"</div>" +
"</div>" +
'<div class="wd-feld" style="margin-bottom:.8rem">' +
'<label for="vw-p-termin">Richttermin</label>' +
'<input type="text" id="vw-p-termin" maxlength="40" value="' + s(p.richttermin || "") +
'" placeholder="z. B. Mitte Oktober" />' +
"</div>" +
'<button type="button" class="wd-btn wd-btn--haupt wd-btn--klein" id="vw-p-speichern">Stand speichern</button>' +
"</div>" +
/* ----- ZAHLUNGSSTAND ----- */
'<div class="vw-abschnitt"><h3>Zahlungsstand</h3>' +
'<label class="vw-pf-schalter" style="margin-bottom:.5rem">' +
'<input type="checkbox" id="vw-p-anz"' + (p.anzahlung_bezahlt ? " checked" : "") + " />" +
"<span>Anzahlung ist eingegangen</span></label>" +
'<label class="vw-pf-schalter" style="margin-bottom:.5rem">' +
'<input type="checkbox" id="vw-p-rest"' + (p.rest_bezahlt ? " checked" : "") + " />" +
"<span>Restbetrag ist eingegangen</span></label>" +
'<label class="vw-pf-schalter">' +
'<input type="checkbox" id="vw-p-portfolio"' + (p.portfolio_freigabe ? " checked" : "") + " />" +
"<span>Darf ins Portfolio</span></label>" +
'<p class="wd-klein wd-leise" style="margin:.5rem 0 0">Wird sofort gespeichert.</p>' +
"</div>" +
/* ----- ÄNDERUNGSWÜNSCHE -----
Der Kunde konnte Ideen einreichen, seit es das Portal gibt. Sie
zu beziffern ging serverseitig auch — nur gab es keine
Oberfläche. Der Kreis war also offen: Er schickt etwas los und
hört nie wieder davon. */
'<div class="vw-abschnitt"><h3>Änderungswünsche (' + d.aenderungen.length + ")</h3>" +
(d.aenderungen.length
? d.aenderungen.map(function (a) {
var offen = a.status === "eingereicht" || a.status === "geprueft";
return '<div class="vw-notiz" data-ae="' + s(a.id) + '">' +
'<div class="vw-notiz-kopf">' + window.WD.datum(a.erstellt_am) +
' · <span class="wd-tag' + (offen ? " wd-tag--gold" : a.status === "angenommen" ? " wd-tag--ok" : "") +
'">' + s(AE_TEXT[a.status] || a.status) + "</span></div>" +
"<p><strong>" + s(a.titel) + "</strong></p>" +
"<p>" + s(a.beschreibung || "") + "</p>" +
(a.zusatzpreis_cent != null
? '<p class="wd-klein wd-leise">Beziffert: ' +
(a.zusatzpreis_cent / 100).toFixed(2).replace(".", ",") + " €" +
(a.zusatzdauer_text ? " · " + s(a.zusatzdauer_text) : "") + "</p>"
: "") +
(offen
? '<div style="margin-top:.6rem">' +
'<div class="wd-feld" style="margin-bottom:.4rem">' +
'<input type="number" class="vw-ae-preis" min="0" step="1" inputmode="numeric" ' +
'placeholder="Preis in Euro" />' +
"</div>" +
'<div class="wd-feld" style="margin-bottom:.4rem">' +
'<input type="text" class="vw-ae-dauer" maxlength="200" placeholder="Zusätzliche Dauer, z. B. 3 Tage" />' +
"</div>" +
'<div class="wd-feld" style="margin-bottom:.5rem">' +
'<textarea class="vw-ae-antwort" rows="2" placeholder="Kurze Erklärung für den Kunden"></textarea>' +
"</div>" +
'<button type="button" class="wd-btn wd-btn--zweit wd-btn--klein" data-beziffern="' +
s(a.id) + '">Angebot an den Kunden schicken</button></div>'
: "") +
"</div>";
}).join("")
: '<p class="wd-klein wd-leise">Noch keine Wünsche eingereicht.</p>') +
"</div>" +
/* ----- NACHRICHTEN ZUM PROJEKT -----
Getrennt vom allgemeinen Postfach, weil sie hier zum Projekt
gehören und im Portal auch dort erscheinen. Sie hier NICHT zu
haben hiess: Der Kunde schreibt im Projekt, und ich kann ihm nur
woanders antworten. */
'<div class="vw-abschnitt"><h3>Nachrichten zum Projekt (' + d.nachrichten.length + ")</h3>" +
(d.nachrichten.length
? '<div class="vw-pf-verlauf" style="max-height:34vh" id="vw-p-blasen">' +
d.nachrichten.map(function (n) {
var vomKunden = n.autor === "kunde";
return '<div class="vw-blase vw-blase--' + (vomKunden ? "kunde" : "wir") + '">' +
'<div class="vw-blase-kopf">' +
(vomKunden ? s(p.kunde_name) : "Du") + " · " + window.WD.datum(n.erstellt_am) +
"</div>" + s(n.text) + "</div>";
}).join("") + "</div>"
: '<p class="wd-klein wd-leise">Noch keine Nachrichten.</p>') +
'<div class="wd-feld" style="margin:.7rem 0 .5rem">' +
'<textarea id="vw-p-nachricht" rows="2" placeholder="Antwort zum Projekt…"></textarea>' +
"</div>" +
'<button type="button" class="wd-btn wd-btn--zweit wd-btn--klein" id="vw-p-senden">Senden</button>' +
"</div>" +
"</div></div>";
blatt.querySelector(".vw-zu").addEventListener("click", schliesseDetail);
@@ -1669,6 +1820,106 @@
});
});
/* ---- Stand ändern ----
Phase und "wer ist am Zug" werden SOFORT gespeichert, ohne
Speichern-Knopf: Es ist ein Klick auf genau einen Wert, da wäre
ein zweiter Klick reine Zeremonie. Die Textfelder brauchen den
Knopf, weil man beim Tippen zwischendurch nicht speichern will. */
function standAendern(felder, danach) {
return api("/webdesign/admin/projekte/" + encodeURIComponent(offenesProjekt),
{ method: "POST", body: felder })
.then(function () { if (danach) danach(); })
.catch(function (f) { window.WD.meldung(meldung, f.message, "fehler"); });
}
blatt.querySelectorAll("[data-phase]").forEach(function (b) {
b.addEventListener("click", function () {
blatt.querySelectorAll("[data-phase]").forEach(function (x) {
x.classList.toggle("vw-an", x === b);
});
standAendern({ status: b.dataset.phase });
});
});
blatt.querySelectorAll("[data-wartet]").forEach(function (b) {
b.addEventListener("click", function () {
blatt.querySelectorAll("[data-wartet]").forEach(function (x) {
x.classList.toggle("vw-an", x === b);
});
standAendern({ wartetAuf: b.dataset.wartet });
});
});
var speichern = blatt.querySelector("#vw-p-speichern");
speichern.addEventListener("click", function () {
window.WD.knopfSperren(speichern, true, "…");
standAendern({
naechsterSchritt: blatt.querySelector("#vw-p-schritt").value,
richttermin: blatt.querySelector("#vw-p-termin").value,
}).then(function () {
window.WD.knopfSperren(speichern, false);
window.WD.meldung(meldung, "Gespeichert — der Kunde sieht es sofort.", "ok");
});
});
[["#vw-p-anz", "anzahlungBezahlt"], ["#vw-p-rest", "restBezahlt"],
["#vw-p-portfolio", "portfolioFreigabe"]].forEach(function (paar2) {
var el = blatt.querySelector(paar2[0]);
el.addEventListener("change", function () {
var felder = {};
felder[paar2[1]] = el.checked;
standAendern(felder);
});
});
/* ---- Änderungswunsch beziffern ---- */
blatt.querySelectorAll("[data-beziffern]").forEach(function (b) {
b.addEventListener("click", async function () {
var kasten = b.closest("[data-ae]");
var preis = kasten.querySelector(".vw-ae-preis").value;
if (!preis || Number(preis) < 0) {
window.WD.meldung(meldung,
"Bitte einen Preis angeben — ohne Preis kann der Kunde nicht entscheiden.", "fehler");
kasten.querySelector(".vw-ae-preis").focus();
return;
}
window.WD.knopfSperren(b, true, "…");
try {
await api("/webdesign/admin/aenderungen/" + encodeURIComponent(b.dataset.beziffern) + "/beziffern",
{ method: "POST", body: {
preisEuro: Number(preis),
dauer: kasten.querySelector(".vw-ae-dauer").value,
antwort: kasten.querySelector(".vw-ae-antwort").value,
} });
window.WD.meldung(meldung, "Angebot ist beim Kunden. Er entscheidet in seinem Portal.", "ok");
oeffneAufgaben(offenesProjekt);
} catch (f2) {
window.WD.knopfSperren(b, false);
window.WD.meldung(meldung, f2.message, "fehler");
}
});
});
/* ---- Nachricht zum Projekt ---- */
var blasen = blatt.querySelector("#vw-p-blasen");
if (blasen) blasen.scrollTop = blasen.scrollHeight;
var pSenden = blatt.querySelector("#vw-p-senden");
pSenden.addEventListener("click", async function () {
var feld = blatt.querySelector("#vw-p-nachricht");
var inhalt = feld.value.trim();
if (!inhalt) { feld.focus(); return; }
window.WD.knopfSperren(pSenden, true, "…");
try {
await api("/webdesign/admin/projekt-nachricht",
{ method: "POST", body: { projektId: offenesProjekt, text: inhalt } });
oeffneAufgaben(offenesProjekt);
} catch (f2) {
window.WD.knopfSperren(pSenden, false);
window.WD.meldung(meldung, f2.message, "fehler");
}
});
var vorlage = blatt.querySelector("#vw-vorlage");
if (vorlage) {
vorlage.addEventListener("click", async function () {
+4 -4
View File
@@ -21,8 +21,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd6" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd6" />
<style>
/* =====================================================================
Zugangswand — nur hier gebraucht, deshalb bewusst NICHT in
@@ -177,8 +177,8 @@
</div>
</main>
<script src="/assets/js/i18n-wd-zugang.js?v=20260823wd5"></script>
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
<script src="/assets/js/i18n-wd-zugang.js?v=20260823wd6"></script>
<script src="/assets/js/wd-core.js?v=20260823wd6"></script>
<script>
(function () {
"use strict";