Verwaltung: Übersicht als Startansicht, Reiter unter den Titel

Die Reiter standen neben dem Titel. Das las sich wie eine einzige lange
Zeile, in der der Titel nur der erste von sechs Knöpfen zu sein schien --
man sah nicht auf einen Blick, in welchem Bereich man war. Jetzt zwei
Zeilen: oben WO man ist, darunter WOHIN man kann.

Die Verwaltung öffnete bisher mit der Anfragenliste. Eine Liste ist eine
Ablage: Sie zeigt, WAS es gibt, nicht was zu TUN ist. Neu ist ein
Cockpit, das in drei Stufen antwortet -- was auf mich wartet, was beim
Kunden liegt, wie es ums Geld steht -- und dann laufende Projekte mit
Fortschritt sowie den Verlauf.

Zwei Grundsätze machen die Zahlen brauchbar: Getrennt nach 'wartet auf
mich' und 'wartet auf den Kunden' (zwölf offene Punkte sind entspannt,
wenn elf beim Kunden liegen). Und das ALTER färbt, nicht die Menge --
vier neue Anfragen sind kein Problem, eine seit sechs Tagen liegende
schon. Ein offener Widerruf ist immer rot, weil eine gesetzliche Frist
läuft.

Jede Kachel ist ein echter <button> und führt in den passenden Reiter.
Farbe ist nie der einzige Träger: Neben jedem farbigen Zustand steht der
Text ('älteste seit 8 Tagen').

Drei Fehler dabei gefunden und behoben:
- Der Titel wurde nur beim Klicken gesetzt. Frisch geladen zeigte die
  Seite das Cockpit, während darüber noch 'Projektanfragen' stand. Die
  Zuordnung Reiter->Titel liegt jetzt ausserhalb des Klick-Zuhörers.
- '.wd h2' überstimmte '.vw-ub-h': 44px Überschrift über 33px Zahl, die
  Seite las sich wie ein Plakat. Dieselbe Spezifitätsfalle wie früher
  bei '.wd a'.
- Eine Antwort mit ok:true aber ohne Inhalt riss die ganze Verwaltung
  mit. Wird jetzt abgefangen -- ein halber Server ist ein realistischer
  Fall.

