Abbruch-Dialog und Farbunterscheidung meins/beim Kunden
ABBRECHEN IN DER OBERFLAECHE Der Server konnte es seit gestern, die Knoepfe fehlten. Jetzt steht am Ende der Projektansicht ein zurueckhaltender Knopf -- bewusst nicht zwischen den anderen: Es ist die seltenste und endgueltigste Handlung an einem Projekt, und ein gleich lauter Knopf daneben laedt zum Verwechseln ein. Beim Aufklappen rechnet die Seite vor: wie viele Schritte erledigt sind, wie viel gezahlt wurde, wie viel davon verdient ist, und was sich daraus als Erstattung ergibt. Der Betrag steht als Vorschlag im Feld und ist aenderbar -- geprueft wird, dass der GEAENDERTE Wert hinausgeht und nicht der vorgeschlagene, sonst waere das Feld eine Attrappe. Gerechnet wird erst beim Aufklappen, nicht beim Oeffnen der Projektansicht: Dazwischen kann man Punkte abgehakt haben, und die Zahlen sollen den Stand von JETZT zeigen. Faellt die Vorschau aus, laesst sich der Betrag von Hand eintragen -- ein ausgefallener Rechendienst darf kein Projekt in der Liste festhalten. Ein bereits abgebrochenes Projekt bekommt keinen Knopf mehr, sondern einen Kasten mit Datum, Grund, wer abgebrochen hat und was zu erstatten war. MEINS ODER SEINS Wunsch: 'ich will dass die kunden sachen auch in der verwaltungs seite von kacheln eine andere farbe haben wie meine damit ich sie gut unterscheide.' Was bei mir liegt, bleibt im Markenblau. Was beim Kunden liegt, bekommt Lila. Gemessen: rgb(127,208,232) gegen rgb(183,157,255). Bewusst NICHT ueber Rot/Gruen: Die Warnstufen sind an das ALTER vergeben und muessen frei bleiben. Eine Kachel, die gleichzeitig 'beim Kunden' und 'seit acht Tagen ueberfaellig' faerben muesste, koennte nur eine der beiden Aussagen zeigen -- und die Frist ist die wichtigere. Dazu eine 3px-Kante links auf beiden Seiten. Farbe allein traegt die Aussage nicht: Wer sie nicht unterscheiden kann, saehe sonst zwei gleich aussehende Bloecke (WCAG 1.4.1). Die Kante ist ein Gegensatz, keine Markierung einer Gruppe -- meins blau, seins lila. Geprueft mit 42 Pruefungen auf Computer und Handy, die messen, was tatsaechlich an den Server geht und welche Farben wirklich berechnet werden. Alle bestehenden Pruefungen weiter gruen. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
/* =====================================================================
|
||||
pruef-abbruch.mjs — Auftrag abbrechen, und meins gegen seins
|
||||
|
||||
ZWEI DINGE, DIE MAN NUR MESSEN KANN
|
||||
|
||||
1. Der Abbruch schickt Geld-Angaben an den Server. Ein Dialog, der
|
||||
hübsch aussieht und den falschen Betrag sendet, besteht jede
|
||||
Sichtprüfung — geprüft wird deshalb, WAS hinausgeht.
|
||||
|
||||
2. Die Farbunterscheidung zwischen "meins" und "beim Kunden" wird an
|
||||
den tatsächlich berechneten Farben gemessen. Eine CSS-Regel, die
|
||||
im Quelltext steht, kann von einer anderen überstimmt werden —
|
||||
dreimal in diesem Projekt schon passiert.
|
||||
===================================================================== */
|
||||
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(4359, r));
|
||||
|
||||
const ergebnisse = [];
|
||||
const pruefe = (n, ok, z) => {
|
||||
ergebnisse.push({ n, ok });
|
||||
console.log((ok ? " ok " : " FEHL ") + n + (z !== undefined ? " -> " + z : ""));
|
||||
};
|
||||
|
||||
const PROJEKT = {
|
||||
id: "p1", nummer: "P-2608-0001", titel: "Website für Musterbau", paket: "website",
|
||||
status: "design", wartet_auf: "dogfather", naechster_schritt: "Entwurf",
|
||||
preis_cent: 200000, anzahlung_cent: 60000, anzahlung_bezahlt: 1, waehrung: "EUR",
|
||||
kunde_id: "k1", kunde_name: "Anna Muster", termin_am: "2026-10-15",
|
||||
};
|
||||
const UEBERSICHT = {
|
||||
ok: true,
|
||||
beiMir: { anfragen: { n: 3, aeltestes: new Date().toISOString() },
|
||||
nachrichten: { n: 0 }, wuensche: { n: 0 }, widerrufe: { n: 0 }, projekte: 2 },
|
||||
beiKunde: { projekte: 1, aufgaben: 4,
|
||||
zahlungen: { n: 2, summe: 120000, aeltestes: new Date().toISOString() },
|
||||
zahlungenText: "1.200,00 €" },
|
||||
geld: { monat: { n: 3, brutto: 250000, netto: 241000, bruttoText: "2.500,00 €", nettoText: "2.410,00 €" },
|
||||
gesamt: { n: 11, bruttoText: "9.800,00 €" }, gebuehrenText: "90,00 €" },
|
||||
projekte: [], verlauf: [], bestand: {}, meldungen: [], meldungenOffen: 0, wartetAufAnzahlung: [],
|
||||
};
|
||||
|
||||
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, vorschauGeholt = 0;
|
||||
|
||||
seite.on("dialog", (d) => d.accept());
|
||||
|
||||
await seite.route("**/webdesign/admin/**", (route) => {
|
||||
const u = new URL(route.request().url()).pathname;
|
||||
if (u.endsWith("/abbruch-vorschau")) {
|
||||
vorschauGeholt++;
|
||||
return route.fulfill({ json: { ok: true,
|
||||
projekt: { id: "p1", nummer: PROJEKT.nummer, uhrLaeuft: true, schonAbgebrochen: false },
|
||||
rechnung: { prozent: 25, fertig: 5, gesamt: 20,
|
||||
bezahltCent: 60000, bezahltText: "600,00 €",
|
||||
behaltenCent: 50000, behaltenText: "500,00 €",
|
||||
erstattungCent: 10000, erstattungText: "100,00 €" } } });
|
||||
}
|
||||
if (u.endsWith("/abbrechen")) {
|
||||
gesendet = route.request().postDataJSON();
|
||||
return route.fulfill({ json: { ok: true, grund: gesendet.grund,
|
||||
erstattungCent: Math.round((gesendet.erstattungEuro || 0) * 100),
|
||||
hinweis: "Zu erstatten: 100,00 €. Die Rückzahlung löst die Seite bewusst nicht selbst aus." } });
|
||||
}
|
||||
if (u.endsWith("/admin/uebersicht")) return route.fulfill({ json: UEBERSICHT });
|
||||
if (u.includes("/admin/projekte/p1/alles")) {
|
||||
return route.fulfill({ json: { ok: true, projekt: PROJEKT, aufgaben: [],
|
||||
fortschritt: { erledigt: 5, gesamt: 20, prozent: 25, inArbeit: "", beimKunden: "" },
|
||||
aenderungen: [], nachrichten: [] } });
|
||||
}
|
||||
if (u.includes("/admin/projekte")) return route.fulfill({ json: { ok: true, projekte: [PROJEKT] } });
|
||||
return route.fulfill({ json: { ok: true, anfragen: [], kunden: [], 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:4359/webdesign/verwaltung.html");
|
||||
await seite.waitForTimeout(800);
|
||||
console.log("\n=== " + wo + " ===");
|
||||
|
||||
/* ---- 1. Meins gegen seins: gemessen, nicht behauptet ---- */
|
||||
const farben = await seite.evaluate(() => {
|
||||
const meins = document.querySelector("#vw-ub-beimir .vw-ub-kachel");
|
||||
const seins = document.querySelector("#vw-ub-beikunde .vw-ub-kachel");
|
||||
const st1 = getComputedStyle(meins), st2 = getComputedStyle(seins);
|
||||
return {
|
||||
meinZahl: st1.color, seinZahl: getComputedStyle(seins.querySelector(".vw-ub-zahl")).color,
|
||||
meinZahlEcht: getComputedStyle(meins.querySelector(".vw-ub-zahl")).color,
|
||||
meinKante: st1.borderLeftColor, seinKante: st2.borderLeftColor,
|
||||
meinBreite: st1.borderLeftWidth, seinBreite: st2.borderLeftWidth,
|
||||
};
|
||||
});
|
||||
pruefe(wo + ": die Zahlen haben unterschiedliche Farben",
|
||||
farben.meinZahlEcht !== farben.seinZahl,
|
||||
"meins " + farben.meinZahlEcht + " vs. beim Kunden " + farben.seinZahl);
|
||||
pruefe(wo + ": auch die Kanten unterscheiden sich",
|
||||
farben.meinKante !== farben.seinKante,
|
||||
farben.meinKante + " vs. " + farben.seinKante);
|
||||
/* Farbe allein trägt die Aussage nicht -- es muss auch eine sichtbare
|
||||
Kante geben, die man ohne Farbunterscheidung wahrnimmt. */
|
||||
pruefe(wo + ": beide tragen eine sichtbare Kante, nicht nur Farbe",
|
||||
parseFloat(farben.meinBreite) >= 3 && parseFloat(farben.seinBreite) >= 3,
|
||||
farben.meinBreite + " / " + farben.seinBreite);
|
||||
|
||||
/* ---- 2. Der Abbruch-Bereich ---- */
|
||||
await seite.click('[data-tab="projekte"]');
|
||||
await seite.waitForTimeout(500);
|
||||
await seite.locator(".vw-karte").first().click();
|
||||
await seite.waitForTimeout(700);
|
||||
|
||||
pruefe(wo + ": es gibt einen Abbruch-Knopf",
|
||||
await seite.locator("#vw-abbruch-auf").isVisible());
|
||||
pruefe(wo + ": das Formular ist zunaechst zu",
|
||||
!(await seite.locator("#vw-abbruch-form").isVisible()));
|
||||
pruefe(wo + ": beim Oeffnen der Projektseite wurde NICHT schon gerechnet",
|
||||
vorschauGeholt === 0, vorschauGeholt);
|
||||
|
||||
await seite.click("#vw-abbruch-auf");
|
||||
await seite.waitForTimeout(700);
|
||||
pruefe(wo + ": es klappt auf", await seite.locator("#vw-abbruch-form").isVisible());
|
||||
pruefe(wo + ": erst jetzt wird gerechnet", vorschauGeholt === 1, vorschauGeholt);
|
||||
|
||||
const rechnung = await seite.locator("#vw-abbruch-rechnung").innerText();
|
||||
pruefe(wo + ": der Fortschritt steht da", /5 von 20/.test(rechnung), rechnung.replace(/\n/g, " | "));
|
||||
pruefe(wo + ": das Bezahlte steht da", /600,00/.test(rechnung), rechnung.replace(/\n/g, " | "));
|
||||
pruefe(wo + ": der Erstattungsvorschlag steht da", /100,00/.test(rechnung), rechnung.replace(/\n/g, " | "));
|
||||
pruefe(wo + ": und ist ins Feld uebernommen",
|
||||
(await seite.locator("#vw-ab-erstattung").inputValue()) === "100.00",
|
||||
await seite.locator("#vw-ab-erstattung").inputValue());
|
||||
|
||||
/* ---- 3. Der eingetragene Betrag muss gelten ---- */
|
||||
await seite.fill("#vw-ab-erstattung", "250");
|
||||
await seite.selectOption("#vw-ab-grund2", "keine_zahlung");
|
||||
await seite.selectOption("#vw-ab-wer", "kunde");
|
||||
await seite.fill("#vw-ab-notiz", "Dreimal erinnert, keine Reaktion.");
|
||||
await seite.click("#vw-abbruch-los");
|
||||
await seite.waitForTimeout(900);
|
||||
|
||||
pruefe(wo + ": der Abbruch geht hinaus", !!gesendet, JSON.stringify(gesendet));
|
||||
pruefe(wo + ": mit dem GEAENDERTEN Betrag, nicht dem Vorschlag",
|
||||
gesendet && gesendet.erstattungEuro === 250, gesendet && gesendet.erstattungEuro);
|
||||
pruefe(wo + ": mit dem gewaehlten Grund",
|
||||
gesendet && gesendet.grund === "keine_zahlung", gesendet && gesendet.grund);
|
||||
pruefe(wo + ": und der Angabe, wer abbricht",
|
||||
gesendet && gesendet.wer === "kunde", gesendet && gesendet.wer);
|
||||
pruefe(wo + ": die Notiz kommt mit",
|
||||
gesendet && /Dreimal erinnert/.test(gesendet.notiz || ""), gesendet && gesendet.notiz);
|
||||
|
||||
const m = await seite.locator("#vw-meldung").innerText().catch(() => "");
|
||||
pruefe(wo + ": der Hinweis sagt, dass die Rueckzahlung von Hand erfolgt",
|
||||
/nicht selbst/.test(m), m);
|
||||
|
||||
/* ---- 4. Ein bereits abgebrochenes Projekt bekommt keinen Knopf ---- */
|
||||
PROJEKT.status = "abgebrochen";
|
||||
PROJEKT.abbruch_am = new Date().toISOString();
|
||||
PROJEKT.abbruch_grund = "keine_zahlung";
|
||||
PROJEKT.abbruch_wer = "kunde";
|
||||
PROJEKT.abbruch_erstattung_cent = 25000;
|
||||
await seite.click('[data-tab="projekte"]');
|
||||
await seite.waitForTimeout(500);
|
||||
await seite.locator(".vw-karte").first().click();
|
||||
await seite.waitForTimeout(700);
|
||||
pruefe(wo + ": ein abgebrochenes Projekt hat keinen Abbruch-Knopf mehr",
|
||||
(await seite.locator("#vw-abbruch-auf").count()) === 0);
|
||||
const info = await seite.locator(".vw-abbruch-info").innerText().catch(() => "");
|
||||
pruefe(wo + ": stattdessen steht da, was passiert ist",
|
||||
/nicht gezahlt/.test(info) && /250,00/.test(info), info.replace(/\n/g, " | "));
|
||||
PROJEKT.status = "design";
|
||||
delete PROJEKT.abbruch_am;
|
||||
|
||||
pruefe(wo + ": kein Skriptfehler", protokoll.length === 0, protokoll.join(" | "));
|
||||
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;
|
||||
+1
-1
@@ -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-v27";
|
||||
const CACHE_NAME = "dogfather-webdesign-v28";
|
||||
|
||||
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
|
||||
HTML-Datei in dieser Liste. */
|
||||
|
||||
@@ -123,6 +123,16 @@
|
||||
.vw-meld-weg:focus-visible { outline: 2px solid rgb(var(--wd-blau-rgb) / .95); outline-offset: 2px; }
|
||||
.vw-meld-alle { margin: .1rem 0 1rem; }
|
||||
|
||||
.vw-abbruch-rechnung {
|
||||
padding: .7rem .85rem; margin-bottom: .8rem;
|
||||
border-radius: var(--wd-radius-m, 14px);
|
||||
background: rgb(255 255 255 / .04);
|
||||
border: 1px solid rgb(255 255 255 / .08);
|
||||
}
|
||||
.wd .vw-abbruch-rechnung p { font-size: .86rem; margin: 0 0 .25rem; }
|
||||
.wd .vw-abbruch-vorschlag { color: var(--wd-gold); }
|
||||
.vw-abbruch-info { border-left: 3px solid #e78278; padding-left: .8rem; }
|
||||
|
||||
.wd .vw-ub-h {
|
||||
font-size: 1.05rem; font-weight: 650;
|
||||
margin: 1.9rem 0 .75rem;
|
||||
@@ -140,6 +150,44 @@
|
||||
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||
margin-bottom: .4rem;
|
||||
}
|
||||
/* ================================================================
|
||||
MEINS ODER SEINS — auf einen Blick unterscheidbar
|
||||
|
||||
Wunsch vom 24.08.2026: "ich will dass die kunden sachen auch in der
|
||||
verwaltungs seite von kacheln eine andere farbe haben wie meine
|
||||
damit ich sie gut unterscheide."
|
||||
|
||||
Umgesetzt über den TON, nicht über die Warnfarbe: Was bei mir liegt,
|
||||
bleibt im Markenblau — das ist meine Arbeit. Was beim Kunden liegt,
|
||||
bekommt Lila. Der Unterschied ist deutlich, ohne dass eine der
|
||||
beiden Seiten nach Alarm aussieht.
|
||||
|
||||
Warum nicht Rot/Grün: Die Warnstufen (gelb, rot) sind an das ALTER
|
||||
vergeben und müssen frei bleiben. Eine Kachel, die gleichzeitig
|
||||
"beim Kunden" und "seit acht Tagen überfällig" färben müsste,
|
||||
könnte nur eine der beiden Aussagen zeigen — und die Frist ist die
|
||||
wichtigere.
|
||||
|
||||
Zusätzlich zur Farbe eine Kante links. Farbe allein trägt die
|
||||
Aussage nicht: Wer sie nicht unterscheiden kann, sähe sonst zwei
|
||||
gleich aussehende Blöcke (WCAG 1.4.1). */
|
||||
#vw-ub-beikunde .vw-ub-kachel {
|
||||
border-color: rgb(var(--wd-lila-rgb) / .28);
|
||||
border-left: 3px solid rgb(var(--wd-lila-rgb) / .55);
|
||||
}
|
||||
#vw-ub-beikunde .vw-ub-kachel .vw-ub-zahl { color: var(--wd-lila); }
|
||||
#vw-ub-beikunde .vw-ub-kachel:hover:not(:disabled) {
|
||||
border-color: rgb(var(--wd-lila-rgb) / .5);
|
||||
}
|
||||
/* Auch die Null-Kachel bleibt zugeordnet — sonst wäre ausgerechnet
|
||||
die gute Nachricht die einzige ohne erkennbare Herkunft. */
|
||||
#vw-ub-beikunde .vw-ub-kachel--leer { border-left-color: rgb(var(--wd-lila-rgb) / .3); }
|
||||
/* Meine Seite bekommt spiegelbildlich eine blaue Kante, damit der
|
||||
Unterschied ein Gegensatz ist und nicht "eine Gruppe ist markiert". */
|
||||
#vw-ub-beimir .vw-ub-kachel { border-left: 3px solid rgb(var(--wd-blau-rgb) / .5); }
|
||||
#vw-ub-beimir .vw-ub-kachel--warn { border-left-color: #e9c46a; }
|
||||
#vw-ub-beimir .vw-ub-kachel--rot { border-left-color: #e78278; }
|
||||
|
||||
@media (max-width: 560px) {
|
||||
/* Zwei Spalten statt einer.
|
||||
|
||||
@@ -1333,6 +1381,11 @@
|
||||
shop: "Shop & umfangreiche Website", verwaltung: "Buchhaltungs- & Steuerverwaltung",
|
||||
betreuung: "Laufende Betreuung", unklar: "Noch unklar",
|
||||
};
|
||||
var ABBRUCH_TEXT = {
|
||||
keine_zahlung: "nicht gezahlt", kunde_abgesprungen: "Kunde abgesprungen",
|
||||
keine_mitwirkung: "keine Mitwirkung", einvernehmlich: "einvernehmlich",
|
||||
mein_ruecktritt: "mein Rücktritt", sonstiges: "sonstiges",
|
||||
};
|
||||
var VORHANDEN_TEXT = { ja: "vorhanden", teilweise: "teilweise", nein: "fehlt noch" };
|
||||
var BUDGET_TEXT = {
|
||||
"bis-500": "bis 500 €", "500-1000": "500 – 1.000 €",
|
||||
@@ -3193,10 +3246,118 @@
|
||||
'<button type="button" class="wd-btn wd-btn--zweit wd-btn--klein" id="vw-p-senden">Senden</button>' +
|
||||
"</div>" +
|
||||
|
||||
/* ----- ABBRECHEN -----
|
||||
|
||||
Steht ganz unten und zurückhaltend gestaltet: Es ist die
|
||||
seltenste und die endgültigste Handlung an einem Projekt. Ein
|
||||
gleich lauter Knopf zwischen den anderen lädt zum Verwechseln
|
||||
ein. */
|
||||
(p.status === "abgebrochen"
|
||||
? '<div class="vw-abschnitt vw-abbruch-info"><h3>Abgebrochen</h3>' +
|
||||
'<p class="wd-klein">' +
|
||||
(p.abbruch_am ? s(window.WD.datum(p.abbruch_am)) : "") +
|
||||
(p.abbruch_grund ? " · Grund: " + s(ABBRUCH_TEXT[p.abbruch_grund] || p.abbruch_grund) : "") +
|
||||
(p.abbruch_wer ? " · durch " + (p.abbruch_wer === "kunde" ? "den Kunden" : "dich") : "") +
|
||||
"</p>" +
|
||||
(p.abbruch_erstattung_cent
|
||||
? '<p class="wd-klein">Zu erstatten war: <strong>' +
|
||||
(p.abbruch_erstattung_cent / 100).toFixed(2).replace(".", ",") + " €</strong></p>"
|
||||
: '<p class="wd-klein wd-leise">Es war nichts zu erstatten.</p>') +
|
||||
"</div>"
|
||||
: '<div class="vw-abschnitt"><h3>Auftrag abbrechen</h3>' +
|
||||
'<p class="wd-klein wd-leise">Wenn der Kunde nicht zahlt, sich nicht mehr meldet ' +
|
||||
'oder selbst abspringt. Die Seite rechnet dir vor, wie viel Arbeit erledigt ist, ' +
|
||||
'und schlägt daraus einen Erstattungsbetrag vor.</p>' +
|
||||
'<button type="button" class="wd-btn wd-btn--still wd-btn--klein" id="vw-abbruch-auf">' +
|
||||
"Abbruch vorbereiten</button>" +
|
||||
'<div id="vw-abbruch-form" hidden style="margin-top:.9rem">' +
|
||||
'<div id="vw-abbruch-rechnung" class="vw-abbruch-rechnung">wird berechnet…</div>' +
|
||||
'<div class="wd-feld"><label for="vw-ab-grund2">Grund</label>' +
|
||||
'<select id="vw-ab-grund2">' +
|
||||
'<option value="keine_zahlung">Der Kunde hat nicht gezahlt</option>' +
|
||||
'<option value="kunde_abgesprungen">Der Kunde ist abgesprungen</option>' +
|
||||
'<option value="keine_mitwirkung">Keine Mitwirkung (Texte, Bilder, Rückmeldung)</option>' +
|
||||
'<option value="einvernehmlich">Einvernehmlich beendet</option>' +
|
||||
'<option value="mein_ruecktritt">Ich trete zurück</option>' +
|
||||
'<option value="sonstiges">Sonstiges</option>' +
|
||||
"</select></div>" +
|
||||
'<div class="wd-feld"><label for="vw-ab-wer">Wer bricht ab?</label>' +
|
||||
'<select id="vw-ab-wer">' +
|
||||
'<option value="kunde">Der Kunde</option>' +
|
||||
'<option value="dogfather">Ich</option>' +
|
||||
"</select></div>" +
|
||||
'<div class="wd-feld"><label for="vw-ab-erstattung">Zu erstatten (Euro)</label>' +
|
||||
'<input type="number" id="vw-ab-erstattung" min="0" step="0.01" inputmode="decimal" />' +
|
||||
'<span class="wd-feld-hilfe">Vorschlag aus dem Fortschritt — änderbar.</span></div>' +
|
||||
'<div class="wd-feld"><label for="vw-ab-notiz">Notiz ' +
|
||||
'<span class="wd-leise">optional, nur für dich</span></label>' +
|
||||
'<textarea id="vw-ab-notiz" rows="2"></textarea></div>' +
|
||||
'<button type="button" class="wd-btn wd-btn--klein" id="vw-abbruch-los">' +
|
||||
"Auftrag jetzt abbrechen</button>" +
|
||||
"</div></div>") +
|
||||
|
||||
"</div></div>";
|
||||
|
||||
blatt.querySelector(".vw-zu").addEventListener("click", schliesseDetail);
|
||||
|
||||
/* ---- Abbrechen ---- */
|
||||
var abbAuf = blatt.querySelector("#vw-abbruch-auf");
|
||||
if (abbAuf) {
|
||||
var abbForm = blatt.querySelector("#vw-abbruch-form");
|
||||
abbAuf.addEventListener("click", async function () {
|
||||
abbForm.hidden = !abbForm.hidden;
|
||||
abbAuf.textContent = abbForm.hidden ? "Abbruch vorbereiten" : "Doch nicht abbrechen";
|
||||
if (abbForm.hidden) return;
|
||||
/* Erst beim Aufklappen rechnen lassen. Die Zahlen sollen den
|
||||
Stand von JETZT zeigen, nicht den vom Öffnen der Projektseite
|
||||
— dazwischen kann man Punkte abgehakt haben. */
|
||||
try {
|
||||
var v = await api("/webdesign/admin/projekte/" + encodeURIComponent(p.id) + "/abbruch-vorschau");
|
||||
var r = v.rechnung || {};
|
||||
blatt.querySelector("#vw-abbruch-rechnung").innerHTML =
|
||||
"<p><strong>" + r.fertig + " von " + r.gesamt + " Schritten erledigt (" +
|
||||
r.prozent + " %)</strong></p>" +
|
||||
"<p>Bezahlt: " + s(r.bezahltText || "0,00 €") + " · davon verdient: " +
|
||||
s(r.behaltenText || "0,00 €") + "</p>" +
|
||||
'<p class="vw-abbruch-vorschlag">Vorschlag zur Erstattung: <strong>' +
|
||||
s(r.erstattungText || "0,00 €") + "</strong></p>";
|
||||
blatt.querySelector("#vw-ab-erstattung").value =
|
||||
((r.erstattungCent || 0) / 100).toFixed(2);
|
||||
} catch (f) {
|
||||
/* Ohne Vorschau trotzdem abbrechen können: Der Betrag lässt
|
||||
sich von Hand eintragen. Ein ausgefallener Rechendienst darf
|
||||
kein Projekt in der Liste festhalten. */
|
||||
blatt.querySelector("#vw-abbruch-rechnung").innerHTML =
|
||||
'<p class="wd-klein">Die Vorschau konnte nicht geladen werden: ' + s(f.message) +
|
||||
" Du kannst den Betrag von Hand eintragen.</p>";
|
||||
}
|
||||
});
|
||||
|
||||
blatt.querySelector("#vw-abbruch-los").addEventListener("click", async function (ev) {
|
||||
/* Nachfragen: Danach ist das Projekt abgebrochen, und offene
|
||||
Rechnungen sind storniert. */
|
||||
if (!confirm("Auftrag " + p.nummer + " wirklich abbrechen? Offene Rechnungen werden storniert.")) return;
|
||||
window.WD.knopfSperren(ev.target, true, "Wird abgebrochen…");
|
||||
try {
|
||||
var a = await api("/webdesign/admin/projekte/" + encodeURIComponent(p.id) + "/abbrechen", {
|
||||
method: "POST",
|
||||
body: {
|
||||
grund: blatt.querySelector("#vw-ab-grund2").value,
|
||||
wer: blatt.querySelector("#vw-ab-wer").value,
|
||||
erstattungEuro: Number(blatt.querySelector("#vw-ab-erstattung").value) || 0,
|
||||
notiz: blatt.querySelector("#vw-ab-notiz").value,
|
||||
},
|
||||
});
|
||||
schliesseDetail();
|
||||
await ladeProjekte();
|
||||
window.WD.meldung(meldung, a.hinweis || "Abgebrochen.", "ok");
|
||||
} catch (f) {
|
||||
window.WD.knopfSperren(ev.target, false);
|
||||
window.WD.meldung(meldung, "Abbrechen fehlgeschlagen: " + f.message, "fehler");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Zustand weiterschalten. */
|
||||
blatt.querySelectorAll("[data-weiter]").forEach(function (b) {
|
||||
b.addEventListener("click", function () {
|
||||
|
||||
Reference in New Issue
Block a user