Rueckmeldung 22.08.2026 mit Bildschirmfoto: "ich will das nicht so ich
will es viel viel viel besser und uebersichtlicher und schoener zentriert
in der mitte."
Berechtigt, und aus mehreren Gruenden. Eine seitlich eingeschobene Leiste
ist fuer so viel Inhalt das falsche Format:
* Der Blick springt beim Oeffnen nach rechts.
* Die Liste dahinter bleibt halb sichtbar und lenkt ab.
* Alles muss sich in eine schmale Spalte quetschen -- auf dem
Bildschirmfoto stand "keine Angabe" dadurch achtmal untereinander,
und die Abwesenheit von Information nahm mehr Platz ein als die
Information selbst.
Jetzt: mittig, 940px breit, zweispaltig ab Tablet. Der Blick bleibt, wo
er ist, und zusammengehoerende Angaben stehen nebeneinander.
Weitere Entscheidungen:
* Mauerwerk-Umbruch (columns) statt Raster. Die Abschnitte sind
unterschiedlich hoch; ein Raster haette grosse Luecken gerissen.
* Jeder Abschnitt bekommt eine eigene Flaeche statt nur einer Trennlinie.
* Die Kopfzeile klebt beim Scrollen oben fest. Bei einer langen Anfrage
weiss man sonst nach dem Scrollen nicht mehr, wessen Daten man liest.
* "keine Angabe" wird kleiner und blasser dargestellt. Es ist die
ABWESENHEIT einer Information und darf nicht aussehen wie eine.
* Die Oeffnen-Animation skaliert dezent aus der Mitte statt von rechts
hereinzufahren, und ist bei prefers-reduced-motion ganz aus.
9/9 Tests weiterhin gruen.
Co-Authored-By: Claude Opus 5 <[email protected]>
1360 lines
63 KiB
HTML
1360 lines
63 KiB
HTML
<!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, gehobene Fassung ----------
|
||
Die erste Fassung war eine Reihe stiller Zahlen. Zahlen, die man
|
||
nur anschauen kann, sind Dekoration. Diese hier sind Werkzeuge:
|
||
jede Kachel filtert die Liste darunter, sobald man sie antippt.
|
||
|
||
Eine Null wird bewusst gedaempft. Sonst konkurriert "0 abgelehnt"
|
||
optisch mit "3 neu" -- und genau die Drei ist die, auf die man
|
||
schauen soll. Was nichts zu tun gibt, soll auch nicht leuchten. */
|
||
.vw-zahlen {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
gap: .7rem; margin-bottom: 1.4rem;
|
||
}
|
||
.vw-zahl {
|
||
position: relative; overflow: hidden;
|
||
display: block; width: 100%; text-align: left; cursor: pointer;
|
||
padding: .95rem 1.05rem;
|
||
border-radius: 16px;
|
||
background: linear-gradient(180deg, rgba(20,32,50,.62), rgba(9,15,26,.8));
|
||
border: 1px solid var(--wd-rand);
|
||
transition: border-color var(--wd-zeit-schnell) ease,
|
||
transform var(--wd-zeit-schnell) ease,
|
||
box-shadow var(--wd-zeit) ease;
|
||
}
|
||
.vw-zahl:hover { border-color: var(--wd-rand-stark); transform: translateY(-2px); }
|
||
.vw-zahl.vw-an {
|
||
border-color: var(--wd-blau);
|
||
box-shadow: 0 0 0 1px var(--wd-blau) inset, 0 14px 34px -20px rgba(127,208,232,.7);
|
||
}
|
||
.vw-zahl-kopf { display: flex; align-items: center; gap: .45rem; margin-bottom: .35rem; }
|
||
.vw-zahl-symbol { font-size: 1rem; line-height: 1; opacity: .85; }
|
||
.vw-zahl-wert {
|
||
font-family: var(--font-head); font-weight: 800;
|
||
font-size: clamp(1.5rem, 3.4vw, 1.9rem); line-height: 1;
|
||
color: var(--wd-blau); font-variant-numeric: tabular-nums;
|
||
}
|
||
.vw-zahl-name { font-size: .78rem; letter-spacing: .03em; color: var(--wd-text-soft); }
|
||
.vw-zahl-zusatz { font-size: .74rem; color: var(--wd-text-soft); margin-top: .3rem; display: block; }
|
||
|
||
/* Null: alles gedaempft, damit es nicht um Aufmerksamkeit buhlt. */
|
||
.vw-zahl--null { opacity: .5; }
|
||
.vw-zahl--null .vw-zahl-wert { color: var(--wd-text-soft); }
|
||
|
||
/* Handlungsbedarf: Gold, und nur hier. */
|
||
.vw-zahl--achtung { border-color: rgba(232,200,119,.34); background: linear-gradient(180deg, rgba(232,200,119,.07), rgba(9,15,26,.8)); }
|
||
.vw-zahl--achtung .vw-zahl-wert { color: var(--wd-gold); }
|
||
.vw-zahl--achtung .vw-zahl-zusatz { color: var(--wd-gold); }
|
||
|
||
/* Dringend: etwas liegt zu lange. */
|
||
.vw-zahl--dringend { border-color: rgba(255,157,157,.4); background: linear-gradient(180deg, rgba(255,157,157,.07), rgba(9,15,26,.8)); }
|
||
.vw-zahl--dringend .vw-zahl-wert,
|
||
.vw-zahl--dringend .vw-zahl-zusatz { color: var(--wd-fehler); }
|
||
|
||
|
||
/* ---------- 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; display: flex; flex-direction: column; gap: .1rem; }
|
||
.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); }
|
||
/* ---------- Detailansicht: zentriertes Fenster ----------
|
||
Vorher eine seitlich eingeschobene Leiste. Fuer so viel Inhalt ist
|
||
das das falsche Format: Der Blick springt beim Oeffnen nach rechts,
|
||
die Liste dahinter bleibt halb sichtbar und lenkt ab, und alles muss
|
||
sich in eine schmale Spalte quetschen -- "keine Angabe" stand
|
||
dadurch achtmal untereinander.
|
||
|
||
Jetzt mittig, breiter und zweispaltig. Der Blick bleibt, wo er ist,
|
||
und zusammengehoerende Angaben stehen nebeneinander statt
|
||
hintereinander. */
|
||
.vw-detail-blatt {
|
||
position: absolute;
|
||
top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||
width: min(940px, calc(100vw - 2rem));
|
||
max-height: min(88vh, 900px);
|
||
background: linear-gradient(180deg, #0b1523, #070c15);
|
||
border: 1px solid var(--wd-rand-stark);
|
||
border-radius: 20px;
|
||
box-shadow: 0 50px 120px -40px rgba(0,0,0,.95), 0 0 0 1px rgba(127,208,232,.06) inset;
|
||
overflow-y: auto;
|
||
padding: clamp(1.2rem, 3vw, 1.9rem) clamp(1rem, 3vw, 2rem) clamp(1.4rem, 3vw, 2rem);
|
||
animation: vw-rein .26s cubic-bezier(.22,1,.36,1) both;
|
||
}
|
||
@keyframes vw-rein {
|
||
from { transform: translate(-50%, -46%) scale(.97); opacity: 0; }
|
||
to { transform: translate(-50%, -50%) scale(1); opacity: 1; }
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.vw-detail-blatt { animation: none; }
|
||
}
|
||
|
||
/* Kopfzeile klebt oben fest — bei langen Anfragen weiss man sonst nach
|
||
dem Scrollen nicht mehr, wessen Daten man gerade liest. */
|
||
.vw-detail-kopf {
|
||
position: sticky; top: calc(clamp(1.2rem, 3vw, 1.9rem) * -1);
|
||
margin: calc(clamp(1.2rem, 3vw, 1.9rem) * -1) calc(clamp(1rem, 3vw, 2rem) * -1) 1.2rem;
|
||
padding: 1.1rem clamp(1rem, 3vw, 2rem);
|
||
background: rgba(11, 21, 35, .96);
|
||
backdrop-filter: blur(10px);
|
||
border-bottom: 1px solid var(--wd-rand);
|
||
border-radius: 20px 20px 0 0;
|
||
z-index: 2;
|
||
}
|
||
|
||
/* Zwei Spalten ab Tablet: die Abschnitte sind unterschiedlich hoch,
|
||
deshalb Mauerwerk statt Raster — sonst entstehen grosse Luecken. */
|
||
@media (min-width: 780px) {
|
||
.vw-detail-spalten { columns: 2; column-gap: 1.4rem; }
|
||
.vw-detail-spalten > .vw-abschnitt {
|
||
break-inside: avoid; -webkit-column-break-inside: avoid;
|
||
display: inline-block; width: 100%;
|
||
}
|
||
}
|
||
|
||
/* Jeder Abschnitt wird eine eigene Flaeche statt nur einer Linie. */
|
||
.vw-detail-spalten > .vw-abschnitt {
|
||
padding: .95rem 1.1rem 1rem;
|
||
border-radius: 14px;
|
||
background: rgba(16, 26, 41, .5);
|
||
border: 1px solid var(--wd-rand);
|
||
margin-bottom: 1.1rem;
|
||
}
|
||
.vw-detail-spalten > .vw-abschnitt > h3 { border-bottom: none; padding-bottom: 0; margin-bottom: .7rem; }
|
||
|
||
/* "keine Angabe" tritt zurueck: Es ist die Abwesenheit einer Information
|
||
und darf nicht so aussehen wie eine. */
|
||
.vw-paar dd.vw-leer { font-size: .88rem; opacity: .55; }
|
||
@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;
|
||
}
|
||
|
||
/* ---------- Anfragekarte, gehobene Fassung ----------
|
||
Die erste Fassung zeigte Nummer, Name, Paket, Status, Datum. Das ist
|
||
korrekt, aber es beantwortet nicht die Fragen, die man beim
|
||
Draufschauen WIRKLICH hat: Wie dringend ist das? Wie viel Geld steckt
|
||
drin? Wie lange liegt es schon? Genau die stehen jetzt drin.
|
||
|
||
Ein farbiger Streifen links codiert den Status zusaetzlich zur
|
||
Textmarke — Farbe ALLEIN waere fuer farbfehlsichtige Menschen keine
|
||
Information, zusammen mit dem Text ist sie ein schneller Anker. */
|
||
.vw-karte {
|
||
position: relative;
|
||
overflow: hidden;
|
||
padding-left: 1.5rem;
|
||
}
|
||
.vw-karte::before {
|
||
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
|
||
background: var(--wd-rand-stark);
|
||
}
|
||
.vw-karte--neu::before { background: var(--wd-gold); }
|
||
.vw-karte--pruefung::before,
|
||
.vw-karte--angebot::before { background: var(--wd-blau); }
|
||
.vw-karte--rueckfrage::before { background: var(--wd-lila); }
|
||
.vw-karte--angenommen::before,
|
||
.vw-karte--abgeschlossen::before { background: var(--wd-ok); }
|
||
.vw-karte--abgelehnt::before { background: var(--wd-fehler); }
|
||
|
||
.vw-karte-marken { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .4rem; }
|
||
.vw-mini {
|
||
font-size: .74rem; font-weight: 600; padding: .15rem .5rem;
|
||
border-radius: 6px; background: rgba(127,208,232,.09);
|
||
color: var(--wd-text-mid); border: 1px solid var(--wd-rand);
|
||
white-space: nowrap;
|
||
}
|
||
.vw-mini--geld { color: var(--wd-gold); border-color: rgba(232,200,119,.3); background: rgba(232,200,119,.07); }
|
||
.vw-mini--alt { color: var(--wd-fehler); border-color: rgba(255,157,157,.3); background: rgba(255,157,157,.06); }
|
||
|
||
/* Leerer Zustand — auch hier fuehren statt nur feststellen. */
|
||
.vw-leer-gross { text-align: center; padding: clamp(2rem,6vw,3.5rem) 1.5rem; }
|
||
.vw-leer-gross-symbol { font-size: 2.6rem; opacity: .45; margin-bottom: .6rem; }
|
||
.vw-leer-gross h3 { margin-bottom: .3em; }
|
||
.vw-leer-gross p { max-width: 46ch; margin-left: auto; margin-right: auto; }
|
||
|
||
|
||
/* ---------- Übernahme: Anfrage wird Kunde ----------
|
||
Der wichtigste Handgriff im ganzen Bereich. Bisher hätte man den
|
||
Namen, die E-Mail, das Paket und den Budgetrahmen von Hand in die
|
||
Kundenmaske abgetippt — vier Gelegenheiten für einen Tippfehler, und
|
||
einer davon (die E-Mail) ist später nicht mehr korrigierbar, weil sie
|
||
der Anmeldename ist. */
|
||
.vw-uebernahme {
|
||
border-radius: var(--wd-r);
|
||
padding: 1.1rem 1.2rem;
|
||
background: linear-gradient(180deg, rgba(127,208,232,.09), rgba(9,15,26,.6));
|
||
border: 1px solid var(--wd-rand-stark);
|
||
}
|
||
.vw-uebernahme h4 { margin: 0 0 .3rem; font-size: 1rem; color: var(--wd-text); }
|
||
.vw-uebernahme p { margin: 0 0 .8rem; font-size: .9rem; color: var(--wd-text-mid); }
|
||
.vw-uebernahme-vorschau {
|
||
display: grid; grid-template-columns: auto 1fr; gap: .25rem .8rem;
|
||
font-size: .88rem; margin-bottom: .9rem;
|
||
padding: .7rem .9rem; border-radius: 10px;
|
||
background: rgba(5,7,11,.5); border: 1px solid var(--wd-rand);
|
||
}
|
||
.vw-uebernahme-vorschau dt { color: var(--wd-text-soft); }
|
||
.vw-uebernahme-vorschau dd { margin: 0; color: var(--wd-text); overflow-wrap: anywhere; }
|
||
|
||
.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">
|
||
Normalerweise landest du hier gar nicht: Wer durch die Zugangswand
|
||
gekommen ist, wird direkt hereingelassen. Diese Maske erscheint nur,
|
||
wenn das nicht möglich war — dann brauchst du deinen persönlichen
|
||
Zugangscode aus dem internen Bereich (derselbe wie fürs Postfach).
|
||
</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) {
|
||
/* Der Ausweis der Zugangswand laeuft nach 15 Minuten ab. Statt
|
||
jemanden mitten im Arbeiten rauszuwerfen, EINMAL still einen
|
||
neuen holen und es nochmal versuchen. Klappt auch das nicht,
|
||
ist die Zugangssitzung selbst weg -- dann zurueck zur
|
||
Anmeldung, nicht zu einer leeren Liste. */
|
||
if (!ausweisVersucht) {
|
||
ausweisVersucht = true;
|
||
var frisch = await ausweisHolen();
|
||
if (frisch) {
|
||
token = frisch; tokenSchreiben(token);
|
||
ausweisVersucht = false;
|
||
return ladeAnfragen();
|
||
}
|
||
}
|
||
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 gesamt = Object.keys(zaehler).reduce(function (n, k) { return n + zaehler[k]; }, 0);
|
||
|
||
/* Der aelteste unbearbeitete Vorgang ist die ehrlichste Kennzahl
|
||
ueberhaupt: Er sagt nicht, wie viel man geschafft hat, sondern wie
|
||
lange jemand schon auf eine Antwort wartet. Genau daran misst ein
|
||
Kunde, ob man zuverlaessig ist. */
|
||
var wartend = alleAnfragen.filter(function (a) {
|
||
return a.status === "neu" || a.status === "rueckfrage";
|
||
});
|
||
var aeltesteTage = wartend.reduce(function (max, a) {
|
||
return Math.max(max, tageSeit(a.erstellt_am));
|
||
}, 0);
|
||
|
||
var kacheln = [
|
||
{
|
||
wert: zaehler.neu || 0, name: "neu, ungelesen", symbol: "📬", filter: "neu",
|
||
achtung: (zaehler.neu || 0) > 0,
|
||
zusatz: (zaehler.neu || 0) > 0 ? "warten auf dich" : "alles gesichtet",
|
||
},
|
||
{
|
||
wert: zaehler.pruefung || 0, name: "in Prüfung", symbol: "🔍", filter: "pruefung",
|
||
zusatz: "du bist dran",
|
||
},
|
||
{
|
||
wert: zaehler.angebot || 0, name: "Angebot raus", symbol: "📄", filter: "angebot",
|
||
zusatz: "Kunde ist dran",
|
||
},
|
||
{
|
||
wert: zaehler.angenommen || 0, name: "angenommen", symbol: "✅", filter: "angenommen",
|
||
zusatz: "Projekt anlegen",
|
||
},
|
||
{
|
||
/* Diese Kachel filtert bewusst NICHT -- sie zeigt einen Zustand,
|
||
keinen Status. Deshalb ist sie auch nicht anklickbar. */
|
||
wert: aeltesteTage, name: aeltesteTage === 1 ? "Tag wartet der Älteste" : "Tage wartet der Älteste",
|
||
symbol: "⏳", filter: null,
|
||
dringend: aeltesteTage >= 7,
|
||
zusatz: aeltesteTage === 0 ? "nichts liegen geblieben"
|
||
: aeltesteTage >= 7 ? "das ist zu lang" : "noch im Rahmen",
|
||
},
|
||
{
|
||
wert: gesamt, name: "insgesamt offen", symbol: "📊", filter: "",
|
||
zusatz: "alle anzeigen",
|
||
},
|
||
];
|
||
|
||
document.getElementById("vw-zahlen").innerHTML = kacheln.map(function (k) {
|
||
var klassen = "vw-zahl";
|
||
if (k.dringend) klassen += " vw-zahl--dringend";
|
||
else if (k.achtung) klassen += " vw-zahl--achtung";
|
||
else if (!k.wert) klassen += " vw-zahl--null";
|
||
if (k.filter !== null && filterStatus === k.filter && !filterArchiv) klassen += " vw-an";
|
||
|
||
var innen =
|
||
'<span class="vw-zahl-kopf">' +
|
||
'<span class="vw-zahl-symbol" aria-hidden="true">' + k.symbol + "</span>" +
|
||
'<span class="vw-zahl-wert">' + k.wert + "</span>" +
|
||
"</span>" +
|
||
'<span class="vw-zahl-name">' + s(k.name) + "</span>" +
|
||
'<span class="vw-zahl-zusatz">' + s(k.zusatz) + "</span>";
|
||
|
||
/* Anklickbares wird ein echter Knopf, nicht ein div mit
|
||
Klickzuhoerer -- sonst ist es mit der Tastatur nicht erreichbar
|
||
und ein Screenreader kuendigt es nicht als Bedienelement an. */
|
||
return k.filter === null
|
||
? '<div class="' + klassen + '" style="cursor:default">' + innen + "</div>"
|
||
: '<button type="button" class="' + klassen + '" data-kachel="' + k.filter +
|
||
'" aria-label="' + s(k.wert + " " + k.name + " anzeigen") + '">' + innen + "</button>";
|
||
}).join("");
|
||
|
||
document.querySelectorAll("[data-kachel]").forEach(function (b) {
|
||
b.addEventListener("click", function () {
|
||
filterStatus = b.dataset.kachel;
|
||
filterArchiv = false;
|
||
ladeAnfragen();
|
||
});
|
||
});
|
||
}
|
||
|
||
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();
|
||
});
|
||
}
|
||
|
||
|
||
/* Ganze Tage seit einem Zeitpunkt. Bewusst ueber den Kalendertag
|
||
gerechnet, nicht ueber 24-Stunden-Bloecke: eine Anfrage von gestern
|
||
23:50 Uhr ist umgangssprachlich "gestern", nicht "vor 0 Tagen". */
|
||
function tageSeit(zeit) {
|
||
if (!zeit) return 0;
|
||
var dann = new Date(zeit);
|
||
if (isNaN(dann.getTime())) return 0;
|
||
var a = new Date(dann.getFullYear(), dann.getMonth(), dann.getDate());
|
||
var heute = new Date();
|
||
var b = new Date(heute.getFullYear(), heute.getMonth(), heute.getDate());
|
||
return Math.round((b - a) / 86400000);
|
||
}
|
||
|
||
function zeitText(tage) {
|
||
if (tage <= 0) return "heute";
|
||
if (tage === 1) return "gestern";
|
||
if (tage < 7) return "vor " + tage + " Tagen";
|
||
if (tage < 14) return "vor einer Woche";
|
||
if (tage < 31) return "vor " + Math.floor(tage / 7) + " Wochen";
|
||
return "vor " + Math.floor(tage / 30) + " Monaten";
|
||
}
|
||
|
||
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) {
|
||
/* Auch hier fuehren statt nur feststellen: Wer keine Anfragen hat,
|
||
soll wissen, WO sie herkommen -- und den Link gleich griffbereit
|
||
haben. */
|
||
liste.innerHTML = alleAnfragen.length
|
||
? '<div class="vw-leer-hinweis">Keine Anfrage passt zu diesem Filter.</div>'
|
||
: filterArchiv
|
||
? '<div class="vw-leer-hinweis">Im Archiv liegt noch nichts. Archivierte Anfragen bleiben vollstaendig erhalten und lassen sich jederzeit zurueckholen.</div>'
|
||
: '<div class="wd-karte vw-leer-gross" style="cursor:default;display:block">' +
|
||
'<div class="vw-leer-gross-symbol" aria-hidden="true">📬</div>' +
|
||
"<h3>Noch keine Anfragen</h3>" +
|
||
'<p class="wd-leise">Sobald jemand das Formular ausfuellt, landet die Anfrage hier — mit allem, was er angegeben hat, und du bekommst eine Benachrichtigung.</p>' +
|
||
'<div class="wd-btn-reihe" style="justify-content:center;margin-top:1rem">' +
|
||
'<a class="wd-btn wd-btn--zweit wd-btn--klein" href="/webdesign/anfrage.html" target="_blank" rel="noopener">Formular ansehen</a>' +
|
||
'<a class="wd-btn wd-btn--still wd-btn--klein" href="/webdesign/" target="_blank" rel="noopener">Zur Seite</a>' +
|
||
"</div></div>";
|
||
return;
|
||
}
|
||
|
||
liste.innerHTML = gefiltert.map(function (a) {
|
||
var tage = tageSeit(a.erstellt_am);
|
||
/* "vor 3 Tagen" sagt mehr als ein Datum. Beim Datum muss man selbst
|
||
rechnen, wie lange jemand schon wartet -- und genau das ist die
|
||
Frage, die zaehlt. Ab sieben Tagen ohne Bearbeitung wird es rot:
|
||
eine Anfrage, die eine Woche liegt, ist ein verlorener Kunde. */
|
||
var liegtLange = tage >= 7 && (a.status === "neu" || a.status === "rueckfrage");
|
||
return '<button type="button" class="vw-karte vw-karte--' + s(a.status) + '" 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>" +
|
||
'<span class="vw-karte-info">' + s(a.email) + "</span>" +
|
||
'<span class="vw-karte-marken">' +
|
||
'<span class="vw-mini">' + s(PAKET_TEXT[a.paket] || a.paket) + "</span>" +
|
||
(a.budget ? '<span class="vw-mini vw-mini--geld">' + s(BUDGET_TEXT[a.budget] || a.budget) + "</span>" : "") +
|
||
(a.termin ? '<span class="vw-mini">Wunsch: ' + s(a.termin) + "</span>" : "") +
|
||
(a.sprache && a.sprache !== "de" ? '<span class="vw-mini">' + s(a.sprache.toUpperCase()) + "</span>" : "") +
|
||
(a.kunde_id ? '<span class="vw-mini">ist Kunde</span>' : "") +
|
||
"</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' + (liegtLange ? " vw-mini--alt" : "") + '">' +
|
||
s(zeitText(tage)) + "</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-detail-spalten">' +
|
||
|
||
'<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>" +
|
||
|
||
(a.kunde_id
|
||
? '<div class="vw-abschnitt"><h3>Kunde</h3>' +
|
||
'<p class="wd-klein wd-leise">Aus dieser Anfrage wurde bereits ein Kundenzugang erstellt. Du findest ihn oben unter „Kunden“.</p></div>'
|
||
: '<div class="vw-abschnitt"><h3>Weiter geht es</h3>' +
|
||
'<div class="vw-uebernahme">' +
|
||
"<h4>Anfrage übernehmen</h4>" +
|
||
"<p>Legt in einem Schritt den Kundenzugang UND das Projekt an — mit den Angaben aus dieser Anfrage. Du bekommst danach sofort den Einladungslink.</p>" +
|
||
'<dl class="vw-uebernahme-vorschau">' +
|
||
"<dt>Kunde</dt><dd>" + s(a.name) + "</dd>" +
|
||
"<dt>E-Mail</dt><dd>" + s(a.email) + "</dd>" +
|
||
"<dt>Projekt</dt><dd>" + s(PAKET_TEXT[a.paket] || a.paket) + " für " + s(a.name) + "</dd>" +
|
||
"<dt>Sprache</dt><dd>" + s((a.sprache || "de").toUpperCase()) + "</dd>" +
|
||
"</dl>" +
|
||
'<div class="wd-feld" style="margin-bottom:.8rem">' +
|
||
'<label for="vw-ue-preis">Gesamtpreis in Euro <span class="wd-leise">optional, später änderbar</span></label>' +
|
||
'<input type="number" id="vw-ue-preis" min="0" step="1" inputmode="numeric" placeholder="z. B. 1500" />' +
|
||
'<span class="wd-feld-hilfe" id="vw-ue-anzahlung">Die 30 % Anzahlung wird daraus berechnet.</span>' +
|
||
"</div>" +
|
||
'<button type="button" class="wd-btn wd-btn--haupt" id="vw-uebernehmen">Kunde und Projekt anlegen</button>' +
|
||
"</div></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>" +
|
||
"</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.");
|
||
}
|
||
});
|
||
|
||
var preisFeld = blatt.querySelector("#vw-ue-preis");
|
||
if (preisFeld) {
|
||
preisFeld.addEventListener("input", function () {
|
||
var wert = Number(preisFeld.value);
|
||
blatt.querySelector("#vw-ue-anzahlung").textContent =
|
||
Number.isFinite(wert) && wert > 0
|
||
? "30 % Anzahlung: " + (Math.round(wert * 100 * 0.3) / 100).toFixed(2).replace(".", ",") + " €"
|
||
: "Die 30 % Anzahlung wird daraus berechnet.";
|
||
});
|
||
}
|
||
|
||
var ueKnopf = blatt.querySelector("#vw-uebernehmen");
|
||
if (ueKnopf) {
|
||
ueKnopf.addEventListener("click", async function (e) {
|
||
window.WD.knopfSperren(e.target, true, "Wird angelegt…");
|
||
try {
|
||
/* Zwei Schritte, bewusst nacheinander und nicht parallel: das
|
||
Projekt braucht die Kunden-Kennung. Schlaegt der zweite
|
||
fehl, existiert der Kunde trotzdem schon -- deshalb sagt die
|
||
Fehlermeldung unten ausdruecklich, dass er angelegt wurde,
|
||
damit niemand es blind noch einmal versucht und dann an der
|
||
doppelten E-Mail scheitert. */
|
||
var kunde = await api("/webdesign/admin/kunden", {
|
||
method: "POST",
|
||
body: {
|
||
name: a.name, email: a.email, telefon: a.telefon || "",
|
||
sprache: a.sprache || "de", freischalten: true,
|
||
},
|
||
});
|
||
|
||
var preis = Number(preisFeld && preisFeld.value);
|
||
try {
|
||
await api("/webdesign/admin/projekte", {
|
||
method: "POST",
|
||
body: {
|
||
kundeId: kunde.id,
|
||
anfrageId: a.id,
|
||
titel: (PAKET_TEXT[a.paket] || a.paket) + " für " + a.name,
|
||
paket: a.paket === "unklar" ? "website" : a.paket,
|
||
preisEuro: Number.isFinite(preis) && preis > 0 ? preis : undefined,
|
||
richttermin: a.termin || "",
|
||
naechsterSchritt: "Briefing-Termin vereinbaren",
|
||
},
|
||
});
|
||
} catch (projektFehler) {
|
||
window.WD.meldung(meldung,
|
||
"Der Kundenzugang wurde angelegt, das Projekt aber nicht: " +
|
||
projektFehler.message + " Du kannst das Projekt unter „Kunden“ nachtragen.", "fehler");
|
||
}
|
||
|
||
schliesseDetail();
|
||
aktiverTab = "kunden";
|
||
document.querySelector('[data-tab="kunden"]').click();
|
||
zeigeLink(kunde.einladungsLink);
|
||
} catch (fehler) {
|
||
window.WD.knopfSperren(e.target, false);
|
||
window.WD.meldung(meldung, fehler.message, "fehler");
|
||
}
|
||
});
|
||
}
|
||
|
||
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--zweit wd-btn--klein" data-projekt-fuer="' + s(k.id) +
|
||
'" data-projekt-name="' + s(k.name) + '">+ Projekt</button> ' +
|
||
'<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-projekt-fuer]").forEach(function (b) {
|
||
b.addEventListener("click", function () {
|
||
zeigeProjektFormular(b.dataset.projektFuer, b.dataset.projektName);
|
||
});
|
||
});
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
|
||
/* -----------------------------------------------------------------
|
||
PROJEKT ANLEGEN
|
||
|
||
Bewusst als Einblendung direkt bei der Kundenzeile statt als eigene
|
||
Seite: man legt ein Projekt IMMER fuer einen bestimmten Kunden an,
|
||
nie im luftleeren Raum. Der Kundenname steht deshalb gross im
|
||
Formular -- so kann man es nicht versehentlich dem Falschen anhaengen.
|
||
----------------------------------------------------------------- */
|
||
function zeigeProjektFormular(kundeId, kundeName) {
|
||
var box = document.getElementById("vw-link-box");
|
||
box.hidden = false;
|
||
box.innerHTML =
|
||
'<div class="wd-karte" style="cursor:default;display:block">' +
|
||
'<h3 style="margin:0 0 .2rem">Neues Projekt</h3>' +
|
||
'<p class="wd-klein" style="color:var(--wd-blau);margin:0 0 1rem">für ' + s(kundeName) + '</p>' +
|
||
'<div class="wd-grid wd-grid--2">' +
|
||
'<div class="wd-feld" id="vw-feld-ptitel">' +
|
||
'<label for="vw-ptitel">Projekttitel<span class="wd-pflicht">*</span></label>' +
|
||
'<input type="text" id="vw-ptitel" placeholder="z. B. Website Handwerksbetrieb Meier" />' +
|
||
'<p class="wd-feld-fehler"></p>' +
|
||
'</div>' +
|
||
'<div class="wd-feld">' +
|
||
'<label for="vw-ppaket">Paket</label>' +
|
||
'<select id="vw-ppaket">' +
|
||
'<option value="onepager">Onepager — ab 300 €</option>' +
|
||
'<option value="website" selected>Professionelle Website — ab 500 €</option>' +
|
||
'<option value="shop">Shop & umfangreiche Website — ab 1.000 €</option>' +
|
||
'<option value="verwaltung">Buchhaltungs- & Steuerverwaltung — ab 1.500 €</option>' +
|
||
'<option value="betreuung">Laufende Betreuung — ab 49 €/Monat</option>' +
|
||
'</select>' +
|
||
'</div>' +
|
||
'<div class="wd-feld">' +
|
||
'<label for="vw-ppreis">Gesamtpreis in Euro</label>' +
|
||
'<input type="number" id="vw-ppreis" min="0" step="1" placeholder="1500" inputmode="numeric" />' +
|
||
'<span class="wd-feld-hilfe" id="vw-panzahlung">Die 30 % Anzahlung wird daraus automatisch berechnet.</span>' +
|
||
'</div>' +
|
||
'<div class="wd-feld">' +
|
||
'<label for="vw-ptermin">Richttermin <span class="wd-leise">optional</span></label>' +
|
||
'<input type="text" id="vw-ptermin" placeholder="z. B. Mitte Oktober" />' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="wd-feld">' +
|
||
'<label for="vw-pschritt">Nächster Schritt <span class="wd-leise">sieht der Kunde sofort</span></label>' +
|
||
'<input type="text" id="vw-pschritt" placeholder="z. B. Briefing-Termin vereinbaren" />' +
|
||
'</div>' +
|
||
'<div class="wd-btn-reihe">' +
|
||
'<button type="button" class="wd-btn wd-btn--haupt" id="vw-projekt-speichern">Projekt anlegen</button>' +
|
||
'<button type="button" class="wd-btn wd-btn--still" id="vw-projekt-abbrechen">Abbrechen</button>' +
|
||
'</div>' +
|
||
'</div>';
|
||
box.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
setTimeout(function () { document.getElementById("vw-ptitel").focus(); }, 100);
|
||
|
||
/* Die Anzahlung live mitrechnen. Sie wird zwar serverseitig berechnet,
|
||
aber wer sie beim Eintippen sieht, merkt eine falsche Null sofort --
|
||
und nicht erst, wenn der Kunde den Betrag ueberweisen soll. */
|
||
var preisFeld = document.getElementById("vw-ppreis");
|
||
preisFeld.addEventListener("input", function () {
|
||
var wert = Number(preisFeld.value);
|
||
document.getElementById("vw-panzahlung").textContent =
|
||
Number.isFinite(wert) && wert > 0
|
||
? "30 % Anzahlung: " + (Math.round(wert * 100 * 0.3) / 100).toFixed(2).replace(".", ",") + " €"
|
||
: "Die 30 % Anzahlung wird daraus automatisch berechnet.";
|
||
});
|
||
|
||
document.getElementById("vw-projekt-abbrechen").addEventListener("click", function () {
|
||
box.hidden = true; box.innerHTML = "";
|
||
});
|
||
|
||
document.getElementById("vw-projekt-speichern").addEventListener("click", async function (e) {
|
||
var titel = document.getElementById("vw-ptitel").value.trim();
|
||
var feld = document.getElementById("vw-feld-ptitel");
|
||
feld.classList.remove("wd-feld--fehler");
|
||
if (!titel) {
|
||
feld.classList.add("wd-feld--fehler");
|
||
feld.querySelector(".wd-feld-fehler").textContent = "Bitte einen Projekttitel angeben.";
|
||
return;
|
||
}
|
||
window.WD.knopfSperren(e.target, true, "Wird angelegt…");
|
||
try {
|
||
var r = await api("/webdesign/admin/projekte", {
|
||
method: "POST",
|
||
body: {
|
||
kundeId: kundeId,
|
||
titel: titel,
|
||
paket: document.getElementById("vw-ppaket").value,
|
||
preisEuro: Number(document.getElementById("vw-ppreis").value) || undefined,
|
||
richttermin: document.getElementById("vw-ptermin").value.trim(),
|
||
naechsterSchritt: document.getElementById("vw-pschritt").value.trim(),
|
||
},
|
||
});
|
||
box.innerHTML = '<div class="vw-link-anzeige">' +
|
||
'<p class="wd-hinweis-titel">Projekt ' + s(r.nummer) + ' angelegt</p>' +
|
||
'<p class="wd-klein" style="margin:0">' +
|
||
(r.anzahlungText ? "Anzahlung 30 %: <strong>" + s(r.anzahlungText) + "</strong>. " : "") +
|
||
'Der Kunde sieht es ab sofort in seinem Projektraum.</p></div>';
|
||
ladeKunden();
|
||
} catch (f) {
|
||
window.WD.knopfSperren(e.target, false);
|
||
window.WD.meldung(meldung, f.message, "fehler");
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ---------------- Start ----------------
|
||
|
||
Reihenfolge bewusst so:
|
||
|
||
1. Ein bereits geholter Ausweis im Sitzungsspeicher.
|
||
2. Sonst: einen neuen bei der Zugangswand holen. Die hat serverseitig
|
||
laengst geprueft, wer hier ist -- ein zweites Passwort danach
|
||
bringt keinen zusaetzlichen Schutz und kostet nur Zeit
|
||
(Rueckmeldung 22.08.2026).
|
||
3. Erst wenn das nicht klappt (Ausweis nicht eingerichtet, Sitzung
|
||
abgelaufen), die eigene Anmeldemaske.
|
||
|
||
Der Ausweis laeuft nach 15 Minuten ab. Deshalb wird bei jedem 401
|
||
einmal stillschweigend ein neuer geholt und die Anfrage wiederholt --
|
||
sonst fliegt man mitten im Arbeiten raus. */
|
||
var ausweisVersucht = false;
|
||
|
||
async function ausweisHolen() {
|
||
try {
|
||
var antwort = await fetch("/webdesign/api-ausweis", { cache: "no-store" });
|
||
if (!antwort.ok) return null;
|
||
var daten = await antwort.json();
|
||
return daten && daten.ausweis ? daten.ausweis : null;
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function start() {
|
||
token = tokenLesen();
|
||
if (token) { zeigeBereich(); ladeAnfragen(); return; }
|
||
|
||
var ausweis = await ausweisHolen();
|
||
if (ausweis) {
|
||
token = ausweis;
|
||
tokenSchreiben(token);
|
||
document.getElementById("vw-name").textContent =
|
||
(document.cookie.match(/dogiwd_rolle=([^;]+)/) || [])[1] === "vanvan" ? "VanVan" : "Dogfather";
|
||
zeigeBereich();
|
||
ladeAnfragen();
|
||
return;
|
||
}
|
||
zeigeAnmeldung();
|
||
}
|
||
|
||
if (window.WD) start();
|
||
else document.addEventListener("wd-bereit", start);
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|