Files
dogfather-universe/webdesign/verwaltung.html
T
DogFatherGitandClaude Opus 5 2542a2598e Kunden und Projekte in der Verwaltung anlegen, inklusive Testzugang
Wunsch 22.08.2026: "ich will einen provisorischen zugang fuer mich".
Der richtige Weg dahin war ohnehin ueberfaellig -- Kundenzugaenge
entstehen ausschliesslich hier, es gibt bewusst keine Selbstregistrierung.

Neu:
* Kundenzugang anlegen (Name, E-Mail, Firma, Sprache, sofort freischalten
  ja/nein) mit Einladungslink
* "Testzugang fuer mich" -- ein Klick, legt einen als Test erkennbaren
  Zugang mit Datumsstempel an. Bewusst NICHT die echte Geschaeftsadresse:
  die E-Mail ist gleichzeitig der Anmeldename und laesst sich aus gutem
  Grund nicht mehr aendern, ein Test wuerde also spaeter mit einem echten
  Kundenkonto kollidieren.
* Zugaenge auflisten, freischalten, sperren, neuen Einladungslink erzeugen
* Projekte anlegen und aendern, Aenderungswuensche beziffern, Nachrichten

Entscheidungen:
* Der Einladungslink geht EINMAL im Klartext raus, direkt beim Anlegen.
  In der Datenbank liegt nur sein Hash. Wer ihn verliert, bekommt einen
  neuen -- das ist sicherer, als ihn dauerhaft abrufbar zu halten. Die
  Oberflaeche sagt das auch klar dazu, sonst klickt man ihn weg und
  wundert sich.
* Ein neuer Link entwertet alle offenen alten. Sonst sammeln sich mehrere
  gueltige Generalschluessel fuer dasselbe Konto an.
* "Sofort freischalten" ist eine bewusste Handlung. Ohne Haekchen wird der
  Zugang angelegt, kommt aber noch nicht hinein -- Masterplan S.12
  verlangt, dass Projektkauf oder Betreuung VOR der Freischaltung geprueft
  werden.
* Sperren beendet laufende Sitzungen sofort, nicht erst nach Ablauf.
* Die 30 % Anzahlung werden aus dem Preis BERECHNET, nicht eingetippt.
  Ein Tippfehler in der Anzahlung faellt sonst erst beim Geldeingang auf.
* Preise kommen als Euro herein und werden sofort in Cent umgerechnet
  (Math.round, damit 49.99 nicht zu 4998 wird). Ab da nie wieder Komma.
* Vier klar unterscheidbare Zustaende in der Liste. Wichtig vor allem
  "Einladung offen": freigeschaltet, aber noch kein Passwort gesetzt --
  der Kunde war also noch nie drin.
* Kopieren faellt auf Markieren zurueck, wenn die Zwischenablage
  blockiert ist. Eine Fehlermeldung waere dort nutzlos.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-22 21:23:15 +02:00

