Files
dogfather-universe/assets/js/wd-portal.js
T
DogFatherGitandClaude Opus 5 8726110035 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]>
2026-08-24 12:23:44 +02:00

970 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
wd-portal.js — Kundenportal, Oberfläche (Masterplan S.12)
Vier Ansichten in einer Seite: Anmeldung, Passwort festlegen,
Übersicht, Projektdetail.
Wichtig zum Verständnis: Diese Datei stellt nur DAR. Sie entscheidet
nichts darüber, wer was sehen darf — das macht ausschließlich der
Server (routes/webdesign-portal.js), weil dieser Code hier im Browser
des Kunden läuft und dort beliebig veränderbar ist. Was hier ankommt,
ist bereits gefiltert.
===================================================================== */
(function () {
"use strict";
var TOKEN = "wd-portal-token";
var token = null;
var s = window.WD.sicher;
var meldung = document.getElementById("po-meldung");
/* Sitzungsspeicher, nicht dauerhaft: derselbe Grundsatz wie überall in
diesem Bereich — beim Schließen ist der Zugang weg. */
function tokenLesen() { try { return sessionStorage.getItem(TOKEN); } catch (e) { return null; } }
function tokenSchreiben(t) {
try { if (t) sessionStorage.setItem(TOKEN, t); else sessionStorage.removeItem(TOKEN); } catch (e) { /* egal */ }
}
function api(pfad, optionen) {
var opt = optionen || {};
opt.headers = Object.assign({ Authorization: "Bearer " + token }, opt.headers || {});
return window.WD.api(pfad, opt);
}
function zeige(welche) {
["po-anmeldung", "po-setzen", "po-uebersicht", "po-detail"].forEach(function (id) {
document.getElementById(id).hidden = id !== welche;
});
}
/* Sitzung abgelaufen: zurück zur Anmeldung mit klarer Ansage. Eine leere
Seite würde aussehen wie "du hast keine Projekte" — eine stille
Falschaussage. */
function behandleFehler(fehler) {
if (fehler && fehler.status === 401) {
token = null; tokenSchreiben(null);
zeige("po-anmeldung");
window.WD.meldung(meldung, window.WD.t("po_abgelaufen"), "fehler");
return true;
}
return false;
}
/* ---------------- Anmeldung ---------------- */
document.getElementById("po-login-form").addEventListener("submit", async function (e) {
e.preventDefault();
var feld = document.getElementById("po-feld-passwort");
var knopf = e.target.querySelector("button");
feld.classList.remove("wd-feld--fehler");
window.WD.meldungAus(meldung);
var email = document.getElementById("po-email").value.trim();
var passwort = document.getElementById("po-passwort").value;
if (!email || !passwort) {
feld.classList.add("wd-feld--fehler");
feld.querySelector(".wd-feld-fehler").textContent = window.WD.t("po_login_leer");
return;
}
window.WD.knopfSperren(knopf, true, window.WD.t("po_anmelden_laeuft"));
try {
var antwort = await window.WD.api("/webdesign/portal/anmelden", {
method: "POST", body: { email: email, passwort: passwort },
});
token = antwort.token;
tokenSchreiben(token);
/* Sprache des Kunden übernehmen, wenn er eine hinterlegt hat —
so kommt er in seiner Sprache an, ohne umschalten zu müssen. */
if (antwort.kunde && antwort.kunde.sprache) window.WD.setSprache(antwort.kunde.sprache);
ladeUebersicht();
} catch (fehler) {
feld.classList.add("wd-feld--fehler");
feld.querySelector(".wd-feld-fehler").textContent = fehler.message || window.WD.t("po_fehler_login");
document.getElementById("po-passwort").select();
} finally {
window.WD.knopfSperren(knopf, false);
}
});
document.getElementById("po-abmelden").addEventListener("click", async function () {
try { await api("/webdesign/portal/abmelden", { method: "POST", body: {} }); } catch (e) { /* egal */ }
token = null; tokenSchreiben(null);
location.href = "/webdesign/portal.html";
});
/* ---------------- Passwort setzen (Einladungslink) ---------------- */
var einladungsToken = new URLSearchParams(location.search).get("einladung");
document.getElementById("po-setz-form").addEventListener("submit", async function (e) {
e.preventDefault();
var feld = document.getElementById("po-feld-neu");
var knopf = e.target.querySelector("button");
feld.classList.remove("wd-feld--fehler");
var passwort = document.getElementById("po-neu").value;
window.WD.knopfSperren(knopf, true, window.WD.t("po_anmelden_laeuft"));
try {
await window.WD.api("/webdesign/portal/passwort", {
method: "POST", body: { token: einladungsToken, passwort: passwort },
});
zeige("po-anmeldung");
window.WD.meldung(meldung, window.WD.t("po_setz_fertig"), "ok");
/* Adresszeile säubern: der Einladungslink ist verbraucht und hat in
der Chronik nichts mehr verloren. */
history.replaceState(null, "", "/webdesign/portal.html");
} catch (fehler) {
feld.classList.add("wd-feld--fehler");
feld.querySelector(".wd-feld-fehler").textContent = fehler.message;
} finally {
window.WD.knopfSperren(knopf, false);
}
});
/* ---------------- Übersicht ---------------- */
var PHASEN = ["briefing", "angebot", "design", "entwicklung", "tests", "abnahme", "uebergeben"];
function statusText(st) { return window.WD.t("po_st_" + st); }
function phasenLeiste(status) {
var jetzt = PHASEN.indexOf(status);
/* Pausiert oder abgebrochen: keine Leiste. Ein Fortschrittsbalken bei
einem gestoppten Projekt wäre irreführend. */
if (jetzt < 0) return "";
return '<div class="po-phasen" aria-hidden="true">' +
PHASEN.map(function (p, i) {
return '<span class="po-phase' + (i < jetzt ? " po-erledigt" : i === jetzt ? " po-jetzt" : "") + '"></span>';
}).join("") + "</div>" +
'<div class="po-phasen-text">' + s(statusText(status)) + " · " +
(jetzt + 1) + "/" + PHASEN.length + "</div>";
}
/* ---------------------------------------------------------------------
LIEFERTERMIN UND VERBLEIBENDE ZEIT
ZWEI FEHLER, DIE HIER VORHER STECKTEN
1. `richttermin` ist FREIER TEXT ("Mitte Oktober", "sobald die Texte
da sind"). Er wurde durch einen Datumsformatierer geschickt --
und der macht daraus "Invalid Date". Der Kunde las an der
wichtigsten Stelle seines Projekts einen englischen Fehlertext.
Freier Text wird jetzt als freier Text gezeigt.
2. Es gab gar keinen echten Termin, an dem man etwas ablesen konnte.
Jetzt gibt es `termin_am` als richtiges Datum daneben.
Die verbleibenden Tage kommen FERTIG vom Server. Hier wird nicht
nachgerechnet: Der Browser kennt die luxemburgischen Feiertage
nicht, und zwei verschiedene Zahlen fuer denselben Termin waeren
schlimmer als gar keine.
--------------------------------------------------------------------- */
function restText(tage) {
if (tage === null || tage === undefined) return null;
if (tage < 0) return { text: window.WD.t("po_ueberfaellig"), ton: "po-frist--rot" };
if (tage === 0) return { text: window.WD.t("po_heute_faellig"), ton: "po-frist--warn" };
if (tage === 1) return { text: window.WD.t("po_noch_ein_tag"), ton: "po-frist--warn" };
return {
text: String(window.WD.t("po_noch_tage")).replace("{n}", tage),
/* Ab fuenf Werktagen wird es eng -- eine Woche. Darunter faerbt
es sich, damit der Kunde merkt, wenn seine Zuarbeit jetzt
wirklich gebraucht wird. */
ton: tage <= 5 ? "po-frist--warn" : "",
};
}
function datumKurz(iso) {
if (!iso) return "";
var t = String(iso).slice(0, 10).split("-");
return t.length === 3 ? t[2] + "." + t[1] + "." + t[0] : String(iso);
}
function terminZeile(p) {
var stuecke = [];
if (p.termin_am) {
var rest = restText(p.verbleibende_werktage);
stuecke.push(
'<p class="wd-klein" style="margin:.45rem 0 0">' +
'<strong>' + s(window.WD.t("po_liefertermin")) + ":</strong> " +
s(datumKurz(p.termin_am)) +
(rest ? ' <span class="po-frist ' + rest.ton + '">' + s(rest.text) + "</span>" : "") +
"</p>"
);
}
/* Der freie Text nur, wenn er etwas ANDERES sagt als das Datum --
sonst stuenden zwei Termine untereinander und man wuesste nicht,
welcher gilt. */
if (p.richttermin && String(p.richttermin).trim()) {
stuecke.push(
'<p class="wd-klein wd-leise" style="margin:.15rem 0 0">' +
s(window.WD.t("po_richttermin")) + ": " + s(p.richttermin) + "</p>"
);
}
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) {
if (wartet !== "kunde" && wartet !== "dogfather") return "";
return '<span class="po-wartet po-wartet--' + wartet + '">' +
s(window.WD.t(wartet === "kunde" ? "po_wartet_kunde" : "po_wartet_dogfather")) + "</span>";
}
/* ---------------- Aufgabenliste (22.08.2026) ----------------
"wenn sie drauf drücken sollen sie auch sehen was ich schon gemacht
habe von dem was im plan war."
Bis hierher sah der Kunde nur die Phase ("Design · 3/7") — eine Zahl
ohne Inhalt. Sie beantwortet die eigentliche Frage nicht: WAS ist
denn schon fertig?
Zwei Entscheidungen prägen diese Ansicht:
1. Punkte, die auf den KUNDEN warten, stehen ganz oben in einem
eigenen Kasten — nicht nur farblich markiert irgendwo mittendrin.
Wer eine lange Liste sieht, liest sie als Bericht über MEINE
Arbeit und übersieht seinen eigenen Teil. Genau daraus entstehen
die meisten Verzögerungen.
2. Der Kunde kann seine eigenen Punkte selbst abhaken. Das ist keine
Spielerei: Es gibt ihm einen sichtbaren Anteil am Fortschritt,
und ich sehe sofort, dass die Texte da sind, ohne dass er extra
schreiben muss.
---------------------------------------------------------------- */
var KATS = ["start", "gestaltung", "umsetzung", "abschluss"];
var KAT_KEY = { start: "po_k1", gestaltung: "po_k2", umsetzung: "po_k3", abschluss: "po_k4" };
function aufgabeZeile(a, hervor) {
var klasse = "po-aufg";
if (a.status === "erledigt") klasse += " po-aufg--fertig";
else if (a.status === "in_arbeit") klasse += " po-aufg--jetzt";
else if (a.status === "entfaellt") klasse += " po-aufg--weg";
var duBistDran = a.wer_dran === "kunde" && a.status !== "erledigt" && a.status !== "entfaellt";
if (duBistDran) klasse += " po-aufg--du";
if (hervor) klasse += " po-aufg--hervor";
/* Nur EIGENE offene Punkte bekommen einen Knopf. Bei allen anderen
ist das Zeichen reine Anzeige — sonst klickt der Kunde darauf,
nichts passiert, und er hält die Seite für kaputt. */
var eigen = a.wer_dran === "kunde" && !a.nicht_enthalten && a.status !== "entfaellt";
var zeichen = a.status === "erledigt" ? "✓" : a.status === "in_arbeit" ? "•" : a.status === "entfaellt" ? "–" : "";
var kaestchen = eigen
? '<button type="button" class="po-aufg-haken" data-haken="' + s(a.id) + '" title="' +
s(window.WD.t(a.status === "erledigt" ? "po_aufg_zurueck" : "po_aufg_hake")) + '" aria-label="' +
s(window.WD.t(a.status === "erledigt" ? "po_aufg_zurueck" : "po_aufg_hake")) + ": " + s(a.titel) + '">' +
zeichen + "</button>"
: '<span class="po-aufg-haken po-aufg-haken--fest" aria-hidden="true">' + zeichen + "</span>";
var marke = a.status === "erledigt"
? '<span class="po-aufg-marke po-aufg-marke--ok">' + s(window.WD.t("po_aufg_erledigt_marke")) + "</span>"
: a.status === "in_arbeit"
? '<span class="po-aufg-marke po-aufg-marke--jetzt">' + s(window.WD.t("po_aufg_arbeit_marke")) + "</span>"
: duBistDran
? '<span class="po-aufg-marke po-aufg-marke--du">' + s(window.WD.t("po_aufg_du_marke")) + "</span>"
: "";
return '<div class="' + klasse + '">' + kaestchen +
'<span class="po-aufg-titel">' + s(a.titel) +
(a.nicht_enthalten ? '<span class="po-aufg-nicht">' + s(window.WD.t("po_aufg_nicht")) + "</span>" : "") +
"</span>" +
'<span class="po-aufg-marken">' + marke + "</span>" +
(a.beschreibung || a.hinweis
? '<span class="po-aufg-text">' + s(a.beschreibung || "") +
(a.hinweis ? " " + s(a.hinweis) : "") + "</span>"
: "") +
"</div>";
}
function aufgabenKarte(d) {
var f = d.fortschritt || { gesamt: 0, erledigt: 0, beimKunden: 0, jeKategorie: {} };
var alle = d.aufgaben || [];
var html = '<div class="wd-karte" style="margin-bottom:1.2rem"><h3>' +
s(window.WD.t("po_aufgaben")) + "</h3>";
if (!alle.length) {
return html + '<p class="wd-leise wd-klein">' + s(window.WD.t("po_aufg_leer")) + "</p></div>";
}
html += '<p class="wd-klein wd-leise" style="margin-top:-.3rem">' +
s(window.WD.t("po_aufg_text")) + "</p>";
/* Grosse Zahl statt Prozent. "9 von 14 erledigt" ist greifbar,
"64 %" ist eine Rechnung, die niemand fuehlt. */
html += '<div class="po-aufg-stand"><b>' + f.erledigt + "</b> " +
s(window.WD.t("po_aufg_fertig").replace("{gesamt}", f.gesamt)) + "</div>";
/* Der Balken in den vier Prozessfarben — dieselben wie auf der
Ablaufseite und in der Verwaltung. */
html += '<div class="po-fortschritt">' + KATS.map(function (k) {
var z = (f.jeKategorie && f.jeKategorie[k]) || { gesamt: 0, erledigt: 0 };
var breite = z.gesamt ? Math.round((z.erledigt / z.gesamt) * 100) : 0;
return '<span class="po-fs-teil" data-k="' + k + '" title="' +
s(window.WD.t(KAT_KEY[k])) + ": " + z.erledigt + "/" + z.gesamt +
'"><i style="width:' + breite + '%"></i></span>';
}).join("") + "</div>";
/* --- Was auf den Kunden wartet, ZUERST und in einem eigenen Kasten. --- */
var meine = alle.filter(function (a) {
return a.wer_dran === "kunde" && a.status !== "erledigt" && a.status !== "entfaellt" && !a.nicht_enthalten;
});
if (meine.length) {
html += '<div class="po-dubist"><h4>' +
s(window.WD.t(meine.length === 1 ? "po_aufg_dubist_eins"
: "po_aufg_dubist_viele").replace("{n}", meine.length)) + "</h4>" +
'<p class="wd-klein wd-leise" style="margin:.1rem 0 .7rem">' +
s(window.WD.t("po_aufg_dubist")) + "</p>" +
meine.map(function (a) { return aufgabeZeile(a, true); }).join("") +
"</div>";
} else {
html += '<p class="po-allesgut">' + s(window.WD.t("po_aufg_allesbei")) + "</p>";
}
/* --- Danach die vollstaendige Liste nach Abschnitten. --- */
html += KATS.map(function (k) {
var drin = alle.filter(function (a) { return a.kategorie === k; });
if (!drin.length) return "";
var z = (f.jeKategorie && f.jeKategorie[k]) || { gesamt: 0, erledigt: 0 };
return '<div class="po-aufg-gruppe" data-k="' + k + '">' +
'<div class="po-aufg-kopf">' + s(window.WD.t(KAT_KEY[k])) +
'<span class="po-aufg-zahl">' + z.erledigt + " / " + z.gesamt + "</span></div>" +
drin.map(function (a) { return aufgabeZeile(a, false); }).join("") +
"</div>";
}).join("");
return html + "</div>";
}
async function ladeUebersicht() {
zeige("po-uebersicht");
var ziel = document.getElementById("po-inhalt");
ziel.innerHTML = '<div class="po-laedt">' + s(window.WD.t("laedt")) + "</div>";
try {
var d = await api("/webdesign/portal/uebersicht");
document.getElementById("po-name").textContent = d.kunde.name || "";
window.WD.meldungAus(meldung);
var html = "";
/* Offene Zahlungen ganz oben — das ist das Einzige, was den Kunden
zum Handeln auffordert. Gold, weil es wirklich wichtig ist. */
if (d.offeneZahlungen.length) {
html += '<div class="wd-hinweis" style="margin-bottom:1.2rem">' +
'<p class="wd-hinweis-titel">' + s(window.WD.t("po_offene_zahlungen")) + "</p>" +
d.offeneZahlungen.map(function (z) {
return "<p><strong>" + s(z.betrag_text) + "</strong> · " + s(z.zweck_text) + "</p>";
}).join("") + "</div>";
}
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) {
/* Ein leerer Bereich mit einem Satz ist eine verpasste Gelegenheit.
Wer hier zum ersten Mal landet, hat gerade sein Passwort gesetzt
und weiss noch nicht, was ihn erwartet. Statt "hier ist nichts"
zeigt der leere Zustand deshalb, WAS gleich hier stehen wird --
und was als Naechstes passiert. Das nimmt die Unsicherheit,
statt sie zu erzeugen. */
html += '<div class="wd-karte po-leer">' +
'<div class="po-leer-symbol" aria-hidden="true">' +
'<img src="/assets/img/logo-dogfather-transparent.png" alt="" width="472" height="476" />' +
"</div>" +
"<h3>" + s(window.WD.t("po_leer_h3")) + "</h3>" +
'<p class="wd-leise">' + s(window.WD.t("po_keine_projekte")) + "</p>" +
/* ZEIGEN statt erzaehlen: eine echte, als Vorschau erkennbare
Projektkarte mit laufender Phasenleiste. Man versteht in zwei
Sekunden, was drei Saetze nicht erklaeren. */
'<div class="po-vorschau">' +
'<span class="po-vorschau-etikett">' + s(window.WD.t("po_vorschau")) + "</span>" +
'<span class="po-vorschau-nr">P-0000-0000</span>' +
"<h4>" + s(window.WD.t("po_vorschau_titel")) + "</h4>" +
'<div class="po-vorschau-phasen" aria-hidden="true">' +
"<span></span><span></span><span></span><span></span><span></span><span></span><span></span>" +
"</div>" +
'<div class="po-vorschau-zeile">' +
'<span class="po-vorschau-marke">' + s(window.WD.t("po_wartet_dogfather")) + "</span>" +
"<span>" + s(window.WD.t("po_st_design")) + " · 3/7</span>" +
"</div>" +
'<p class="po-vorschau-schritt"><strong>' + s(window.WD.t("po_naechster_schritt")) +
":</strong> " + s(window.WD.t("po_vorschau_schritt")) + "</p>" +
/* Legende: Farbe allein ist nie Information. Wer sie nicht
unterscheiden kann, liest hier trotzdem, was sie bedeutet. */
/* Vier Abschnitte statt drei Zustaende: Ob eine Phase
erledigt ist, sieht man an der Position. Wertvoller ist,
WAS gerade passiert. */
'<div class="po-legende">' +
'<span><i class="po-l-1"></i>' + s(window.WD.t("po_k1")) + "</span>" +
'<span><i class="po-l-2"></i>' + s(window.WD.t("po_k2")) + "</span>" +
'<span><i class="po-l-3"></i>' + s(window.WD.t("po_k3")) + "</span>" +
'<span><i class="po-l-4"></i>' + s(window.WD.t("po_k4")) + "</span>" +
"</div>" +
"</div>" +
'<div class="po-leer-liste">' +
[["po_leer_1", "po_leer_1t"], ["po_leer_2", "po_leer_2t"],
["po_leer_3", "po_leer_3t"], ["po_leer_4", "po_leer_4t"]]
.map(function (paar, i) {
return '<div class="po-leer-punkt">' +
'<span class="po-leer-nr">' + (i + 1) + "</span>" +
"<div><strong>" + s(window.WD.t(paar[0])) + "</strong>" +
"<span>" + s(window.WD.t(paar[1])) + "</span></div></div>";
}).join("") +
"</div>" +
'<p class="wd-klein wd-leise po-leer-fuss">' + s(window.WD.t("po_leer_fuss")) + "</p>" +
"</div>";
} else {
html += d.projekte.map(function (p) {
return '<button type="button" class="wd-karte po-projekt" data-projekt="' + s(p.id) + '">' +
'<span class="po-projekt-kopf">' +
'<span class="po-projekt-nr">' + s(p.nummer) + "</span>" +
"<h3>" + s(p.titel) + "</h3>" +
wartetMarke(p.wartet_auf) +
"</span>" +
phasenLeiste(p.status) +
(p.naechster_schritt
? '<p class="wd-klein" style="margin:.7rem 0 .3rem"><strong>' +
s(window.WD.t("po_naechster_schritt")) + ":</strong> " + s(p.naechster_schritt) + "</p>"
: "") +
terminZeile(p) +
(p.offene_aenderungen || p.ungelesene_nachrichten
? '<p class="wd-klein" style="margin:.5rem 0 0;color:var(--wd-blau)">' +
(p.ungelesene_nachrichten ? p.ungelesene_nachrichten + " " + s(window.WD.t("po_nachrichten")) : "") +
(p.ungelesene_nachrichten && p.offene_aenderungen ? " · " : "") +
(p.offene_aenderungen ? p.offene_aenderungen + " " + s(window.WD.t("po_ideen")) : "") +
"</p>"
: "") +
"</button>";
}).join("");
}
if (d.abos.length) {
html += '<h2 style="margin-top:1.8rem">' + s(window.WD.t("po_betreuung")) + "</h2>" +
'<div class="wd-karte"><ul class="po-liste">' +
d.abos.map(function (a) {
return '<li class="po-eintrag"><span class="po-eintrag-haupt">' +
'<span class="po-eintrag-name">' + s(a.paket) + "</span>" +
'<span class="po-eintrag-neben">' + s(a.preis_text) +
(a.naechste_zahlung ? " · " + s(window.WD.t("po_naechste_zahlung")) + ": " + window.WD.datum(a.naechste_zahlung) : "") +
"</span></span></li>";
}).join("") + "</ul></div>";
}
/* Der allgemeine Nachrichtenbereich steht auf der ÜBERSICHT, nicht
im Projekt. Genau das war der Punkt: Wer kein Projekt hat — oder
eine Frage, die zu keinem gehört — konnte vorher gar nicht
schreiben und musste zur E-Mail greifen. Damit war der Verlauf
weg, sobald man ihn brauchte.
Er wird nachgeladen, nicht mitgeliefert: Die Übersicht soll
sofort dastehen. Ein paar Zehntelsekunden später erscheinen die
Nachrichten — das ist besser, als die ganze Seite auf sie warten
zu lassen. */
html += '<div id="po-postfach" style="margin-top:1.8rem"></div>';
ziel.innerHTML = html;
ziel.querySelectorAll("[data-projekt]").forEach(function (b) {
b.addEventListener("click", function () { ladeProjekt(b.dataset.projekt); });
});
ladePostfach();
} catch (fehler) {
if (behandleFehler(fehler)) return;
ziel.innerHTML = "";
window.WD.meldung(meldung, fehler.message, "fehler");
}
}
/* ---------------- Projektdetail ---------------- */
/* ---------------- Allgemeines Postfach ----------------
"auf dieser seite sollen die kunden mir auch nachrichten hinterlassen
können, für noch kleinigkeiten oder sachen die ich brauch."
Bewusst niedrigschwellig: kein Betreff, kein Pflichtfeld, der Bezug
zu einem Projekt ist freiwillig. Wer glaubt, eine Nachricht müsse
eine "richtige" Anfrage sein, schreibt gar nicht erst — und genau
die kurzen Fragen sollen hier landen.
---------------------------------------------------------------- */
async function ladePostfach() {
var kasten = document.getElementById("po-postfach");
if (!kasten) return;
var d;
try {
d = await api("/webdesign/portal/postfach");
} catch (fehler) {
/* Still bleiben. Der Bereich ist eine Ergänzung — eine rote
Fehlermeldung über der Projektübersicht wäre alarmierender, als
der Anlass es hergibt. */
return;
}
var html = "<h2>" + s(window.WD.t("po_postfach")) + "</h2>" +
'<div class="wd-karte">' +
'<p class="wd-klein wd-leise" style="margin-top:0">' + s(window.WD.t("po_postfach_text")) + "</p>";
/* 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>";
} else {
html += '<div class="po-pf-verlauf" id="po-pf-verlauf">' + nachrichten.map(function (n) {
var vonMir = n.autor === "kunde";
return '<div class="po-nachricht po-nachricht--' + (vonMir ? "ich" : "du") + '">' +
'<div class="po-nachricht-kopf">' +
(vonMir ? "" : s(window.WD.t("po_von_dogfather")) + " · ") +
window.WD.datum(n.erstellt_am) +
(n.projekt_titel ? " · " + s(n.projekt_titel) : "") +
"</div><p>" + s(n.text) + "</p></div>";
}).join("") + "</div>";
}
html += '<div class="wd-feld" style="margin:1rem 0 .6rem" id="po-pf-feld">' +
'<textarea id="po-pf-text" rows="3" placeholder="' +
s(window.WD.t("po_postfach_platzhalter")) + '"></textarea>' +
'<p class="wd-feld-fehler"></p></div>';
if (d.projekte && d.projekte.length) {
html += '<div class="wd-feld" style="margin-bottom:.6rem">' +
'<label for="po-pf-projekt" class="wd-klein">' + s(window.WD.t("po_postfach_bezug")) + "</label>" +
'<select id="po-pf-projekt"><option value="">' +
s(window.WD.t("po_postfach_kein_bezug")) + "</option>" +
d.projekte.map(function (p) {
return '<option value="' + s(p.id) + '">' + s(p.titel) + "</option>";
}).join("") + "</select></div>";
}
html += '<button type="button" class="wd-btn wd-btn--zweit wd-btn--klein" id="po-pf-senden">' +
s(window.WD.t("po_senden")) + "</button></div>";
kasten.innerHTML = html;
var lauf = document.getElementById("po-pf-verlauf");
if (lauf) lauf.scrollTop = lauf.scrollHeight;
var eingabe = document.getElementById("po-pf-text");
var feld = document.getElementById("po-pf-feld");
var knopf = document.getElementById("po-pf-senden");
knopf.addEventListener("click", async function () {
var inhalt = eingabe.value.trim();
feld.classList.remove("wd-feld--fehler");
if (!inhalt) {
feld.classList.add("wd-feld--fehler");
feld.querySelector(".wd-feld-fehler").textContent = window.WD.t("po_postfach_leer_fehler");
eingabe.focus();
return;
}
var wahl = document.getElementById("po-pf-projekt");
window.WD.knopfSperren(knopf, true, "…");
try {
await api("/webdesign/portal/postfach", {
method: "POST",
body: { text: inhalt, projektId: wahl ? wahl.value : null },
});
window.WD.meldung(meldung, window.WD.t("po_postfach_gesendet"), "ok");
await ladePostfach();
} catch (fehler) {
if (behandleFehler(fehler)) return;
window.WD.knopfSperren(knopf, false);
window.WD.meldung(meldung, fehler.message, "fehler");
}
});
}
var aktuellesProjekt = null;
document.getElementById("po-zurueck").addEventListener("click", function () {
aktuellesProjekt = null;
ladeUebersicht();
});
async function ladeProjekt(id) {
aktuellesProjekt = id;
zeige("po-detail");
var ziel = document.getElementById("po-detail-inhalt");
ziel.innerHTML = '<div class="po-laedt">' + s(window.WD.t("laedt")) + "</div>";
try {
/* Beides gleichzeitig anfordern statt nacheinander. Nacheinander
waere die Wartezeit die Summe beider Anfragen — bei einer
Mobilfunkverbindung deutlich spuerbar.
Die Aufgabenliste darf dabei fehlschlagen, ohne die ganze Seite
mitzureissen: Sie ist eine Ergaenzung, keine Voraussetzung. Wer
wegen einer leeren Liste seine Zahlungen und Dateien nicht mehr
saehe, waere schlechter dran als vorher. */
var beides = await Promise.all([
api("/webdesign/portal/projekt/" + encodeURIComponent(id)),
api("/webdesign/portal/projekt/" + encodeURIComponent(id) + "/aufgaben")
.catch(function () { return { aufgaben: [], fortschritt: null }; }),
]);
var d = beides[0];
var aufg = beides[1];
var p = d.projekt;
var html = "";
html += '<div class="wd-karte" style="margin-bottom:1.2rem">' +
'<span class="po-projekt-nr">' + s(p.nummer) + "</span>" +
"<h2 style=\"margin:.2rem 0 .6rem\">" + s(p.titel) + "</h2>" +
wartetMarke(p.wartet_auf) +
phasenLeiste(p.status) +
(p.naechster_schritt
? '<p style="margin:.8rem 0 0"><strong>' + s(window.WD.t("po_naechster_schritt")) +
":</strong> " + s(p.naechster_schritt) + "</p>"
: "") +
"</div>";
/* Die Aufgabenliste steht GANZ OBEN, direkt nach dem Projektkopf.
Sie beantwortet die Frage, mit der man das Projekt oeffnet
("wie weit ist es?"). Zahlungen und Dateien sind wichtig, aber
sie sind nicht der Grund fuer den Klick. */
html += aufgabenKarte(aufg);
/* --- Zahlungen --- */
html += '<div class="wd-karte" style="margin-bottom:1.2rem"><h3>' + s(window.WD.t("po_zahlungen")) + "</h3>";
if (!d.zahlungen.length) {
html += '<p class="wd-leise wd-klein">' + s(window.WD.t("po_keine_zahlungen")) + "</p>";
} else {
html += '<ul class="po-liste">' + d.zahlungen.map(function (z) {
var offen = z.status === "offen" || z.status === "freigegeben";
return '<li class="po-eintrag"><span class="po-eintrag-haupt">' +
'<span class="po-eintrag-name">' + s(z.betrag_text) + "</span>" +
'<span class="po-eintrag-neben">' + s(z.zweck_text) + "</span></span>" +
'<span class="wd-tag' + (z.status === "bezahlt" ? " wd-tag--ok" : offen ? " wd-tag--gold" : "") + '">' +
s(window.WD.t("po_za_" + (z.status === "fehlge" ? "fehlge" : z.status))) + "</span>" +
(offen ? '<button type="button" class="wd-btn wd-btn--haupt wd-btn--klein" data-zahlung="' +
s(z.id) + '">' + s(window.WD.t("po_jetzt_bezahlen")) + "</button>" : "") +
"</li>";
}).join("") + "</ul>";
}
html += "</div>";
/* --- Dateien --- */
html += '<div class="wd-karte" style="margin-bottom:1.2rem"><h3>' + s(window.WD.t("po_dateien")) + "</h3>";
if (!d.dateien.length) {
html += '<p class="wd-leise wd-klein">' + s(window.WD.t("po_keine_dateien")) + "</p>";
} else {
html += '<ul class="po-liste">' + d.dateien.map(function (f) {
return '<li class="po-eintrag"><span class="po-eintrag-haupt">' +
'<span class="po-eintrag-name">' + s(f.anzeigename) + "</span>" +
'<span class="po-eintrag-neben">' + window.WD.datum(f.erstellt_am) +
(f.version > 1 ? " · " + s(window.WD.t("po_version")) + " " + f.version : "") +
"</span></span></li>";
}).join("") + "</ul>";
}
html += "</div>";
/* --- Ideen & Änderungswünsche --- */
html += '<div class="wd-karte" style="margin-bottom:1.2rem"><h3>' + s(window.WD.t("po_ideen")) + "</h3>" +
'<p class="wd-klein wd-leise">' + s(window.WD.t("po_ideen_text")) + "</p>";
if (d.aenderungen.length) {
html += d.aenderungen.map(function (a) {
var entscheidbar = a.status === "angeboten";
return '<div class="po-idee' + (entscheidbar ? " po-idee--offen" : "") + '">' +
'<div class="po-idee-kopf"><h4>' + s(a.titel) + "</h4>" +
'<span class="wd-tag' + (entscheidbar ? " wd-tag--gold" : a.status === "angenommen" ? " wd-tag--ok" : "") + '">' +
s(window.WD.t("po_ae_" + a.status)) + "</span></div>" +
'<p class="wd-klein" style="margin:0 0 .4rem">' + s(a.beschreibung) + "</p>" +
(a.aufwand_text ? '<p class="wd-klein wd-leise" style="margin:0">' + s(a.aufwand_text) + "</p>" : "") +
(a.zusatzpreis_text
? '<p class="wd-klein" style="margin:.3rem 0 0"><strong>' + s(window.WD.t("po_zusatzpreis")) +
": " + s(a.zusatzpreis_text) + "</strong>" +
(a.zusatzdauer_text ? " · " + s(window.WD.t("po_zusatzdauer")) + ": " + s(a.zusatzdauer_text) : "") +
"</p>"
: "") +
(entscheidbar
? '<div class="wd-btn-reihe" style="margin-top:.8rem">' +
'<button type="button" class="wd-btn wd-btn--haupt wd-btn--klein" data-ja="' + s(a.id) + '">' +
s(window.WD.t("po_annehmen")) + "</button>" +
'<button type="button" class="wd-btn wd-btn--still wd-btn--klein" data-nein="' + s(a.id) + '">' +
s(window.WD.t("po_ablehnen")) + "</button></div>"
: "") +
"</div>";
}).join("");
} else {
html += '<p class="wd-leise wd-klein">' + s(window.WD.t("po_keine_ideen")) + "</p>";
}
html += '<div class="wd-feld" style="margin:1rem 0 .6rem">' +
"<label for=\"po-idee-titel\">" + s(window.WD.t("po_idee_titel")) + "</label>" +
'<input type="text" id="po-idee-titel" maxlength="160" />' +
"</div>" +
'<div class="wd-feld" style="margin-bottom:.6rem" id="po-feld-idee">' +
"<label for=\"po-idee-text\">" + s(window.WD.t("po_idee_text")) + "</label>" +
'<textarea id="po-idee-text" rows="3"></textarea>' +
'<p class="wd-feld-fehler"></p></div>' +
'<button type="button" class="wd-btn wd-btn--zweit wd-btn--klein" id="po-idee-senden">' +
s(window.WD.t("po_idee_senden")) + "</button></div>";
/* --- Nachrichten --- */
html += '<div class="wd-karte"><h3>' + s(window.WD.t("po_nachrichten")) + "</h3>";
if (!d.nachrichten.length) {
html += '<p class="wd-leise wd-klein">' + s(window.WD.t("po_keine_nachrichten")) + "</p>";
} else {
html += d.nachrichten.map(function (n) {
var vonMir = n.autor === "kunde";
return '<div class="po-nachricht po-nachricht--' + (vonMir ? "ich" : "du") + '">' +
'<div class="po-nachricht-kopf">' + (vonMir ? "" : "Dogfather · ") +
window.WD.datum(n.erstellt_am) + "</div><p>" + s(n.text) + "</p></div>";
}).join("");
}
html += '<div class="wd-feld" style="margin:1rem 0 .6rem">' +
'<textarea id="po-nachricht-text" rows="2" placeholder="' +
s(window.WD.t("po_nachricht_platzhalter")) + '"></textarea></div>' +
'<button type="button" class="wd-btn wd-btn--zweit wd-btn--klein" id="po-nachricht-senden">' +
s(window.WD.t("po_senden")) + "</button></div>";
ziel.innerHTML = html;
/* --- Verknüpfungen --- */
ziel.querySelectorAll("[data-ja], [data-nein]").forEach(function (b) {
b.addEventListener("click", async function () {
var annehmen = !!b.dataset.ja;
var aeId = b.dataset.ja || b.dataset.nein;
/* Nur beim Annehmen nachfragen — das erzeugt eine Zahlungspflicht.
Ablehnen ist folgenlos und braucht keine Hürde. */
if (annehmen && !confirm(window.WD.t("po_annehmen_frage"))) return;
window.WD.knopfSperren(b, true, "…");
try {
await api("/webdesign/portal/aenderung/" + encodeURIComponent(aeId) + "/entscheiden",
{ method: "POST", body: { annehmen: annehmen } });
ladeProjekt(id);
} catch (f) {
if (behandleFehler(f)) return;
window.WD.knopfSperren(b, false);
window.WD.meldung(meldung, f.message, "fehler");
}
});
});
/* Eigene Punkte abhaken. Der Punkt erscheint in der Liste zweimal
(oben unter "Das brauche ich noch von dir" und unten im
Abschnitt), deshalb wird nach dem Umschalten die ganze Ansicht
neu aufgebaut — sonst stuenden beide Fassungen widerspruechlich
nebeneinander. */
ziel.querySelectorAll("[data-haken]").forEach(function (b) {
b.addEventListener("click", async function () {
window.WD.knopfSperren(b, true, "…");
try {
await api("/webdesign/portal/aufgabe/" + encodeURIComponent(b.dataset.haken) + "/erledigt",
{ method: "POST", body: {} });
ladeProjekt(id);
} catch (f) {
if (behandleFehler(f)) return;
window.WD.knopfSperren(b, false);
window.WD.meldung(meldung, f.message, "fehler");
}
});
});
ziel.querySelector("#po-idee-senden").addEventListener("click", async function (e) {
var titel = ziel.querySelector("#po-idee-titel").value.trim();
var beschreibung = ziel.querySelector("#po-idee-text").value.trim();
var feld = ziel.querySelector("#po-feld-idee");
feld.classList.remove("wd-feld--fehler");
if (!titel || !beschreibung) {
feld.classList.add("wd-feld--fehler");
feld.querySelector(".wd-feld-fehler").textContent = window.WD.t("po_idee_leer");
return;
}
window.WD.knopfSperren(e.target, true, "…");
try {
await api("/webdesign/portal/aenderung",
{ method: "POST", body: { projektId: id, titel: titel, beschreibung: beschreibung } });
ladeProjekt(id);
} catch (f) {
if (behandleFehler(f)) return;
window.WD.knopfSperren(e.target, false);
window.WD.meldung(meldung, f.message, "fehler");
}
});
ziel.querySelector("#po-nachricht-senden").addEventListener("click", async function (e) {
var feld = ziel.querySelector("#po-nachricht-text");
var inhalt = feld.value.trim();
if (!inhalt) { feld.focus(); return; }
window.WD.knopfSperren(e.target, true, "…");
try {
await api("/webdesign/portal/nachricht",
{ method: "POST", body: { projektId: id, text: inhalt } });
ladeProjekt(id);
} catch (f) {
if (behandleFehler(f)) return;
window.WD.knopfSperren(e.target, false);
window.WD.meldung(meldung, f.message, "fehler");
}
});
/* Zahlungsknöpfe: die eigentliche PayPal-Anbindung folgt. Bis dahin
ein ehrlicher Hinweis statt eines Knopfes, der nichts tut. */
ziel.querySelectorAll("[data-zahlung]").forEach(function (b) {
b.addEventListener("click", function () {
window.WD.meldung(meldung,
"Die Zahlung über PayPal wird gerade eingerichtet. Bitte melde dich kurz bei Dogfather.", "fehler");
});
});
} catch (fehler) {
if (behandleFehler(fehler)) return;
ziel.innerHTML = "";
window.WD.meldung(meldung, fehler.message, "fehler");
}
}
/* ---------------- Start ---------------- */
async function start() {
/* Einladungslink hat Vorrang: wer gerade sein Passwort setzen soll,
will nicht zuerst eine Anmeldemaske sehen. */
if (einladungsToken) {
try {
var info = await window.WD.api("/webdesign/portal/einladung?token=" + encodeURIComponent(einladungsToken));
document.getElementById("po-setz-fuer").textContent = info.email || "";
zeige("po-setzen");
return;
} catch (fehler) {
zeige("po-anmeldung");
window.WD.meldung(meldung, fehler.message, "fehler");
return;
}
}
token = tokenLesen();
if (!token) { zeige("po-anmeldung"); return; }
ladeUebersicht();
}
/* Nach einem Sprachwechsel die per JS gebauten Teile neu zeichnen. */
document.addEventListener("wd-sprache-geaendert", function () {
if (!token) return;
if (aktuellesProjekt) ladeProjekt(aktuellesProjekt);
else if (!document.getElementById("po-uebersicht").hidden) ladeUebersicht();
});
if (window.WD) 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.");
}
});
})();