Angebote in der Oberflaeche: schicken und im Portal zusagen
VERWALTUNG
Der Angebotsbereich steht in der Anfrage-Detailansicht ueber dem
direkten Annehmen. Der uebliche Weg ist: Angebot raus, Kunde sagt zu,
Projekt entsteht -- direkt anzunehmen ist der Sonderfall (man hat schon
telefoniert). Stuende der Sonderfall oben, waere er der naheliegende
Griff, und man verschenkte den Beleg, den eine Zusage im Portal erzeugt.
Vorbelegt sind Paket, Preis, Anzahlung, Laufzeit, Ablaufdatum und der
komplette Leistungstext. Zu tun bleibt: Paket bestaetigen, Preis
bestaetigen. Der Leistungstext ist eingeklappt statt die Seite zu
fluten, aber aenderbar.
Ein Paketwechsel laedt ALLES neu -- auch den Leistungstext. Ein
stehengebliebener Text vom vorigen Paket waere der schlimmste Fall: Er
ginge unbemerkt hinaus, und im Angebot staende etwas, das zum Preis
nicht passt. Genau das prueft der Test.
PORTAL
Die Angebotskarte steht ganz oben, vor allen Projekten: Ein Angebot ist
das Einzige im Portal, das eine ENTSCHEIDUNG verlangt -- alles andere
ist Auskunft. Der eingefrorene Leistungstext wird lesbar aufbereitet
(Zwischenueberschriften, Punkte), nicht als Textblock hingeworfen.
Vor der Zusage wird gefragt, und die Frage nennt die Folgen ('verbindlich',
'Anzahlung'). Nicht aus Foermlichkeit: Hier entsteht ein Vertrag, und ein
Knopf, der beim Danebentippen einen Auftrag ausloest, waere eine Falle.
Beim Ablehnen wird NICHT gefragt -- das ist folgenlos.
ZWEI FEHLER GEFUNDEN, BEIDE IM NORMALFALL
- Die Angebotskarten standen im Zweig 'es gibt Projekte'. Ein Kunde mit
offenem Angebot hat aber typischerweise noch KEIN Projekt -- genau der
Normalfall -- und saeh damit nichts. Der Server lieferte das Angebot,
die Seite zeichnete es nur nie. Nur im Browsertest sichtbar.
- Eine Antwort ohne 'nachrichten' liess das Postfach werfen, und weil der
Fehler den Aufbau abbrach, erschien danach GAR NICHTS mehr -- auch nicht
das Angebot, das ueber allem stehen sollte. Dieselbe Sorte wie zuvor im
Cockpit: Ein halber Server ist ein realistischer Fall.
Neun Sprachschluessel in fuenf Sprachen ergaenzt.
Geprueft mit 54 Pruefungen auf Computer und Handy, die messen, was
tatsaechlich an den Server geht. Alle bestehenden weiter gruen.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -186,6 +186,43 @@ window.I18N_WD = {
|
|||||||
pt: "O Dogfather está a tratar",
|
pt: "O Dogfather está a tratar",
|
||||||
},
|
},
|
||||||
po_richttermin: { de: "Richttermin", "de-CH": "Richttermin", en: "Target date", fr: "Date cible", pt: "Data prevista" },
|
po_richttermin: { de: "Richttermin", "de-CH": "Richttermin", en: "Target date", fr: "Date cible", pt: "Data prevista" },
|
||||||
|
po_ag_neu: { de: "Angebot", "de-CH": "Angebot", en: "Offer", fr: "Devis", pt: "Proposta" },
|
||||||
|
po_ag_angenommen: { de: "angenommen", "de-CH": "angenommen", en: "accepted", fr: "accepté", pt: "aceite" },
|
||||||
|
po_ag_abgelaufen: { de: "abgelaufen", "de-CH": "abgelaufen", en: "expired", fr: "expiré", pt: "expirado" },
|
||||||
|
po_ag_anzahlung: {
|
||||||
|
de: "Anzahlung zum Start: {betrag} (30 %)",
|
||||||
|
"de-CH": "Anzahlung zum Start: {betrag} (30 %)",
|
||||||
|
en: "Deposit to start: {betrag} (30%)",
|
||||||
|
fr: "Acompte au démarrage : {betrag} (30 %)",
|
||||||
|
pt: "Sinal para começar: {betrag} (30 %)" },
|
||||||
|
po_ag_dauer: {
|
||||||
|
de: "Umsetzung in etwa {n} Werktagen ab Zahlungseingang",
|
||||||
|
"de-CH": "Umsetzung in etwa {n} Werktagen ab Zahlungseingang",
|
||||||
|
en: "Delivery in about {n} working days from receipt of payment",
|
||||||
|
fr: "Réalisation en environ {n} jours ouvrables après réception du paiement",
|
||||||
|
pt: "Execução em cerca de {n} dias úteis após a receção do pagamento" },
|
||||||
|
po_ag_gueltig: {
|
||||||
|
de: "Gültig bis {datum}",
|
||||||
|
"de-CH": "Gültig bis {datum}",
|
||||||
|
en: "Valid until {datum}", fr: "Valable jusqu’au {datum}", pt: "Válido até {datum}" },
|
||||||
|
po_ag_zusagen: {
|
||||||
|
de: "Angebot annehmen", "de-CH": "Angebot annehmen",
|
||||||
|
en: "Accept offer", fr: "Accepter le devis", pt: "Aceitar proposta" },
|
||||||
|
po_ag_ablehnen: {
|
||||||
|
de: "Doch nicht", "de-CH": "Doch nicht",
|
||||||
|
en: "Not right now", fr: "Pas pour l’instant", pt: "Agora não" },
|
||||||
|
po_ag_abgelaufen_text: {
|
||||||
|
de: "Dieses Angebot ist abgelaufen. Schreib mir kurz — ich mache dir gern ein neues.",
|
||||||
|
"de-CH": "Dieses Angebot ist abgelaufen. Schreib mir kurz — ich mache dir gern ein neues.",
|
||||||
|
en: "This offer has expired. Just message me and I will gladly make you a new one.",
|
||||||
|
fr: "Ce devis a expiré. Écrivez-moi, je vous en établirai volontiers un nouveau.",
|
||||||
|
pt: "Esta proposta expirou. Escreva-me e farei com gosto uma nova." },
|
||||||
|
po_ag_sicher: {
|
||||||
|
de: "Angebot verbindlich annehmen? Danach lege ich dein Projekt an und du bekommst die Anzahlung als Rechnung.",
|
||||||
|
"de-CH": "Angebot verbindlich annehmen? Danach lege ich dein Projekt an und du bekommst die Anzahlung als Rechnung.",
|
||||||
|
en: "Accept this offer bindingly? I will then set up your project and you will receive the deposit invoice.",
|
||||||
|
fr: "Accepter ce devis de manière ferme ? Je créerai ensuite votre projet et vous recevrez la facture d’acompte.",
|
||||||
|
pt: "Aceitar esta proposta de forma vinculativa? Depois crio o seu projeto e receberá a fatura do sinal." },
|
||||||
po_liefertermin: { de: "Liefertermin", "de-CH": "Liefertermin", en: "Delivery date", fr: "Date de livraison", pt: "Data de entrega" },
|
po_liefertermin: { de: "Liefertermin", "de-CH": "Liefertermin", en: "Delivery date", fr: "Date de livraison", pt: "Data de entrega" },
|
||||||
po_noch_tage: { de: "noch {n} Werktage", "de-CH": "noch {n} Werktage", en: "{n} working days left", fr: "encore {n} jours ouvrables", pt: "faltam {n} dias úteis" },
|
po_noch_tage: { de: "noch {n} Werktage", "de-CH": "noch {n} Werktage", en: "{n} working days left", fr: "encore {n} jours ouvrables", pt: "faltam {n} dias úteis" },
|
||||||
po_noch_ein_tag: { de: "noch 1 Werktag", "de-CH": "noch 1 Werktag", en: "1 working day left", fr: "encore 1 jour ouvrable", pt: "falta 1 dia útil" },
|
po_noch_ein_tag: { de: "noch 1 Werktag", "de-CH": "noch 1 Werktag", en: "1 working day left", fr: "encore 1 jour ouvrable", pt: "falta 1 dia útil" },
|
||||||
|
|||||||
+127
-2
@@ -203,6 +203,70 @@
|
|||||||
return stuecke.join("");
|
return stuecke.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------
|
||||||
|
ANGEBOTE
|
||||||
|
|
||||||
|
Stehen GANZ oben, vor allen Projekten. Ein Angebot ist das Einzige
|
||||||
|
im Portal, das eine Entscheidung verlangt — alles andere ist
|
||||||
|
Auskunft. Stünde es zwischen den laufenden Projekten, sähe man es
|
||||||
|
erst nach dem Scrollen, und der Zusage-Knopf wäre eine Fussnote.
|
||||||
|
|
||||||
|
Der Leistungstext kommt FERTIG vom Server (er wurde beim Absenden
|
||||||
|
eingefroren). Hier wird er nur noch dargestellt: **fett** wird zur
|
||||||
|
Zwischenüberschrift, Zeilen mit · zu Punkten. Absichtlich keine
|
||||||
|
allgemeine Auszeichnungssprache — es soll genau das erscheinen, was
|
||||||
|
geschickt wurde, und nichts, was ein Text zufällig auslöst.
|
||||||
|
--------------------------------------------------------------------- */
|
||||||
|
function leistungenZeichnen(text) {
|
||||||
|
if (!text) return "";
|
||||||
|
return text.split("\n").map(function (z) {
|
||||||
|
var t = z.trim();
|
||||||
|
if (!t) return "";
|
||||||
|
var fett = t.match(/^\*\*(.+)\*\*$/);
|
||||||
|
if (fett) return '<h4 class="po-ag-gruppe">' + s(fett[1]) + "</h4>";
|
||||||
|
if (t.indexOf("\u00b7") === 0) return '<li>' + s(t.slice(1).trim()) + "</li>";
|
||||||
|
return "<p>" + s(t) + "</p>";
|
||||||
|
}).join("").replace(/(<li>.*?<\/li>)+/g, function (m) {
|
||||||
|
return '<ul class="po-ag-liste">' + m + "</ul>";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function angebotsKarten(angebote) {
|
||||||
|
if (!angebote || !angebote.length) return "";
|
||||||
|
return angebote.map(function (a) {
|
||||||
|
var offen = a.status === "gesendet";
|
||||||
|
return '<div class="wd-karte po-angebot' + (offen ? " po-angebot--offen" : "") +
|
||||||
|
'" data-angebot="' + s(a.id) + '">' +
|
||||||
|
'<span class="po-ag-marke">' +
|
||||||
|
(a.status === "angenommen" ? s(window.WD.t("po_ag_angenommen"))
|
||||||
|
: a.abgelaufen ? s(window.WD.t("po_ag_abgelaufen"))
|
||||||
|
: s(window.WD.t("po_ag_neu"))) + "</span>" +
|
||||||
|
"<h3>" + s(a.titel) + "</h3>" +
|
||||||
|
'<p class="po-ag-preis">' + s(a.preis_text) + "</p>" +
|
||||||
|
'<p class="wd-klein wd-leise">' + s(window.WD.t("po_ag_anzahlung"))
|
||||||
|
.replace("{betrag}", s(a.anzahlung_text)) + "</p>" +
|
||||||
|
(a.laufzeit_werktage
|
||||||
|
? '<p class="wd-klein wd-leise">' + s(window.WD.t("po_ag_dauer"))
|
||||||
|
.replace("{n}", a.laufzeit_werktage) + "</p>"
|
||||||
|
: "") +
|
||||||
|
'<div class="po-ag-leistungen">' + leistungenZeichnen(a.leistungen_text) + "</div>" +
|
||||||
|
(a.hinweis_text ? '<p class="wd-klein">' + s(a.hinweis_text) + "</p>" : "") +
|
||||||
|
(offen && a.annehmbar
|
||||||
|
? '<p class="wd-klein wd-leise">' + s(window.WD.t("po_ag_gueltig"))
|
||||||
|
.replace("{datum}", s(a.gueltig_bis || "")) + "</p>" +
|
||||||
|
'<div class="po-ag-knoepfe">' +
|
||||||
|
'<button type="button" class="wd-btn wd-btn--haupt" data-zusagen="' + s(a.id) + '">' +
|
||||||
|
s(window.WD.t("po_ag_zusagen")) + "</button>" +
|
||||||
|
'<button type="button" class="wd-btn wd-btn--still wd-btn--klein" data-ablehnen="' +
|
||||||
|
s(a.id) + '">' + s(window.WD.t("po_ag_ablehnen")) + "</button>" +
|
||||||
|
"</div>"
|
||||||
|
: a.abgelaufen
|
||||||
|
? '<p class="wd-klein">' + s(window.WD.t("po_ag_abgelaufen_text")) + "</p>"
|
||||||
|
: "") +
|
||||||
|
"</div>";
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
function wartetMarke(wartet) {
|
function wartetMarke(wartet) {
|
||||||
if (wartet !== "kunde" && wartet !== "dogfather") return "";
|
if (wartet !== "kunde" && wartet !== "dogfather") return "";
|
||||||
return '<span class="po-wartet po-wartet--' + wartet + '">' +
|
return '<span class="po-wartet po-wartet--' + wartet + '">' +
|
||||||
@@ -359,6 +423,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html += "<h2>" + s(window.WD.t("po_projekte")) + "</h2>";
|
html += "<h2>" + s(window.WD.t("po_projekte")) + "</h2>";
|
||||||
|
/* Angebote stehen VOR der Weiche, nicht in einem ihrer Zweige.
|
||||||
|
|
||||||
|
Der erste Entwurf setzte sie in den Zweig "es gibt Projekte".
|
||||||
|
Ein Kunde mit einem offenen Angebot hat aber typischerweise noch
|
||||||
|
KEIN Projekt -- genau der Normalfall -- und saeh damit nichts.
|
||||||
|
Aufgefallen erst im Browsertest: Der Server lieferte das Angebot,
|
||||||
|
die Seite zeichnete es nur nie. */
|
||||||
|
html += angebotsKarten(d.angebote);
|
||||||
|
|
||||||
if (!d.projekte.length) {
|
if (!d.projekte.length) {
|
||||||
/* Ein leerer Bereich mit einem Satz ist eine verpasste Gelegenheit.
|
/* Ein leerer Bereich mit einem Satz ist eine verpasste Gelegenheit.
|
||||||
Wer hier zum ersten Mal landet, hat gerade sein Passwort gesetzt
|
Wer hier zum ersten Mal landet, hat gerade sein Passwort gesetzt
|
||||||
@@ -502,10 +575,20 @@
|
|||||||
'<div class="wd-karte">' +
|
'<div class="wd-karte">' +
|
||||||
'<p class="wd-klein wd-leise" style="margin-top:0">' + s(window.WD.t("po_postfach_text")) + "</p>";
|
'<p class="wd-klein wd-leise" style="margin-top:0">' + s(window.WD.t("po_postfach_text")) + "</p>";
|
||||||
|
|
||||||
if (!d.nachrichten.length) {
|
/* Fehlt die Liste ganz, wird daraus eine leere -- statt eines
|
||||||
|
Fehlers, der die ganze Portalseite mitreisst.
|
||||||
|
|
||||||
|
Aufgefallen am 24.08.2026: Eine Antwort ohne 'nachrichten' liess
|
||||||
|
das Postfach werfen, und weil der Fehler den Aufbau abbrach,
|
||||||
|
erschien danach GAR NICHTS mehr -- auch nicht das Angebot, das
|
||||||
|
ueber allem stehen sollte. Dieselbe Sorte Fehler wie zuvor im
|
||||||
|
Cockpit der Verwaltung: Ein halber Server ist ein realistischer
|
||||||
|
Fall, und dann soll der Rest der Seite trotzdem stehen. */
|
||||||
|
var nachrichten = d.nachrichten || [];
|
||||||
|
if (!nachrichten.length) {
|
||||||
html += '<p class="wd-leise wd-klein">' + s(window.WD.t("po_postfach_leer")) + "</p>";
|
html += '<p class="wd-leise wd-klein">' + s(window.WD.t("po_postfach_leer")) + "</p>";
|
||||||
} else {
|
} else {
|
||||||
html += '<div class="po-pf-verlauf" id="po-pf-verlauf">' + d.nachrichten.map(function (n) {
|
html += '<div class="po-pf-verlauf" id="po-pf-verlauf">' + nachrichten.map(function (n) {
|
||||||
var vonMir = n.autor === "kunde";
|
var vonMir = n.autor === "kunde";
|
||||||
return '<div class="po-nachricht po-nachricht--' + (vonMir ? "ich" : "du") + '">' +
|
return '<div class="po-nachricht po-nachricht--' + (vonMir ? "ich" : "du") + '">' +
|
||||||
'<div class="po-nachricht-kopf">' +
|
'<div class="po-nachricht-kopf">' +
|
||||||
@@ -841,4 +924,46 @@
|
|||||||
|
|
||||||
if (window.WD) start();
|
if (window.WD) start();
|
||||||
else document.addEventListener("wd-bereit", start);
|
else document.addEventListener("wd-bereit", start);
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------
|
||||||
|
ZUSAGE UND ABLEHNUNG
|
||||||
|
|
||||||
|
Über Ereignis-Delegation am Dokument, weil die Karten bei jedem
|
||||||
|
Laden neu entstehen — einzeln angehängte Zuhörer wären nach der
|
||||||
|
ersten Aktualisierung verschwunden.
|
||||||
|
|
||||||
|
Vor der Zusage wird gefragt. Nicht aus Förmlichkeit: Hier entsteht
|
||||||
|
ein Vertrag, und ein Knopf, der beim Danebentippen einen Auftrag
|
||||||
|
auslöst, wäre eine Falle. Beim Ablehnen wird NICHT gefragt — das
|
||||||
|
ist folgenlos und jederzeit umkehrbar (man schreibt einfach).
|
||||||
|
--------------------------------------------------------------------- */
|
||||||
|
document.addEventListener("click", async function (e) {
|
||||||
|
var ja = e.target.closest ? e.target.closest("[data-zusagen]") : null;
|
||||||
|
var nein = e.target.closest ? e.target.closest("[data-ablehnen]") : null;
|
||||||
|
if (!ja && !nein) return;
|
||||||
|
|
||||||
|
var id = (ja || nein).dataset[ja ? "zusagen" : "ablehnen"];
|
||||||
|
var knopf = ja || nein;
|
||||||
|
|
||||||
|
if (ja && !confirm(window.WD.t("po_ag_sicher"))) return;
|
||||||
|
|
||||||
|
window.WD.knopfSperren(knopf, true, "…");
|
||||||
|
try {
|
||||||
|
var r = await window.WD.api(
|
||||||
|
"/webdesign/portal/angebote/" + encodeURIComponent(id) + (ja ? "/zusagen" : "/ablehnen"),
|
||||||
|
{ method: "POST", body: {} });
|
||||||
|
if (ja) {
|
||||||
|
/* Nach der Zusage vollständig neu laden: Es ist ein Projekt
|
||||||
|
entstanden, eine Rechnung, und das Angebot hat einen anderen
|
||||||
|
Zustand. Einzelne Stellen nachzuziehen hiesse, jede davon
|
||||||
|
einzeln richtig zu treffen. */
|
||||||
|
alert(r.hinweis || "Danke!");
|
||||||
|
}
|
||||||
|
location.reload();
|
||||||
|
} catch (f) {
|
||||||
|
window.WD.knopfSperren(knopf, false);
|
||||||
|
alert(f.message || "Das hat leider nicht geklappt.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
/* =====================================================================
|
||||||
|
pruef-angebot.mjs — Angebot rausschicken und im Portal zusagen
|
||||||
|
|
||||||
|
Zwei Seiten, ein Vorgang. Geprüft wird jeweils, WAS an den Server
|
||||||
|
geht — ein Dialog, der gut aussieht und den falschen Preis sendet,
|
||||||
|
besteht jede Sichtprüfung.
|
||||||
|
|
||||||
|
Der heikelste Punkt: Wechselt man das Paket, müssen Preis UND
|
||||||
|
Leistungstext neu geladen werden. Ein stehengebliebener Text vom
|
||||||
|
vorigen Paket ginge unbemerkt hinaus — im Angebot stünde dann etwas,
|
||||||
|
das zum Preis nicht passt.
|
||||||
|
===================================================================== */
|
||||||
|
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(4361, r));
|
||||||
|
|
||||||
|
const ergebnisse = [];
|
||||||
|
const pruefe = (n, ok, z) => {
|
||||||
|
ergebnisse.push({ n, ok });
|
||||||
|
console.log((ok ? " ok " : " FEHL ") + n + (z !== undefined ? " -> " + z : ""));
|
||||||
|
};
|
||||||
|
|
||||||
|
const LEIST_WEBSITE = "**Aufbau**\n· Startseite gestalten\n· Unterseiten anlegen\n\n"
|
||||||
|
+ "**Was ich von dir brauche**\n· Texte\n· Bilder";
|
||||||
|
const LEIST_SHOP = "**Shop**\n· Produkte anlegen\n· Bezahlung einrichten\n\n"
|
||||||
|
+ "**Was ich von dir brauche**\n· Produktdaten";
|
||||||
|
|
||||||
|
const ANFRAGE = {
|
||||||
|
id: "t1", nummer: "A-2608-0007", name: "Anna Muster", email: "[email protected]",
|
||||||
|
paket: "website", status: "neu", sprache: "de", archiviert: 0, kunde_id: null,
|
||||||
|
details: "Bitte modern.", erstellt_am: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------- TEIL 1: Verwaltung ---------- */
|
||||||
|
for (const [wo, angaben] of [
|
||||||
|
["Computer", { viewport: { width: 1440, height: 950 } }],
|
||||||
|
["Handy", { ...devices["iPhone 14 Pro"] }],
|
||||||
|
]) {
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const seite = await (await browser.newContext(angaben)).newPage();
|
||||||
|
const protokoll = [];
|
||||||
|
seite.on("pageerror", (e) => protokoll.push(e.message));
|
||||||
|
let gesendet = null, vorschauen = [];
|
||||||
|
|
||||||
|
await seite.route("**/webdesign/admin/**", (route) => {
|
||||||
|
const u = new URL(route.request().url());
|
||||||
|
if (u.pathname.endsWith("/angebot-vorschau")) {
|
||||||
|
const paket = u.searchParams.get("paket") || "website";
|
||||||
|
vorschauen.push(paket);
|
||||||
|
return route.fulfill({ json: { ok: true, paket,
|
||||||
|
titel: "Website für Anna Muster",
|
||||||
|
preisEuro: paket === "shop" ? 2990 : 1490,
|
||||||
|
anzahlungEuro: paket === "shop" ? 897 : 447,
|
||||||
|
werktage: paket === "shop" ? 30 : 20,
|
||||||
|
gueltigBis: "2026-09-21",
|
||||||
|
leistungen: paket === "shop" ? LEIST_SHOP : LEIST_WEBSITE,
|
||||||
|
sprache: "de", kunde: { name: "Anna Muster" }, schonUnterwegs: null } });
|
||||||
|
}
|
||||||
|
if (u.pathname.endsWith("/angebot")) {
|
||||||
|
gesendet = route.request().postDataJSON();
|
||||||
|
return route.fulfill({ json: { ok: true, id: "ag1", nummer: "AG-2608-0001",
|
||||||
|
preisText: "2.990,00 €", anzahlungText: "897,00 €", gueltigBis: "2026-09-21",
|
||||||
|
hinweis: "Das Angebot liegt im Portal des Kunden." } });
|
||||||
|
}
|
||||||
|
if (u.pathname.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: {}, meldungen: [], wartetAufAnzahlung: [] } });
|
||||||
|
}
|
||||||
|
if (u.pathname.includes("/admin/anfragen/t1")) {
|
||||||
|
return route.fulfill({ json: { ok: true, anfrage: ANFRAGE, notizen: [] } });
|
||||||
|
}
|
||||||
|
if (u.pathname.includes("/admin/anfragen")) return route.fulfill({ json: { ok: true, anfragen: [ANFRAGE] } });
|
||||||
|
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:4361/webdesign/verwaltung.html");
|
||||||
|
await seite.waitForTimeout(700);
|
||||||
|
console.log("\n=== Verwaltung · " + wo + " ===");
|
||||||
|
|
||||||
|
await seite.click('[data-tab="anfragen"]');
|
||||||
|
await seite.waitForTimeout(500);
|
||||||
|
await seite.locator(".vw-karte").first().click();
|
||||||
|
await seite.waitForTimeout(900);
|
||||||
|
|
||||||
|
pruefe(wo + ": der Angebotsbereich ist da", await seite.locator("#vw-angebot").isVisible());
|
||||||
|
pruefe(wo + ": die Vorschau wurde geholt", vorschauen.length === 1, vorschauen.join(","));
|
||||||
|
pruefe(wo + ": das Paket ist aus der Anfrage vorbelegt",
|
||||||
|
(await seite.locator("#vw-ag-paket").inputValue()) === "website",
|
||||||
|
await seite.locator("#vw-ag-paket").inputValue());
|
||||||
|
pruefe(wo + ": der Preis ist vorbelegt",
|
||||||
|
(await seite.locator("#vw-ag-preis").inputValue()) === "1490",
|
||||||
|
await seite.locator("#vw-ag-preis").inputValue());
|
||||||
|
pruefe(wo + ": die Anzahlung wird angezeigt",
|
||||||
|
/447,00/.test(await seite.locator("#vw-ag-anzahlung").innerText()),
|
||||||
|
await seite.locator("#vw-ag-anzahlung").innerText());
|
||||||
|
pruefe(wo + ": Laufzeit und Ablauf stehen da",
|
||||||
|
/20 Werktage/.test(await seite.locator("#vw-ag-gueltig").innerText()),
|
||||||
|
await seite.locator("#vw-ag-gueltig").innerText());
|
||||||
|
/* Der Leistungstext ist vorbelegt -- das ist der Kern von "so wenig
|
||||||
|
wie moeglich schreiben". */
|
||||||
|
const leist = await seite.locator("#vw-ag-leistungen").inputValue();
|
||||||
|
pruefe(wo + ": der Leistungsumfang ist vorbelegt", leist.includes("Startseite"), leist.slice(0, 50));
|
||||||
|
pruefe(wo + ": und ist eingeklappt, statt die Seite zu fluten",
|
||||||
|
!(await seite.locator("#vw-ag-leistungen").isVisible()));
|
||||||
|
|
||||||
|
/* ---- Paketwechsel: der heikle Punkt ---- */
|
||||||
|
await seite.selectOption("#vw-ag-paket", "shop");
|
||||||
|
await seite.waitForTimeout(700);
|
||||||
|
pruefe(wo + ": ein Paketwechsel laedt neu", vorschauen.length === 2, vorschauen.join(","));
|
||||||
|
pruefe(wo + ": der Preis passt sich an",
|
||||||
|
(await seite.locator("#vw-ag-preis").inputValue()) === "2990",
|
||||||
|
await seite.locator("#vw-ag-preis").inputValue());
|
||||||
|
const leist2 = await seite.locator("#vw-ag-leistungen").inputValue();
|
||||||
|
pruefe(wo + ": und der Leistungstext wechselt MIT",
|
||||||
|
leist2.includes("Produkte anlegen") && !leist2.includes("Startseite"), leist2.slice(0, 60));
|
||||||
|
|
||||||
|
await seite.click("#vw-ag-senden");
|
||||||
|
await seite.waitForTimeout(900);
|
||||||
|
pruefe(wo + ": das Angebot geht hinaus", !!gesendet, JSON.stringify(gesendet).slice(0, 90));
|
||||||
|
pruefe(wo + ": mit dem gewechselten Paket", gesendet && gesendet.paket === "shop", gesendet && gesendet.paket);
|
||||||
|
pruefe(wo + ": mit dem passenden Preis", gesendet && gesendet.preisEuro === 2990, gesendet && gesendet.preisEuro);
|
||||||
|
pruefe(wo + ": und dem passenden Leistungstext",
|
||||||
|
gesendet && /Produkte anlegen/.test(gesendet.leistungen || ""),
|
||||||
|
(gesendet && gesendet.leistungen || "").slice(0, 40));
|
||||||
|
|
||||||
|
const m = await seite.locator("#vw-meldung").innerText().catch(() => "");
|
||||||
|
pruefe(wo + ": die Meldung nennt Nummer und Betraege",
|
||||||
|
/AG-2608-0001/.test(m) && /2.990,00/.test(m) && /897,00/.test(m), m);
|
||||||
|
pruefe(wo + ": kein Skriptfehler", protokoll.length === 0, protokoll.join(" | "));
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- TEIL 2: Portal ---------- */
|
||||||
|
const ANGEBOT = {
|
||||||
|
id: "ag1", nummer: "AG-2608-0001", paket: "shop", titel: "Onlineshop für Anna Muster",
|
||||||
|
preis_cent: 299000, anzahlung_cent: 89700, waehrung: "EUR", laufzeit_werktage: 30,
|
||||||
|
leistungen_text: LEIST_SHOP, hinweis_text: null, status: "gesendet",
|
||||||
|
gueltig_bis: "2026-09-21", preis_text: "2.990,00 €", anzahlung_text: "897,00 €",
|
||||||
|
annehmbar: true, abgelaufen: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [wo, angaben] of [
|
||||||
|
["Computer", { viewport: { width: 1440, height: 950 } }],
|
||||||
|
["Handy", { ...devices["iPhone 14 Pro"] }],
|
||||||
|
]) {
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const seite = await (await browser.newContext(angaben)).newPage();
|
||||||
|
const protokoll = [];
|
||||||
|
seite.on("pageerror", (e) => protokoll.push(e.message));
|
||||||
|
let zugesagt = null, gefragt = null;
|
||||||
|
/* Nur den ERSTEN Dialog festhalten. Es erscheinen zwei: die
|
||||||
|
Rueckfrage vor der Zusage und danach die Bestaetigung. Die erste
|
||||||
|
Fassung ueberschrieb die Rueckfrage mit der Bestaetigung und pruefte
|
||||||
|
damit den falschen Text. */
|
||||||
|
seite.on("dialog", (d) => { if (gefragt === null) gefragt = d.message(); d.accept(); });
|
||||||
|
|
||||||
|
await seite.route("**/webdesign/portal/**", (route) => {
|
||||||
|
const u = new URL(route.request().url()).pathname;
|
||||||
|
if (u.endsWith("/zusagen")) {
|
||||||
|
zugesagt = u;
|
||||||
|
return route.fulfill({ json: { ok: true, projektNummer: "P-2608-0001",
|
||||||
|
anzahlungText: "897,00 €", hinweis: "Danke! Dein Projekt ist angelegt." } });
|
||||||
|
}
|
||||||
|
if (u.endsWith("/uebersicht")) {
|
||||||
|
return route.fulfill({ json: { ok: true,
|
||||||
|
kunde: { name: "Anna Muster", email: "[email protected]", sprache: "de" },
|
||||||
|
angebote: [ANGEBOT], projekte: [], abos: [], offeneZahlungen: [],
|
||||||
|
dateien: [], nachrichten: [] } });
|
||||||
|
}
|
||||||
|
/* Das Postfach braucht seine eigene Antwort. Ohne sie fiel es in
|
||||||
|
diesen Auffang, bekam ein blankes {ok:true} -- und riss frueher
|
||||||
|
die ganze Seite mit. Seit dem 24.08.2026 haelt es das aus; die
|
||||||
|
richtige Antwort steht hier trotzdem, damit der Test den
|
||||||
|
Normalfall prueft und nicht den Notfall. */
|
||||||
|
if (u.endsWith("/postfach")) return route.fulfill({ json: { ok: true, nachrichten: [] } });
|
||||||
|
return route.fulfill({ json: { ok: true, nachrichten: [], dateien: [] } });
|
||||||
|
});
|
||||||
|
await seite.addInitScript(() => {
|
||||||
|
sessionStorage.setItem("wd-portal-token", "t");
|
||||||
|
sessionStorage.setItem("wd-sitzung-aktiv", "1");
|
||||||
|
});
|
||||||
|
|
||||||
|
await seite.goto("http://127.0.0.1:4361/webdesign/portal.html");
|
||||||
|
await seite.waitForTimeout(900);
|
||||||
|
console.log("\n=== Portal · " + wo + " ===");
|
||||||
|
|
||||||
|
const karte = seite.locator("[data-angebot]").first();
|
||||||
|
const daIst = await karte.count();
|
||||||
|
pruefe(wo + ": die Angebotskarte erscheint", daIst === 1, daIst);
|
||||||
|
if (daIst) {
|
||||||
|
const txt = await karte.innerText();
|
||||||
|
pruefe(wo + ": der Preis steht gross da", /2\.990,00/.test(txt), txt.split("\n").slice(0, 4).join(" | "));
|
||||||
|
pruefe(wo + ": die Anzahlung wird genannt", /897,00/.test(txt));
|
||||||
|
pruefe(wo + ": die Dauer wird genannt", /30 Werktagen/.test(txt), txt.match(/.*Werktagen.*/) || "");
|
||||||
|
pruefe(wo + ": der Leistungsumfang ist lesbar aufbereitet",
|
||||||
|
(await karte.locator(".po-ag-gruppe").count()) >= 1
|
||||||
|
&& (await karte.locator(".po-ag-liste li").count()) >= 2,
|
||||||
|
"Gruppen " + (await karte.locator(".po-ag-gruppe").count())
|
||||||
|
+ ", Punkte " + (await karte.locator(".po-ag-liste li").count()));
|
||||||
|
pruefe(wo + ": die Gueltigkeit steht dabei", /21\.09\.2026|2026-09-21/.test(txt), txt);
|
||||||
|
|
||||||
|
/* Vor der Zusage MUSS gefragt werden -- hier entsteht ein Vertrag. */
|
||||||
|
await seite.locator("[data-zusagen]").first().click();
|
||||||
|
await seite.waitForTimeout(900);
|
||||||
|
pruefe(wo + ": vor der Zusage wird gefragt", !!gefragt, String(gefragt).slice(0, 70));
|
||||||
|
pruefe(wo + ": die Frage nennt die Folgen",
|
||||||
|
/verbindlich/.test(gefragt || "") && /Anzahlung/.test(gefragt || ""), gefragt);
|
||||||
|
pruefe(wo + ": die Zusage geht hinaus", !!zugesagt, String(zugesagt));
|
||||||
|
}
|
||||||
|
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;
|
||||||
@@ -47,6 +47,48 @@
|
|||||||
entspannt laeuft, soll keine Alarmfarbe sehen. Erst wenn es eng
|
entspannt laeuft, soll keine Alarmfarbe sehen. Erst wenn es eng
|
||||||
wird, kommt Farbe dazu -- und der TEXT sagt es ebenfalls, damit die
|
wird, kommt Farbe dazu -- und der TEXT sagt es ebenfalls, damit die
|
||||||
Aussage auch ankommt, wenn man Farben nicht unterscheiden kann. */
|
Aussage auch ankommt, wenn man Farben nicht unterscheiden kann. */
|
||||||
|
/* ---------- Angebot ----------
|
||||||
|
|
||||||
|
Deutlich hervorgehoben, aber nicht schreiend: Es ist das Einzige im
|
||||||
|
Portal, das eine Entscheidung verlangt, und es soll auffallen —
|
||||||
|
ohne dass die Seite dabei nach Werbung aussieht. Deshalb ein
|
||||||
|
ruhiger Goldton am Rand statt einer farbigen Fläche. */
|
||||||
|
.po-angebot { position: relative; }
|
||||||
|
.po-angebot--offen {
|
||||||
|
border-color: rgb(var(--wd-gold-rgb) / .45);
|
||||||
|
box-shadow: 0 0 0 1px rgb(var(--wd-gold-rgb) / .12), var(--wd-hoehe-1);
|
||||||
|
}
|
||||||
|
.po-ag-marke {
|
||||||
|
display: inline-block; margin-bottom: .5rem;
|
||||||
|
padding: .16rem .6rem; border-radius: 999px;
|
||||||
|
font-size: .74rem; font-weight: 800; letter-spacing: .04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: rgb(var(--wd-gold-rgb) / .16); color: var(--wd-gold);
|
||||||
|
}
|
||||||
|
.wd .po-ag-preis {
|
||||||
|
font-size: clamp(1.5rem, 4vw, 2rem); font-weight: 700;
|
||||||
|
color: var(--wd-blau); margin: .2rem 0 .3rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.po-ag-leistungen { margin: 1rem 0; }
|
||||||
|
.wd .po-ag-gruppe {
|
||||||
|
font-size: .9rem; font-weight: 700; margin: .9rem 0 .3rem;
|
||||||
|
color: var(--wd-text);
|
||||||
|
}
|
||||||
|
.po-ag-liste { margin: 0 0 .2rem; padding-left: 1.1rem; }
|
||||||
|
.wd .po-ag-liste li { font-size: .88rem; margin-bottom: .2rem; color: var(--wd-text-soft); }
|
||||||
|
.po-ag-knoepfe {
|
||||||
|
display: flex; flex-wrap: wrap; gap: .6rem; align-items: center;
|
||||||
|
margin-top: 1rem; padding-top: .9rem;
|
||||||
|
border-top: 1px solid var(--wd-rand);
|
||||||
|
}
|
||||||
|
/* Der Zusage-Knopf nimmt auf dem Handy die volle Breite — er ist das
|
||||||
|
Ziel dieser Karte, und ein halbbreiter Knopf neben einem
|
||||||
|
Abbrechen-Knopf lädt zum Verwechseln ein. */
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.po-ag-knoepfe > .wd-btn--haupt { width: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
.po-frist {
|
.po-frist {
|
||||||
display: inline-block; margin-left: .35rem;
|
display: inline-block; margin-left: .35rem;
|
||||||
padding: .1rem .5rem; border-radius: 999px;
|
padding: .1rem .5rem; border-radius: 999px;
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@
|
|||||||
Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den
|
Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den
|
||||||
Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten
|
Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten
|
||||||
Stand fest. */
|
Stand fest. */
|
||||||
const CACHE_NAME = "dogfather-webdesign-v28";
|
const CACHE_NAME = "dogfather-webdesign-v29";
|
||||||
|
|
||||||
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
|
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
|
||||||
HTML-Datei in dieser Liste. */
|
HTML-Datei in dieser Liste. */
|
||||||
|
|||||||
+125
-1
@@ -123,6 +123,14 @@
|
|||||||
.vw-meld-weg:focus-visible { outline: 2px solid rgb(var(--wd-blau-rgb) / .95); outline-offset: 2px; }
|
.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-meld-alle { margin: .1rem 0 1rem; }
|
||||||
|
|
||||||
|
.vw-ag-details { margin: .6rem 0 .8rem; }
|
||||||
|
.wd .vw-ag-details summary {
|
||||||
|
cursor: pointer; font-size: .86rem; color: var(--wd-text-soft);
|
||||||
|
padding: .3rem 0; min-height: 44px; display: flex; align-items: center;
|
||||||
|
}
|
||||||
|
.wd .vw-ag-details summary:hover { color: var(--wd-text); }
|
||||||
|
.wd .vw-ag-details textarea { font-family: var(--font-body); font-size: .86rem; line-height: 1.5; }
|
||||||
|
|
||||||
.vw-abbruch-rechnung {
|
.vw-abbruch-rechnung {
|
||||||
padding: .7rem .85rem; margin-bottom: .8rem;
|
padding: .7rem .85rem; margin-bottom: .8rem;
|
||||||
border-radius: var(--wd-radius-m, 14px);
|
border-radius: var(--wd-radius-m, 14px);
|
||||||
@@ -2288,7 +2296,50 @@
|
|||||||
(a.kunde_id
|
(a.kunde_id
|
||||||
? '<div class="vw-abschnitt"><h3>Kunde</h3>' +
|
? '<div class="vw-abschnitt"><h3>Kunde</h3>' +
|
||||||
'<p class="wd-klein wd-leise">Aus dieser Anfrage wurde bereits ein Kundenzugang erstellt. Du findest ihn oben unter „Kunden“.</p></div>'
|
'<p class="wd-klein wd-leise">Aus dieser Anfrage wurde bereits ein Kundenzugang erstellt. Du findest ihn oben unter „Kunden“.</p></div>'
|
||||||
: '<div class="vw-abschnitt"><h3>Entscheiden</h3>' +
|
: '<div class="vw-abschnitt"><h3>Angebot schicken</h3>' +
|
||||||
|
'<div class="vw-uebernahme" id="vw-angebot">' +
|
||||||
|
/* Das Angebot steht VOR dem direkten Annehmen.
|
||||||
|
|
||||||
|
Der übliche Weg ist: Angebot raus, Kunde sagt zu, Projekt
|
||||||
|
entsteht. Direkt anzunehmen ist der Sonderfall (man hat
|
||||||
|
schon telefoniert). Stünde der Sonderfall oben, wäre er
|
||||||
|
der naheliegende Griff — und man verschenkte den Beleg,
|
||||||
|
den eine Zusage im Portal erzeugt. */
|
||||||
|
'<p class="wd-klein wd-leise" style="margin-top:0">Der Leistungsumfang kommt ' +
|
||||||
|
'aus der Standardliste des Pakets — du wählst nur Paket und Preis. Der Kunde ' +
|
||||||
|
'sagt im Portal mit einem Klick zu; die Anzahlung entsteht dabei von selbst.</p>' +
|
||||||
|
'<div class="wd-grid wd-grid--2" style="gap:.7rem">' +
|
||||||
|
'<div class="wd-feld">' +
|
||||||
|
'<label for="vw-ag-paket">Paket</label>' +
|
||||||
|
'<select id="vw-ag-paket">' +
|
||||||
|
'<option value="onepager">Onepager</option>' +
|
||||||
|
'<option value="website">Website</option>' +
|
||||||
|
'<option value="shop">Onlineshop</option>' +
|
||||||
|
'<option value="verwaltung">Verwaltungsbereich</option>' +
|
||||||
|
'<option value="betreuung">Laufende Betreuung</option>' +
|
||||||
|
'<option value="unklar">Noch unklar</option>' +
|
||||||
|
'</select>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="wd-feld">' +
|
||||||
|
'<label for="vw-ag-preis">Preis in Euro</label>' +
|
||||||
|
'<input type="number" id="vw-ag-preis" min="1" step="1" inputmode="numeric" />' +
|
||||||
|
'<span class="wd-feld-hilfe" id="vw-ag-anzahlung">…</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<details class="vw-ag-details">' +
|
||||||
|
'<summary>Leistungsumfang ansehen und ändern</summary>' +
|
||||||
|
'<div class="wd-feld" style="margin-top:.6rem">' +
|
||||||
|
'<textarea id="vw-ag-leistungen" rows="12"></textarea>' +
|
||||||
|
'<span class="wd-feld-hilfe">Kommt aus der Standardliste. Was du hier ' +
|
||||||
|
'änderst, wird beim Absenden festgeschrieben.</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'</details>' +
|
||||||
|
'<p class="wd-klein wd-leise" id="vw-ag-gueltig"></p>' +
|
||||||
|
'<button type="button" class="wd-btn wd-btn--haupt" id="vw-ag-senden" ' +
|
||||||
|
'style="width:100%">Angebot schicken</button>' +
|
||||||
|
'</div></div>' +
|
||||||
|
|
||||||
|
'<div class="vw-abschnitt"><h3>Oder direkt entscheiden</h3>' +
|
||||||
'<div class="vw-uebernahme" id="vw-annahme">' +
|
'<div class="vw-uebernahme" id="vw-annahme">' +
|
||||||
"<h4>Annehmen — und alles läuft</h4>" +
|
"<h4>Annehmen — und alles läuft</h4>" +
|
||||||
/* Was genau passiert, steht DA, bevor man drückt. Ein Knopf,
|
/* Was genau passiert, steht DA, bevor man drückt. Ein Knopf,
|
||||||
@@ -2447,6 +2498,79 @@
|
|||||||
return t.length === 3 ? t[2] + "." + t[1] + "." + t[0] : String(iso);
|
return t.length === 3 ? t[2] + "." + t[1] + "." + t[0] : String(iso);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Angebot ----
|
||||||
|
|
||||||
|
Die Vorschau füllt alles vor. Wechselt man das Paket, wird neu
|
||||||
|
geladen: Preis, Laufzeit UND Leistungstext hängen daran. Ein
|
||||||
|
stehengebliebener Text vom vorigen Paket wäre der schlimmste Fall --
|
||||||
|
er ginge unbemerkt so hinaus, und im Angebot stünde etwas, das zum
|
||||||
|
Preis nicht passt. */
|
||||||
|
var agKasten = blatt.querySelector("#vw-angebot");
|
||||||
|
if (agKasten) {
|
||||||
|
var agPaket = blatt.querySelector("#vw-ag-paket");
|
||||||
|
var agPreis = blatt.querySelector("#vw-ag-preis");
|
||||||
|
var agLeist = blatt.querySelector("#vw-ag-leistungen");
|
||||||
|
var agGueltig = blatt.querySelector("#vw-ag-gueltig");
|
||||||
|
var agAnz = blatt.querySelector("#vw-ag-anzahlung");
|
||||||
|
var agGueltigBis = null;
|
||||||
|
|
||||||
|
var agAnzahlungZeigen = function () {
|
||||||
|
var wert = Number(agPreis.value);
|
||||||
|
agAnz.textContent = Number.isFinite(wert) && wert > 0
|
||||||
|
? "30 % Anzahlung: " + (Math.round(wert * 30) / 100).toFixed(2).replace(".", ",") + " €"
|
||||||
|
: "Die 30 % Anzahlung wird daraus berechnet.";
|
||||||
|
};
|
||||||
|
|
||||||
|
var agLaden = async function (paket) {
|
||||||
|
try {
|
||||||
|
var v = await api("/webdesign/admin/anfragen/" + encodeURIComponent(a.id)
|
||||||
|
+ "/angebot-vorschau" + (paket ? "?paket=" + encodeURIComponent(paket) : ""));
|
||||||
|
if (!v || !v.ok) return;
|
||||||
|
agPaket.value = v.paket;
|
||||||
|
agPreis.value = v.preisEuro;
|
||||||
|
agLeist.value = v.leistungen || "";
|
||||||
|
agGueltigBis = v.gueltigBis;
|
||||||
|
agGueltig.textContent = "Läuft " + v.werktage + " Werktage · gültig bis "
|
||||||
|
+ datumKurz(v.gueltigBis);
|
||||||
|
agAnzahlungZeigen();
|
||||||
|
if (v.schonUnterwegs) {
|
||||||
|
agGueltig.textContent = "Achtung: " + v.schonUnterwegs.nummer
|
||||||
|
+ " ist bereits unterwegs. Zieh es zurück, bevor du ein neues schickst.";
|
||||||
|
}
|
||||||
|
} catch (f) {
|
||||||
|
agGueltig.textContent = "Vorschlag konnte nicht geladen werden: " + f.message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
agLaden(null);
|
||||||
|
agPaket.addEventListener("change", function () { agLaden(agPaket.value); });
|
||||||
|
agPreis.addEventListener("input", agAnzahlungZeigen);
|
||||||
|
|
||||||
|
blatt.querySelector("#vw-ag-senden").addEventListener("click", async function (e) {
|
||||||
|
var preis = Number(agPreis.value);
|
||||||
|
if (!Number.isFinite(preis) || preis <= 0) {
|
||||||
|
window.WD.meldung(meldung, "Bitte einen Preis eintragen.", "fehler");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.WD.knopfSperren(e.target, true, "Wird geschickt…");
|
||||||
|
try {
|
||||||
|
var r = await api("/webdesign/admin/anfragen/" + encodeURIComponent(a.id) + "/angebot", {
|
||||||
|
method: "POST",
|
||||||
|
body: { paket: agPaket.value, preisEuro: preis,
|
||||||
|
leistungen: agLeist.value, gueltigBis: agGueltigBis },
|
||||||
|
});
|
||||||
|
schliesseDetail();
|
||||||
|
await ladeAnfragen();
|
||||||
|
window.WD.meldung(meldung,
|
||||||
|
"Angebot " + r.nummer + " ist raus: " + r.preisText + " · Anzahlung "
|
||||||
|
+ r.anzahlungText + " · gültig bis " + datumKurz(r.gueltigBis)
|
||||||
|
+ ". " + (r.hinweis || ""), "ok");
|
||||||
|
} catch (f) {
|
||||||
|
window.WD.knopfSperren(e.target, false);
|
||||||
|
window.WD.meldung(meldung, "Angebot konnte nicht raus: " + f.message, "fehler");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var annahmeKasten = blatt.querySelector("#vw-annahme");
|
var annahmeKasten = blatt.querySelector("#vw-annahme");
|
||||||
if (annahmeKasten) {
|
if (annahmeKasten) {
|
||||||
var terminFeld = blatt.querySelector("#vw-an-termin");
|
var terminFeld = blatt.querySelector("#vw-an-termin");
|
||||||
|
|||||||
Reference in New Issue
Block a user