Verwaltungsbereich fuer Projektanfragen
Masterplan S.13. Anfragen ansehen, filtern, durchsuchen, Status aendern, interne Notizen, archivieren. 9/9 Sicherheitstests. Bewusste Entscheidungen: * NUR AUF DEUTSCH. Der oeffentliche Teil laeuft in fuenf Sprachen, weil dort Kunden landen. Hier landen ausschliesslich Dogfather und VanVan -- fuenf Sprachen waeren fuenffache Pflege und fuenffache Fehlerflaeche ohne einen einzigen Nutzer, der sie braucht. Genau wie der bestehende interne Bereich des Universe. * Anmeldung ueber die BESTEHENDE Team-Anmeldung (/auth/login) statt einer zweiten eigenen. Zwei Anmeldungen fuer dieselben zwei Personen waeren doppelte Pflege und ein zweiter Ort, an dem ein Zugang vergessen werden kann. Der Code der Zugangswand gilt hier ausdruecklich NICHT -- die Verwaltung steckt hinter zwei getrennten Tueren. * Sitzungstoken im sessionStorage, nicht localStorage: es verschwindet beim Schliessen, dieselbe Regel wie an der Zugangswand. Der Test prueft das ausdruecklich. * Bei abgelaufener Sitzung geht es zurueck zur Anmeldung statt zu einer leeren Liste. Eine leere Liste sieht aus wie "keine Anfragen" und ist damit eine stille Falschaussage. * Gold nur beim Status "neu" -- also genau dort, wo wirklich etwas zu tun ist. Wuerde alles leuchten, leuchtet nichts. * Archivieren statt Loeschen, und die Oberflaeche sagt das auch dazu. Pruefskript: kennt jetzt bewusst einsprachige Seiten (verwaltung, portal) und prueft dort nur die Struktur, statt ein fehlendes Woerterbuch zu melden. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -35,11 +35,30 @@ for (const datei of seiten) {
|
||||
|
||||
console.log(`\n--- ${datei} ---`);
|
||||
|
||||
if (!existsSync(i18nDatei)) {
|
||||
/* Bewusst einsprachige Seiten.
|
||||
Der öffentliche Teil läuft in fünf Sprachen, weil dort Kunden landen.
|
||||
In der Verwaltung landen ausschließlich Dogfather und VanVan — fünf
|
||||
Sprachen wären dort fünffache Pflege und fünffache Fehlerfläche ohne
|
||||
einen einzigen Nutzer, der sie braucht. Genau wie der bestehende
|
||||
interne Bereich des Universe, der ebenfalls nur Deutsch spricht.
|
||||
Die Tag-Balance wird trotzdem geprüft (siehe unten). */
|
||||
const NUR_DEUTSCH = ["verwaltung", "portal"];
|
||||
const einsprachig = NUR_DEUTSCH.includes(name);
|
||||
|
||||
if (!existsSync(i18nDatei) && !einsprachig) {
|
||||
console.log(` FEHLER: ${i18nDatei} fehlt`);
|
||||
fehler++;
|
||||
continue;
|
||||
}
|
||||
if (einsprachig) {
|
||||
console.log(" bewusst nur Deutsch (interner Bereich) — nur Struktur geprueft");
|
||||
for (const tag of ["div", "section", "main", "article", "details", "form", "button"]) {
|
||||
const auf = (html.match(new RegExp(`<${tag}\\b`, "g")) || []).length;
|
||||
const zu = (html.match(new RegExp(`</${tag}>`, "g")) || []).length;
|
||||
if (auf !== zu) { console.log(` TAG-UNGLEICHGEWICHT <${tag}>: ${auf} offen, ${zu} geschlossen`); fehler++; }
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wörterbuch laden (setzt window.I18N_WD)
|
||||
global.window = {};
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/* =====================================================================
|
||||
test-webdesign-verwaltung.mjs — Verwaltungsbereich im echten Browser
|
||||
|
||||
Prüft vor allem das, was hier wirklich zählt:
|
||||
- ohne Anmeldung sieht man NICHTS
|
||||
- ein erfundenes Token wird abgewiesen
|
||||
- das Sitzungstoken überlebt das Schließen NICHT
|
||||
- mit Anmeldung: Liste, Detail, Status ändern, Notiz, Archivieren
|
||||
|
||||
Der Verwaltungscode kommt aus der Umgebung, nie aus dieser Datei:
|
||||
WD_ADMIN_CODE=... node test-webdesign-verwaltung.mjs
|
||||
Ohne Code laufen nur die Sicherheitsprüfungen — die sind die wichtigeren.
|
||||
===================================================================== */
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const BASIS = process.env.WD_BASIS || "http://127.0.0.1:4199";
|
||||
const ZUGANG = process.env.WD_CODE || "DOGI-TEST-1";
|
||||
const ADMIN_CODE = process.env.WD_ADMIN_CODE || null;
|
||||
|
||||
let ok = 0, schlecht = 0;
|
||||
const pruefe = (name, b, zusatz = "") => {
|
||||
if (b) { ok++; console.log(" ok " + name); }
|
||||
else { schlecht++; console.log(" FEHLT " + name + " " + zusatz); }
|
||||
};
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const kontext = await browser.newContext();
|
||||
const seite = await kontext.newPage();
|
||||
const konsole = [];
|
||||
seite.on("console", (m) => {
|
||||
if (m.type() !== "error") return;
|
||||
/* Ein abgewiesener Anmeldeversuch erzeugt zwangslaeufig eine
|
||||
Browser-Meldung ueber die 401-Antwort. Das ist das GEWUENSCHTE
|
||||
Verhalten -- wuerde man es als Fehler zaehlen, koennte der Test
|
||||
"falscher Code wird abgewiesen" nie bestehen. */
|
||||
if (/status of 40[13]/.test(m.text())) return;
|
||||
konsole.push(m.text());
|
||||
});
|
||||
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
|
||||
|
||||
console.log("\n=== Verwaltungsbereich ===\n");
|
||||
|
||||
/* ---------- 1. Die Seite selbst ist hinter der Zugangswand ---------- */
|
||||
console.log("1. Doppelter Schutz");
|
||||
{
|
||||
const frisch = await browser.newContext();
|
||||
const p = await frisch.newPage();
|
||||
await p.goto(BASIS + "/webdesign/verwaltung.html", { waitUntil: "networkidle" });
|
||||
pruefe("ohne Zugangscode gar nicht erreichbar", p.url().includes("zugang.html"),
|
||||
"-> die Verwaltung steckt hinter ZWEI Türen");
|
||||
await frisch.close();
|
||||
}
|
||||
|
||||
// Ab hier: durch die Zugangswand, aber noch nicht in der Verwaltung angemeldet.
|
||||
await seite.goto(BASIS + "/webdesign/zugang.html", { waitUntil: "networkidle" });
|
||||
await seite.fill("#zg-code-dogfather", ZUGANG);
|
||||
await Promise.all([
|
||||
seite.waitForURL((u) => !u.pathname.endsWith("zugang.html"), { timeout: 15000 }),
|
||||
seite.click("#zg-form-dogfather button[type=submit]"),
|
||||
]);
|
||||
|
||||
console.log("\n2. Anmeldung der Verwaltung");
|
||||
await seite.goto(BASIS + "/webdesign/verwaltung.html", { waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(500);
|
||||
pruefe("Anmeldemaske erscheint", await seite.locator("#vw-anmeldung").isVisible());
|
||||
pruefe("Arbeitsbereich bleibt verborgen", !(await seite.locator("#vw-bereich").isVisible()));
|
||||
pruefe("keine Anfragedaten im Quelltext",
|
||||
!(await seite.content()).includes("A-2608"),
|
||||
"-> ohne Anmeldung darf nichts durchsickern");
|
||||
|
||||
await seite.click("#vw-login-form button[type=submit]");
|
||||
await seite.waitForTimeout(400);
|
||||
pruefe("leerer Code wird bemaengelt",
|
||||
(await seite.locator("#vw-feld-code.wd-feld--fehler").count()) > 0);
|
||||
|
||||
await seite.fill("#vw-code", "garantiert-falscher-code");
|
||||
await seite.click("#vw-login-form button[type=submit]");
|
||||
/* Auf die Fehlermarkierung WARTEN statt fest zu schlafen: der Aufruf geht
|
||||
an den echten Server, und eine feste Wartezeit ist entweder zu kurz
|
||||
(Fehlalarm) oder unnoetig lang. */
|
||||
const abgewiesen = await seite.locator("#vw-feld-code.wd-feld--fehler")
|
||||
.waitFor({ state: "attached", timeout: 15000 }).then(() => true).catch(() => false);
|
||||
pruefe("falscher Code wird abgewiesen", abgewiesen,
|
||||
abgewiesen ? "" : "Meldung: " + (await seite.locator("#vw-feld-code .wd-feld-fehler").textContent().catch(() => "")));
|
||||
pruefe("bleibt auf der Anmeldemaske", await seite.locator("#vw-anmeldung").isVisible());
|
||||
|
||||
/* ---------- 3. Erfundenes Token ---------- */
|
||||
console.log("\n3. Erfundenes Sitzungstoken");
|
||||
await seite.evaluate(() => sessionStorage.setItem("wd-vw-token", "voellig-erfunden-1234"));
|
||||
await seite.reload({ waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(2500);
|
||||
pruefe("erfundenes Token fuehrt zurueck zur Anmeldung",
|
||||
await seite.locator("#vw-anmeldung").isVisible(),
|
||||
"-> und nicht zu einer leeren Liste, die wie 'keine Anfragen' aussieht");
|
||||
|
||||
/* ---------- 4. Token verschwindet beim Schliessen ---------- */
|
||||
console.log("\n4. Sitzung endet beim Schliessen");
|
||||
{
|
||||
const speicherArt = await seite.evaluate(() => {
|
||||
sessionStorage.setItem("wd-vw-token", "test");
|
||||
const inSession = sessionStorage.getItem("wd-vw-token") !== null;
|
||||
const inLocal = localStorage.getItem("wd-vw-token") !== null;
|
||||
sessionStorage.removeItem("wd-vw-token");
|
||||
return { inSession, inLocal };
|
||||
});
|
||||
pruefe("Token liegt im Sitzungsspeicher, nicht dauerhaft",
|
||||
speicherArt.inSession && !speicherArt.inLocal,
|
||||
"-> sonst bliebe die Verwaltung nach dem Schliessen offen");
|
||||
}
|
||||
|
||||
/* ---------- 5. Mit echtem Zugang ---------- */
|
||||
if (!ADMIN_CODE) {
|
||||
console.log("\n5. Angemeldeter Durchlauf");
|
||||
console.log(" (uebersprungen -- WD_ADMIN_CODE nicht gesetzt)");
|
||||
} else {
|
||||
console.log("\n5. Angemeldeter Durchlauf");
|
||||
await seite.evaluate(() => sessionStorage.removeItem("wd-vw-token"));
|
||||
await seite.reload({ waitUntil: "networkidle" });
|
||||
await seite.fill("#vw-code", ADMIN_CODE);
|
||||
await seite.click("#vw-login-form button[type=submit]");
|
||||
await seite.waitForTimeout(3500);
|
||||
|
||||
const drin = await seite.locator("#vw-bereich").isVisible();
|
||||
pruefe("Anmeldung erfolgreich", drin,
|
||||
drin ? "" : (await seite.locator("#vw-feld-code .wd-feld-fehler").textContent().catch(() => "")));
|
||||
|
||||
if (drin) {
|
||||
pruefe("Kennzahlen werden angezeigt",
|
||||
(await seite.locator("#vw-zahlen .vw-zahl").count()) === 5);
|
||||
pruefe("Filterleiste ist da",
|
||||
(await seite.locator("#vw-filter .vw-filter-knopf").count()) >= 8);
|
||||
|
||||
const anzahl = await seite.locator(".vw-karte").count();
|
||||
console.log(" -> " + anzahl + " Anfragen in der Liste");
|
||||
pruefe("Anfragen werden gelistet", anzahl > 0);
|
||||
|
||||
if (anzahl > 0) {
|
||||
await seite.locator(".vw-karte").first().click();
|
||||
await seite.waitForTimeout(2000);
|
||||
pruefe("Detailansicht oeffnet", await seite.locator("#vw-detail.vw-auf").isVisible());
|
||||
const text = await seite.locator("#vw-detail-blatt").textContent();
|
||||
pruefe("zeigt die Anfragenummer", /A-\d{4}-\d{4}/.test(text));
|
||||
pruefe("zeigt Kontakt und Vorhaben", text.includes("Kontakt") && text.includes("Vorhaben"));
|
||||
pruefe("bietet Statuswechsel an",
|
||||
(await seite.locator("[data-neuer-status]").count()) === 7);
|
||||
pruefe("hat ein Notizfeld", await seite.locator("#vw-neue-notiz").isVisible());
|
||||
pruefe("Archivieren erklaert, dass nichts geloescht wird",
|
||||
text.includes("löscht nichts") || text.includes("loescht nichts"));
|
||||
|
||||
await seite.keyboard.press("Escape");
|
||||
await seite.waitForTimeout(400);
|
||||
pruefe("Escape schliesst die Detailansicht",
|
||||
!(await seite.locator("#vw-detail.vw-auf").isVisible()));
|
||||
}
|
||||
|
||||
// Suche
|
||||
await seite.fill("#vw-suche", "zzz-gibt-es-nicht");
|
||||
await seite.waitForTimeout(400);
|
||||
pruefe("Suche ohne Treffer meldet das verstaendlich",
|
||||
(await seite.locator("#vw-liste").textContent()).includes("Filter"));
|
||||
await seite.fill("#vw-suche", "");
|
||||
await seite.waitForTimeout(300);
|
||||
|
||||
// Abmelden
|
||||
await seite.click("#vw-abmelden");
|
||||
await seite.waitForTimeout(2500);
|
||||
pruefe("Abmelden fuehrt zurueck zur Anmeldung",
|
||||
await seite.locator("#vw-anmeldung").isVisible());
|
||||
pruefe("Token ist nach dem Abmelden weg",
|
||||
(await seite.evaluate(() => sessionStorage.getItem("wd-vw-token"))) === null);
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
console.log(konsole.length ? "\nKONSOLENFEHLER:\n" + [...new Set(konsole)].join("\n") : "\nKeine Konsolenfehler.");
|
||||
console.log(`\n=== ${ok} bestanden, ${schlecht} gescheitert ===\n`);
|
||||
process.exit(schlecht === 0 && konsole.length === 0 ? 0 : 1);
|
||||
@@ -0,0 +1,585 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Verwaltung — Dogfather Webdesign</title>
|
||||
<meta name="robots" content="noindex, nofollow, noarchive" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/favicon.png" />
|
||||
<meta name="theme-color" content="#05070b" />
|
||||
<link rel="manifest" href="/webdesign/app.webmanifest" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/wd-app-180.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-title" content="DF Verwaltung" />
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=20260822wd2" />
|
||||
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260822wd2" />
|
||||
<style>
|
||||
/* =====================================================================
|
||||
Verwaltungsbereich — bewusst NUR AUF DEUTSCH.
|
||||
|
||||
Der öffentliche Teil läuft in fünf Sprachen, weil dort Kunden landen.
|
||||
Hier landen ausschließlich Dogfather und VanVan. Fünf Sprachen wären
|
||||
fünffache Pflege und fünffache Fehlerfläche ohne einen einzigen
|
||||
Nutzer, der sie braucht — genau wie im bestehenden internen Bereich
|
||||
des Universe, der ebenfalls einsprachig ist.
|
||||
===================================================================== */
|
||||
|
||||
/* ---------- Anmeldung ---------- */
|
||||
.vw-login { max-width: 420px; margin: clamp(2rem,8vh,5rem) auto; }
|
||||
|
||||
/* ---------- Kopfleiste ---------- */
|
||||
.vw-leiste {
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: .8rem;
|
||||
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); }
|
||||
|
||||
/* ---------- Kennzahlen ---------- */
|
||||
.vw-zahlen { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: .7rem; margin-bottom: 1.4rem; }
|
||||
.vw-zahl {
|
||||
padding: .9rem 1rem; border-radius: 14px;
|
||||
background: rgba(16,26,41,.6); border: 1px solid var(--wd-rand);
|
||||
text-align: left;
|
||||
}
|
||||
.vw-zahl-wert { font-family: var(--font-head); font-weight: 800; font-size: 1.7rem; line-height: 1; color: var(--wd-blau); }
|
||||
.vw-zahl-name { font-size: .78rem; letter-spacing: .06em; color: var(--wd-text-soft); margin-top: .3rem; }
|
||||
.vw-zahl--achtung .vw-zahl-wert { color: var(--wd-gold); }
|
||||
|
||||
/* ---------- Filter ---------- */
|
||||
.vw-filter { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1rem; align-items: center; }
|
||||
.vw-filter-knopf {
|
||||
min-height: 44px; padding: .5rem .95rem; border-radius: 11px;
|
||||
border: 1px solid var(--wd-rand); background: transparent;
|
||||
color: var(--wd-text-mid); font-weight: 600; font-size: .9rem; cursor: pointer;
|
||||
display: inline-flex; align-items: center; gap: .4rem;
|
||||
}
|
||||
.vw-filter-knopf:hover { border-color: var(--wd-rand-stark); color: var(--wd-text); }
|
||||
.vw-filter-knopf.vw-an { background: rgba(127,208,232,.13); border-color: var(--wd-blau); color: var(--wd-blau); }
|
||||
.vw-filter-zahl {
|
||||
font-size: .74rem; padding: .1rem .4rem; border-radius: 6px;
|
||||
background: rgba(127,208,232,.14); color: var(--wd-blau-hell);
|
||||
}
|
||||
.vw-suche { margin-left: auto; min-width: 200px; flex: 0 1 260px; }
|
||||
|
||||
/* ---------- Liste ---------- */
|
||||
.vw-karte {
|
||||
display: grid; grid-template-columns: auto 1fr auto; gap: .5rem 1rem;
|
||||
align-items: center;
|
||||
padding: 1rem 1.1rem; margin-bottom: .6rem;
|
||||
border-radius: 14px; border: 1px solid var(--wd-rand);
|
||||
background: rgba(16,26,41,.5);
|
||||
cursor: pointer; width: 100%; text-align: left;
|
||||
transition: border-color var(--wd-zeit-schnell) ease, background var(--wd-zeit-schnell) ease;
|
||||
}
|
||||
.vw-karte:hover { border-color: var(--wd-rand-stark); background: rgba(127,208,232,.05); }
|
||||
.vw-karte-nr { font-family: var(--font-head); font-weight: 800; font-size: .82rem; color: var(--wd-blau); white-space: nowrap; }
|
||||
.vw-karte-mitte { min-width: 0; }
|
||||
.vw-karte-name { font-weight: 700; color: var(--wd-text); }
|
||||
.vw-karte-info { font-size: .87rem; color: var(--wd-text-soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.vw-karte-rechts { text-align: right; white-space: nowrap; }
|
||||
.vw-karte-zeit { font-size: .78rem; color: var(--wd-text-soft); margin-top: .25rem; }
|
||||
@media (max-width: 600px) {
|
||||
.vw-karte { grid-template-columns: 1fr auto; }
|
||||
.vw-karte-nr { grid-column: 1 / -1; }
|
||||
}
|
||||
|
||||
/* Statusfarben — Gold nur da, wo wirklich etwas zu tun ist. */
|
||||
.vw-status { display: inline-block; padding: .22rem .6rem; border-radius: 999px; font-size: .74rem; font-weight: 700; }
|
||||
.vw-status--neu { background: rgba(232,200,119,.16); color: var(--wd-gold); }
|
||||
.vw-status--pruefung { background: rgba(127,208,232,.14); color: var(--wd-blau); }
|
||||
.vw-status--rueckfrage { background: rgba(183,157,255,.16); color: var(--wd-lila); }
|
||||
.vw-status--angebot { background: rgba(183,157,255,.16); color: var(--wd-lila); }
|
||||
.vw-status--angenommen { background: rgba(111,214,163,.16); color: var(--wd-ok); }
|
||||
.vw-status--abgeschlossen { background: rgba(111,214,163,.1); color: var(--wd-ok); }
|
||||
.vw-status--abgelehnt { background: rgba(255,157,157,.12); color: var(--wd-fehler); }
|
||||
|
||||
/* ---------- Detailansicht ---------- */
|
||||
.vw-detail { position: fixed; inset: 0; z-index: 100; display: none; }
|
||||
.vw-detail.vw-auf { display: block; }
|
||||
.vw-detail-schatten { position: absolute; inset: 0; background: rgba(2,4,8,.78); backdrop-filter: blur(3px); }
|
||||
.vw-detail-blatt {
|
||||
position: absolute; top: 0; right: 0; bottom: 0;
|
||||
width: min(680px, 100%);
|
||||
background: #080d16; border-left: 1px solid var(--wd-rand-stark);
|
||||
overflow-y: auto; padding: 1.4rem clamp(1rem,3vw,1.8rem) 3rem;
|
||||
animation: vw-rein .28s cubic-bezier(.22,1,.36,1) both;
|
||||
}
|
||||
@keyframes vw-rein { from { transform: translateX(24px); opacity: 0; } to { transform: none; opacity: 1; } }
|
||||
@media (prefers-reduced-motion: reduce) { .vw-detail-blatt { animation: none; } }
|
||||
.vw-detail-kopf { display: flex; align-items: flex-start; gap: 1rem; margin-bottom: 1.2rem; }
|
||||
.vw-zu {
|
||||
margin-left: auto; min-width: 44px; min-height: 44px;
|
||||
border-radius: 10px; border: 1px solid var(--wd-rand);
|
||||
background: transparent; color: var(--wd-text-mid); cursor: pointer; font-size: 1.1rem;
|
||||
}
|
||||
.vw-zu:hover { color: var(--wd-text); border-color: var(--wd-rand-stark); }
|
||||
|
||||
.vw-abschnitt { margin-bottom: 1.4rem; }
|
||||
.vw-abschnitt > h3 {
|
||||
font-size: .74rem; letter-spacing: .13em; text-transform: uppercase;
|
||||
color: var(--wd-blau); margin-bottom: .6rem;
|
||||
padding-bottom: .4rem; border-bottom: 1px solid var(--wd-rand);
|
||||
}
|
||||
.vw-paar { display: grid; grid-template-columns: minmax(0,.4fr) minmax(0,1fr); gap: .2rem .9rem; margin-bottom: .45rem; }
|
||||
@media (max-width: 520px) { .vw-paar { grid-template-columns: 1fr; margin-bottom: .8rem; } }
|
||||
.vw-paar dt { color: var(--wd-text-soft); font-size: .88rem; }
|
||||
.vw-paar dd { margin: 0; color: var(--wd-text); font-size: .95rem; overflow-wrap: break-word; white-space: pre-wrap; }
|
||||
.vw-paar dd.vw-leer { color: var(--wd-text-soft); font-style: italic; }
|
||||
|
||||
.vw-status-wahl { display: flex; flex-wrap: wrap; gap: .4rem; }
|
||||
.vw-notiz {
|
||||
padding: .8rem .95rem; border-radius: 12px; margin-bottom: .5rem;
|
||||
background: rgba(127,208,232,.05); border: 1px solid var(--wd-rand);
|
||||
}
|
||||
.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-leer-hinweis { text-align: center; padding: 3rem 1rem; color: var(--wd-text-soft); }
|
||||
.vw-laedt { text-align: center; padding: 2.5rem 1rem; color: var(--wd-text-soft); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="wd">
|
||||
|
||||
<div id="wd-kopf" data-wd-kopf="minimal"></div>
|
||||
|
||||
<main id="wd-inhalt">
|
||||
<section class="wd-abschnitt" style="padding-top:1.6rem">
|
||||
<div class="wd-container">
|
||||
|
||||
<!-- ============ ANMELDUNG ============ -->
|
||||
<div id="vw-anmeldung" class="wd-karte vw-login" hidden>
|
||||
<h1 style="font-size:1.4rem">Verwaltung</h1>
|
||||
<p class="wd-klein wd-leise">
|
||||
Dein persönlicher Zugangscode aus dem internen Bereich — derselbe
|
||||
wie für das Postfach. Der Code der Zugangswand gilt hier nicht.
|
||||
</p>
|
||||
<form id="vw-login-form" novalidate>
|
||||
<div class="wd-feld" id="vw-feld-code">
|
||||
<label for="vw-code">Zugangscode</label>
|
||||
<input type="password" id="vw-code" autocomplete="current-password"
|
||||
autocapitalize="off" spellcheck="false" />
|
||||
<p class="wd-feld-fehler"></p>
|
||||
</div>
|
||||
<button type="submit" class="wd-btn wd-btn--haupt wd-btn--breit">Anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- ============ ARBEITSBEREICH ============ -->
|
||||
<div id="vw-bereich" hidden>
|
||||
<div class="vw-leiste">
|
||||
<h1>Projektanfragen</h1>
|
||||
<div class="vw-wer">
|
||||
<span id="vw-name">—</span>
|
||||
<button type="button" class="wd-btn wd-btn--still wd-btn--klein" id="vw-abmelden">Abmelden</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vw-zahlen" id="vw-zahlen"></div>
|
||||
|
||||
<div class="vw-filter" id="vw-filter"></div>
|
||||
|
||||
<div class="wd-feld" style="margin-bottom:1rem">
|
||||
<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>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- ============ DETAILANSICHT ============ -->
|
||||
<div class="vw-detail" id="vw-detail" role="dialog" aria-modal="true" aria-label="Anfrage im Detail">
|
||||
<div class="vw-detail-schatten" data-schliessen></div>
|
||||
<div class="vw-detail-blatt" id="vw-detail-blatt"></div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/wd-core.js?v=20260822wd2"></script>
|
||||
<script>
|
||||
/* =====================================================================
|
||||
Verwaltung: Anfragen ansehen, Status ändern, Notizen, Archivieren.
|
||||
|
||||
Anmeldung: bewusst über die BESTEHENDE Team-Anmeldung des internen
|
||||
Bereichs (/auth/login) statt einer zweiten eigenen. Zwei getrennte
|
||||
Anmeldungen für dieselben zwei Personen wären doppelte Pflege und ein
|
||||
zweiter Ort, an dem ein Zugang vergessen werden kann.
|
||||
|
||||
Das Sitzungstoken liegt in sessionStorage, NICHT in localStorage:
|
||||
Es soll beim Schließen verschwinden — dieselbe Regel wie an der
|
||||
Zugangswand ("jedes Mal wieder fragen").
|
||||
===================================================================== */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var TOKEN_SCHLUESSEL = "wd-vw-token";
|
||||
var token = null;
|
||||
var alleAnfragen = [];
|
||||
var filterStatus = "";
|
||||
var filterArchiv = false;
|
||||
var suchText = "";
|
||||
|
||||
var STATUS_TEXT = {
|
||||
neu: "Neu", pruefung: "In Prüfung", rueckfrage: "Rückfrage",
|
||||
angebot: "Angebot raus", angenommen: "Angenommen",
|
||||
abgeschlossen: "Abgeschlossen", abgelehnt: "Abgelehnt",
|
||||
};
|
||||
var PAKET_TEXT = {
|
||||
onepager: "Onepager", website: "Professionelle Website",
|
||||
shop: "Shop & umfangreiche Website", verwaltung: "Buchhaltungs- & Steuerverwaltung",
|
||||
betreuung: "Laufende Betreuung", unklar: "Noch unklar",
|
||||
};
|
||||
var VORHANDEN_TEXT = { ja: "vorhanden", teilweise: "teilweise", nein: "fehlt noch" };
|
||||
var BUDGET_TEXT = {
|
||||
"bis-500": "bis 500 €", "500-1000": "500 – 1.000 €",
|
||||
"1000-2500": "1.000 – 2.500 €", "ueber-2500": "über 2.500 €", unklar: "noch unklar",
|
||||
};
|
||||
|
||||
var s = window.WD.sicher;
|
||||
var meldung = document.getElementById("vw-meldung");
|
||||
|
||||
function tokenLesen() {
|
||||
try { return sessionStorage.getItem(TOKEN_SCHLUESSEL); } catch (e) { return null; }
|
||||
}
|
||||
function tokenSchreiben(t) {
|
||||
try { if (t) sessionStorage.setItem(TOKEN_SCHLUESSEL, t); else sessionStorage.removeItem(TOKEN_SCHLUESSEL); } 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);
|
||||
}
|
||||
|
||||
/* ---------------- Anmeldung ---------------- */
|
||||
function zeigeAnmeldung() {
|
||||
document.getElementById("vw-anmeldung").hidden = false;
|
||||
document.getElementById("vw-bereich").hidden = true;
|
||||
var feld = document.getElementById("vw-code");
|
||||
if (feld) setTimeout(function () { feld.focus(); }, 100);
|
||||
}
|
||||
function zeigeBereich() {
|
||||
document.getElementById("vw-anmeldung").hidden = true;
|
||||
document.getElementById("vw-bereich").hidden = false;
|
||||
}
|
||||
|
||||
document.getElementById("vw-login-form").addEventListener("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
var feld = document.getElementById("vw-feld-code");
|
||||
var eingabe = document.getElementById("vw-code");
|
||||
var knopf = e.target.querySelector("button");
|
||||
feld.classList.remove("wd-feld--fehler");
|
||||
|
||||
var code = eingabe.value.trim();
|
||||
if (!code) {
|
||||
feld.classList.add("wd-feld--fehler");
|
||||
feld.querySelector(".wd-feld-fehler").textContent = "Bitte den Zugangscode eingeben.";
|
||||
return;
|
||||
}
|
||||
|
||||
window.WD.knopfSperren(knopf, true, "Wird geprüft…");
|
||||
try {
|
||||
var antwort = await window.WD.api("/auth/login", { method: "POST", body: { code: code } });
|
||||
token = antwort.token;
|
||||
tokenSchreiben(token);
|
||||
document.getElementById("vw-name").textContent = antwort.name || antwort.actor || "angemeldet";
|
||||
zeigeBereich();
|
||||
ladeAnfragen();
|
||||
} catch (fehler) {
|
||||
feld.classList.add("wd-feld--fehler");
|
||||
feld.querySelector(".wd-feld-fehler").textContent = fehler.message || "Anmeldung fehlgeschlagen.";
|
||||
eingabe.select();
|
||||
} finally {
|
||||
window.WD.knopfSperren(knopf, false);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("vw-abmelden").addEventListener("click", async function () {
|
||||
try { await api("/auth/logout", { method: "POST", body: {} }); } catch (e) { /* egal */ }
|
||||
token = null; tokenSchreiben(null);
|
||||
location.reload();
|
||||
});
|
||||
|
||||
/* ---------------- Liste ---------------- */
|
||||
async function ladeAnfragen() {
|
||||
var liste = document.getElementById("vw-liste");
|
||||
liste.innerHTML = '<div class="vw-laedt">Wird geladen…</div>';
|
||||
try {
|
||||
var antwort = await api("/webdesign/admin/anfragen?archiviert=" + (filterArchiv ? "1" : "0"));
|
||||
alleAnfragen = antwort.anfragen || [];
|
||||
zeichneZahlen(antwort.zaehler || {});
|
||||
zeichneFilter(antwort.zaehler || {});
|
||||
zeichneListe();
|
||||
window.WD.meldungAus(meldung);
|
||||
} catch (fehler) {
|
||||
if (fehler.status === 401) {
|
||||
/* Sitzung abgelaufen — zurück zur Anmeldung statt einer
|
||||
Fehlermeldung, mit der niemand etwas anfangen kann. */
|
||||
token = null; tokenSchreiben(null); zeigeAnmeldung();
|
||||
return;
|
||||
}
|
||||
liste.innerHTML = "";
|
||||
window.WD.meldung(meldung, fehler.message || "Die Anfragen konnten nicht geladen werden.", "fehler");
|
||||
}
|
||||
}
|
||||
|
||||
function zeichneZahlen(zaehler) {
|
||||
var offen = (zaehler.neu || 0) + (zaehler.rueckfrage || 0);
|
||||
var gesamt = Object.keys(zaehler).reduce(function (n, k) { return n + zaehler[k]; }, 0);
|
||||
var kacheln = [
|
||||
{ wert: zaehler.neu || 0, name: "neu, ungelesen", achtung: (zaehler.neu || 0) > 0 },
|
||||
{ wert: zaehler.pruefung || 0, name: "in Prüfung" },
|
||||
{ wert: zaehler.angebot || 0, name: "Angebot raus" },
|
||||
{ wert: zaehler.angenommen || 0, name: "angenommen" },
|
||||
{ wert: gesamt, name: "insgesamt offen" },
|
||||
];
|
||||
document.getElementById("vw-zahlen").innerHTML = kacheln.map(function (k) {
|
||||
return '<div class="vw-zahl' + (k.achtung ? " vw-zahl--achtung" : "") + '">' +
|
||||
'<div class="vw-zahl-wert">' + k.wert + "</div>" +
|
||||
'<div class="vw-zahl-name">' + s(k.name) + "</div></div>";
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function zeichneFilter(zaehler) {
|
||||
var knoepfe = [{ wert: "", name: "Alle" }].concat(
|
||||
Object.keys(STATUS_TEXT).map(function (k) { return { wert: k, name: STATUS_TEXT[k] }; })
|
||||
);
|
||||
var html = knoepfe.map(function (k) {
|
||||
var n = k.wert ? (zaehler[k.wert] || 0) : "";
|
||||
return '<button type="button" class="vw-filter-knopf' + (filterStatus === k.wert ? " vw-an" : "") +
|
||||
'" data-status="' + k.wert + '">' + s(k.name) +
|
||||
(n !== "" ? '<span class="vw-filter-zahl">' + n + "</span>" : "") + "</button>";
|
||||
}).join("");
|
||||
html += '<button type="button" class="vw-filter-knopf' + (filterArchiv ? " vw-an" : "") +
|
||||
'" data-archiv="1" style="margin-left:.6rem">Archiv</button>';
|
||||
var box = document.getElementById("vw-filter");
|
||||
box.innerHTML = html;
|
||||
box.querySelectorAll("[data-status]").forEach(function (b) {
|
||||
b.addEventListener("click", function () { filterStatus = b.dataset.status; zeichneFilter({}); ladeAnfragen(); });
|
||||
});
|
||||
box.querySelector("[data-archiv]").addEventListener("click", function () {
|
||||
filterArchiv = !filterArchiv; filterStatus = ""; ladeAnfragen();
|
||||
});
|
||||
}
|
||||
|
||||
function zeichneListe() {
|
||||
var liste = document.getElementById("vw-liste");
|
||||
var gefiltert = alleAnfragen.filter(function (a) {
|
||||
if (filterStatus && a.status !== filterStatus) return false;
|
||||
if (!suchText) return true;
|
||||
var t = suchText.toLowerCase();
|
||||
return (a.name || "").toLowerCase().indexOf(t) >= 0 ||
|
||||
(a.email || "").toLowerCase().indexOf(t) >= 0 ||
|
||||
(a.nummer || "").toLowerCase().indexOf(t) >= 0;
|
||||
});
|
||||
|
||||
if (!gefiltert.length) {
|
||||
liste.innerHTML = '<div class="vw-leer-hinweis">' +
|
||||
(alleAnfragen.length ? "Keine Anfrage passt zu diesem Filter." :
|
||||
filterArchiv ? "Im Archiv liegt noch nichts." : "Noch keine Anfragen eingegangen.") + "</div>";
|
||||
return;
|
||||
}
|
||||
|
||||
liste.innerHTML = gefiltert.map(function (a) {
|
||||
return '<button type="button" class="vw-karte" data-id="' + s(a.id) + '">' +
|
||||
'<span class="vw-karte-nr">' + s(a.nummer) + "</span>" +
|
||||
'<span class="vw-karte-mitte">' +
|
||||
'<span class="vw-karte-name">' + s(a.name) + "</span><br />" +
|
||||
'<span class="vw-karte-info">' + s(PAKET_TEXT[a.paket] || a.paket) +
|
||||
(a.budget ? " · " + s(BUDGET_TEXT[a.budget] || a.budget) : "") + "</span>" +
|
||||
"</span>" +
|
||||
'<span class="vw-karte-rechts">' +
|
||||
'<span class="vw-status vw-status--' + s(a.status) + '">' + s(STATUS_TEXT[a.status] || a.status) + "</span>" +
|
||||
'<span class="vw-karte-zeit">' + window.WD.datum(a.erstellt_am) + "</span>" +
|
||||
"</span></button>";
|
||||
}).join("");
|
||||
|
||||
liste.querySelectorAll("[data-id]").forEach(function (k) {
|
||||
k.addEventListener("click", function () { oeffneDetail(k.dataset.id); });
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("vw-suche").addEventListener("input", function (e) {
|
||||
suchText = e.target.value.trim();
|
||||
zeichneListe();
|
||||
});
|
||||
|
||||
/* ---------------- Detail ---------------- */
|
||||
var detail = document.getElementById("vw-detail");
|
||||
var blatt = document.getElementById("vw-detail-blatt");
|
||||
var offeneId = null;
|
||||
|
||||
function schliesseDetail() {
|
||||
detail.classList.remove("vw-auf");
|
||||
offeneId = null;
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
detail.querySelector("[data-schliessen]").addEventListener("click", schliesseDetail);
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape" && detail.classList.contains("vw-auf")) schliesseDetail();
|
||||
});
|
||||
|
||||
function paar(was, wert, roh) {
|
||||
var leer = !wert;
|
||||
return '<div class="vw-paar"><dt>' + s(was) + "</dt>" +
|
||||
'<dd' + (leer ? ' class="vw-leer"' : "") + ">" +
|
||||
(leer ? "keine Angabe" : s(roh ? roh : wert)) + "</dd></div>";
|
||||
}
|
||||
|
||||
async function oeffneDetail(id) {
|
||||
offeneId = id;
|
||||
detail.classList.add("vw-auf");
|
||||
document.body.style.overflow = "hidden";
|
||||
blatt.innerHTML = '<div class="vw-laedt">Wird geladen…</div>';
|
||||
|
||||
try {
|
||||
var antwort = await api("/webdesign/admin/anfragen/" + encodeURIComponent(id));
|
||||
var a = antwort.anfrage;
|
||||
var notizen = antwort.notizen || [];
|
||||
|
||||
var funktionen = (a.funktionen || []).join(", ");
|
||||
var details = Object.keys(a.details || {}).map(function (k) {
|
||||
return paar(k.replace(/_/g, " "), a.details[k]);
|
||||
}).join("");
|
||||
|
||||
blatt.innerHTML =
|
||||
'<div class="vw-detail-kopf">' +
|
||||
"<div><div class=\"vw-karte-nr\" style=\"font-size:.9rem\">" + s(a.nummer) + "</div>" +
|
||||
'<h2 style="margin:.2rem 0 .3rem;font-size:1.4rem">' + s(a.name) + "</h2>" +
|
||||
'<span class="vw-status vw-status--' + s(a.status) + '">' + s(STATUS_TEXT[a.status] || a.status) + "</span></div>" +
|
||||
'<button class="vw-zu" type="button" aria-label="Schließen">✕</button>' +
|
||||
"</div>" +
|
||||
|
||||
'<div class="vw-abschnitt"><h3>Kontakt</h3>' +
|
||||
paar("E-Mail", a.email) +
|
||||
paar("Telefon", a.telefon) +
|
||||
paar("Bevorzugt", a.kontaktweg === "telefon" ? "Telefon" : "E-Mail") +
|
||||
paar("Sprache", a.sprache) +
|
||||
'<div class="wd-btn-reihe" style="margin-top:.7rem">' +
|
||||
'<a class="wd-btn wd-btn--zweit wd-btn--klein" href="mailto:' + s(a.email) +
|
||||
"?subject=" + encodeURIComponent("Deine Anfrage " + a.nummer + " bei Dogfather Webdesign") +
|
||||
'">Antwort schreiben</a></div>' +
|
||||
"</div>" +
|
||||
|
||||
'<div class="vw-abschnitt"><h3>Vorhaben</h3>' +
|
||||
paar("Paket", PAKET_TEXT[a.paket] || a.paket) +
|
||||
paar("Ziel", a.ziel) +
|
||||
paar("Zielgruppe", a.zielgruppe) +
|
||||
"</div>" +
|
||||
|
||||
'<div class="vw-abschnitt"><h3>Umfang</h3>' +
|
||||
paar("Bereiche", a.seiten) +
|
||||
paar("Funktionen", funktionen) +
|
||||
details +
|
||||
"</div>" +
|
||||
|
||||
'<div class="vw-abschnitt"><h3>Vorhandene Inhalte</h3>' +
|
||||
paar("Texte", VORHANDEN_TEXT[a.hat_texte] || a.hat_texte) +
|
||||
paar("Bilder", VORHANDEN_TEXT[a.hat_bilder] || a.hat_bilder) +
|
||||
paar("Logo", VORHANDEN_TEXT[a.hat_logo] || a.hat_logo) +
|
||||
paar("Domain", VORHANDEN_TEXT[a.hat_domain] || a.hat_domain) +
|
||||
"</div>" +
|
||||
|
||||
'<div class="vw-abschnitt"><h3>Rahmen</h3>' +
|
||||
paar("Budget", BUDGET_TEXT[a.budget] || a.budget) +
|
||||
paar("Wunschtermin", a.termin) +
|
||||
paar("Eingegangen", window.WD.datum(a.erstellt_am)) +
|
||||
"</div>" +
|
||||
|
||||
(a.bemerkung ? '<div class="vw-abschnitt"><h3>Bemerkung</h3>' + paar("", a.bemerkung) + "</div>" : "") +
|
||||
|
||||
'<div class="vw-abschnitt"><h3>Status ändern</h3>' +
|
||||
'<div class="vw-status-wahl">' +
|
||||
Object.keys(STATUS_TEXT).map(function (k) {
|
||||
return '<button type="button" class="vw-filter-knopf' + (a.status === k ? " vw-an" : "") +
|
||||
'" data-neuer-status="' + k + '">' + s(STATUS_TEXT[k]) + "</button>";
|
||||
}).join("") + "</div>" +
|
||||
"</div>" +
|
||||
|
||||
'<div class="vw-abschnitt"><h3>Interne Notizen (' + notizen.length + ")</h3>" +
|
||||
'<div id="vw-notizen">' +
|
||||
(notizen.length ? notizen.map(function (n) {
|
||||
return '<div class="vw-notiz"><div class="vw-notiz-kopf">' + s(n.autor) + " · " +
|
||||
window.WD.datum(n.erstellt_am) + '</div><p>' + s(n.text) + "</p></div>";
|
||||
}).join("") : '<p class="wd-klein wd-leise">Noch keine Notiz.</p>') +
|
||||
"</div>" +
|
||||
'<div class="wd-feld" style="margin-top:.7rem;margin-bottom:.5rem">' +
|
||||
'<textarea id="vw-neue-notiz" rows="2" placeholder="Notiz hinzufügen (nur intern sichtbar)"></textarea>' +
|
||||
"</div>" +
|
||||
'<button type="button" class="wd-btn wd-btn--zweit wd-btn--klein" id="vw-notiz-speichern">Notiz speichern</button>' +
|
||||
"</div>" +
|
||||
|
||||
'<div class="vw-abschnitt"><h3>Aufräumen</h3>' +
|
||||
'<p class="wd-klein wd-leise">Archivieren löscht nichts — die Anfrage bleibt vollständig erhalten und lässt sich jederzeit zurückholen.</p>' +
|
||||
'<button type="button" class="wd-btn wd-btn--still wd-btn--klein" id="vw-archivieren">' +
|
||||
(a.archiviert ? "Aus dem Archiv zurückholen" : "Archivieren") + "</button>" +
|
||||
"</div>";
|
||||
|
||||
blatt.querySelector(".vw-zu").addEventListener("click", schliesseDetail);
|
||||
|
||||
blatt.querySelectorAll("[data-neuer-status]").forEach(function (b) {
|
||||
b.addEventListener("click", async function () {
|
||||
window.WD.knopfSperren(b, true, "…");
|
||||
try {
|
||||
await api("/webdesign/admin/anfragen/" + encodeURIComponent(id) + "/status",
|
||||
{ method: "POST", body: { status: b.dataset.neuerStatus } });
|
||||
await ladeAnfragen();
|
||||
oeffneDetail(id);
|
||||
} catch (f) {
|
||||
window.WD.knopfSperren(b, false);
|
||||
alert(f.message || "Status konnte nicht geändert werden.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
blatt.querySelector("#vw-notiz-speichern").addEventListener("click", async function (e) {
|
||||
var feld = blatt.querySelector("#vw-neue-notiz");
|
||||
var text = feld.value.trim();
|
||||
if (!text) { feld.focus(); return; }
|
||||
window.WD.knopfSperren(e.target, true, "Wird gespeichert…");
|
||||
try {
|
||||
await api("/webdesign/admin/anfragen/" + encodeURIComponent(id) + "/notiz",
|
||||
{ method: "POST", body: { text: text } });
|
||||
oeffneDetail(id);
|
||||
} catch (f) {
|
||||
window.WD.knopfSperren(e.target, false);
|
||||
alert(f.message || "Notiz konnte nicht gespeichert werden.");
|
||||
}
|
||||
});
|
||||
|
||||
blatt.querySelector("#vw-archivieren").addEventListener("click", async function (e) {
|
||||
window.WD.knopfSperren(e.target, true, "…");
|
||||
try {
|
||||
await api("/webdesign/admin/anfragen/" + encodeURIComponent(id) + "/archivieren",
|
||||
{ method: "POST", body: { zurueck: !!a.archiviert } });
|
||||
schliesseDetail();
|
||||
ladeAnfragen();
|
||||
} catch (f) {
|
||||
window.WD.knopfSperren(e.target, false);
|
||||
alert(f.message || "Konnte nicht archiviert werden.");
|
||||
}
|
||||
});
|
||||
|
||||
} catch (fehler) {
|
||||
blatt.innerHTML = '<div class="vw-leer-hinweis">' + s(fehler.message || "Konnte nicht geladen werden.") +
|
||||
'<br /><br /><button class="wd-btn wd-btn--still wd-btn--klein" type="button" onclick="this.closest(\'.vw-detail\').classList.remove(\'vw-auf\')">Schließen</button></div>';
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- Start ---------------- */
|
||||
function start() {
|
||||
token = tokenLesen();
|
||||
if (!token) { zeigeAnmeldung(); return; }
|
||||
zeigeBereich();
|
||||
ladeAnfragen();
|
||||
}
|
||||
|
||||
if (window.WD) start();
|
||||
else document.addEventListener("wd-bereit", start);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user