/* =====================================================================
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 '
";
}
function wartetMarke(wartet) {
if (wartet !== "kunde" && wartet !== "dogfather") return "";
return '' +
s(window.WD.t(wartet === "kunde" ? "po_wartet_kunde" : "po_wartet_dogfather")) + "";
}
/* ---------------- 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
? '"
: '' + zeichen + "";
var marke = a.status === "erledigt"
? '' + s(window.WD.t("po_aufg_erledigt_marke")) + ""
: a.status === "in_arbeit"
? '' + s(window.WD.t("po_aufg_arbeit_marke")) + ""
: duBistDran
? '' + s(window.WD.t("po_aufg_du_marke")) + ""
: "";
return '
";
}
function aufgabenKarte(d) {
var f = d.fortschritt || { gesamt: 0, erledigt: 0, beimKunden: 0, jeKategorie: {} };
var alle = d.aufgaben || [];
var html = '
' +
s(window.WD.t("po_aufgaben")) + "
";
if (!alle.length) {
return html + '
' + s(window.WD.t("po_aufg_leer")) + "
";
}
html += '
' +
s(window.WD.t("po_aufg_text")) + "
";
/* Grosse Zahl statt Prozent. "9 von 14 erledigt" ist greifbar,
"64 %" ist eine Rechnung, die niemand fuehlt. */
html += '
";
/* --- 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 += '
";
}).join("");
return html + "";
}
async function ladeUebersicht() {
zeige("po-uebersicht");
var ziel = document.getElementById("po-inhalt");
ziel.innerHTML = '
' + s(window.WD.t("laedt")) + "
";
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 += '
";
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 += '
' +
'
' +
'' +
"
" +
"
" + s(window.WD.t("po_leer_h3")) + "
" +
'
' + s(window.WD.t("po_keine_projekte")) + "
" +
/* ZEIGEN statt erzaehlen: eine echte, als Vorschau erkennbare
Projektkarte mit laufender Phasenleiste. Man versteht in zwei
Sekunden, was drei Saetze nicht erklaeren. */
'
" +
/* 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. */
'
";
}
/* 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 += '';
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 = "
';
if (d.projekte && d.projekte.length) {
html += '
' +
'" +
'
";
}
html += '
";
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 = '
' + s(window.WD.t("laedt")) + "
";
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 += '
";
/* 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 += '