Annehmen war bisher nur ein Etikett: Man stellte den Status um, und es passierte nichts. Kunde, Projekt, Aufgabenliste, Termin und Zugang musste man danach von Hand in fünf Schritten nachbauen. Der alte Code gab das im Kommentar selbst zu -- schlug der zweite von zwei Aufrufen fehl, stand der Kunde schon in der Datenbank und man durfte es nicht noch einmal versuchen. Jetzt macht das EIN Aufruf, ganz oder gar nicht: Kunde finden oder anlegen, Projekt anlegen, die komplette Aufgabenliste des Pakets einsetzen, Liefertermin berechnen, Einladungslink erzeugen. Der Kunde verfolgt ab diesem Moment alles in seinem Portal. Die Zeit läuft wirklich: - Eigenes Datumsfeld (termin_am) neben dem freien Text. Aus 'Mitte Oktober' kann man keine verbleibenden Tage rechnen. - Werktage statt Kalendertage, inklusive luxemburgischer Feiertage. Die beweglichen werden über die Osterformel berechnet statt gepflegt -- eine Liste ist im übernächsten Jahr lautlos falsch. - Vorschlag je Paket (Onepager 10, Website 20, Shop 30 Werktage), überschreibbar vor dem Bestätigen. - Verbleibende Zeit wird SERVERSEITIG gerechnet. Der Browser kennt die Feiertage nicht; zwei verschiedene Zahlen für denselben Termin wären schlimmer als gar keine. Ablehnen mit Grund und vorbereiteter Absage in fünf Sprachen, Text serverseitig erzeugt und vor dem Abschicken lesbar. Ein laufendes Projekt lässt sich nicht nachträglich als Anfrage ablehnen. Sechs Fehler dabei gefunden: - Ein unbekanntes Paket hätte GAR KEINEN Termin bekommen statt des Ersatzwerts. Zwei Funktionen lasen dieselbe Tabelle, nur eine hatte einen Rückfallwert. Eine fehlende Zahl fällt nirgends auf. - wd_anfragen hat keine Spalte 'firma' -- better-sqlite3 weist undefined ab, die ganze Annahme wäre gescheitert. - Migrationen stehen in einer ausdrücklichen Liste; 0015 fehlte darin. - datumKurz() wurde aufgerufen, gab es aber nicht. Der Fehler wäre erst NACH dem Anlegen aufgetreten. - Der Installations-Hinweis lag fest über dem Annehmen-Knopf und machte ihn auf dem Handy untreffbar. Die Seite reserviert jetzt Platz dafür. - 'richttermin' ist freier Text, lief aber durch einen Datumsformatierer: Der Kunde las 'Invalid Date' an der wichtigsten Stelle seines Projekts. Geprüft: 49 Prüfungen der Terminrechnung gegen nachschlagbare Osterdaten und Wochentage, 70 gegen eine echte Datenbank (darunter: zweiter Klick legt nichts doppelt an, Abbruch mittendrin lässt NICHTS zurück, gesperrter Kunde wird nicht still entsperrt), 42 im Browser auf Computer und Handy, 40 im Portal. Alle bestehenden Prüfungen weiter grün. Co-Authored-By: Claude Opus 5 <[email protected]>
214 lines
11 KiB
JavaScript
214 lines
11 KiB
JavaScript
/* =====================================================================
|
|
pruef-annahme.mjs — Annehmen und Ablehnen in der Verwaltung
|
|
|
|
Geprüft wird nicht, ob Knöpfe da sind, sondern WAS sie senden. Ein
|
|
Knopf, der sich färbt und nichts abschickt, besteht jede Sichtprüfung.
|
|
|
|
Der wichtigste Fall ist der, den man beim Bauen vergisst: Der erste
|
|
Entwurf rief nach dem Annehmen ein `datumKurz()` auf, das es gar nicht
|
|
gab. Der Fehler wäre erst NACHDEM alles angelegt war aufgetreten — man
|
|
hätte einen fertigen Kunden, ein fertiges Projekt und auf dem Schirm
|
|
eine rote Meldung. Deshalb wird hier auch das Protokoll mitgelesen.
|
|
===================================================================== */
|
|
import { chromium, devices } from "playwright";
|
|
import { createServer } from "node:http";
|
|
import { readFileSync, existsSync } from "node:fs";
|
|
import { join, extname } from "node:path";
|
|
|
|
const W = process.cwd();
|
|
const T = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript",
|
|
".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml",
|
|
".webp": "image/webp", ".ico": "image/x-icon", ".json": "application/json" };
|
|
const srv = createServer((q, r) => {
|
|
const p = join(W, decodeURIComponent(q.url.split("?")[0]));
|
|
if (existsSync(p) && !p.endsWith("/")) {
|
|
r.writeHead(200, { "content-type": T[extname(p)] || "text/plain" });
|
|
return r.end(readFileSync(p));
|
|
}
|
|
r.writeHead(404).end("");
|
|
});
|
|
await new Promise((r) => srv.listen(4351, r));
|
|
|
|
const ergebnisse = [];
|
|
function pruefe(name, ok, zusatz) {
|
|
ergebnisse.push({ name, ok });
|
|
console.log((ok ? " ok " : " FEHL ") + name + (zusatz !== undefined ? " -> " + zusatz : ""));
|
|
}
|
|
|
|
const ANFRAGE = {
|
|
id: "t1", nummer: "A-2608-0007", name: "Anna Muster", email: "[email protected]",
|
|
telefon: "+352 691 000000", kontaktweg: "email", paket: "website",
|
|
ziel: "Mehr Anfragen", zielgruppe: "Privatkunden", details: "Bitte modern und schlicht.",
|
|
budget: "1000-2500", termin: "Mitte Oktober", status: "neu", sprache: "de",
|
|
archiviert: 0, kunde_id: null, erstellt_am: new Date().toISOString(),
|
|
};
|
|
const VORSCHAU = {
|
|
ok: true, paket: "website", titelVorschlag: "Website für Anna Muster",
|
|
terminVorschlag: "2026-09-21", werktage: 20, laufend: false, schritte: 18,
|
|
kunde: null, kundeNeu: true, bereitsAngenommen: null,
|
|
};
|
|
|
|
for (const [wo, angaben] of [
|
|
["Computer", { viewport: { width: 1440, height: 950 } }],
|
|
["Handy", { ...devices["iPhone 14 Pro"] }],
|
|
]) {
|
|
const browser = await chromium.launch();
|
|
const kontext = await browser.newContext(angaben);
|
|
const seite = await kontext.newPage();
|
|
const protokoll = [];
|
|
seite.on("pageerror", (e) => protokoll.push(e.message));
|
|
|
|
let gesendet = null, abgelehnt = null, vorschauGeholt = 0, absageGeholt = [];
|
|
|
|
await seite.route("**/webdesign/admin/**", (route) => {
|
|
const u = new URL(route.request().url());
|
|
const pf = u.pathname;
|
|
if (pf.endsWith("/annahme-vorschau")) { vorschauGeholt++; return route.fulfill({ json: VORSCHAU }); }
|
|
if (pf.endsWith("/absage-vorschau")) {
|
|
absageGeholt.push(u.searchParams.get("grund"));
|
|
return route.fulfill({ json: { ok: true, sprache: "de", grund: u.searchParams.get("grund"),
|
|
text: "Hallo Anna Muster,\n\nvielen Dank für deine Anfrage.\n\nGrund: "
|
|
+ u.searchParams.get("grund") + "\n\nViele Grüße\nDogfather Webdesign" } });
|
|
}
|
|
if (pf.endsWith("/annehmen")) {
|
|
gesendet = route.request().postDataJSON();
|
|
return route.fulfill({ json: { ok: true, projektId: "p9", projektNummer: "P-2608-0003",
|
|
kundeId: "k9", kundeNeu: true, termin: gesendet.termin || "2026-09-21", werktage: 20,
|
|
schritte: 18, einladungsLink: null,
|
|
hinweis: "Der Kunde hatte bereits einen Zugang — er sieht das Projekt sofort." } });
|
|
}
|
|
if (pf.endsWith("/ablehnen")) {
|
|
abgelehnt = route.request().postDataJSON();
|
|
return route.fulfill({ json: { ok: true, grund: abgelehnt.grund, sprache: "de",
|
|
text: "…", imPostfach: true } });
|
|
}
|
|
if (pf.includes("/admin/anfragen/t1")) {
|
|
return route.fulfill({ json: { ok: true, anfrage: ANFRAGE, notizen: [] } });
|
|
}
|
|
if (pf.includes("/admin/anfragen")) return route.fulfill({ json: { ok: true, anfragen: [ANFRAGE] } });
|
|
if (pf.endsWith("/admin/uebersicht")) {
|
|
return route.fulfill({ json: { ok: true,
|
|
beiMir: { anfragen: { n: 1 }, nachrichten: { n: 0 }, wuensche: { n: 0 }, widerrufe: { n: 0 }, projekte: 0 },
|
|
beiKunde: { projekte: 0, aufgaben: 0, zahlungen: { n: 0, summe: 0 }, zahlungenText: "0,00 €" },
|
|
geld: { monat: { n: 0, brutto: 0, netto: 0, bruttoText: "0,00 €", nettoText: "0,00 €" },
|
|
gesamt: { n: 0, bruttoText: "0,00 €" }, gebuehrenText: "0,00 €" },
|
|
projekte: [], verlauf: [], bestand: {} } });
|
|
}
|
|
/* Ein Kunden- oder Projekt-Anlegen darf hier NICHT mehr vorkommen.
|
|
Vorher waren das zwei getrennte Aufrufe; taucht einer wieder auf,
|
|
ist der alte Weg zurueck. */
|
|
if (pf.endsWith("/admin/kunden") && route.request().method() === "POST") {
|
|
gesendet = { FEHLER: "alter Weg: Kunde einzeln angelegt" };
|
|
return route.fulfill({ json: { ok: true, id: "k9", einladungsLink: "x" } });
|
|
}
|
|
if (pf.endsWith("/admin/projekte") && route.request().method() === "POST") {
|
|
gesendet = { FEHLER: "alter Weg: Projekt einzeln angelegt" };
|
|
return route.fulfill({ json: { ok: true } });
|
|
}
|
|
return route.fulfill({ json: { ok: true, kunden: [], projekte: [], zahlungen: [], verlaeufe: [] } });
|
|
});
|
|
await seite.route("**/auth/**", (r) => r.fulfill({ json: { ok: true, user: { name: "Test" } } }));
|
|
await seite.addInitScript(() => {
|
|
sessionStorage.setItem("wd-vw-token", "t");
|
|
sessionStorage.setItem("wd-sitzung-aktiv", "1");
|
|
});
|
|
|
|
await seite.goto("http://127.0.0.1:4351/webdesign/verwaltung.html");
|
|
await seite.waitForTimeout(700);
|
|
console.log("\n=== " + wo + " ===");
|
|
|
|
await seite.click('[data-tab="anfragen"]');
|
|
await seite.waitForTimeout(500);
|
|
await seite.locator(".vw-karte").first().click();
|
|
await seite.waitForTimeout(800);
|
|
|
|
/* ---- 1. Die Vorschau wird geladen und gezeigt ---- */
|
|
pruefe(wo + ": die Vorschau wird beim Oeffnen geholt", vorschauGeholt === 1, vorschauGeholt);
|
|
pruefe(wo + ": der vorgeschlagene Titel steht da",
|
|
(await seite.locator("#vw-an-titel").innerText()).includes("Anna Muster"),
|
|
await seite.locator("#vw-an-titel").innerText());
|
|
pruefe(wo + ": die Zahl der Arbeitsschritte steht da",
|
|
(await seite.locator("#vw-an-schritte").innerText()).includes("18"),
|
|
await seite.locator("#vw-an-schritte").innerText());
|
|
pruefe(wo + ": das Terminfeld ist vorbelegt",
|
|
(await seite.locator("#vw-an-termin").inputValue()) === "2026-09-21",
|
|
await seite.locator("#vw-an-termin").inputValue());
|
|
/* Der Hinweis muss erklaeren, WORAUS die Zahl entsteht -- ein Datum
|
|
ohne Begruendung wirkt geraten. */
|
|
pruefe(wo + ": der Hinweis nennt Werktage und abgezogene Feiertage",
|
|
/20 Werktage/.test(await seite.locator("#vw-an-termin-hilfe").innerText())
|
|
&& /Feiertage/.test(await seite.locator("#vw-an-termin-hilfe").innerText()),
|
|
await seite.locator("#vw-an-termin-hilfe").innerText());
|
|
|
|
/* ---- 2. Die 30 % werden mitgerechnet ---- */
|
|
await seite.fill("#vw-ue-preis", "2000");
|
|
await seite.waitForTimeout(200);
|
|
pruefe(wo + ": die Anzahlung wird waehrend des Tippens gerechnet",
|
|
(await seite.locator("#vw-ue-anzahlung").innerText()).includes("600,00"),
|
|
await seite.locator("#vw-ue-anzahlung").innerText());
|
|
|
|
/* ---- 3. Annehmen sendet EINEN Aufruf mit allem ---- */
|
|
await seite.fill("#vw-an-termin", "2026-10-15");
|
|
await seite.click("#vw-uebernehmen");
|
|
await seite.waitForTimeout(900);
|
|
|
|
pruefe(wo + ": es geht genau EIN Aufruf hinaus, nicht der alte Zweischritt",
|
|
gesendet && !gesendet.FEHLER, JSON.stringify(gesendet));
|
|
pruefe(wo + ": der eigene Termin wird mitgeschickt",
|
|
gesendet && gesendet.termin === "2026-10-15", gesendet && gesendet.termin);
|
|
pruefe(wo + ": der Preis wird mitgeschickt",
|
|
gesendet && gesendet.preisEuro === 2000, gesendet && gesendet.preisEuro);
|
|
|
|
/* ---- 4. Die Rueckmeldung ist verstaendlich ---- */
|
|
const meldung = await seite.locator("#vw-meldung").innerText().catch(() => "");
|
|
pruefe(wo + ": die Meldung nennt die Projektnummer", meldung.includes("P-2608-0003"), meldung);
|
|
pruefe(wo + ": sie nennt die Zahl der Arbeitsschritte", meldung.includes("18"), meldung);
|
|
pruefe(wo + ": sie nennt den Liefertermin lesbar (15.10.2026)",
|
|
meldung.includes("15.10.2026"), meldung);
|
|
/* Genau hier steckte der Fehler des ersten Entwurfs: datumKurz() gab
|
|
es nicht, und der Fehler trat erst NACH dem Anlegen auf. */
|
|
pruefe(wo + ": dabei entstand KEIN Skriptfehler", protokoll.length === 0, protokoll.join(" | "));
|
|
|
|
/* ---- 5. Ablehnen ---- */
|
|
await seite.locator(".vw-karte").first().click();
|
|
await seite.waitForTimeout(700);
|
|
pruefe(wo + ": das Ablehnen-Formular ist zunaechst zu",
|
|
!(await seite.locator("#vw-ablehnen-form").isVisible()));
|
|
await seite.click("#vw-ablehnen-auf");
|
|
await seite.waitForTimeout(600);
|
|
pruefe(wo + ": es klappt auf", await seite.locator("#vw-ablehnen-form").isVisible());
|
|
pruefe(wo + ": der Absagetext wird geladen",
|
|
(await seite.locator("#vw-ab-text").inputValue()).includes("Anna Muster"),
|
|
(await seite.locator("#vw-ab-text").inputValue()).slice(0, 50));
|
|
/* Der Text darf NICHT im Browser bearbeitbar sein -- er ist eine
|
|
geschaeftliche Aeusserung und entsteht auf dem Server. */
|
|
pruefe(wo + ": der Text ist nur zum Lesen",
|
|
await seite.locator("#vw-ab-text").evaluate((e) => e.readOnly));
|
|
|
|
await seite.selectOption("#vw-ab-grund", "budget");
|
|
await seite.waitForTimeout(500);
|
|
pruefe(wo + ": ein anderer Grund laedt einen anderen Text",
|
|
absageGeholt.includes("budget"), absageGeholt.join(","));
|
|
|
|
await seite.click("#vw-ab-still");
|
|
await seite.waitForTimeout(700);
|
|
pruefe(wo + ": beim internen Ablehnen wird senden=false geschickt",
|
|
abgelehnt && abgelehnt.senden === false && abgelehnt.grund === "budget",
|
|
JSON.stringify(abgelehnt));
|
|
const m2 = await seite.locator("#vw-meldung").innerText().catch(() => "");
|
|
pruefe(wo + ": die Meldung sagt, dass nichts hinausging",
|
|
/nur intern/.test(m2), m2);
|
|
|
|
pruefe(wo + ": bis zum Schluss kein Skriptfehler", protokoll.length === 0, protokoll.join(" | "));
|
|
|
|
await seite.locator(".vw-karte").first().click();
|
|
await seite.waitForTimeout(700);
|
|
await seite.screenshot({ path: "bild-annahme-" + wo.toLowerCase() + ".png", fullPage: true });
|
|
await browser.close();
|
|
}
|
|
|
|
srv.close();
|
|
const f = ergebnisse.filter((e) => !e.ok).length;
|
|
console.log(`\n===== ${ergebnisse.length - f} bestanden, ${f} fehlgeschlagen =====`);
|
|
process.exitCode = f ? 1 : 0;
|