831 lines
38 KiB
HTML
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.
<!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-tabs { display: flex; gap: .4rem; }
.vw-link-anzeige {
padding: 1rem; border-radius: 14px;
background: rgba(232,200,119,.08); border: 1px solid rgba(232,200,119,.35);
}
.vw-link-anzeige code {
display: block; word-break: break-all; margin: .6rem 0;
font-size: .84rem; color: var(--wd-gold); line-height: 1.5;
}
.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 id="vw-titel">Projektanfragen</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" data-tab="kunden">Kunden</button>
</div>
<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>
<!-- ============ KUNDEN ============ -->
<div id="vw-kunden-bereich" hidden>
<div class="wd-karte" style="margin-bottom:1.2rem">
<h2 style="font-size:1.15rem">Neuen Kundenzugang anlegen</h2>
<p class="wd-klein wd-leise">
Der Kunde bekommt einen Einladungslink und setzt sein Passwort selbst.
Den Link siehst du <strong>nur einmal</strong> — danach steht in der
Datenbank nur noch sein verschlüsselter Abdruck. Verloren? Einfach
einen neuen erzeugen.
</p>
<form id="vw-kunde-form" novalidate>
<div class="wd-grid wd-grid--2">
<div class="wd-feld" id="vw-feld-kname">
<label for="vw-kname">Name<span class="wd-pflicht">*</span></label>
<input type="text" id="vw-kname" autocomplete="off" />
<p class="wd-feld-fehler"></p>
</div>
<div class="wd-feld" id="vw-feld-kmail">
<label for="vw-kmail">E-Mail<span class="wd-pflicht">*</span></label>
<input type="email" id="vw-kmail" autocomplete="off" inputmode="email" />
<p class="wd-feld-fehler"></p>
</div>
<div class="wd-feld">
<label for="vw-kfirma">Firma <span class="wd-leise">optional</span></label>
<input type="text" id="vw-kfirma" autocomplete="off" />
</div>
<div class="wd-feld">
<label for="vw-ksprache">Sprache</label>
<select id="vw-ksprache">
<option value="de">Deutsch</option>
<option value="de-CH">Schwiizerdütsch</option>
<option value="en">English</option>
<option value="fr">Français</option>
<option value="pt">Português</option>
</select>
</div>
</div>
<label class="wd-wahl-kachel" style="display:block;margin-bottom:.9rem">
<input type="checkbox" id="vw-kfrei" checked />
<span class="wd-wahl-inhalt">
<span class="wd-wahl-titel">Sofort freischalten</span>
<span class="wd-wahl-text">Ohne Häkchen wird der Zugang angelegt, kommt aber noch nicht hinein — für Kunden, deren Projekt erst später startet.</span>
</span>
</label>
<div class="wd-btn-reihe">
<button type="submit" class="wd-btn wd-btn--haupt">Zugang anlegen</button>
<button type="button" class="wd-btn wd-btn--zweit" id="vw-testzugang">Testzugang für mich</button>
</div>
</form>
<div id="vw-link-box" hidden style="margin-top:1.2rem"></div>
</div>
<h2 style="font-size:1.15rem">Bestehende Zugänge</h2>
<div id="vw-kunden-liste"><div class="vw-laedt">Wird geladen…</div></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>';
}
}
/* =================================================================
KUNDEN
================================================================= */
var aktiverTab = "anfragen";
document.querySelectorAll("[data-tab]").forEach(function (b) {
b.addEventListener("click", function () {
aktiverTab = b.dataset.tab;
document.querySelectorAll("[data-tab]").forEach(function (x) {
x.classList.toggle("vw-an", x.dataset.tab === aktiverTab);
});
var istAnfragen = aktiverTab === "anfragen";
document.getElementById("vw-titel").textContent = istAnfragen ? "Projektanfragen" : "Kunden";
document.getElementById("vw-zahlen").hidden = !istAnfragen;
document.getElementById("vw-filter").hidden = !istAnfragen;
document.getElementById("vw-suche").closest(".wd-feld").hidden = !istAnfragen;
document.getElementById("vw-liste").hidden = !istAnfragen;
document.getElementById("vw-kunden-bereich").hidden = istAnfragen;
if (!istAnfragen) ladeKunden();
});
});
async function ladeKunden() {
var ziel = document.getElementById("vw-kunden-liste");
ziel.innerHTML = '<div class="vw-laedt">Wird geladen…</div>';
try {
var d = await api("/webdesign/admin/kunden");
if (!d.kunden.length) {
ziel.innerHTML = '<div class="vw-leer-hinweis">Noch keine Kundenzugaenge angelegt.</div>';
return;
}
ziel.innerHTML = d.kunden.map(function (k) {
/* Der Zustand eines Zugangs hat vier Stufen, und alle muessen auf
einen Blick unterscheidbar sein. Besonders wichtig ist
"freigeschaltet, aber noch kein Passwort gesetzt": das heisst,
die Einladung liegt noch offen und der Kunde war noch nie drin. */
var marke = k.gesperrt
? '<span class="vw-status vw-status--abgelehnt">gesperrt</span>'
: !k.freigeschaltet
? '<span class="vw-status vw-status--neu">nicht freigeschaltet</span>'
: !k.hat_passwort
? '<span class="vw-status vw-status--rueckfrage">Einladung offen</span>'
: '<span class="vw-status vw-status--angenommen">aktiv</span>';
return '<div class="vw-karte" style="cursor:default">' +
'<span class="vw-karte-nr">' + (k.projekte || 0) + ' Proj.</span>' +
'<span class="vw-karte-mitte">' +
'<span class="vw-karte-name">' + s(k.name) + (k.firma ? " · " + s(k.firma) : "") + "</span><br />" +
'<span class="vw-karte-info">' + s(k.email) + "</span>" +
"</span>" +
'<span class="vw-karte-rechts">' + marke +
'<span class="vw-karte-zeit">' +
(k.letzter_login ? "zuletzt " + window.WD.datum(k.letzter_login) : "noch nie angemeldet") +
"</span>" +
'<span style="display:block;margin-top:.5rem">' +
'<button type="button" class="wd-btn wd-btn--still wd-btn--klein" data-neuer-link="' + s(k.id) + '">Neuer Link</button> ' +
'<button type="button" class="wd-btn wd-btn--still wd-btn--klein" data-schalten="' + s(k.id) +
'" data-frei="' + (k.freigeschaltet ? "0" : "1") + '">' +
(k.freigeschaltet ? "Sperren" : "Freischalten") + "</button>" +
"</span>" +
"</span></div>";
}).join("");
ziel.querySelectorAll("[data-neuer-link]").forEach(function (b) {
b.addEventListener("click", async function () {
window.WD.knopfSperren(b, true, "…");
try {
var r = await api("/webdesign/admin/kunden/" + encodeURIComponent(b.dataset.neuerLink) + "/einladung",
{ method: "POST", body: {} });
zeigeLink(r.einladungsLink);
} catch (f) { window.WD.meldung(meldung, f.message, "fehler"); }
finally { window.WD.knopfSperren(b, false); }
});
});
ziel.querySelectorAll("[data-schalten]").forEach(function (b) {
b.addEventListener("click", async function () {
var frei = b.dataset.frei === "1";
window.WD.knopfSperren(b, true, "…");
try {
await api("/webdesign/admin/kunden/" + encodeURIComponent(b.dataset.schalten) + "/zugang",
{ method: "POST", body: { freigeschaltet: frei } });
ladeKunden();
} catch (f) {
window.WD.knopfSperren(b, false);
window.WD.meldung(meldung, f.message, "fehler");
}
});
});
} catch (fehler) {
if (fehler.status === 401) { token = null; tokenSchreiben(null); zeigeAnmeldung(); return; }
ziel.innerHTML = "";
window.WD.meldung(meldung, fehler.message, "fehler");
}
}
/* Der Link wird EINMAL angezeigt. Deshalb gross, kopierbar und mit einer
klaren Ansage, dass er nicht wiederkommt -- sonst klickt man ihn weg
und wundert sich. */
function zeigeLink(link) {
var box = document.getElementById("vw-link-box");
box.hidden = false;
box.innerHTML = '<div class="vw-link-anzeige">' +
'<p class="wd-hinweis-titel">Einladungslink — wird nur dieses eine Mal angezeigt</p>' +
"<code>" + s(link) + "</code>" +
'<div class="wd-btn-reihe">' +
'<button type="button" class="wd-btn wd-btn--haupt wd-btn--klein" id="vw-link-kopieren">Link kopieren</button>' +
'<a class="wd-btn wd-btn--zweit wd-btn--klein" href="' + s(link) + '" target="_blank" rel="noopener">Selbst oeffnen</a>' +
"</div>" +
'<p class="wd-klein wd-leise" style="margin:.6rem 0 0">Gueltig 14 Tage. Danach oder bei Verlust einfach einen neuen erzeugen.</p>' +
"</div>";
box.scrollIntoView({ behavior: "smooth", block: "center" });
document.getElementById("vw-link-kopieren").addEventListener("click", async function (e) {
try {
await navigator.clipboard.writeText(link);
e.target.textContent = "Kopiert ✓";
} catch (f) {
/* Die Zwischenablage kann blockiert sein (aelterer Browser, kein
HTTPS, verweigerte Berechtigung). Dann den Text markieren statt
einer Fehlermeldung, mit der niemand etwas anfangen kann. */
var bereich = document.createRange();
bereich.selectNodeContents(box.querySelector("code"));
var auswahl = getSelection();
auswahl.removeAllRanges();
auswahl.addRange(bereich);
e.target.textContent = "markiert — jetzt kopieren";
}
});
}
document.getElementById("vw-kunde-form").addEventListener("submit", async function (e) {
e.preventDefault();
await legeKundeAn({
name: document.getElementById("vw-kname").value.trim(),
email: document.getElementById("vw-kmail").value.trim(),
firma: document.getElementById("vw-kfirma").value.trim(),
sprache: document.getElementById("vw-ksprache").value,
freischalten: document.getElementById("vw-kfrei").checked,
}, e.target.querySelector('button[type="submit"]'));
});
/* "Testzugang fuer mich": legt einen Zugang mit einer eindeutig als Test
erkennbaren Adresse an. Bewusst NICHT die echte Geschaeftsadresse --
sonst kollidiert der Test spaeter mit einem echten Kundenkonto, und
die E-Mail-Adresse laesst sich aus gutem Grund nicht mehr aendern. */
document.getElementById("vw-testzugang").addEventListener("click", async function (e) {
var stempel = new Date().toISOString().slice(0, 10).replace(/-/g, "");
await legeKundeAn({
name: "Testzugang (" + stempel + ")",
email: "test+" + stempel + "@dogfather-universe.com",
firma: "Eigener Test",
sprache: window.WD.getSprache(),
freischalten: true,
}, e.target);
});
async function legeKundeAn(daten, knopf) {
["vw-feld-kname", "vw-feld-kmail"].forEach(function (id) {
document.getElementById(id).classList.remove("wd-feld--fehler");
});
window.WD.meldungAus(meldung);
window.WD.knopfSperren(knopf, true, "Wird angelegt…");
try {
var r = await api("/webdesign/admin/kunden", { method: "POST", body: daten });
zeigeLink(r.einladungsLink);
document.getElementById("vw-kunde-form").reset();
document.getElementById("vw-kfrei").checked = true;
ladeKunden();
} catch (fehler) {
window.WD.meldung(meldung, fehler.message, "fehler");
} finally {
window.WD.knopfSperren(knopf, false);
}
}
/* ---------------- Start ---------------- */
function start() {
token = tokenLesen();
if (!token) { zeigeAnmeldung(); return; }
zeigeBereich();
ladeAnfragen();
}
if (window.WD) start();
else document.addEventListener("wd-bereit", start);
})();
</script>
</body>
</html>