Geprüft: 22 Serverprüfungen gegen eine echte Datenbank (inkl. vier
Fällen, die belegen, dass die Rechteprüfung wirklich greift), 56 im
Browser auf Computer und Handy, 69 im bestehenden Verwaltungstest,
0 Fundstellen im Design-/WCAG-Test, 5 im Farbsystemtest.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-23 19:21:12 +02:00
co-authored by Claude Opus 5
parent 4fe66e1e0e
commit c1cb51884f
8 changed files with 1057 additions and 24 deletions
+482 -22
View File
@@ -28,14 +28,202 @@
.vw-login { max-width: 420px; margin: clamp(2rem,8vh,5rem) auto; }
/* ---------- Kopfleiste ---------- */
/* Zwei Zeilen statt einer: oben WO man ist, darunter WOHIN man kann.
Vorher standen Titel und Reiter nebeneinander. Das liest sich wie
eine einzige lange Zeile, in der der Titel nur der erste von sechs
Knöpfen zu sein scheint -- man sieht nicht mehr auf einen Blick, in
welchem Bereich man gerade ist. Getrennt beantwortet die erste Zeile
"wo bin ich", die zweite "wo kann ich hin". */
.vw-leiste {
display: flex; flex-wrap: wrap; align-items: center; gap: .8rem;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: .6rem 1rem;
margin-bottom: 1.2rem;
}
.vw-leiste h1 { margin: 0; font-size: clamp(1.4rem, 3.4vw, 2rem); }
.vw-wer { margin-left: auto; display: flex; align-items: center; gap: .6rem; font-size: .9rem; color: var(--wd-text-soft); }
.vw-leiste h1 {
margin: 0; font-size: clamp(1.4rem, 3.4vw, 2rem);
grid-column: 1; grid-row: 1;
}
.vw-leiste .vw-wer { grid-column: 2; grid-row: 1; }
/* Die Reiter über die volle Breite darunter. */
.vw-leiste .vw-tabs {
grid-column: 1 / -1; grid-row: 2;
padding-top: .2rem;
}
@media (max-width: 560px) {
/* Auf dem Handy darf der Name weichen -- der Platz gehört dem Titel. */
.vw-leiste { grid-template-columns: 1fr; }
.vw-leiste .vw-wer { grid-column: 1; grid-row: 3; justify-content: flex-start; }
.vw-leiste .vw-tabs { grid-row: 2; }
}
.vw-wer { display: flex; justify-content: flex-end; align-items: center; gap: .6rem; font-size: .9rem; color: var(--wd-text-soft); }
/* ================================================================
ÜBERSICHT — das Cockpit
Gestalterische Grundregel hier: Ruhe als Normalzustand. Eine
Verwaltung, in der alles leuchtet, hat keine Dringlichkeitsstufen
mehr — man sieht überall Farbe und deshalb nirgends ein Signal.
Deshalb sind die Kacheln standardmäßig zurückhaltend, und nur was
wirklich liegen geblieben ist, bekommt Farbe.
Farbe ist außerdem NIE der einzige Träger einer Aussage. Neben
jedem farbigen Zustand steht ein Text ("älteste seit 6 Tagen"),
damit die Information auch ankommt, wenn man Farben nicht
unterscheiden kann (WCAG 1.4.1).
================================================================ */
/* Zwei Klassen, nicht eine -- und das ist kein Schoenheitsfehler.
Mit `.vw-ub-h` (eine Klasse) gewinnt `.wd h2` (Klasse + Element),
und die Zwischenueberschriften erschienen in 44px Schaugroesse:
Sie waren doppelt so gross wie die Zahlen, um die es geht, und die
Seite las sich wie ein Plakat statt wie ein Arbeitsplatz. Dieselbe
Falle wie frueher bei `.wd a` und `.wd .wd-btn--haupt`. */
.wd .vw-ub-h {
font-size: 1.05rem; font-weight: 650;
margin: 1.9rem 0 .75rem;
letter-spacing: .01em; color: var(--wd-text);
/* Eine leise Linie darunter gliedert die drei Bloecke, ohne sie mit
Kaesten zu umranden -- Rahmen um Rahmen wuerde unruhig. */
padding-bottom: .35rem;
border-bottom: 1px solid rgb(255 255 255 / .07);
}
.wd .vw-ub-h:first-child { margin-top: 0; }
.vw-ub-hinweis { margin: -.4rem 0 .8rem; max-width: 62ch; }
.vw-ub-gitter {
display: grid; gap: .7rem;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
margin-bottom: .4rem;
}
.vw-ub-kachel {
display: flex; flex-direction: column; align-items: flex-start; gap: .18rem;
text-align: left; width: 100%;
padding: .95rem 1rem;
border-radius: var(--wd-radius-m, 14px);
border: 1px solid rgb(var(--wd-blau-rgb) / .16);
background: var(--wd-flaeche-2, #16191d);
color: var(--wd-text);
cursor: pointer;
font: inherit;
box-shadow: var(--wd-hoehe-1);
transition: transform .16s ease, border-color .16s ease, background-color .16s ease;
}
.vw-ub-kachel:hover:not(:disabled) {
transform: translateY(-2px);
border-color: rgb(var(--wd-blau-rgb) / .42);
}
/* Sichtbarer Fokus, weil das hier eine Tastatur-bedienbare Fläche ist
und die Standardmarkierung auf dunklem Grund untergeht. */
.vw-ub-kachel:focus-visible {
outline: 2px solid rgb(var(--wd-blau-rgb) / .95);
outline-offset: 2px;
}
.vw-ub-kachel:disabled { cursor: default; }
.vw-ub-zahl {
font-size: clamp(1.6rem, 4vw, 2.05rem); font-weight: 700; line-height: 1.05;
/* Ziffern in gleicher Breite: Sonst springt die Zahl beim
Aktualisieren seitlich, weil eine 1 schmaler ist als eine 8. */
font-variant-numeric: tabular-nums;
color: rgb(var(--wd-blau-rgb));
}
.vw-ub-titel { font-size: .92rem; font-weight: 600; }
.vw-ub-unten { font-size: .8rem; color: var(--wd-text-soft); }
/* Nichts zu tun — die Kachel tritt zurück, statt zu verschwinden.
Weg wäre falsch: "0 offene Anfragen" ist eine Aussage, und wenn die
Kachel fehlt, weiß man nicht, ob geprüft wurde. */
.vw-ub-kachel--leer { opacity: .62; border-color: rgb(255 255 255 / .07); }
.vw-ub-kachel--leer .vw-ub-zahl { color: var(--wd-text-soft); }
/* Zwei Stufen statt einer. Ein einziger Warnzustand zwingt dazu, alles
Liegengebliebene gleich laut zu machen — dann geht das wirklich
Dringende im Dauerwarnen unter. */
.vw-ub-kachel--warn {
border-color: rgb(233 196 106 / .48);
background: linear-gradient(rgb(233 196 106 / .05), rgb(233 196 106 / .05)), var(--wd-flaeche-2, #16191d);
}
.vw-ub-kachel--warn .vw-ub-zahl { color: #e9c46a; }
.vw-ub-kachel--rot {
border-color: rgb(231 130 120 / .55);
background: linear-gradient(rgb(231 130 120 / .06), rgb(231 130 120 / .06)), var(--wd-flaeche-2, #16191d);
}
/* Gedämpftes Korallrot statt reinem Rot: auf dunklem Grund ist volles
Rot blendend und schlecht lesbar — dieser Ton hält den Kontrast und
bleibt trotzdem ruhig genug fürs lange Hinsehen. */
.vw-ub-kachel--rot .vw-ub-zahl { color: #e78278; }
/* ---- Projekte ---- */
.vw-ub-projekt {
display: block; width: 100%; text-align: left;
padding: .85rem 1rem; margin-bottom: .55rem;
border-radius: var(--wd-radius-m, 14px);
border: 1px solid rgb(255 255 255 / .08);
background: var(--wd-flaeche-2, #16191d);
color: var(--wd-text); cursor: pointer; font: inherit;
transition: border-color .16s ease, transform .16s ease;
}
.vw-ub-projekt:hover { border-color: rgb(var(--wd-blau-rgb) / .38); transform: translateY(-1px); }
.vw-ub-projekt:focus-visible { outline: 2px solid rgb(var(--wd-blau-rgb) / .95); outline-offset: 2px; }
.vw-ub-pkopf {
display: flex; flex-wrap: wrap; align-items: baseline;
justify-content: space-between; gap: .5rem;
}
.vw-ub-pname { font-weight: 650; font-size: .98rem; }
.vw-ub-pwer {
font-size: .74rem; padding: .12rem .5rem; border-radius: 999px;
background: rgb(255 255 255 / .07); color: var(--wd-text-soft);
white-space: nowrap;
}
/* Nur das, was bei MIR liegt, wird hervorgehoben. Was beim Kunden
liegt, ist keine Aufgabe und braucht keine Aufmerksamkeit. */
.vw-ub-pwer--ich {
background: rgb(var(--wd-blau-rgb) / .17);
color: rgb(var(--wd-blau-rgb));
}
.vw-ub-pkunde { display: block; font-size: .8rem; color: var(--wd-text-soft); margin-top: .12rem; }
.vw-ub-balken {
display: block; height: 6px; margin: .55rem 0 .3rem;
border-radius: 999px; background: rgb(255 255 255 / .08); overflow: hidden;
}
.vw-ub-balken-voll {
display: block; height: 100%; border-radius: 999px;
background: linear-gradient(90deg, rgb(var(--wd-blau-rgb) / .75), rgb(var(--wd-gold-rgb) / .8));
transition: width .5s ease;
}
.vw-ub-punten { display: block; font-size: .78rem; color: var(--wd-text-soft); font-variant-numeric: tabular-nums; }
/* ---- Verlauf ---- */
.vw-ub-verlauf { list-style: none; margin: 0; padding: 0; }
.vw-ub-verlauf li {
display: grid; grid-template-columns: auto 1fr auto; gap: .2rem .8rem;
align-items: baseline;
padding: .42rem .2rem;
border-bottom: 1px solid rgb(255 255 255 / .055);
font-size: .85rem;
}
.vw-ub-verlauf li:last-child { border-bottom: 0; }
.vw-ub-vzeit { color: var(--wd-text-soft); font-variant-numeric: tabular-nums; white-space: nowrap; }
.vw-ub-vwas { color: var(--wd-text); }
.vw-ub-vwer { color: var(--wd-text-soft); font-size: .78rem; white-space: nowrap; }
@media (max-width: 560px) {
/* Auf dem Handy untereinander statt in drei Spalten — sonst wird die
mittlere Spalte so schmal, dass jede Zeile dreifach umbricht. */
.vw-ub-verlauf li { grid-template-columns: 1fr; }
.vw-ub-vwer { white-space: normal; }
}
@media (prefers-reduced-motion: reduce) {
.vw-ub-kachel, .vw-ub-projekt, .vw-ub-balken-voll { transition: none; }
.vw-ub-kachel:hover:not(:disabled), .vw-ub-projekt:hover { transform: none; }
}
/* ---------- Kennzahlen, gehobene Fassung ----------
Die erste Fassung war eine Reihe stiller Zahlen. Zahlen, die man
nur anschauen kann, sind Dekoration. Diese hier sind Werkzeuge:
@@ -323,7 +511,7 @@
.vw-notiz-kopf { font-size: .76rem; color: var(--wd-text-soft); margin-bottom: .3rem; }
.vw-notiz p { margin: 0; font-size: .93rem; white-space: pre-wrap; }
.vw-tabs { display: flex; gap: .4rem; }
.vw-tabs { display: flex; flex-wrap: wrap; gap: .4rem; }
.vw-link-anzeige {
padding: 1rem; border-radius: 14px;
background: rgb(var(--wd-gold-rgb) / .08); border: 1px solid rgb(var(--wd-gold-rgb) / .35);
@@ -633,9 +821,11 @@
<!-- ============ ARBEITSBEREICH ============ -->
<div id="vw-bereich" hidden>
<div class="vw-leiste">
<h1 id="vw-titel">Projektanfragen</h1>
<h1 id="vw-titel">Übersicht</h1>
<div class="vw-tabs">
<button type="button" class="vw-filter-knopf vw-an" data-tab="anfragen">Anfragen</button>
<button type="button" class="vw-filter-knopf vw-an" data-tab="uebersicht">Übersicht</button>
<button type="button" class="vw-filter-knopf" data-tab="anfragen">Anfragen<span
class="vw-abzeichen" id="vw-anf-zaehler" hidden>0</span></button>
<button type="button" class="vw-filter-knopf" data-tab="projekte">Projekte</button>
<button type="button" class="vw-filter-knopf" data-tab="kunden">Kunden</button>
<button type="button" class="vw-filter-knopf" data-tab="zahlungen">Zahlungen</button>
@@ -648,17 +838,56 @@
</div>
</div>
<div class="vw-zahlen" id="vw-zahlen"></div>
<!-- ============ ÜBERSICHT — das Cockpit ============
<div class="vw-filter" id="vw-filter"></div>
Die Verwaltung öffnete bisher mit der Anfragenliste. Eine
Liste ist eine Ablage: Sie zeigt, WAS es gibt, nicht was zu
TUN ist. Wer hereinkommt, musste durch fünf Reiter klicken,
um sich ein Bild zu machen — und übersah dabei zuverlässig
das eine, das seit Tagen liegt.
<div class="wd-feld" style="margin-bottom:1rem">
Der Aufbau folgt einer Frage in drei Stufen:
1. Was wartet auf MICH? (das ist heute meine Arbeit)
2. Was wartet auf den KUNDEN? (da bin ich raus)
3. Wie steht es ums Geld? (Umsatz, Gebühren, Offenes)
Erst danach Projekte und Verlauf. -->
<div id="vw-uebersicht-bereich">
<div class="vw-laedt" id="vw-ub-laedt">Wird geladen…</div>
<div id="vw-ub-inhalt" hidden>
<h2 class="vw-ub-h">Was auf dich wartet</h2>
<div class="vw-ub-gitter" id="vw-ub-beimir"></div>
<h2 class="vw-ub-h">Was beim Kunden liegt</h2>
<p class="wd-klein wd-leise vw-ub-hinweis">
Getrennt aufgeführt, weil es nicht dasselbe ist: Zwölf offene
Punkte sind entspannt, wenn elf davon beim Kunden liegen — und
ein Problem, wenn alle elf bei dir liegen.
</p>
<div class="vw-ub-gitter" id="vw-ub-beikunde"></div>
<h2 class="vw-ub-h">Geld</h2>
<div class="vw-ub-gitter" id="vw-ub-geld"></div>
<h2 class="vw-ub-h">Laufende Projekte</h2>
<div id="vw-ub-projekte"></div>
<h2 class="vw-ub-h">Zuletzt geschehen</h2>
<div id="vw-ub-verlauf"></div>
</div>
</div>
<div class="vw-zahlen" id="vw-zahlen" hidden></div>
<div class="vw-filter" id="vw-filter" hidden></div>
<div class="wd-feld" style="margin-bottom:1rem" hidden>
<input type="search" id="vw-suche" class="vw-suche" placeholder="Nach Name, E-Mail oder Nummer suchen…"
style="width:100%" aria-label="Anfragen durchsuchen" />
</div>
<div class="wd-meldung" id="vw-meldung" hidden></div>
<div id="vw-liste"><div class="vw-laedt">Wird geladen…</div></div>
<div id="vw-liste" hidden><div class="vw-laedt">Wird geladen…</div></div>
<!-- ============ KUNDEN ============ -->
<div id="vw-kunden-bereich" hidden>
@@ -956,7 +1185,7 @@
tokenSchreiben(token);
document.getElementById("vw-name").textContent = antwort.name || antwort.actor || "angemeldet";
zeigeBereich();
ladeAnfragen();
ladeUebersicht();
/* Der Postfach-Zaehler direkt nach der Anmeldung. Ohne das sieht man
neue Nachrichten erst, wenn man den Reiter zufaellig anklickt --
also womoeglich tagelang nicht. */
@@ -978,6 +1207,225 @@
});
/* ---------------- Liste ---------------- */
/* =================================================================
ÜBERSICHT — das Cockpit
Zwei Entscheidungen tragen diese Ansicht:
1. JEDE KACHEL IST EIN KNOPF. Eine Zahl, die man nur anschauen kann,
ist Dekoration. "3 ungelesene Nachrichten" ist erst dann
nützlich, wenn ein Klick darauf im Postfach landet. Deshalb sind
es <button>-Elemente und keine <div>s — damit sie auch mit der
Tastatur erreichbar sind und ein Screenreader sie als bedienbar
ansagt.
2. DAS ALTER FÄRBT, NICHT DIE MENGE. Vier neue Anfragen sind kein
Problem. Eine Anfrage, die seit sechs Tagen liegt, ist eines.
Deshalb entscheidet das Alter des ÄLTESTEN Vorgangs über die
Warnfarbe — nicht die Anzahl.
================================================================= */
/* Alter in Tagen. Bewusst über Kalendertage statt über 24-Stunden-
Schritte: Wer Montag früh eine Anfrage bekommt, hat sie Dienstag
früh "seit gestern" liegen — auch wenn erst 23 Stunden vergangen
sind. Das entspricht dem, wie ein Kunde die Wartezeit erlebt. */
function tageSeit(iso) {
if (!iso) return null;
var d = new Date(iso);
if (isNaN(d)) return null;
var a = new Date(d.getFullYear(), d.getMonth(), d.getDate());
var h = new Date();
h = new Date(h.getFullYear(), h.getMonth(), h.getDate());
return Math.round((h - a) / 864e5);
}
function altersText(iso) {
var t = tageSeit(iso);
if (t === null) return "";
if (t <= 0) return "seit heute";
if (t === 1) return "seit gestern";
return "seit " + t + " Tagen";
}
/* Ab wann es dringend wird. Drei Werktage sind die Grenze, unterhalb
derer sich niemand vergessen fühlt — darüber schon. */
function dringlichkeit(iso, grenze) {
var t = tageSeit(iso);
if (t === null) return "";
if (t >= (grenze || 3) * 2) return " vw-ub-kachel--rot";
if (t >= (grenze || 3)) return " vw-ub-kachel--warn";
return "";
}
function kachel(o) {
var leer = !o.zahl || o.zahl === "0" || o.zahl === 0;
/* Eine Kachel mit Null ist eine gute Nachricht und darf sich
zurücknehmen — sie soll den Blick nicht von dem wegziehen, was
wirklich offen ist. */
return '<button type="button" class="vw-ub-kachel' +
(leer ? " vw-ub-kachel--leer" : (o.ton || "")) + '"' +
(o.ziel ? ' data-ziel="' + s(o.ziel) + '"' : ' disabled') +
' aria-label="' + s(o.titel + ": " + o.zahl + (o.unten ? ", " + o.unten : "")) + '">' +
'<span class="vw-ub-zahl">' + s(String(o.zahl)) + '</span>' +
'<span class="vw-ub-titel">' + s(o.titel) + '</span>' +
(o.unten ? '<span class="vw-ub-unten">' + s(o.unten) + '</span>' : "") +
'</button>';
}
var uebersichtGeladen = 0;
async function ladeUebersicht() {
var laedt = document.getElementById("vw-ub-laedt");
var inhalt = document.getElementById("vw-ub-inhalt");
/* Beim erneuten Öffnen NICHT wieder auf "Wird geladen…" zurückfallen.
Der alte Stand ist ein paar Sekunden alt und damit brauchbarer als
ein leerer Kasten — er wird still ersetzt, sobald die neuen Zahlen
da sind. */
if (!uebersichtGeladen) { laedt.hidden = false; inhalt.hidden = true; }
var d;
try {
d = await api("/webdesign/admin/uebersicht");
} catch (fehler) {
laedt.hidden = true;
if (!uebersichtGeladen) {
inhalt.hidden = false;
document.getElementById("vw-ub-beimir").innerHTML =
'<p class="wd-meldung wd-meldung--fehler">Die Übersicht konnte nicht geladen werden' +
(fehler && fehler.status ? " (Fehler " + s(String(fehler.status)) + ")" : "") +
'. Die einzelnen Reiter oben funktionieren unabhängig davon.</p>';
}
return;
}
if (!d || !d.ok) return;
/* Eine Antwort, die zwar ok meldet, aber die Teile nicht mitbringt,
darf die Seite nicht sprengen. Vorher lief sie geradewegs in
d.beiMir.anfragen und warf einen Fehler, der die restliche
Verwaltung mitriss -- aufgefallen am 23.08.2026, als eine
Ersatzantwort im Test genau das ausloeste. Ein halber Server ist
ein realistischer Fall (alte Fassung noch nicht neu gestartet),
und dann sollen die uebrigen Reiter weiter benutzbar sein. */
if (!d.beiMir || !d.beiKunde || !d.geld) {
laedt.hidden = true;
inhalt.hidden = false;
document.getElementById("vw-ub-beimir").innerHTML =
'<p class="wd-meldung">Die Übersicht ist gerade nicht verfügbar. ' +
'Die Reiter oben funktionieren unabhängig davon.</p>';
return;
}
uebersichtGeladen = 1;
laedt.hidden = true;
inhalt.hidden = false;
var m = d.beiMir, k = d.beiKunde, g = d.geld;
document.getElementById("vw-ub-beimir").innerHTML =
kachel({ zahl: m.anfragen.n, titel: "offene Anfragen", ziel: "anfragen",
unten: m.anfragen.n ? "älteste " + altersText(m.anfragen.aeltestes) : "alles beantwortet",
ton: dringlichkeit(m.anfragen.aeltestes, 2) }) +
kachel({ zahl: m.nachrichten.n, titel: "ungelesene Nachrichten", ziel: "postfach",
unten: m.nachrichten.n ? "älteste " + altersText(m.nachrichten.aeltestes) : "nichts Neues",
ton: dringlichkeit(m.nachrichten.aeltestes, 2) }) +
kachel({ zahl: m.wuensche.n, titel: "Änderungswünsche zu prüfen", ziel: "projekte",
unten: m.wuensche.n ? "ältester " + altersText(m.wuensche.aeltestes) : "keine offen",
ton: dringlichkeit(m.wuensche.aeltestes, 3) }) +
kachel({ zahl: m.projekte, titel: "Projekte am Zug", ziel: "projekte",
unten: m.projekte ? "du bist dran" : "nichts liegt bei dir" }) +
/* Ein Widerruf hat eine gesetzliche Frist. Er wird deshalb IMMER
rot dargestellt, sobald einer offen ist — unabhängig vom Alter.
Hier auf einen Schwellwert zu warten wäre der falsche Ort für
Zurückhaltung. */
(m.widerrufe.n
? kachel({ zahl: m.widerrufe.n, titel: "Widerrufe unbearbeitet", ziel: "zahlungen",
unten: "gesetzliche Frist läuft", ton: " vw-ub-kachel--rot" })
: "");
document.getElementById("vw-ub-beikunde").innerHTML =
kachel({ zahl: k.projekte, titel: "Projekte warten auf Kunden", ziel: "projekte",
unten: k.projekte ? "du bist hier raus" : "—" }) +
kachel({ zahl: k.aufgaben, titel: "offene Kundenaufgaben", ziel: "projekte",
unten: k.aufgaben ? "z. B. Texte, Bilder, Freigaben" : "nichts ausstehend" }) +
kachel({ zahl: k.zahlungen.n, titel: "unbezahlte Rechnungen", ziel: "zahlungen",
unten: k.zahlungen.n ? k.zahlungenText + " offen · älteste "
+ altersText(k.zahlungen.aeltestes) : "alles beglichen",
ton: dringlichkeit(k.zahlungen.aeltestes, 14) });
document.getElementById("vw-ub-geld").innerHTML =
kachel({ zahl: g.monat.bruttoText, titel: "Umsatz diesen Monat", ziel: "zahlungen",
unten: g.monat.n + (g.monat.n === 1 ? " Zahlung" : " Zahlungen") }) +
/* Der Unterschied zwischen Umsatz und dem, was ankommt, gehört
sichtbar gemacht. Sonst rechnet man am Jahresende mit der
falschen Zahl — PayPal behält seinen Anteil lautlos ein. */
kachel({ zahl: g.gebuehrenText, titel: "davon PayPal-Gebühr", ziel: "zahlungen",
unten: "netto angekommen: " + g.monat.nettoText }) +
kachel({ zahl: k.zahlungenText, titel: "noch ausstehend", ziel: "zahlungen",
unten: k.zahlungen.n ? "aus " + k.zahlungen.n + " Rechnung" +
(k.zahlungen.n === 1 ? "" : "en") : "nichts offen" }) +
kachel({ zahl: g.gesamt.bruttoText, titel: "Umsatz gesamt", ziel: "zahlungen",
unten: g.gesamt.n + " Zahlungen bisher" });
/* ---- Projekte mit Fortschrittsbalken ---- */
var pz = document.getElementById("vw-ub-projekte");
if (!d.projekte.length) {
pz.innerHTML = '<p class="wd-klein wd-leise">Zurzeit läuft kein Projekt.</p>';
} else {
pz.innerHTML = d.projekte.map(function (p) {
var ganz = p.aufg_gesamt || 0;
var teil = p.aufg_fertig || 0;
var pro = ganz ? Math.round((teil / ganz) * 100) : 0;
var beiMir = p.wartet_auf === "dogfather";
return '<button type="button" class="vw-ub-projekt" data-ziel="projekte">' +
'<span class="vw-ub-pkopf">' +
'<span class="vw-ub-pname">' + s(p.titel || "Ohne Titel") + '</span>' +
'<span class="vw-ub-pwer' + (beiMir ? " vw-ub-pwer--ich" : "") + '">' +
(beiMir ? "du bist dran" : "wartet auf Kunden") + '</span>' +
'</span>' +
'<span class="vw-ub-pkunde">' + s(p.kunde_name || "") +
' · ' + s(p.nummer || "") +
(p.richttermin ? ' · Richttermin ' + s(p.richttermin) : "") + '</span>' +
/* Der Balken trägt die Zahl daneben im Text. Eine Farbe allein
ist keine Information — wer sie nicht unterscheiden kann,
sieht sonst gar nichts. */
'<span class="vw-ub-balken" role="img" aria-label="' +
s(teil + " von " + ganz + " Schritten erledigt") + '">' +
'<span class="vw-ub-balken-voll" style="width:' + pro + '%"></span>' +
'</span>' +
'<span class="vw-ub-punten">' + teil + ' von ' + ganz + ' Schritten · ' + pro + ' %</span>' +
'</button>';
}).join("");
}
/* ---- Verlauf ---- */
var vz = document.getElementById("vw-ub-verlauf");
if (!d.verlauf.length) {
vz.innerHTML = '<p class="wd-klein wd-leise">Noch nichts geschehen.</p>';
} else {
vz.innerHTML = '<ul class="vw-ub-verlauf">' + d.verlauf.map(function (v) {
return '<li><span class="vw-ub-vzeit">' +
s(window.WD.datumZeit ? window.WD.datumZeit(v.erstellt_am) : (v.erstellt_am || "")) +
'</span><span class="vw-ub-vwas">' + s(v.aktion || "") + '</span>' +
'<span class="vw-ub-vwer">' + s(v.bereich || "") +
(v.wer ? " · " + s(v.wer) : "") + '</span></li>';
}).join("") + '</ul>';
}
/* Der Zähler am Anfragen-Reiter, damit man auch aus einer anderen
Ansicht heraus sieht, dass etwas hereingekommen ist. */
var az = document.getElementById("vw-anf-zaehler");
if (az) { az.textContent = m.anfragen.n; az.hidden = !m.anfragen.n; }
}
/* Ein Klick auf eine Kachel führt in den passenden Reiter. Über
Ereignis-Delegation, weil die Kacheln bei jedem Laden neu entstehen
— einzeln angehängte Zuhörer wären nach der ersten Aktualisierung
verschwunden. */
document.addEventListener("click", function (e) {
var ziel = e.target.closest ? e.target.closest("[data-ziel]") : null;
if (!ziel) return;
var knopf = document.querySelector('[data-tab="' + ziel.dataset.ziel + '"]');
if (knopf) knopf.click();
});
async function ladeAnfragen() {
var liste = document.getElementById("vw-liste");
liste.innerHTML = '<div class="vw-laedt">Wird geladen…</div>';
@@ -1522,7 +1970,25 @@
/* =================================================================
KUNDEN
================================================================= */
var aktiverTab = "anfragen";
/* Reiter -> Titel und zugehoeriger Kasten.
Steht bewusst AUSSERHALB des Klick-Zuhoerers. Solange die Zuordnung
nur beim Klicken gelesen wurde, konnte die Ueberschrift beim Laden
gar nicht stimmen: Am 23.08.2026 zeigte die frisch geoeffnete
Verwaltung das Cockpit, waehrend darueber noch "Projektanfragen"
stand -- der Titel wurde erst beim ersten Klick richtig. Jetzt gibt
es eine Quelle, aus der sich beides speist. */
var ANSICHTEN = {
uebersicht:{ titel: "Übersicht", kasten: "vw-uebersicht-bereich" },
anfragen: { titel: "Projektanfragen", kasten: null },
projekte: { titel: "Projekte", kasten: "vw-projekte-bereich" },
kunden: { titel: "Kunden", kasten: "vw-kunden-bereich" },
zahlungen:{ titel: "Zahlungen", kasten: "vw-zahlungen-bereich" },
postfach: { titel: "Postfach", kasten: "vw-postfach-bereich" },
};
var aktiverTab = "uebersicht";
document.getElementById("vw-titel").textContent = ANSICHTEN[aktiverTab].titel;
document.querySelectorAll("[data-tab]").forEach(function (b) {
b.addEventListener("click", function () {
@@ -1537,14 +2003,6 @@
uebereinander. Deshalb jetzt eine Zuordnung: Reiter -> Titel und
zugehoeriger Kasten. Alles andere wird ausgeblendet, ohne dass
man es einzeln aufzaehlen muss. */
var ANSICHTEN = {
anfragen: { titel: "Projektanfragen", kasten: null },
projekte: { titel: "Projekte", kasten: "vw-projekte-bereich" },
kunden: { titel: "Kunden", kasten: "vw-kunden-bereich" },
zahlungen:{ titel: "Zahlungen", kasten: "vw-zahlungen-bereich" },
postfach: { titel: "Postfach", kasten: "vw-postfach-bereich" },
};
var istAnfragen = aktiverTab === "anfragen";
document.getElementById("vw-titel").textContent = ANSICHTEN[aktiverTab].titel;
@@ -1560,6 +2018,8 @@
if (id) document.getElementById(id).hidden = name !== aktiverTab;
});
if (aktiverTab === "uebersicht") ladeUebersicht();
if (aktiverTab === "anfragen") ladeAnfragen();
if (aktiverTab === "kunden") ladeKunden();
if (aktiverTab === "projekte") ladeProjekte();
if (aktiverTab === "zahlungen") ladeZahlungen();
@@ -2788,7 +3248,7 @@
funktionierende Sitzung durch einen untauglichen Ausweis zu
ersetzen. */
tokenQuelle = "code";
zeigeBereich(); ladeAnfragen(); zaehlerHolen();
zeigeBereich(); ladeUebersicht(); zaehlerHolen();
return;
}
@@ -2800,7 +3260,7 @@
document.getElementById("vw-name").textContent =
(document.cookie.match(/dogiwd_rolle=([^;]+)/) || [])[1] === "vanvan" ? "VanVan" : "Dogfather";
zeigeBereich();
ladeAnfragen();
ladeUebersicht();
/* Auch auf diesem Weg. Der Ausweis ist der ÜBLICHE Einstieg (man
kommt von der Zugangswand und ist sofort drin) -- ohne diese
Zeile bliebe der Zähler ausgerechnet im Normalfall leer. */