Portal: Aufgabenliste und Postfach fuer den Kunden
Schritt 4 von 4 -- damit sind beide Wuensche vom 22.08.2026 vollstaendig:
"wenn sie drauf druecken sollen sie auch sehen was ich schon gemacht habe
von dem was im plan war" und "die kunden sollen mir auch nachrichten
hinterlassen koennen".
AUFGABENLISTE IM PROJEKT
Bisher sah der Kunde nur die Phase ("Design . 3/7") -- eine Zahl ohne
Inhalt. Sie beantwortet die eigentliche Frage nicht: WAS ist denn fertig?
Die Liste steht GANZ OBEN, direkt nach dem Projektkopf, noch vor
Zahlungen und Dateien. Die sind wichtig, aber sie sind nicht der Grund
fuer den Klick.
Zwei Entscheidungen praegen die Ansicht:
1. Offene Kundenpunkte stehen in einem EIGENEN Kasten ganz oben ("Das
brauche ich noch von dir"), nicht nur farblich markiert irgendwo
mittendrin. Wer eine lange Liste sieht, liest sie als Bericht ueber
fremde Arbeit und ueberliest seinen eigenen Teil -- genau daraus
entstehen die meisten Verzoegerungen. Ist nichts offen, steht das
auch da: "Von dir wird gerade nichts gebraucht." Eine gute Nachricht
darf ausgesprochen werden.
2. Der Kunde hakt seine eigenen Punkte selbst ab. Nur diese haben einen
Knopf; bei fremden Punkten ist das Zeichen reine Anzeige. Ein Knopf,
der nichts tut, laesst die Seite kaputt wirken. Geprueft: 4 Knoepfe
bei 2 offenen eigenen Punkten (sie erscheinen zweimal), 5 feste.
Grosse Zahl statt Prozent: "2 von 6 erledigt" ist greifbar, "33 %" ist
eine Rechnung, die niemand fuehlt. Der Balken traegt die vier
Prozessfarben -- dieselben wie auf der Ablaufseite und in der Verwaltung.
Der Ton ist bewusst gewaehlt. Der Kunde liest die Liste, wenn er unsicher
ist -- also im Zweifel schon angespannt. "Wir warten auf dich" waere
Druck, "Das brauche ich noch von dir" ist eine Bitte.
POSTFACH AUF DER UEBERSICHT
Bewusst auf der Uebersicht, nicht im Projekt: Wer kein Projekt hat -- oder
eine Frage, die zu keinem gehoert -- konnte vorher gar nicht schreiben und
musste zur E-Mail greifen. Damit war der Verlauf weg, sobald man ihn
brauchte.
Niedrigschwellig: kein Betreff, kein Pflichtfeld, der Projektbezug ist
freiwillig. Wer glaubt, eine Nachricht muesse eine "richtige" Anfrage
sein, schreibt gar nicht erst -- und genau die kurzen Fragen sollen hier
landen. Wird nachgeladen, damit die Uebersicht sofort dasteht.
Projekt- und Aufgabendaten werden gleichzeitig angefordert statt
nacheinander -- sonst waere die Wartezeit die Summe beider Anfragen. Die
Aufgabenliste darf dabei fehlschlagen, ohne die Seite mitzureissen: Wer
wegen einer leeren Liste seine Zahlungen nicht mehr saehe, waere
schlechter dran als vorher.
EIN SICHTBARER FEHLER GEFUNDEN
Auf dem Screenshot stand "Ideen & Aenderungswuensche". Ursache ist
eine Falle mit zwei Wegen: Texte ueber data-i18n landen als HTML in der
Seite, dort ist "&" richtig. Derselbe Text im JavaScript laeuft aber
durch die Absicherung und wird ein zweites Mal kodiert. Derselbe Baustein
ist also je nach Verwendungsort richtig oder falsch.
Das kann man nicht im Kopf behalten -- deshalb neu pruef-texte.mjs mit
ZWEI Pruefungen:
Anzeige: alle 11 Seiten x 5 Sprachen = 55 Kombinationen, sichtbarer
Text darf keine Kodierungsreste enthalten.
Quelltext: welcher Baustein mit "&" wird irgendwo abgesichert
eingesetzt?
Beide sind noetig. Gegengeprueft: Die Anzeigepruefung allein haette den
Fehler NICHT gefunden, weil die Ideen-Karte erst nach einer Anmeldung
erscheint. Die Quelltextpruefung findet ihn ohne Anzeige.
Auch die Quelltextpruefung selbst wurde zweimal gegengeprueft: Zuerst
meldete sie zusaetzlich po_senden ("Senden", voellig harmlos) -- ihre
Blockerkennung lief bis zum naechsten Schluessel und schluckte dabei
einen Kommentar mit "&". Jetzt zaehlt sie geschweifte Klammern. Ein
Fehlalarm im Pruefwerkzeug ist fast so schaedlich wie ein uebersehener
Fehler: Man gewoehnt sich daran, ihn wegzusehen.
GEPRUEFT
32 Pruefungen im Portal (Computer und Handy), alle bestanden. Darunter:
114 Texte in allen fuenf Sprachen vollstaendig, Kundenpunkte stehen vor
der langen Liste, Aufgaben vor den Zahlungen, Abhaken meldet den
richtigen Punkt, alle Kaestchen mindestens 24 px, keine Fehler im
Protokoll. Dazu 55 Seiten-Sprach-Kombinationen ohne Kodierungsreste.
Ein Fehler im Test selbst gefunden und behoben: Die Adressabfrage prueft
jetzt den PFAD statt der ganzen Adresse. Der API-Server heisst
"postfach.dogfather-universe.com" -- ein includes("/postfach") traf den
HOSTNAMEN und damit jede Anfrage, auch die Uebersicht.
Versionsstempel und Cache-Name auf v5.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -1390,3 +1390,155 @@ a.wd-karte:hover, .wd-karte--klickbar:hover {
|
|||||||
.po-legende .po-l-2 { background: var(--wd-p2); }
|
.po-legende .po-l-2 { background: var(--wd-p2); }
|
||||||
.po-legende .po-l-3 { background: var(--wd-p3); }
|
.po-legende .po-l-3 { background: var(--wd-p3); }
|
||||||
.po-legende .po-l-4 { background: var(--wd-p4); }
|
.po-legende .po-l-4 { background: var(--wd-p4); }
|
||||||
|
|
||||||
|
/* =====================================================================
|
||||||
|
PORTAL — AUFGABENLISTE UND POSTFACH (22.08.2026)
|
||||||
|
|
||||||
|
"wenn sie drauf drücken sollen sie auch sehen was ich schon gemacht
|
||||||
|
habe von dem was im plan war."
|
||||||
|
|
||||||
|
Dieselben vier Prozessfarben wie auf der Ablaufseite, in der Vorschau
|
||||||
|
und in der Verwaltung. Das ist der eigentliche Wert: Eine Farbe
|
||||||
|
bedeutet im ganzen Bereich dasselbe — sonst wäre sie Dekoration.
|
||||||
|
===================================================================== */
|
||||||
|
|
||||||
|
/* Die grosse Zahl. "9 von 14 erledigt" ist greifbar, "64 %" ist eine
|
||||||
|
Rechnung, die niemand fühlt. */
|
||||||
|
.po-aufg-stand { margin: .9rem 0 .2rem; font-size: .92rem; color: var(--wd-text-soft); }
|
||||||
|
.po-aufg-stand b {
|
||||||
|
font-family: var(--font-head); font-weight: 800;
|
||||||
|
font-size: 1.7rem; color: var(--wd-text); margin-right: .3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.po-fortschritt { display: flex; gap: 4px; height: 8px; margin-bottom: 1.2rem; }
|
||||||
|
.po-fs-teil { flex: 1; border-radius: 999px; background: rgba(255,255,255,.07); position: relative; overflow: hidden; }
|
||||||
|
.po-fs-teil > i { position: absolute; inset: 0 auto 0 0; border-radius: 999px; }
|
||||||
|
.po-fs-teil[data-k="start"] > i { background: var(--wd-p1); }
|
||||||
|
.po-fs-teil[data-k="gestaltung"] > i { background: var(--wd-p2); }
|
||||||
|
.po-fs-teil[data-k="umsetzung"] > i { background: var(--wd-p3); }
|
||||||
|
.po-fs-teil[data-k="abschluss"] > i { background: var(--wd-p4); }
|
||||||
|
|
||||||
|
/* ---------- "Das brauche ich noch von dir" ----------
|
||||||
|
Ein EIGENER Kasten ganz oben, nicht nur eine farbige Zeile irgendwo
|
||||||
|
in der Liste. Wer eine lange Liste sieht, liest sie als Bericht über
|
||||||
|
fremde Arbeit und überliest seinen eigenen Teil — und genau daraus
|
||||||
|
entstehen die meisten Verzögerungen.
|
||||||
|
|
||||||
|
Gold, weil es im ganzen Bereich für "das betrifft dich" steht. Nicht
|
||||||
|
Rot: Es ist eine Bitte, kein Fehler. */
|
||||||
|
.po-dubist {
|
||||||
|
margin: 0 0 1.3rem;
|
||||||
|
padding: .9rem 1rem 1rem;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(232,200,119,.07);
|
||||||
|
border: 1px solid rgba(232,200,119,.32);
|
||||||
|
}
|
||||||
|
.po-dubist h4 {
|
||||||
|
margin: 0; font-size: 1rem; color: var(--wd-gold);
|
||||||
|
font-family: var(--font-head); font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Der beruhigende Gegenpart. Wenn nichts offen ist, soll das auch
|
||||||
|
dastehen — Abwesenheit einer Aufforderung ist eine gute Nachricht und
|
||||||
|
darf ruhig ausgesprochen werden. */
|
||||||
|
.po-allesgut {
|
||||||
|
margin: 0 0 1.3rem; padding: .75rem .95rem;
|
||||||
|
border-radius: 12px; font-size: .9rem;
|
||||||
|
background: rgba(111,214,163,.08);
|
||||||
|
border: 1px solid rgba(111,214,163,.28);
|
||||||
|
color: var(--wd-ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Gruppen und Zeilen ---------- */
|
||||||
|
.po-aufg-gruppe { margin-bottom: 1.1rem; }
|
||||||
|
.po-aufg-kopf {
|
||||||
|
display: flex; align-items: baseline; gap: .5rem;
|
||||||
|
font-size: .74rem; letter-spacing: .13em; text-transform: uppercase;
|
||||||
|
margin-bottom: .45rem; padding-bottom: .35rem;
|
||||||
|
border-bottom: 1px solid var(--wd-rand);
|
||||||
|
}
|
||||||
|
.po-aufg-zahl { margin-left: auto; letter-spacing: 0; text-transform: none; font-size: .8rem; color: var(--wd-text-soft); }
|
||||||
|
.po-aufg-gruppe[data-k="start"] .po-aufg-kopf { color: var(--wd-p1); }
|
||||||
|
.po-aufg-gruppe[data-k="gestaltung"] .po-aufg-kopf { color: var(--wd-p2); }
|
||||||
|
.po-aufg-gruppe[data-k="umsetzung"] .po-aufg-kopf { color: var(--wd-p3); }
|
||||||
|
.po-aufg-gruppe[data-k="abschluss"] .po-aufg-kopf { color: var(--wd-p4); }
|
||||||
|
|
||||||
|
/* Zeile und Spalte ausdrücklich für JEDES Kind.
|
||||||
|
|
||||||
|
Ohne Spaltenangabe geht es schief, und zwar auf eine Art, die man
|
||||||
|
nicht errät: Das Raster platziert zuerst alle Elemente mit
|
||||||
|
FESTGELEGTER Zeile und erst danach die freien. Kästchen und Marke
|
||||||
|
haben beide eine feste Zeile — sie würden zusammen nach vorne rutschen
|
||||||
|
und der Titel landete ganz rechts. Genau das ist am 22.08.2026 in der
|
||||||
|
Verwaltung passiert und erst am Screenshot aufgefallen. */
|
||||||
|
.po-aufg {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0,1fr) auto;
|
||||||
|
gap: .12rem .7rem; align-items: start;
|
||||||
|
padding: .55rem .6rem; margin-bottom: .25rem;
|
||||||
|
border-radius: 10px; border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
.po-aufg-haken { grid-row: 1 / span 2; grid-column: 1; }
|
||||||
|
.po-aufg-titel { grid-row: 1; grid-column: 2; font-size: .94rem; color: var(--wd-text); overflow-wrap: anywhere; }
|
||||||
|
.po-aufg-marken { grid-row: 1; grid-column: 3; }
|
||||||
|
.po-aufg-text { grid-row: 2; grid-column: 2 / span 2; font-size: .82rem; color: var(--wd-text-soft); line-height: 1.5; }
|
||||||
|
|
||||||
|
.po-aufg--fertig .po-aufg-titel { color: var(--wd-text-soft); text-decoration: line-through; text-decoration-thickness: 1px; }
|
||||||
|
.po-aufg--weg .po-aufg-titel { color: var(--wd-text-soft); text-decoration: line-through; opacity: .55; }
|
||||||
|
.po-aufg--du { background: rgba(232,200,119,.06); }
|
||||||
|
|
||||||
|
/* Im oberen Kasten stehen dieselben Punkte noch einmal. Dort brauchen
|
||||||
|
sie den eigenen Hintergrund nicht — der Kasten ist schon golden, ein
|
||||||
|
zweiter Goldton darin würde nur schmutzig wirken. */
|
||||||
|
.po-aufg--hervor { background: transparent; padding-left: 0; padding-right: 0; }
|
||||||
|
|
||||||
|
/* Das Kästchen. Bei eigenen Punkten ein echter Knopf (tastaturbedienbar,
|
||||||
|
44px Trefferfläche über das Innenabstands-Polster), sonst nur Anzeige.
|
||||||
|
Ein Knopf, der nichts tut, lässt die Seite kaputt wirken. */
|
||||||
|
.po-aufg-haken {
|
||||||
|
width: 26px; height: 26px; margin-top: .05rem; padding: 0;
|
||||||
|
border-radius: 8px; border: 2px solid var(--wd-rand-stark);
|
||||||
|
background: transparent; color: transparent;
|
||||||
|
display: grid; place-items: center;
|
||||||
|
font-size: .95rem; line-height: 1;
|
||||||
|
transition: all var(--wd-zeit-schnell) ease;
|
||||||
|
}
|
||||||
|
button.po-aufg-haken { cursor: pointer; }
|
||||||
|
button.po-aufg-haken:hover { border-color: var(--wd-gold); background: rgba(232,200,119,.1); }
|
||||||
|
.po-aufg-haken--fest { cursor: default; }
|
||||||
|
.po-aufg--fertig .po-aufg-haken { background: var(--wd-ok); border-color: var(--wd-ok); color: #06120c; }
|
||||||
|
.po-aufg--jetzt .po-aufg-haken { border-color: var(--wd-p2); color: var(--wd-p2); }
|
||||||
|
.po-aufg--weg .po-aufg-haken { border-style: dashed; color: var(--wd-text-soft); }
|
||||||
|
/* Offene EIGENE Punkte: der Rand ist golden, damit klar ist, dass hier
|
||||||
|
geklickt werden kann und soll. */
|
||||||
|
.po-aufg--du button.po-aufg-haken { border-color: rgba(232,200,119,.55); }
|
||||||
|
|
||||||
|
.po-aufg-marke {
|
||||||
|
display: inline-block; white-space: nowrap;
|
||||||
|
font-size: .68rem; letter-spacing: .05em; text-transform: uppercase;
|
||||||
|
padding: .14rem .45rem; border-radius: 999px;
|
||||||
|
}
|
||||||
|
.po-aufg-marke--ok { background: rgba(111,214,163,.13); color: var(--wd-ok); }
|
||||||
|
.po-aufg-marke--jetzt { background: rgba(183,157,255,.15); color: var(--wd-p2); }
|
||||||
|
.po-aufg-marke--du { background: rgba(232,200,119,.16); color: var(--wd-gold); }
|
||||||
|
.po-aufg-nicht {
|
||||||
|
display: inline-block; margin-left: .4rem;
|
||||||
|
font-size: .68rem; letter-spacing: .05em; text-transform: uppercase;
|
||||||
|
padding: .12rem .4rem; border-radius: 999px;
|
||||||
|
background: rgba(255,157,157,.12); color: var(--wd-fehler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auf dem Handy rutscht die Marke unter den Titel — nebeneinander bliebe
|
||||||
|
für den Titel eine Spalte von zwei Wörtern Breite. */
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.po-aufg { grid-template-columns: auto minmax(0,1fr); }
|
||||||
|
.po-aufg-marken { grid-row: 2; grid-column: 2; margin-bottom: .15rem; }
|
||||||
|
.po-aufg-text { grid-row: 3; grid-column: 2; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Postfach auf der Übersicht ---------- */
|
||||||
|
.po-pf-verlauf {
|
||||||
|
max-height: 46vh; overflow-y: auto;
|
||||||
|
margin: .8rem 0 .2rem; padding-right: .3rem;
|
||||||
|
display: flex; flex-direction: column; gap: .5rem;
|
||||||
|
}
|
||||||
|
|||||||
+186
-1
@@ -205,7 +205,13 @@ window.I18N_WD = {
|
|||||||
po_senden: { de: "Senden", "de-CH": "Schicke", en: "Send", fr: "Envoyer", pt: "Enviar" },
|
po_senden: { de: "Senden", "de-CH": "Schicke", en: "Send", fr: "Envoyer", pt: "Enviar" },
|
||||||
|
|
||||||
/* ---------- Änderungswünsche ---------- */
|
/* ---------- Änderungswünsche ---------- */
|
||||||
po_ideen: { de: "Ideen & Änderungswünsche", "de-CH": "Ideä & Änderigswünsch", en: "Ideas & change requests", fr: "Idées & demandes de modification", pt: "Ideias & pedidos de alteração" },
|
/* Hier steht ein ECHTES "&", kein "&" -- anders als in den meisten
|
||||||
|
anderen i18n-Dateien. Der Grund: Texte, die über data-i18n gesetzt
|
||||||
|
werden, landen als HTML in der Seite, dort ist "&" richtig.
|
||||||
|
Dieser Text wird aber im JavaScript durch die Absicherung geschickt
|
||||||
|
(s(...)), und die kodiert das "&" ein zweites Mal -- auf dem
|
||||||
|
Bildschirm stand "Ideen &amp; Änderungswünsche". */
|
||||||
|
po_ideen: { de: "Ideen & Änderungswünsche", "de-CH": "Ideä & Änderigswünsch", en: "Ideas & change requests", fr: "Idées & demandes de modification", pt: "Ideias & pedidos de alteração" },
|
||||||
po_ideen_text: {
|
po_ideen_text: {
|
||||||
de: "Trag hier ein, was dir noch einfällt. Ich prüfe Aufwand und Preis und melde mich — du entscheidest danach in Ruhe.",
|
de: "Trag hier ein, was dir noch einfällt. Ich prüfe Aufwand und Preis und melde mich — du entscheidest danach in Ruhe.",
|
||||||
"de-CH": "Trag do ii, was dir no iifallt. Ich prüefe Ufwand und Priis und mälde mich — du entscheidsch dernoch i Rueh.",
|
"de-CH": "Trag do ii, was dir no iifallt. Ich prüefe Ufwand und Priis und mälde mich — du entscheidsch dernoch i Rueh.",
|
||||||
@@ -268,4 +274,183 @@ window.I18N_WD = {
|
|||||||
fr: "Ta session a expiré. Merci de te reconnecter.",
|
fr: "Ta session a expiré. Merci de te reconnecter.",
|
||||||
pt: "A tua sessão expirou. Inicia sessão novamente.",
|
pt: "A tua sessão expirou. Inicia sessão novamente.",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* ---------- Aufgabenliste (22.08.2026) ----------
|
||||||
|
"wenn sie drauf drücken sollen sie auch sehen was ich schon gemacht
|
||||||
|
habe von dem was im plan war."
|
||||||
|
|
||||||
|
Der Ton ist hier besonders wichtig. Der Kunde liest diese Liste,
|
||||||
|
wenn er unsicher ist, wie weit es steht — also im Zweifel schon
|
||||||
|
leicht angespannt. Sie muss beruhigen und klar sagen, was von IHM
|
||||||
|
erwartet wird, ohne zu drängen. "Wir warten auf dich" wäre Druck;
|
||||||
|
"Das brauche ich noch von dir" ist eine Bitte. */
|
||||||
|
po_aufgaben: {
|
||||||
|
de: "Was schon erledigt ist",
|
||||||
|
"de-CH": "Was scho erledigt isch",
|
||||||
|
en: "What's already done",
|
||||||
|
fr: "Ce qui est déjà fait",
|
||||||
|
pt: "O que já está feito",
|
||||||
|
},
|
||||||
|
po_aufg_text: {
|
||||||
|
de: "Jeder Punkt aus deinem Plan — abgehakt, sobald er fertig ist. Du siehst jederzeit, woran gerade gearbeitet wird und was noch von dir gebraucht wird.",
|
||||||
|
"de-CH": "Jede Punkt us dim Plan — abghakt, sobald er fertig isch. Du gsehsch jederziit, wo grad dra gschaffet wird und was no vo dir brucht wird.",
|
||||||
|
en: "Every point from your plan — ticked off as soon as it's finished. You can always see what's being worked on and what's still needed from you.",
|
||||||
|
fr: "Chaque point de ton plan — coché dès qu'il est terminé. Tu vois à tout moment ce sur quoi je travaille et ce qu'il me faut encore de ta part.",
|
||||||
|
pt: "Cada ponto do teu plano — assinalado assim que estiver pronto. Vês sempre no que se está a trabalhar e o que ainda é preciso da tua parte.",
|
||||||
|
},
|
||||||
|
po_aufg_leer: {
|
||||||
|
de: "Die Punkte erscheinen hier, sobald dein Plan steht.",
|
||||||
|
"de-CH": "D Pünkt erschiined do, sobald din Plan stoht.",
|
||||||
|
en: "The points appear here as soon as your plan is set.",
|
||||||
|
fr: "Les points apparaîtront ici dès que ton plan sera établi.",
|
||||||
|
pt: "Os pontos aparecem aqui assim que o teu plano estiver definido.",
|
||||||
|
},
|
||||||
|
po_aufg_fertig: {
|
||||||
|
de: "von {gesamt} erledigt",
|
||||||
|
"de-CH": "vo {gesamt} erledigt",
|
||||||
|
en: "of {gesamt} done",
|
||||||
|
fr: "sur {gesamt} terminés",
|
||||||
|
pt: "de {gesamt} concluídos",
|
||||||
|
},
|
||||||
|
po_aufg_dubist: {
|
||||||
|
de: "Das brauche ich noch von dir",
|
||||||
|
"de-CH": "Das bruuch ich no vo dir",
|
||||||
|
en: "This is what I still need from you",
|
||||||
|
fr: "Voici ce qu'il me faut encore de ta part",
|
||||||
|
pt: "Isto é o que ainda preciso da tua parte",
|
||||||
|
},
|
||||||
|
po_aufg_dubist_eins: {
|
||||||
|
de: "Ein Punkt wartet auf dich",
|
||||||
|
"de-CH": "Ei Punkt wartet uf dich",
|
||||||
|
en: "One point is waiting for you",
|
||||||
|
fr: "Un point t'attend",
|
||||||
|
pt: "Um ponto está à tua espera",
|
||||||
|
},
|
||||||
|
po_aufg_dubist_viele: {
|
||||||
|
de: "{n} Punkte warten auf dich",
|
||||||
|
"de-CH": "{n} Pünkt warted uf dich",
|
||||||
|
en: "{n} points are waiting for you",
|
||||||
|
fr: "{n} points t'attendent",
|
||||||
|
pt: "{n} pontos estão à tua espera",
|
||||||
|
},
|
||||||
|
po_aufg_allesbei: {
|
||||||
|
de: "Von dir wird gerade nichts gebraucht — lehn dich zurück.",
|
||||||
|
"de-CH": "Vo dir wird grad nüt brucht — lehn di zrugg.",
|
||||||
|
en: "Nothing is needed from you right now — sit back.",
|
||||||
|
fr: "Rien n'est attendu de toi pour l'instant — tu peux te détendre.",
|
||||||
|
pt: "Neste momento não é preciso nada da tua parte — descontrai.",
|
||||||
|
},
|
||||||
|
po_aufg_erledigt_marke: {
|
||||||
|
de: "erledigt",
|
||||||
|
"de-CH": "erledigt",
|
||||||
|
en: "done",
|
||||||
|
fr: "terminé",
|
||||||
|
pt: "concluído",
|
||||||
|
},
|
||||||
|
po_aufg_arbeit_marke: {
|
||||||
|
de: "läuft gerade",
|
||||||
|
"de-CH": "lauft grad",
|
||||||
|
en: "in progress",
|
||||||
|
fr: "en cours",
|
||||||
|
pt: "em curso",
|
||||||
|
},
|
||||||
|
po_aufg_du_marke: {
|
||||||
|
de: "bei dir",
|
||||||
|
"de-CH": "bi dir",
|
||||||
|
en: "your turn",
|
||||||
|
fr: "à toi",
|
||||||
|
pt: "contigo",
|
||||||
|
},
|
||||||
|
po_aufg_nicht: {
|
||||||
|
de: "nicht im Paket",
|
||||||
|
"de-CH": "nöd im Paket",
|
||||||
|
en: "not included",
|
||||||
|
fr: "hors forfait",
|
||||||
|
pt: "não incluído",
|
||||||
|
},
|
||||||
|
po_aufg_hake: {
|
||||||
|
de: "Erledigt — als erledigt melden",
|
||||||
|
"de-CH": "Erledigt — als erledigt mälde",
|
||||||
|
en: "Done — mark as done",
|
||||||
|
fr: "Fait — marquer comme terminé",
|
||||||
|
pt: "Feito — marcar como concluído",
|
||||||
|
},
|
||||||
|
po_aufg_zurueck: {
|
||||||
|
de: "Doch noch offen",
|
||||||
|
"de-CH": "Doch no offe",
|
||||||
|
en: "Still open after all",
|
||||||
|
fr: "Finalement pas terminé",
|
||||||
|
pt: "Afinal ainda em aberto",
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ---------- Allgemeines Postfach (22.08.2026) ----------
|
||||||
|
"auf dieser seite sollen die kunden mir auch nachrichten hinterlassen
|
||||||
|
können, für noch kleinigkeiten oder sachen die ich brauch."
|
||||||
|
|
||||||
|
Bewusst niedrigschwellig formuliert. Wer glaubt, eine Nachricht
|
||||||
|
müsse eine "richtige" Anfrage sein, schreibt gar nicht erst — und
|
||||||
|
genau die kurzen Fragen sollen ja hier landen. */
|
||||||
|
po_postfach: {
|
||||||
|
de: "Kurz etwas fragen",
|
||||||
|
"de-CH": "Churz öppis frooge",
|
||||||
|
en: "Ask something quickly",
|
||||||
|
fr: "Une question rapide",
|
||||||
|
pt: "Perguntar algo rápido",
|
||||||
|
},
|
||||||
|
po_postfach_text: {
|
||||||
|
de: "Für alles Kleine zwischendurch — eine Frage, ein Hinweis, etwas Vergessenes. Auch wenn es zu keinem bestimmten Projekt gehört. Kein Anlass ist zu klein.",
|
||||||
|
"de-CH": "Für alles Chliine zwüschedure — e Frog, en Hiiwiis, öppis Vergässnigs. Au wenns zu kem bstimmte Projekt ghört. Kein Aalass isch z chlii.",
|
||||||
|
en: "For all the small things in between — a question, a note, something forgotten. Even if it doesn't belong to a particular project. No reason is too small.",
|
||||||
|
fr: "Pour tout ce qui est petit entre-temps — une question, une remarque, un oubli. Même si cela ne concerne aucun projet précis. Aucune raison n'est trop petite.",
|
||||||
|
pt: "Para todas as pequenas coisas pelo meio — uma pergunta, um aviso, algo esquecido. Mesmo que não pertença a um projeto específico. Nenhum motivo é pequeno demais.",
|
||||||
|
},
|
||||||
|
po_postfach_leer: {
|
||||||
|
de: "Noch nichts geschrieben. Schreib einfach los.",
|
||||||
|
"de-CH": "No nüt gschriebe. Schriib eifach los.",
|
||||||
|
en: "Nothing written yet. Just go ahead.",
|
||||||
|
fr: "Rien d'écrit pour l'instant. Lance-toi.",
|
||||||
|
pt: "Ainda nada escrito. Escreve à vontade.",
|
||||||
|
},
|
||||||
|
po_postfach_platzhalter: {
|
||||||
|
de: "Was möchtest du fragen oder mitteilen?",
|
||||||
|
"de-CH": "Was wotsch frooge oder mitteile?",
|
||||||
|
en: "What would you like to ask or tell me?",
|
||||||
|
fr: "Que veux-tu demander ou signaler ?",
|
||||||
|
pt: "O que queres perguntar ou dizer?",
|
||||||
|
},
|
||||||
|
po_postfach_bezug: {
|
||||||
|
de: "Worum geht es? (freiwillig)",
|
||||||
|
"de-CH": "Um was gohts? (freiwillig)",
|
||||||
|
en: "What is it about? (optional)",
|
||||||
|
fr: "De quoi s'agit-il ? (facultatif)",
|
||||||
|
pt: "Sobre o que é? (opcional)",
|
||||||
|
},
|
||||||
|
po_postfach_kein_bezug: {
|
||||||
|
de: "— nichts Bestimmtes —",
|
||||||
|
"de-CH": "— nüt Bstimmts —",
|
||||||
|
en: "— nothing in particular —",
|
||||||
|
fr: "— rien de particulier —",
|
||||||
|
pt: "— nada em particular —",
|
||||||
|
},
|
||||||
|
po_postfach_leer_fehler: {
|
||||||
|
de: "Bitte schreib zuerst etwas.",
|
||||||
|
"de-CH": "Bitte schriib zerscht öppis.",
|
||||||
|
en: "Please write something first.",
|
||||||
|
fr: "Merci d'écrire quelque chose d'abord.",
|
||||||
|
pt: "Escreve algo primeiro.",
|
||||||
|
},
|
||||||
|
po_postfach_gesendet: {
|
||||||
|
de: "Ist angekommen. Ich melde mich.",
|
||||||
|
"de-CH": "Isch aacho. Ich mälde mich.",
|
||||||
|
en: "Received. I'll get back to you.",
|
||||||
|
fr: "Bien reçu. Je reviens vers toi.",
|
||||||
|
pt: "Recebido. Volto a contactar-te.",
|
||||||
|
},
|
||||||
|
po_von_dogfather: {
|
||||||
|
de: "Dogfather",
|
||||||
|
"de-CH": "Dogfather",
|
||||||
|
en: "Dogfather",
|
||||||
|
fr: "Dogfather",
|
||||||
|
pt: "Dogfather",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+275
-1
@@ -144,6 +144,133 @@
|
|||||||
s(window.WD.t(wartet === "kunde" ? "po_wartet_kunde" : "po_wartet_dogfather")) + "</span>";
|
s(window.WD.t(wartet === "kunde" ? "po_wartet_kunde" : "po_wartet_dogfather")) + "</span>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------- Aufgabenliste (22.08.2026) ----------------
|
||||||
|
"wenn sie drauf drücken sollen sie auch sehen was ich schon gemacht
|
||||||
|
habe von dem was im plan war."
|
||||||
|
|
||||||
|
Bis hierher sah der Kunde nur die Phase ("Design · 3/7") — eine Zahl
|
||||||
|
ohne Inhalt. Sie beantwortet die eigentliche Frage nicht: WAS ist
|
||||||
|
denn schon fertig?
|
||||||
|
|
||||||
|
Zwei Entscheidungen prägen diese Ansicht:
|
||||||
|
|
||||||
|
1. Punkte, die auf den KUNDEN warten, stehen ganz oben in einem
|
||||||
|
eigenen Kasten — nicht nur farblich markiert irgendwo mittendrin.
|
||||||
|
Wer eine lange Liste sieht, liest sie als Bericht über MEINE
|
||||||
|
Arbeit und übersieht seinen eigenen Teil. Genau daraus entstehen
|
||||||
|
die meisten Verzögerungen.
|
||||||
|
|
||||||
|
2. Der Kunde kann seine eigenen Punkte selbst abhaken. Das ist keine
|
||||||
|
Spielerei: Es gibt ihm einen sichtbaren Anteil am Fortschritt,
|
||||||
|
und ich sehe sofort, dass die Texte da sind, ohne dass er extra
|
||||||
|
schreiben muss.
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
var KATS = ["start", "gestaltung", "umsetzung", "abschluss"];
|
||||||
|
var KAT_KEY = { start: "po_k1", gestaltung: "po_k2", umsetzung: "po_k3", abschluss: "po_k4" };
|
||||||
|
|
||||||
|
function aufgabeZeile(a, hervor) {
|
||||||
|
var klasse = "po-aufg";
|
||||||
|
if (a.status === "erledigt") klasse += " po-aufg--fertig";
|
||||||
|
else if (a.status === "in_arbeit") klasse += " po-aufg--jetzt";
|
||||||
|
else if (a.status === "entfaellt") klasse += " po-aufg--weg";
|
||||||
|
var duBistDran = a.wer_dran === "kunde" && a.status !== "erledigt" && a.status !== "entfaellt";
|
||||||
|
if (duBistDran) klasse += " po-aufg--du";
|
||||||
|
if (hervor) klasse += " po-aufg--hervor";
|
||||||
|
|
||||||
|
/* Nur EIGENE offene Punkte bekommen einen Knopf. Bei allen anderen
|
||||||
|
ist das Zeichen reine Anzeige — sonst klickt der Kunde darauf,
|
||||||
|
nichts passiert, und er hält die Seite für kaputt. */
|
||||||
|
var eigen = a.wer_dran === "kunde" && !a.nicht_enthalten && a.status !== "entfaellt";
|
||||||
|
var zeichen = a.status === "erledigt" ? "✓" : a.status === "in_arbeit" ? "•" : a.status === "entfaellt" ? "–" : "";
|
||||||
|
|
||||||
|
var kaestchen = eigen
|
||||||
|
? '<button type="button" class="po-aufg-haken" data-haken="' + s(a.id) + '" title="' +
|
||||||
|
s(window.WD.t(a.status === "erledigt" ? "po_aufg_zurueck" : "po_aufg_hake")) + '" aria-label="' +
|
||||||
|
s(window.WD.t(a.status === "erledigt" ? "po_aufg_zurueck" : "po_aufg_hake")) + ": " + s(a.titel) + '">' +
|
||||||
|
zeichen + "</button>"
|
||||||
|
: '<span class="po-aufg-haken po-aufg-haken--fest" aria-hidden="true">' + zeichen + "</span>";
|
||||||
|
|
||||||
|
var marke = a.status === "erledigt"
|
||||||
|
? '<span class="po-aufg-marke po-aufg-marke--ok">' + s(window.WD.t("po_aufg_erledigt_marke")) + "</span>"
|
||||||
|
: a.status === "in_arbeit"
|
||||||
|
? '<span class="po-aufg-marke po-aufg-marke--jetzt">' + s(window.WD.t("po_aufg_arbeit_marke")) + "</span>"
|
||||||
|
: duBistDran
|
||||||
|
? '<span class="po-aufg-marke po-aufg-marke--du">' + s(window.WD.t("po_aufg_du_marke")) + "</span>"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return '<div class="' + klasse + '">' + kaestchen +
|
||||||
|
'<span class="po-aufg-titel">' + s(a.titel) +
|
||||||
|
(a.nicht_enthalten ? '<span class="po-aufg-nicht">' + s(window.WD.t("po_aufg_nicht")) + "</span>" : "") +
|
||||||
|
"</span>" +
|
||||||
|
'<span class="po-aufg-marken">' + marke + "</span>" +
|
||||||
|
(a.beschreibung || a.hinweis
|
||||||
|
? '<span class="po-aufg-text">' + s(a.beschreibung || "") +
|
||||||
|
(a.hinweis ? " " + s(a.hinweis) : "") + "</span>"
|
||||||
|
: "") +
|
||||||
|
"</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function aufgabenKarte(d) {
|
||||||
|
var f = d.fortschritt || { gesamt: 0, erledigt: 0, beimKunden: 0, jeKategorie: {} };
|
||||||
|
var alle = d.aufgaben || [];
|
||||||
|
|
||||||
|
var html = '<div class="wd-karte" style="margin-bottom:1.2rem"><h3>' +
|
||||||
|
s(window.WD.t("po_aufgaben")) + "</h3>";
|
||||||
|
|
||||||
|
if (!alle.length) {
|
||||||
|
return html + '<p class="wd-leise wd-klein">' + s(window.WD.t("po_aufg_leer")) + "</p></div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '<p class="wd-klein wd-leise" style="margin-top:-.3rem">' +
|
||||||
|
s(window.WD.t("po_aufg_text")) + "</p>";
|
||||||
|
|
||||||
|
/* Grosse Zahl statt Prozent. "9 von 14 erledigt" ist greifbar,
|
||||||
|
"64 %" ist eine Rechnung, die niemand fuehlt. */
|
||||||
|
html += '<div class="po-aufg-stand"><b>' + f.erledigt + "</b> " +
|
||||||
|
s(window.WD.t("po_aufg_fertig").replace("{gesamt}", f.gesamt)) + "</div>";
|
||||||
|
|
||||||
|
/* Der Balken in den vier Prozessfarben — dieselben wie auf der
|
||||||
|
Ablaufseite und in der Verwaltung. */
|
||||||
|
html += '<div class="po-fortschritt">' + KATS.map(function (k) {
|
||||||
|
var z = (f.jeKategorie && f.jeKategorie[k]) || { gesamt: 0, erledigt: 0 };
|
||||||
|
var breite = z.gesamt ? Math.round((z.erledigt / z.gesamt) * 100) : 0;
|
||||||
|
return '<span class="po-fs-teil" data-k="' + k + '" title="' +
|
||||||
|
s(window.WD.t(KAT_KEY[k])) + ": " + z.erledigt + "/" + z.gesamt +
|
||||||
|
'"><i style="width:' + breite + '%"></i></span>';
|
||||||
|
}).join("") + "</div>";
|
||||||
|
|
||||||
|
/* --- Was auf den Kunden wartet, ZUERST und in einem eigenen Kasten. --- */
|
||||||
|
var meine = alle.filter(function (a) {
|
||||||
|
return a.wer_dran === "kunde" && a.status !== "erledigt" && a.status !== "entfaellt" && !a.nicht_enthalten;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (meine.length) {
|
||||||
|
html += '<div class="po-dubist"><h4>' +
|
||||||
|
s(window.WD.t(meine.length === 1 ? "po_aufg_dubist_eins"
|
||||||
|
: "po_aufg_dubist_viele").replace("{n}", meine.length)) + "</h4>" +
|
||||||
|
'<p class="wd-klein wd-leise" style="margin:.1rem 0 .7rem">' +
|
||||||
|
s(window.WD.t("po_aufg_dubist")) + "</p>" +
|
||||||
|
meine.map(function (a) { return aufgabeZeile(a, true); }).join("") +
|
||||||
|
"</div>";
|
||||||
|
} else {
|
||||||
|
html += '<p class="po-allesgut">' + s(window.WD.t("po_aufg_allesbei")) + "</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Danach die vollstaendige Liste nach Abschnitten. --- */
|
||||||
|
html += KATS.map(function (k) {
|
||||||
|
var drin = alle.filter(function (a) { return a.kategorie === k; });
|
||||||
|
if (!drin.length) return "";
|
||||||
|
var z = (f.jeKategorie && f.jeKategorie[k]) || { gesamt: 0, erledigt: 0 };
|
||||||
|
return '<div class="po-aufg-gruppe" data-k="' + k + '">' +
|
||||||
|
'<div class="po-aufg-kopf">' + s(window.WD.t(KAT_KEY[k])) +
|
||||||
|
'<span class="po-aufg-zahl">' + z.erledigt + " / " + z.gesamt + "</span></div>" +
|
||||||
|
drin.map(function (a) { return aufgabeZeile(a, false); }).join("") +
|
||||||
|
"</div>";
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
return html + "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
async function ladeUebersicht() {
|
async function ladeUebersicht() {
|
||||||
zeige("po-uebersicht");
|
zeige("po-uebersicht");
|
||||||
var ziel = document.getElementById("po-inhalt");
|
var ziel = document.getElementById("po-inhalt");
|
||||||
@@ -261,10 +388,23 @@
|
|||||||
}).join("") + "</ul></div>";
|
}).join("") + "</ul></div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Der allgemeine Nachrichtenbereich steht auf der ÜBERSICHT, nicht
|
||||||
|
im Projekt. Genau das war der Punkt: Wer kein Projekt hat — oder
|
||||||
|
eine Frage, die zu keinem gehört — konnte vorher gar nicht
|
||||||
|
schreiben und musste zur E-Mail greifen. Damit war der Verlauf
|
||||||
|
weg, sobald man ihn brauchte.
|
||||||
|
|
||||||
|
Er wird nachgeladen, nicht mitgeliefert: Die Übersicht soll
|
||||||
|
sofort dastehen. Ein paar Zehntelsekunden später erscheinen die
|
||||||
|
Nachrichten — das ist besser, als die ganze Seite auf sie warten
|
||||||
|
zu lassen. */
|
||||||
|
html += '<div id="po-postfach" style="margin-top:1.8rem"></div>';
|
||||||
|
|
||||||
ziel.innerHTML = html;
|
ziel.innerHTML = html;
|
||||||
ziel.querySelectorAll("[data-projekt]").forEach(function (b) {
|
ziel.querySelectorAll("[data-projekt]").forEach(function (b) {
|
||||||
b.addEventListener("click", function () { ladeProjekt(b.dataset.projekt); });
|
b.addEventListener("click", function () { ladeProjekt(b.dataset.projekt); });
|
||||||
});
|
});
|
||||||
|
ladePostfach();
|
||||||
} catch (fehler) {
|
} catch (fehler) {
|
||||||
if (behandleFehler(fehler)) return;
|
if (behandleFehler(fehler)) return;
|
||||||
ziel.innerHTML = "";
|
ziel.innerHTML = "";
|
||||||
@@ -273,6 +413,100 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- Projektdetail ---------------- */
|
/* ---------------- Projektdetail ---------------- */
|
||||||
|
/* ---------------- Allgemeines Postfach ----------------
|
||||||
|
"auf dieser seite sollen die kunden mir auch nachrichten hinterlassen
|
||||||
|
können, für noch kleinigkeiten oder sachen die ich brauch."
|
||||||
|
|
||||||
|
Bewusst niedrigschwellig: kein Betreff, kein Pflichtfeld, der Bezug
|
||||||
|
zu einem Projekt ist freiwillig. Wer glaubt, eine Nachricht müsse
|
||||||
|
eine "richtige" Anfrage sein, schreibt gar nicht erst — und genau
|
||||||
|
die kurzen Fragen sollen hier landen.
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
async function ladePostfach() {
|
||||||
|
var kasten = document.getElementById("po-postfach");
|
||||||
|
if (!kasten) return;
|
||||||
|
|
||||||
|
var d;
|
||||||
|
try {
|
||||||
|
d = await api("/webdesign/portal/postfach");
|
||||||
|
} catch (fehler) {
|
||||||
|
/* Still bleiben. Der Bereich ist eine Ergänzung — eine rote
|
||||||
|
Fehlermeldung über der Projektübersicht wäre alarmierender, als
|
||||||
|
der Anlass es hergibt. */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = "<h2>" + s(window.WD.t("po_postfach")) + "</h2>" +
|
||||||
|
'<div class="wd-karte">' +
|
||||||
|
'<p class="wd-klein wd-leise" style="margin-top:0">' + s(window.WD.t("po_postfach_text")) + "</p>";
|
||||||
|
|
||||||
|
if (!d.nachrichten.length) {
|
||||||
|
html += '<p class="wd-leise wd-klein">' + s(window.WD.t("po_postfach_leer")) + "</p>";
|
||||||
|
} else {
|
||||||
|
html += '<div class="po-pf-verlauf" id="po-pf-verlauf">' + d.nachrichten.map(function (n) {
|
||||||
|
var vonMir = n.autor === "kunde";
|
||||||
|
return '<div class="po-nachricht po-nachricht--' + (vonMir ? "ich" : "du") + '">' +
|
||||||
|
'<div class="po-nachricht-kopf">' +
|
||||||
|
(vonMir ? "" : s(window.WD.t("po_von_dogfather")) + " · ") +
|
||||||
|
window.WD.datum(n.erstellt_am) +
|
||||||
|
(n.projekt_titel ? " · " + s(n.projekt_titel) : "") +
|
||||||
|
"</div><p>" + s(n.text) + "</p></div>";
|
||||||
|
}).join("") + "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '<div class="wd-feld" style="margin:1rem 0 .6rem" id="po-pf-feld">' +
|
||||||
|
'<textarea id="po-pf-text" rows="3" placeholder="' +
|
||||||
|
s(window.WD.t("po_postfach_platzhalter")) + '"></textarea>' +
|
||||||
|
'<p class="wd-feld-fehler"></p></div>';
|
||||||
|
|
||||||
|
if (d.projekte && d.projekte.length) {
|
||||||
|
html += '<div class="wd-feld" style="margin-bottom:.6rem">' +
|
||||||
|
'<label for="po-pf-projekt" class="wd-klein">' + s(window.WD.t("po_postfach_bezug")) + "</label>" +
|
||||||
|
'<select id="po-pf-projekt"><option value="">' +
|
||||||
|
s(window.WD.t("po_postfach_kein_bezug")) + "</option>" +
|
||||||
|
d.projekte.map(function (p) {
|
||||||
|
return '<option value="' + s(p.id) + '">' + s(p.titel) + "</option>";
|
||||||
|
}).join("") + "</select></div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '<button type="button" class="wd-btn wd-btn--zweit wd-btn--klein" id="po-pf-senden">' +
|
||||||
|
s(window.WD.t("po_senden")) + "</button></div>";
|
||||||
|
|
||||||
|
kasten.innerHTML = html;
|
||||||
|
|
||||||
|
var lauf = document.getElementById("po-pf-verlauf");
|
||||||
|
if (lauf) lauf.scrollTop = lauf.scrollHeight;
|
||||||
|
|
||||||
|
var eingabe = document.getElementById("po-pf-text");
|
||||||
|
var feld = document.getElementById("po-pf-feld");
|
||||||
|
var knopf = document.getElementById("po-pf-senden");
|
||||||
|
|
||||||
|
knopf.addEventListener("click", async function () {
|
||||||
|
var inhalt = eingabe.value.trim();
|
||||||
|
feld.classList.remove("wd-feld--fehler");
|
||||||
|
if (!inhalt) {
|
||||||
|
feld.classList.add("wd-feld--fehler");
|
||||||
|
feld.querySelector(".wd-feld-fehler").textContent = window.WD.t("po_postfach_leer_fehler");
|
||||||
|
eingabe.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var wahl = document.getElementById("po-pf-projekt");
|
||||||
|
window.WD.knopfSperren(knopf, true, "…");
|
||||||
|
try {
|
||||||
|
await api("/webdesign/portal/postfach", {
|
||||||
|
method: "POST",
|
||||||
|
body: { text: inhalt, projektId: wahl ? wahl.value : null },
|
||||||
|
});
|
||||||
|
window.WD.meldung(meldung, window.WD.t("po_postfach_gesendet"), "ok");
|
||||||
|
await ladePostfach();
|
||||||
|
} catch (fehler) {
|
||||||
|
if (behandleFehler(fehler)) return;
|
||||||
|
window.WD.knopfSperren(knopf, false);
|
||||||
|
window.WD.meldung(meldung, fehler.message, "fehler");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var aktuellesProjekt = null;
|
var aktuellesProjekt = null;
|
||||||
|
|
||||||
document.getElementById("po-zurueck").addEventListener("click", function () {
|
document.getElementById("po-zurueck").addEventListener("click", function () {
|
||||||
@@ -287,7 +521,21 @@
|
|||||||
ziel.innerHTML = '<div class="po-laedt">' + s(window.WD.t("laedt")) + "</div>";
|
ziel.innerHTML = '<div class="po-laedt">' + s(window.WD.t("laedt")) + "</div>";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var d = await api("/webdesign/portal/projekt/" + encodeURIComponent(id));
|
/* Beides gleichzeitig anfordern statt nacheinander. Nacheinander
|
||||||
|
waere die Wartezeit die Summe beider Anfragen — bei einer
|
||||||
|
Mobilfunkverbindung deutlich spuerbar.
|
||||||
|
|
||||||
|
Die Aufgabenliste darf dabei fehlschlagen, ohne die ganze Seite
|
||||||
|
mitzureissen: Sie ist eine Ergaenzung, keine Voraussetzung. Wer
|
||||||
|
wegen einer leeren Liste seine Zahlungen und Dateien nicht mehr
|
||||||
|
saehe, waere schlechter dran als vorher. */
|
||||||
|
var beides = await Promise.all([
|
||||||
|
api("/webdesign/portal/projekt/" + encodeURIComponent(id)),
|
||||||
|
api("/webdesign/portal/projekt/" + encodeURIComponent(id) + "/aufgaben")
|
||||||
|
.catch(function () { return { aufgaben: [], fortschritt: null }; }),
|
||||||
|
]);
|
||||||
|
var d = beides[0];
|
||||||
|
var aufg = beides[1];
|
||||||
var p = d.projekt;
|
var p = d.projekt;
|
||||||
var html = "";
|
var html = "";
|
||||||
|
|
||||||
@@ -302,6 +550,12 @@
|
|||||||
: "") +
|
: "") +
|
||||||
"</div>";
|
"</div>";
|
||||||
|
|
||||||
|
/* Die Aufgabenliste steht GANZ OBEN, direkt nach dem Projektkopf.
|
||||||
|
Sie beantwortet die Frage, mit der man das Projekt oeffnet
|
||||||
|
("wie weit ist es?"). Zahlungen und Dateien sind wichtig, aber
|
||||||
|
sie sind nicht der Grund fuer den Klick. */
|
||||||
|
html += aufgabenKarte(aufg);
|
||||||
|
|
||||||
/* --- Zahlungen --- */
|
/* --- Zahlungen --- */
|
||||||
html += '<div class="wd-karte" style="margin-bottom:1.2rem"><h3>' + s(window.WD.t("po_zahlungen")) + "</h3>";
|
html += '<div class="wd-karte" style="margin-bottom:1.2rem"><h3>' + s(window.WD.t("po_zahlungen")) + "</h3>";
|
||||||
if (!d.zahlungen.length) {
|
if (!d.zahlungen.length) {
|
||||||
@@ -420,6 +674,26 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Eigene Punkte abhaken. Der Punkt erscheint in der Liste zweimal
|
||||||
|
(oben unter "Das brauche ich noch von dir" und unten im
|
||||||
|
Abschnitt), deshalb wird nach dem Umschalten die ganze Ansicht
|
||||||
|
neu aufgebaut — sonst stuenden beide Fassungen widerspruechlich
|
||||||
|
nebeneinander. */
|
||||||
|
ziel.querySelectorAll("[data-haken]").forEach(function (b) {
|
||||||
|
b.addEventListener("click", async function () {
|
||||||
|
window.WD.knopfSperren(b, true, "…");
|
||||||
|
try {
|
||||||
|
await api("/webdesign/portal/aufgabe/" + encodeURIComponent(b.dataset.haken) + "/erledigt",
|
||||||
|
{ method: "POST", body: {} });
|
||||||
|
ladeProjekt(id);
|
||||||
|
} catch (f) {
|
||||||
|
if (behandleFehler(f)) return;
|
||||||
|
window.WD.knopfSperren(b, false);
|
||||||
|
window.WD.meldung(meldung, f.message, "fehler");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
ziel.querySelector("#po-idee-senden").addEventListener("click", async function (e) {
|
ziel.querySelector("#po-idee-senden").addEventListener("click", async function (e) {
|
||||||
var titel = ziel.querySelector("#po-idee-titel").value.trim();
|
var titel = ziel.querySelector("#po-idee-titel").value.trim();
|
||||||
var beschreibung = ziel.querySelector("#po-idee-text").value.trim();
|
var beschreibung = ziel.querySelector("#po-idee-text").value.trim();
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
/* Prüft die Aufgabenliste und das Postfach im Kundenportal.
|
||||||
|
|
||||||
|
Wichtig sind hier drei Dinge, die man leicht falsch baut:
|
||||||
|
|
||||||
|
1. Der Kunde darf NUR seine eigenen Punkte anklicken. Ein Knopf, der
|
||||||
|
nichts tut, lässt die Seite kaputt wirken.
|
||||||
|
2. Offene Kundenpunkte müssen VOR der langen Liste stehen — sonst
|
||||||
|
liest man die Liste als Bericht und überliest seinen Teil.
|
||||||
|
3. Alle fünf Sprachen müssen vollständig sein. Ein fehlender
|
||||||
|
Schlüssel fällt sonst erst einem französischen Kunden auf.
|
||||||
|
*/
|
||||||
|
import { chromium } from "playwright";
|
||||||
|
import { createServer } from "node:http";
|
||||||
|
import { readFileSync, existsSync } from "node:fs";
|
||||||
|
import { join, extname } from "node:path";
|
||||||
|
|
||||||
|
const W = process.cwd();
|
||||||
|
const T = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript",
|
||||||
|
".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml",
|
||||||
|
".webp": "image/webp", ".ico": "image/x-icon", ".json": "application/json" };
|
||||||
|
const srv = createServer((q, r) => {
|
||||||
|
const p = join(W, decodeURIComponent(q.url.split("?")[0]));
|
||||||
|
if (existsSync(p) && !p.endsWith("/")) {
|
||||||
|
r.writeHead(200, { "content-type": T[extname(p)] || "text/plain" });
|
||||||
|
return r.end(readFileSync(p));
|
||||||
|
}
|
||||||
|
r.writeHead(404).end("");
|
||||||
|
});
|
||||||
|
await new Promise((r) => srv.listen(4326, r));
|
||||||
|
|
||||||
|
const AUFGABEN = [
|
||||||
|
{ id: "a1", titel: "Briefing-Gespräch führen", beschreibung: "Ziel und Umfang klären.",
|
||||||
|
kategorie: "start", status: "erledigt", wer_dran: "dogfather", nicht_enthalten: 0, hinweis: null },
|
||||||
|
{ id: "a2", titel: "Texte und Bilder zusammenstellen", beschreibung: "Lieber früh als vollständig.",
|
||||||
|
kategorie: "start", status: "offen", wer_dran: "kunde", nicht_enthalten: 0, hinweis: null },
|
||||||
|
{ id: "a3", titel: "Entwurf der Startseite", beschreibung: null,
|
||||||
|
kategorie: "gestaltung", status: "erledigt", wer_dran: "dogfather", nicht_enthalten: 0, hinweis: null },
|
||||||
|
{ id: "a4", titel: "Entwurf ansehen und Rückmeldung geben", beschreibung: null,
|
||||||
|
kategorie: "gestaltung", status: "offen", wer_dran: "kunde", nicht_enthalten: 0, hinweis: null },
|
||||||
|
{ id: "a5", titel: "Alle Unterseiten bauen", beschreibung: null,
|
||||||
|
kategorie: "umsetzung", status: "in_arbeit", wer_dran: "dogfather", nicht_enthalten: 0, hinweis: null },
|
||||||
|
{ id: "a6", titel: "Mehrsprachigkeit", beschreibung: null,
|
||||||
|
kategorie: "umsetzung", status: "offen", wer_dran: "dogfather", nicht_enthalten: 1,
|
||||||
|
hinweis: "Auf Wunsch als Zusatz." },
|
||||||
|
{ id: "a7", titel: "Seite live schalten", beschreibung: null,
|
||||||
|
kategorie: "abschluss", status: "offen", wer_dran: "dogfather", nicht_enthalten: 0, hinweis: null },
|
||||||
|
];
|
||||||
|
|
||||||
|
const FORTSCHRITT = { gesamt: 6, erledigt: 2, inArbeit: 1, beimKunden: 2, prozent: 33,
|
||||||
|
jeKategorie: { start: { gesamt: 2, erledigt: 1 }, gestaltung: { gesamt: 2, erledigt: 1 },
|
||||||
|
umsetzung: { gesamt: 1, erledigt: 0 }, abschluss: { gesamt: 1, erledigt: 0 } } };
|
||||||
|
|
||||||
|
const PROJEKT = { id: "p1", nummer: "P-0001", titel: "Website für Musterbau",
|
||||||
|
status: "design", wartet_auf: "kunde", naechster_schritt: "Deine Rückmeldung zum Entwurf." };
|
||||||
|
|
||||||
|
const ergebnisse = [];
|
||||||
|
function pruefe(name, ok, zusatz) {
|
||||||
|
ergebnisse.push({ name, ok });
|
||||||
|
console.log((ok ? " ok " : " FEHL ") + name + (zusatz ? " -> " + zusatz : ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
|
||||||
|
/* ---- 1. Vollständigkeit aller fünf Sprachen ---- */
|
||||||
|
{
|
||||||
|
const p = await browser.newPage();
|
||||||
|
await p.goto("http://127.0.0.1:4326/webdesign/portal.html");
|
||||||
|
await p.waitForTimeout(600);
|
||||||
|
const luecken = await p.evaluate(() => {
|
||||||
|
const w = window.I18N_WD || {};
|
||||||
|
const sprachen = ["de", "de-CH", "en", "fr", "pt"];
|
||||||
|
const fehlt = [];
|
||||||
|
for (const [k, v] of Object.entries(w)) {
|
||||||
|
for (const sp of sprachen) {
|
||||||
|
if (!v[sp] || !String(v[sp]).trim()) fehlt.push(k + "/" + sp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { fehlt, anzahl: Object.keys(w).length };
|
||||||
|
});
|
||||||
|
console.log("\n=== Sprachen ===");
|
||||||
|
pruefe("alle Texte in allen fünf Sprachen vorhanden", luecken.fehlt.length === 0,
|
||||||
|
luecken.fehlt.slice(0, 6).join(", "));
|
||||||
|
console.log(" (" + luecken.anzahl + " Texte geprüft)");
|
||||||
|
await p.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [wo, breite, hoehe] of [["Computer", 1280, 950], ["Handy", 390, 844]]) {
|
||||||
|
const seite = await browser.newPage({ viewport: { width: breite, height: hoehe } });
|
||||||
|
const fehlerImProtokoll = [];
|
||||||
|
seite.on("pageerror", (e) => fehlerImProtokoll.push(e.message));
|
||||||
|
|
||||||
|
let abgehakt = null;
|
||||||
|
// Nur die API abfangen, nicht die Seite selbst.
|
||||||
|
await seite.route("**/webdesign/portal/**", (route) => {
|
||||||
|
/* Auf den PFAD prüfen, nicht auf die ganze Adresse.
|
||||||
|
|
||||||
|
Der API-Server heisst "postfach.dogfather-universe.com" -- ein
|
||||||
|
url.includes("/postfach") trifft also den HOSTNAMEN und damit
|
||||||
|
JEDE Anfrage. Beim ersten Lauf bekam deshalb auch die Übersicht
|
||||||
|
die Postfach-Antwort, und der Fehler sah aus wie ein Fehler im
|
||||||
|
Portal ("Cannot read properties of undefined"). Er lag im Test. */
|
||||||
|
const u = new URL(route.request().url()).pathname;
|
||||||
|
if (u.includes("/projekt/p1/aufgaben")) {
|
||||||
|
return route.fulfill({ json: { ok: true, projekt: PROJEKT, aufgaben: AUFGABEN, fortschritt: FORTSCHRITT } });
|
||||||
|
}
|
||||||
|
if (u.includes("/aufgabe/") && u.includes("/erledigt")) {
|
||||||
|
abgehakt = u.split("/aufgabe/")[1].split("/")[0];
|
||||||
|
return route.fulfill({ json: { ok: true, status: "erledigt", fortschritt: FORTSCHRITT } });
|
||||||
|
}
|
||||||
|
if (u.includes("/projekt/p1")) {
|
||||||
|
return route.fulfill({ json: { ok: true, projekt: PROJEKT,
|
||||||
|
zahlungen: [], dateien: [], aenderungen: [], nachrichten: [] } });
|
||||||
|
}
|
||||||
|
if (u.includes("/postfach")) {
|
||||||
|
if (route.request().method() === "POST") return route.fulfill({ json: { ok: true, id: "x" } });
|
||||||
|
return route.fulfill({ json: { ok: true, projekte: [{ id: "p1", titel: "Website für Musterbau" }],
|
||||||
|
nachrichten: [
|
||||||
|
{ id: "m1", autor: "kunde", text: "Kurze Frage zur Domain.", projekt_titel: null,
|
||||||
|
erstellt_am: new Date(Date.now() - 7200000).toISOString() },
|
||||||
|
{ id: "m2", autor: "dogfather", text: "Die läuft schon auf dich, alles gut.",
|
||||||
|
projekt_titel: null, erstellt_am: new Date(Date.now() - 3600000).toISOString() },
|
||||||
|
] } });
|
||||||
|
}
|
||||||
|
if (u.includes("/uebersicht")) {
|
||||||
|
return route.fulfill({ json: { ok: true, kunde: { name: "Anna" },
|
||||||
|
projekte: [PROJEKT], offeneZahlungen: [], abos: [] } });
|
||||||
|
}
|
||||||
|
return route.fulfill({ json: { ok: true } });
|
||||||
|
});
|
||||||
|
await seite.addInitScript(() => {
|
||||||
|
sessionStorage.setItem("wd-portal-token", "t");
|
||||||
|
sessionStorage.setItem("wd-sitzung-aktiv", "1");
|
||||||
|
});
|
||||||
|
|
||||||
|
await seite.goto("http://127.0.0.1:4326/webdesign/portal.html");
|
||||||
|
await seite.waitForTimeout(1000);
|
||||||
|
console.log("\n=== " + wo + " ===");
|
||||||
|
|
||||||
|
// ---- Postfach auf der Übersicht ----
|
||||||
|
pruefe(wo + ": Postfach steht auf der Übersicht", await seite.locator("#po-pf-senden").count() === 1);
|
||||||
|
pruefe(wo + ": bisherige Nachrichten sind da", await seite.locator("#po-postfach .po-nachricht").count() === 2);
|
||||||
|
|
||||||
|
// Leere Nachricht muss abgelehnt werden, nicht stillschweigend gesendet
|
||||||
|
await seite.click("#po-pf-senden");
|
||||||
|
await seite.waitForTimeout(300);
|
||||||
|
pruefe(wo + ": leere Nachricht wird abgelehnt",
|
||||||
|
await seite.locator("#po-pf-feld.wd-feld--fehler").count() === 1);
|
||||||
|
|
||||||
|
await seite.screenshot({ path: `pruef-portal-postfach-${wo.toLowerCase()}.png`, fullPage: true });
|
||||||
|
|
||||||
|
// ---- Projekt öffnen ----
|
||||||
|
await seite.click("[data-projekt]");
|
||||||
|
await seite.waitForTimeout(900);
|
||||||
|
|
||||||
|
const m = await seite.evaluate(() => {
|
||||||
|
const gruppen = Array.from(document.querySelectorAll(".po-aufg-gruppe"));
|
||||||
|
const farben = gruppen.map((g) => getComputedStyle(g.querySelector(".po-aufg-kopf")).color);
|
||||||
|
const dubist = document.querySelector(".po-dubist");
|
||||||
|
const ersteGruppe = gruppen[0];
|
||||||
|
const knoepfe = document.querySelectorAll("button.po-aufg-haken");
|
||||||
|
const feste = document.querySelectorAll(".po-aufg-haken--fest");
|
||||||
|
const aufgKarte = document.querySelector(".po-aufg-stand");
|
||||||
|
const zahlKarte = Array.from(document.querySelectorAll(".wd-karte h3"))
|
||||||
|
.find((h) => h.textContent.includes("Zahlung"));
|
||||||
|
|
||||||
|
// Anordnung einer Zeile pruefen (Lehre aus der Verwaltung)
|
||||||
|
const z = document.querySelector(".po-aufg-gruppe .po-aufg");
|
||||||
|
const k = z?.querySelector(".po-aufg-haken")?.getBoundingClientRect();
|
||||||
|
const t = z?.querySelector(".po-aufg-titel")?.getBoundingClientRect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
gruppen: gruppen.map((g) => g.dataset.k),
|
||||||
|
farbenVerschieden: new Set(farben).size,
|
||||||
|
dubistDa: !!dubist,
|
||||||
|
dubistVorListe: dubist && ersteGruppe
|
||||||
|
? dubist.getBoundingClientRect().top < ersteGruppe.getBoundingClientRect().top : false,
|
||||||
|
knoepfe: knoepfe.length,
|
||||||
|
feste: feste.length,
|
||||||
|
standDa: !!aufgKarte,
|
||||||
|
aufgabenVorZahlungen: aufgKarte && zahlKarte
|
||||||
|
? aufgKarte.getBoundingClientRect().top < zahlKarte.getBoundingClientRect().top : false,
|
||||||
|
kaestchenVorTitel: k && t ? k.right <= t.left + 1 : false,
|
||||||
|
balkenTeile: document.querySelectorAll(".po-fs-teil").length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
pruefe(wo + ": alle vier Abschnitte erscheinen",
|
||||||
|
m.gruppen.join(",") === "start,gestaltung,umsetzung,abschluss", m.gruppen.join(","));
|
||||||
|
pruefe(wo + ": jeder Abschnitt hat eine eigene Farbe", m.farbenVerschieden === 4, "n=" + m.farbenVerschieden);
|
||||||
|
pruefe(wo + ": Fortschrittsbalken hat vier Teile", m.balkenTeile === 4);
|
||||||
|
pruefe(wo + ": „Das brauche ich von dir“ ist da", m.dubistDa);
|
||||||
|
pruefe(wo + ": … und steht VOR der langen Liste", m.dubistVorListe);
|
||||||
|
pruefe(wo + ": Aufgabenliste steht vor den Zahlungen", m.aufgabenVorZahlungen);
|
||||||
|
pruefe(wo + ": grosse Zahl statt Prozent", m.standDa);
|
||||||
|
pruefe(wo + ": Kästchen steht links vom Titel", m.kaestchenVorTitel);
|
||||||
|
|
||||||
|
/* Der Kunde hat 2 offene eigene Punkte. Sie erscheinen zweimal (oben
|
||||||
|
und in der Gruppe) -> 4 Knöpfe. Alle übrigen Kästchen sind fest. */
|
||||||
|
pruefe(wo + ": nur eigene Punkte sind anklickbar", m.knoepfe === 4, "Knöpfe=" + m.knoepfe + ", fest=" + m.feste);
|
||||||
|
pruefe(wo + ": fremde Punkte haben KEINEN Knopf", m.feste === 5, "n=" + m.feste);
|
||||||
|
|
||||||
|
// Abhaken loest den richtigen Aufruf aus
|
||||||
|
await seite.locator("button.po-aufg-haken").first().click();
|
||||||
|
await seite.waitForTimeout(700);
|
||||||
|
pruefe(wo + ": Abhaken meldet den richtigen Punkt", abgehakt === "a2", "gemeldet: " + abgehakt);
|
||||||
|
|
||||||
|
if (breite < 900) {
|
||||||
|
const zuKlein = await seite.evaluate(() =>
|
||||||
|
Array.from(document.querySelectorAll("button.po-aufg-haken"))
|
||||||
|
.filter((b) => { const r = b.getBoundingClientRect(); return r.width < 24 || r.height < 24; }).length);
|
||||||
|
pruefe(wo + ": Kästchen sind gross genug zum Antippen", zuKlein === 0, "zu klein: " + zuKlein);
|
||||||
|
}
|
||||||
|
|
||||||
|
pruefe(wo + ": keine Fehler im Protokoll", fehlerImProtokoll.length === 0, fehlerImProtokoll.join(" | "));
|
||||||
|
|
||||||
|
await seite.screenshot({ path: `pruef-portal-aufgaben-${wo.toLowerCase()}.png`, fullPage: true });
|
||||||
|
await seite.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
srv.close();
|
||||||
|
const fehler = ergebnisse.filter((e) => !e.ok).length;
|
||||||
|
console.log(`\n===== ${ergebnisse.length - fehler} bestanden, ${fehler} fehlgeschlagen =====`);
|
||||||
|
process.exitCode = fehler ? 1 : 0;
|
||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
/* Sucht doppelt kodierte Zeichen auf ALLEN Webdesign-Seiten in ALLEN
|
||||||
|
fünf Sprachen.
|
||||||
|
|
||||||
|
Anlass: Im Portal stand sichtbar "Ideen &amp; Änderungswünsche".
|
||||||
|
Ursache ist eine Falle, die sich leicht wiederholt — es gibt zwei
|
||||||
|
Wege, wie ein Text in die Seite kommt:
|
||||||
|
|
||||||
|
data-i18n="schluessel" -> wird als HTML eingesetzt.
|
||||||
|
Dort ist "&" RICHTIG.
|
||||||
|
s(window.WD.t("schluessel")) -> wird abgesichert eingesetzt.
|
||||||
|
Dort wird "&" ein zweites Mal
|
||||||
|
kodiert und erscheint wörtlich.
|
||||||
|
|
||||||
|
Derselbe Textbaustein kann also je nach Verwendungsort richtig oder
|
||||||
|
falsch sein. Das kann man nicht zuverlässig im Kopf behalten —
|
||||||
|
deshalb prüft es der Browser: Was am Ende auf dem Bildschirm steht,
|
||||||
|
darf keine Kodierungsreste enthalten.
|
||||||
|
*/
|
||||||
|
import { chromium } from "playwright";
|
||||||
|
import { createServer } from "node:http";
|
||||||
|
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
||||||
|
import { join, extname } from "node:path";
|
||||||
|
|
||||||
|
const W = process.cwd();
|
||||||
|
const T = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript",
|
||||||
|
".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml",
|
||||||
|
".webp": "image/webp", ".ico": "image/x-icon", ".json": "application/json" };
|
||||||
|
const srv = createServer((q, r) => {
|
||||||
|
const p = join(W, decodeURIComponent(q.url.split("?")[0]));
|
||||||
|
if (existsSync(p) && !p.endsWith("/")) {
|
||||||
|
r.writeHead(200, { "content-type": T[extname(p)] || "text/plain" });
|
||||||
|
return r.end(readFileSync(p));
|
||||||
|
}
|
||||||
|
r.writeHead(404).end("");
|
||||||
|
});
|
||||||
|
await new Promise((r) => srv.listen(4327, r));
|
||||||
|
|
||||||
|
const SEITEN = readdirSync(join(W, "webdesign"))
|
||||||
|
.filter((f) => f.endsWith(".html"));
|
||||||
|
const SPRACHEN = ["de", "de-CH", "en", "fr", "pt"];
|
||||||
|
/* Reste, die eine doppelte Kodierung hinterlässt. Sie dürfen im
|
||||||
|
sichtbaren Text nie vorkommen. */
|
||||||
|
const RESTE = ["&", "<", ">", """, "'", " "];
|
||||||
|
|
||||||
|
/* =====================================================================
|
||||||
|
TEIL 1 — Prüfung am QUELLTEXT
|
||||||
|
|
||||||
|
Die Browserprüfung weiter unten sieht nur, was gerade auf dem
|
||||||
|
Bildschirm steht. Grosse Teile des Portals und der Verwaltung
|
||||||
|
erscheinen aber erst NACH einer Anmeldung — genau dort sass der
|
||||||
|
Fehler, den es zu finden galt ("Ideen &amp; Änderungswünsche").
|
||||||
|
Gegengeprüft: Die Browserprüfung allein hätte ihn NICHT gefunden.
|
||||||
|
|
||||||
|
Diese Prüfung braucht keine Anzeige. Sie fragt nur:
|
||||||
|
Gibt es einen Textbaustein mit "&", der irgendwo durch die
|
||||||
|
Absicherung s(...) geschickt wird? Dann ist er dort doppelt kodiert.
|
||||||
|
===================================================================== */
|
||||||
|
function quelltextPruefen() {
|
||||||
|
const i18nDateien = readdirSync(join(W, "assets", "js"))
|
||||||
|
.filter((f) => f.startsWith("i18n-wd-") && f.endsWith(".js"));
|
||||||
|
|
||||||
|
// Alle Schlüssel einsammeln, deren Text ein "&" enthält.
|
||||||
|
const verdaechtig = new Map(); // schluessel -> datei
|
||||||
|
for (const datei of i18nDateien) {
|
||||||
|
const inhalt = readFileSync(join(W, "assets", "js", datei), "utf-8");
|
||||||
|
/* Der Block eines Schlüssels endet dort, wo seine geschweifte
|
||||||
|
Klammer wieder zugeht — nicht erst beim nächsten Schlüssel.
|
||||||
|
|
||||||
|
Der Unterschied ist nicht theoretisch: Zwischen zwei Schlüsseln
|
||||||
|
stehen Kommentare, und einer davon lautet "Ideen & Änderungs-
|
||||||
|
wünsche". Mit der Regel "bis zum nächsten Schlüssel" hat der
|
||||||
|
VORHERIGE Schlüssel (po_senden, ein schlichtes "Senden") diesen
|
||||||
|
Kommentar mitgeschluckt und wurde fälschlich gemeldet. Ein
|
||||||
|
Fehlalarm im Prüfwerkzeug ist fast so schädlich wie ein
|
||||||
|
übersehener Fehler — man gewöhnt sich daran, ihn wegzusehen. */
|
||||||
|
const zeilen = inhalt.split("\n");
|
||||||
|
let aktuell = null;
|
||||||
|
let text = "";
|
||||||
|
let tiefe = 0;
|
||||||
|
const abschliessen = () => {
|
||||||
|
if (aktuell && text.includes("&")) verdaechtig.set(aktuell, datei);
|
||||||
|
aktuell = null; text = ""; tiefe = 0;
|
||||||
|
};
|
||||||
|
for (const z of zeilen) {
|
||||||
|
if (!aktuell) {
|
||||||
|
const m = z.match(/^ ([a-zA-Z0-9_]+):\s*\{/);
|
||||||
|
if (!m) continue;
|
||||||
|
aktuell = m[1]; text = z;
|
||||||
|
tiefe = (z.match(/\{/g) || []).length - (z.match(/\}/g) || []).length;
|
||||||
|
if (tiefe <= 0) abschliessen();
|
||||||
|
} else {
|
||||||
|
text += "\n" + z;
|
||||||
|
tiefe += (z.match(/\{/g) || []).length - (z.match(/\}/g) || []).length;
|
||||||
|
if (tiefe <= 0) abschliessen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
abschliessen();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wo werden sie abgesichert eingesetzt?
|
||||||
|
const quellen = [
|
||||||
|
...readdirSync(join(W, "assets", "js")).filter((f) => f.startsWith("wd-") && f.endsWith(".js"))
|
||||||
|
.map((f) => ["assets/js/" + f, readFileSync(join(W, "assets", "js", f), "utf-8")]),
|
||||||
|
...SEITEN.map((f) => ["webdesign/" + f, readFileSync(join(W, "webdesign", f), "utf-8")]),
|
||||||
|
];
|
||||||
|
|
||||||
|
const funde = [];
|
||||||
|
for (const [schluessel, datei] of verdaechtig) {
|
||||||
|
// s(window.WD.t("key")) oder s(t("key"))
|
||||||
|
const muster = new RegExp(
|
||||||
|
"\\bs\\(\\s*(?:window\\.WD\\.)?t\\(\\s*[\"']" + schluessel + "[\"']", "");
|
||||||
|
for (const [name, inhalt] of quellen) {
|
||||||
|
if (muster.test(inhalt)) funde.push({ schluessel, datei, benutztIn: name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Quelltext: ${verdaechtig.size} Textbausteine enthalten "&".`);
|
||||||
|
if (funde.length) {
|
||||||
|
for (const f of funde) {
|
||||||
|
console.log(` FEHL ${f.schluessel} (${f.datei}) wird in ${f.benutztIn} abgesichert eingesetzt`);
|
||||||
|
console.log(` -> dort muss ein echtes "&" stehen, sonst erscheint "&" wörtlich.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(" ok keiner davon wird durch die Absicherung geschickt.\n");
|
||||||
|
}
|
||||||
|
return funde.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
let quellFehler = quelltextPruefen();
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const seite = await browser.newPage({ viewport: { width: 1280, height: 900 } });
|
||||||
|
// Die API gibt es hier nicht -- leere, gültige Antworten genügen.
|
||||||
|
await seite.route("**/webdesign/admin/**", (r) => r.fulfill({ json: { ok: true, anfragen: [], kunden: [], projekte: [], verlaeufe: [] } }));
|
||||||
|
await seite.route("**/webdesign/portal/**", (r) => r.fulfill({ json: { ok: true } }));
|
||||||
|
await seite.route("**/webdesign/api-ausweis", (r) => r.fulfill({ json: {} }));
|
||||||
|
await seite.addInitScript(() => { sessionStorage.setItem("wd-sitzung-aktiv", "1"); });
|
||||||
|
|
||||||
|
let treffer = 0;
|
||||||
|
let geprueft = 0;
|
||||||
|
|
||||||
|
for (const datei of SEITEN) {
|
||||||
|
for (const sprache of SPRACHEN) {
|
||||||
|
await seite.addInitScript(`localStorage.setItem("dogi-sprache", ${JSON.stringify(sprache)})`);
|
||||||
|
await seite.goto("http://127.0.0.1:4327/webdesign/" + datei);
|
||||||
|
await seite.waitForTimeout(320);
|
||||||
|
geprueft++;
|
||||||
|
|
||||||
|
const gefunden = await seite.evaluate((reste) => {
|
||||||
|
const raus = [];
|
||||||
|
const lauf = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
||||||
|
let n;
|
||||||
|
while ((n = lauf.nextNode())) {
|
||||||
|
const eltern = n.parentElement;
|
||||||
|
if (!eltern || eltern.closest("script, style, code, pre")) continue;
|
||||||
|
const t = n.nodeValue;
|
||||||
|
for (const r of reste) {
|
||||||
|
if (t.includes(r)) {
|
||||||
|
raus.push({ rest: r, text: t.trim().slice(0, 70), wo: eltern.tagName.toLowerCase() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return raus;
|
||||||
|
}, RESTE);
|
||||||
|
|
||||||
|
if (gefunden.length) {
|
||||||
|
treffer += gefunden.length;
|
||||||
|
for (const g of gefunden) {
|
||||||
|
console.log(` FEHL ${datei} [${sprache}] <${g.wo}> ${g.rest} in: "${g.text}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
srv.close();
|
||||||
|
console.log(`\n${geprueft} Seiten-Sprach-Kombinationen geprüft (${SEITEN.length} Seiten x ${SPRACHEN.length} Sprachen).`);
|
||||||
|
const gesamt = treffer + quellFehler;
|
||||||
|
console.log(gesamt === 0
|
||||||
|
? "===== keine doppelt kodierten Zeichen gefunden (Quelltext + Anzeige) ====="
|
||||||
|
: `===== ${gesamt} Fundstellen (${quellFehler} im Quelltext, ${treffer} in der Anzeige) =====`);
|
||||||
|
process.exitCode = gesamt ? 1 : 0;
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
|
||||||
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
|
||||||
<style>
|
<style>
|
||||||
/* Zeitstrahl: die Linie verbindet die Nummernkreise. Sie liegt hinter
|
/* Zeitstrahl: die Linie verbindet die Nummernkreise. Sie liegt hinter
|
||||||
den Kreisen (z-index) und endet beim letzten Schritt, damit sie nicht
|
den Kreisen (z-index) und endet beim letzten Schritt, damit sie nicht
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
|
|
||||||
<div id="wd-fuss"></div>
|
<div id="wd-fuss"></div>
|
||||||
|
|
||||||
<script src="/assets/js/i18n-wd-ablauf.js?v=20260822wd4"></script>
|
<script src="/assets/js/i18n-wd-ablauf.js?v=20260823wd5"></script>
|
||||||
<script src="/assets/js/wd-core.js?v=20260822wd4"></script>
|
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
|
||||||
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
|
||||||
<style>
|
<style>
|
||||||
/* ---------- Schrittanzeige ----------
|
/* ---------- Schrittanzeige ----------
|
||||||
Zeigt alle vier Schritte gleichzeitig statt nur "3 von 4". Man sieht
|
Zeigt alle vier Schritte gleichzeitig statt nur "3 von 4". Man sieht
|
||||||
@@ -418,8 +418,8 @@
|
|||||||
|
|
||||||
<div id="wd-fuss"></div>
|
<div id="wd-fuss"></div>
|
||||||
|
|
||||||
<script src="/assets/js/i18n-wd-anfrage.js?v=20260822wd4"></script>
|
<script src="/assets/js/i18n-wd-anfrage.js?v=20260823wd5"></script>
|
||||||
<script src="/assets/js/wd-core.js?v=20260822wd4"></script>
|
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
|
||||||
<script src="/assets/js/wd-anfrage.js?v=20260822wd4"></script>
|
<script src="/assets/js/wd-anfrage.js?v=20260823wd5"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+4
-4
@@ -13,8 +13,8 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
|
||||||
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
|
||||||
<style>
|
<style>
|
||||||
.fq-thema { scroll-margin-top: 90px; }
|
.fq-thema { scroll-margin-top: 90px; }
|
||||||
.fq-thema + .fq-thema { margin-top: clamp(2rem, 4vw, 3rem); }
|
.fq-thema + .fq-thema { margin-top: clamp(2rem, 4vw, 3rem); }
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
|
|
||||||
<div id="wd-fuss"></div>
|
<div id="wd-fuss"></div>
|
||||||
|
|
||||||
<script src="/assets/js/i18n-wd-faq.js?v=20260822wd4"></script>
|
<script src="/assets/js/i18n-wd-faq.js?v=20260823wd5"></script>
|
||||||
<script src="/assets/js/wd-core.js?v=20260822wd4"></script>
|
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
|
||||||
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
|
||||||
<style>
|
<style>
|
||||||
/* ---------- Nur auf der Startseite gebraucht ---------- */
|
/* ---------- Nur auf der Startseite gebraucht ---------- */
|
||||||
|
|
||||||
@@ -407,8 +407,8 @@
|
|||||||
|
|
||||||
<div id="wd-fuss"></div>
|
<div id="wd-fuss"></div>
|
||||||
|
|
||||||
<script src="/assets/js/i18n-wd-index.js?v=20260822wd4"></script>
|
<script src="/assets/js/i18n-wd-index.js?v=20260823wd5"></script>
|
||||||
<script src="/assets/js/wd-core.js?v=20260822wd4"></script>
|
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
|
||||||
<script>
|
<script>
|
||||||
/* ---------------------------------------------------------------------
|
/* ---------------------------------------------------------------------
|
||||||
Marken-Intro (Masterplan S.3, Schritt 02: "Nach erfolgreichem Code
|
Marken-Intro (Masterplan S.3, Schritt 02: "Nach erfolgreichem Code
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
|
||||||
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
|
||||||
<style>
|
<style>
|
||||||
/* Sprungmarken.
|
/* Sprungmarken.
|
||||||
Frueher eine waagerecht wischbare Leiste. Rueckmeldung 22.08.2026:
|
Frueher eine waagerecht wischbare Leiste. Rueckmeldung 22.08.2026:
|
||||||
@@ -331,7 +331,7 @@
|
|||||||
|
|
||||||
<div id="wd-fuss"></div>
|
<div id="wd-fuss"></div>
|
||||||
|
|
||||||
<script src="/assets/js/i18n-wd-leistungen.js?v=20260822wd4"></script>
|
<script src="/assets/js/i18n-wd-leistungen.js?v=20260823wd5"></script>
|
||||||
<script src="/assets/js/wd-core.js?v=20260822wd4"></script>
|
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
|
||||||
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
|
||||||
<style>
|
<style>
|
||||||
.po-login { max-width: 440px; margin: clamp(1.5rem,6vh,4rem) auto; }
|
.po-login { max-width: 440px; margin: clamp(1.5rem,6vh,4rem) auto; }
|
||||||
|
|
||||||
@@ -134,8 +134,8 @@
|
|||||||
|
|
||||||
<div id="wd-fuss"></div>
|
<div id="wd-fuss"></div>
|
||||||
|
|
||||||
<script src="/assets/js/i18n-wd-portal.js?v=20260822wd4"></script>
|
<script src="/assets/js/i18n-wd-portal.js?v=20260823wd5"></script>
|
||||||
<script src="/assets/js/wd-core.js?v=20260822wd4"></script>
|
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
|
||||||
<script src="/assets/js/wd-portal.js?v=20260822wd4"></script>
|
<script src="/assets/js/wd-portal.js?v=20260823wd5"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
|
||||||
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
|
||||||
<style>
|
<style>
|
||||||
.pfo-projekt { overflow: hidden; padding: 0; margin-bottom: clamp(1.4rem, 3vw, 2.2rem); }
|
.pfo-projekt { overflow: hidden; padding: 0; margin-bottom: clamp(1.4rem, 3vw, 2.2rem); }
|
||||||
.pfo-bild {
|
.pfo-bild {
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
|
|
||||||
<div id="wd-fuss"></div>
|
<div id="wd-fuss"></div>
|
||||||
|
|
||||||
<script src="/assets/js/i18n-wd-portfolio.js?v=20260822wd4"></script>
|
<script src="/assets/js/i18n-wd-portfolio.js?v=20260823wd5"></script>
|
||||||
<script src="/assets/js/wd-core.js?v=20260822wd4"></script>
|
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
|
||||||
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
|
||||||
<style>
|
<style>
|
||||||
.pr-preis-gross { font-family: var(--font-head); font-weight: 800; color: var(--wd-blau); white-space: nowrap; }
|
.pr-preis-gross { font-family: var(--font-head); font-weight: 800; color: var(--wd-blau); white-space: nowrap; }
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
|
|
||||||
<div id="wd-fuss"></div>
|
<div id="wd-fuss"></div>
|
||||||
|
|
||||||
<script src="/assets/js/i18n-wd-preise.js?v=20260822wd4"></script>
|
<script src="/assets/js/i18n-wd-preise.js?v=20260823wd5"></script>
|
||||||
<script src="/assets/js/wd-core.js?v=20260822wd4"></script>
|
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@
|
|||||||
Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den
|
Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den
|
||||||
Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten
|
Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten
|
||||||
Stand fest. */
|
Stand fest. */
|
||||||
const CACHE_NAME = "dogfather-webdesign-v4";
|
const CACHE_NAME = "dogfather-webdesign-v5";
|
||||||
|
|
||||||
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
|
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
|
||||||
HTML-Datei in dieser Liste. */
|
HTML-Datei in dieser Liste. */
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
|
||||||
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
|
||||||
<style>
|
<style>
|
||||||
.ue-kopf { display: grid; grid-template-columns: minmax(0, .85fr) minmax(0, 1.15fr); gap: clamp(1.5rem, 4vw, 3rem); align-items: center; }
|
.ue-kopf { display: grid; grid-template-columns: minmax(0, .85fr) minmax(0, 1.15fr); gap: clamp(1.5rem, 4vw, 3rem); align-items: center; }
|
||||||
/* Auf dem Handy zuerst der Text, dann das Bild: sonst schiebt ein
|
/* Auf dem Handy zuerst der Text, dann das Bild: sonst schiebt ein
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
|
|
||||||
<div id="wd-fuss"></div>
|
<div id="wd-fuss"></div>
|
||||||
|
|
||||||
<script src="/assets/js/i18n-wd-ueber.js?v=20260822wd4"></script>
|
<script src="/assets/js/i18n-wd-ueber.js?v=20260823wd5"></script>
|
||||||
<script src="/assets/js/wd-core.js?v=20260822wd4"></script>
|
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
<link rel="apple-touch-icon" href="/assets/img/wd-app-180.png" />
|
<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-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-title" content="DF Verwaltung" />
|
<meta name="apple-mobile-web-app-title" content="DF Verwaltung" />
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
|
||||||
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
|
||||||
<style>
|
<style>
|
||||||
/* =====================================================================
|
/* =====================================================================
|
||||||
Verwaltungsbereich — bewusst NUR AUF DEUTSCH.
|
Verwaltungsbereich — bewusst NUR AUF DEUTSCH.
|
||||||
@@ -753,7 +753,7 @@
|
|||||||
<div class="vw-detail-blatt" id="vw-detail-blatt"></div>
|
<div class="vw-detail-blatt" id="vw-detail-blatt"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/assets/js/wd-core.js?v=20260822wd4"></script>
|
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
|
||||||
<script>
|
<script>
|
||||||
/* =====================================================================
|
/* =====================================================================
|
||||||
Verwaltung: Anfragen ansehen, Status ändern, Notizen, Archivieren.
|
Verwaltung: Anfragen ansehen, Status ändern, Notizen, Archivieren.
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd5" />
|
||||||
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260822wd4" />
|
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd5" />
|
||||||
<style>
|
<style>
|
||||||
/* =====================================================================
|
/* =====================================================================
|
||||||
Zugangswand — nur hier gebraucht, deshalb bewusst NICHT in
|
Zugangswand — nur hier gebraucht, deshalb bewusst NICHT in
|
||||||
@@ -177,8 +177,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="/assets/js/i18n-wd-zugang.js?v=20260822wd4"></script>
|
<script src="/assets/js/i18n-wd-zugang.js?v=20260823wd5"></script>
|
||||||
<script src="/assets/js/wd-core.js?v=20260822wd4"></script>
|
<script src="/assets/js/wd-core.js?v=20260823wd5"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|||||||
Reference in New Issue
Block a user