diff --git a/assets/js/i18n-wd-portal.js b/assets/js/i18n-wd-portal.js new file mode 100644 index 00000000..7503efce --- /dev/null +++ b/assets/js/i18n-wd-portal.js @@ -0,0 +1,189 @@ +/* ===================================================================== + i18n-wd-portal.js — Kundenportal (Masterplan S.12), fünf Sprachen. + + Anders als die Verwaltung ist das Portal mehrsprachig: hier landen + Kunden, und die können Franzosen oder Portugiesen sein. Die Sprache + folgt derselben Auswahl wie auf der öffentlichen Seite. + ===================================================================== */ +window.I18N_WD = { + seiten_titel: { + de: "Kundenportal — Dogfather Webdesign", + "de-CH": "Kundeportal — Dogfather Webdesign", + en: "Client portal — Dogfather Web Design", + fr: "Espace client — Dogfather Web Design", + pt: "Portal do cliente — Dogfather Web Design", + }, + + /* ---------- Anmeldung ---------- */ + po_login_h1: { de: "Dein Projektraum", "de-CH": "Din Projektruum", en: "Your project space", fr: "Ton espace projet", pt: "O teu espaço de projeto" }, + po_login_text: { + de: "Hier siehst du deinen Projektstand, alle Dateien und offene Punkte. Zugangsdaten bekommst du von Dogfather, sobald dein Projekt startet.", + "de-CH": "Do gsehsch din Projektstand, alli Datei und offni Pünkt. Zuegangsdate überchunnsch vom Dogfather, sobald dis Projekt startet.", + en: "Here you see your project status, all files and open points. You receive your access details from Dogfather once your project starts.", + fr: "Ici tu vois l'état de ton projet, tous les fichiers et les points ouverts. Tes accès te sont remis par Dogfather au démarrage du projet.", + pt: "Aqui vês o estado do teu projeto, todos os ficheiros e pontos em aberto. Recebes os teus acessos do Dogfather quando o projeto arrancar.", + }, + po_email: { de: "E-Mail", "de-CH": "E-Mail", en: "E-mail", fr: "E-mail", pt: "E-mail" }, + po_passwort: { de: "Passwort", "de-CH": "Passwort", en: "Password", fr: "Mot de passe", pt: "Palavra-passe" }, + po_anmelden: { de: "Anmelden", "de-CH": "Aamälde", en: "Sign in", fr: "Se connecter", pt: "Entrar" }, + po_anmelden_laeuft: { de: "Wird geprüft…", "de-CH": "Wird prüeft…", en: "Checking…", fr: "Vérification…", pt: "A verificar…" }, + po_abmelden: { de: "Abmelden", "de-CH": "Abmälde", en: "Sign out", fr: "Se déconnecter", pt: "Sair" }, + po_login_leer: { + de: "Bitte E-Mail und Passwort eingeben.", + "de-CH": "Bitte E-Mail und Passwort iigäh.", + en: "Please enter e-mail and password.", + fr: "Merci de saisir l'e-mail et le mot de passe.", + pt: "Introduz o e-mail e a palavra-passe.", + }, + po_kein_zugang: { + de: "Noch keinen Zugang? Der Projektraum wird freigeschaltet, sobald dein Projekt oder deine Betreuung beginnt.", + "de-CH": "No kein Zuegang? Dr Projektruum wird freigschalte, sobald dis Projekt oder dini Betreuig aafoht.", + en: "No access yet? The project space is unlocked once your project or support plan begins.", + fr: "Pas encore d'accès ? L'espace projet est activé dès le démarrage de ton projet ou de ton suivi.", + pt: "Ainda sem acesso? O espaço de projeto é ativado quando o teu projeto ou acompanhamento começar.", + }, + + /* ---------- Passwort setzen ---------- */ + po_setz_h1: { de: "Passwort festlegen", "de-CH": "Passwort festleggä", en: "Set your password", fr: "Définir ton mot de passe", pt: "Definir a palavra-passe" }, + po_setz_text: { + de: "Wähl ein Passwort für deinen Projektraum. Ein kurzer Satz ist sicherer und leichter zu merken als ein kryptisches Wort.", + "de-CH": "Wähl es Passwort für din Projektruum. En churze Satz isch sicherer und liechter z merke als es kryptischs Wort.", + en: "Choose a password for your project space. A short sentence is safer and easier to remember than a cryptic word.", + fr: "Choisis un mot de passe pour ton espace projet. Une courte phrase est plus sûre et plus facile à retenir qu'un mot cryptique.", + pt: "Escolhe uma palavra-passe para o teu espaço. Uma frase curta é mais segura e fácil de lembrar do que uma palavra críptica.", + }, + po_setz_knopf: { de: "Passwort speichern", "de-CH": "Passwort speichere", en: "Save password", fr: "Enregistrer le mot de passe", pt: "Guardar palavra-passe" }, + po_setz_fertig: { + de: "Passwort gespeichert. Du kannst dich jetzt anmelden.", + "de-CH": "Passwort gspeicheret. Du chasch dich jetzt aamälde.", + en: "Password saved. You can sign in now.", + fr: "Mot de passe enregistré. Tu peux te connecter.", + pt: "Palavra-passe guardada. Já podes entrar.", + }, + + /* ---------- Übersicht ---------- */ + po_hallo: { de: "Hallo", "de-CH": "Hoi", en: "Hello", fr: "Bonjour", pt: "Olá" }, + po_projekte: { de: "Deine Projekte", "de-CH": "Dini Projekt", en: "Your projects", fr: "Tes projets", pt: "Os teus projetos" }, + po_keine_projekte: { + de: "Hier erscheint dein Projekt, sobald es angelegt ist.", + "de-CH": "Do erschiint dis Projekt, sobald es aagleit isch.", + en: "Your project appears here as soon as it is set up.", + fr: "Ton projet apparaîtra ici dès sa création.", + pt: "O teu projeto aparece aqui assim que for criado.", + }, + po_betreuung: { de: "Laufende Betreuung", "de-CH": "Laufendi Betreuig", en: "Ongoing support", fr: "Suivi en cours", pt: "Acompanhamento em curso" }, + po_offene_zahlungen: { de: "Offene Zahlungen", "de-CH": "Offni Zahlige", en: "Open payments", fr: "Paiements en attente", pt: "Pagamentos em aberto" }, + po_jetzt_bezahlen: { de: "Jetzt bezahlen", "de-CH": "Jetzt zahle", en: "Pay now", fr: "Payer maintenant", pt: "Pagar agora" }, + po_naechste_zahlung: { de: "Nächste Zahlung", "de-CH": "Nöchsti Zahlig", en: "Next payment", fr: "Prochain paiement", pt: "Próximo pagamento" }, + + /* ---------- Projektstatus ---------- */ + po_status: { de: "Stand", "de-CH": "Stand", en: "Status", fr: "État", pt: "Estado" }, + po_st_briefing: { de: "Briefing", "de-CH": "Briefing", en: "Briefing", fr: "Briefing", pt: "Briefing" }, + po_st_angebot: { de: "Angebot", "de-CH": "Aagebot", en: "Quote", fr: "Devis", pt: "Orçamento" }, + po_st_design: { de: "Design", "de-CH": "Design", en: "Design", fr: "Design", pt: "Design" }, + po_st_entwicklung: { de: "Entwicklung", "de-CH": "Entwicklig", en: "Development", fr: "Développement", pt: "Desenvolvimento" }, + po_st_tests: { de: "Tests", "de-CH": "Teschts", en: "Testing", fr: "Tests", pt: "Testes" }, + po_st_abnahme: { de: "Abnahme", "de-CH": "Abnahm", en: "Acceptance", fr: "Validation", pt: "Aceitação" }, + po_st_uebergeben: { de: "Übergeben", "de-CH": "Übergäh", en: "Handed over", fr: "Remis", pt: "Entregue" }, + po_st_pausiert: { de: "Pausiert", "de-CH": "Pausiert", en: "Paused", fr: "En pause", pt: "Em pausa" }, + po_st_abgebrochen: { de: "Abgebrochen", "de-CH": "Abbroche", en: "Cancelled", fr: "Annulé", pt: "Cancelado" }, + + po_naechster_schritt: { de: "Nächster Schritt", "de-CH": "Nöchste Schritt", en: "Next step", fr: "Prochaine étape", pt: "Próximo passo" }, + po_wartet_kunde: { + de: "Wir warten auf dich", + "de-CH": "Mir warted uf dich", + en: "Waiting for you", + fr: "En attente de ta part", + pt: "À espera de ti", + }, + po_wartet_dogfather: { + de: "Dogfather ist dran", + "de-CH": "Dr Dogfather isch dra", + en: "Dogfather is working on it", + fr: "Dogfather s'en occupe", + pt: "O Dogfather está a tratar", + }, + po_richttermin: { de: "Richttermin", "de-CH": "Richttermin", en: "Target date", fr: "Date cible", pt: "Data prevista" }, + po_oeffnen: { de: "Öffnen", "de-CH": "Ufmache", en: "Open", fr: "Ouvrir", pt: "Abrir" }, + po_zurueck: { de: "Zurück zur Übersicht", "de-CH": "Zrugg zur Übersicht", en: "Back to overview", fr: "Retour à l'aperçu", pt: "Voltar à visão geral" }, + + /* ---------- Projektdetail ---------- */ + po_dateien: { de: "Dateien", "de-CH": "Datei", en: "Files", fr: "Fichiers", pt: "Ficheiros" }, + po_keine_dateien: { de: "Noch keine Dateien.", "de-CH": "No kei Datei.", en: "No files yet.", fr: "Aucun fichier pour l'instant.", pt: "Ainda sem ficheiros." }, + po_version: { de: "Fassung", "de-CH": "Fassig", en: "Version", fr: "Version", pt: "Versão" }, + po_nachrichten: { de: "Nachrichten", "de-CH": "Nachrichte", en: "Messages", fr: "Messages", pt: "Mensagens" }, + po_keine_nachrichten: { de: "Noch keine Nachrichten.", "de-CH": "No kei Nachrichte.", en: "No messages yet.", fr: "Aucun message pour l'instant.", pt: "Ainda sem mensagens." }, + po_nachricht_platzhalter: { + de: "Frage oder Rückmeldung schreiben…", + "de-CH": "Frog oder Rückmäldig schriibe…", + en: "Write a question or some feedback…", + fr: "Écris une question ou un retour…", + pt: "Escreve uma pergunta ou um comentário…", + }, + po_senden: { de: "Senden", "de-CH": "Schicke", en: "Send", fr: "Envoyer", pt: "Enviar" }, + + /* ---------- Ä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: { + 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.", + en: "Note down whatever comes to mind. I review effort and price and get back to you — you then decide at your own pace.", + fr: "Note ici tout ce qui te vient. J'évalue l'effort et le prix et je reviens vers toi — tu décides ensuite tranquillement.", + pt: "Aponta aqui o que te ocorrer. Avalio esforço e preço e volto a contactar-te — depois decides com calma.", + }, + po_idee_titel: { de: "Kurz gesagt", "de-CH": "Churz gseit", en: "In short", fr: "En bref", pt: "Em resumo" }, + po_idee_text: { de: "Beschreibung", "de-CH": "Beschriibig", en: "Description", fr: "Description", pt: "Descrição" }, + po_idee_senden: { de: "Idee einreichen", "de-CH": "Idee iireiche", en: "Submit idea", fr: "Envoyer l'idée", pt: "Enviar ideia" }, + po_idee_leer: { + de: "Bitte gib der Idee einen Titel und beschreib sie kurz.", + "de-CH": "Bitte gib dr Idee en Titel und beschriib si churz.", + en: "Please give the idea a title and describe it briefly.", + fr: "Merci de donner un titre à l'idée et de la décrire brièvement.", + pt: "Dá um título à ideia e descreve-a brevemente.", + }, + po_keine_ideen: { de: "Noch keine Ideen eingereicht.", "de-CH": "No kei Ideä iigreicht.", en: "No ideas submitted yet.", fr: "Aucune idée soumise pour l'instant.", pt: "Ainda sem ideias submetidas." }, + + po_ae_eingereicht: { de: "Eingereicht", "de-CH": "Iigreicht", en: "Submitted", fr: "Soumise", pt: "Submetida" }, + po_ae_geprueft: { de: "Wird geprüft", "de-CH": "Wird prüeft", en: "Under review", fr: "En cours d'examen", pt: "Em análise" }, + po_ae_angeboten: { de: "Angebot liegt vor", "de-CH": "Aagebot ligt vor", en: "Quote available", fr: "Devis disponible", pt: "Orçamento disponível" }, + po_ae_angenommen: { de: "Angenommen", "de-CH": "Aagno", en: "Accepted", fr: "Acceptée", pt: "Aceite" }, + po_ae_abgelehnt: { de: "Abgelehnt", "de-CH": "Abglehnt", en: "Declined", fr: "Refusée", pt: "Recusada" }, + po_ae_umgesetzt: { de: "Umgesetzt", "de-CH": "Umgsetzt", en: "Implemented", fr: "Réalisée", pt: "Implementada" }, + + po_zusatzpreis: { de: "Zusätzlich", "de-CH": "Zuesätzlich", en: "Additional", fr: "En supplément", pt: "Adicional" }, + po_zusatzdauer: { de: "Zusätzliche Dauer", "de-CH": "Zuesätzlichi Duur", en: "Additional time", fr: "Délai supplémentaire", pt: "Tempo adicional" }, + po_annehmen: { de: "Annehmen", "de-CH": "Aanäh", en: "Accept", fr: "Accepter", pt: "Aceitar" }, + po_ablehnen: { de: "Ablehnen", "de-CH": "Ablähne", en: "Decline", fr: "Refuser", pt: "Recusar" }, + po_annehmen_frage: { + de: "Damit nimmst du das Zusatzangebot verbindlich an. Fortfahren?", + "de-CH": "Demit nimmsch s Zuesatzaagebot verbindlich aa. Wiitermache?", + en: "This accepts the additional quote as binding. Continue?", + fr: "Cela accepte le devis complémentaire de manière ferme. Continuer ?", + pt: "Isto aceita o orçamento adicional de forma vinculativa. Continuar?", + }, + + /* ---------- Zahlungen ---------- */ + po_zahlungen: { de: "Zahlungen", "de-CH": "Zahlige", en: "Payments", fr: "Paiements", pt: "Pagamentos" }, + po_za_offen: { de: "Offen", "de-CH": "Offe", en: "Open", fr: "En attente", pt: "Em aberto" }, + po_za_freigegeben: { de: "In Bearbeitung", "de-CH": "I Bearbeitig", en: "Processing", fr: "En cours", pt: "Em processamento" }, + po_za_bezahlt: { de: "Bezahlt", "de-CH": "Zahlt", en: "Paid", fr: "Payé", pt: "Pago" }, + po_za_fehlge: { de: "Fehlgeschlagen", "de-CH": "Fählgschlage", en: "Failed", fr: "Échoué", pt: "Falhou" }, + po_za_erstattet: { de: "Erstattet", "de-CH": "Erstattet", en: "Refunded", fr: "Remboursé", pt: "Reembolsado" }, + po_keine_zahlungen: { de: "Keine Zahlungen vorgemerkt.", "de-CH": "Kei Zahlige vorgmerkt.", en: "No payments due.", fr: "Aucun paiement prévu.", pt: "Sem pagamentos previstos." }, + + /* ---------- Fehler ---------- */ + po_fehler_login: { + de: "E-Mail oder Passwort stimmt nicht.", + "de-CH": "E-Mail oder Passwort stimmt nöd.", + en: "E-mail or password is incorrect.", + fr: "L'e-mail ou le mot de passe est incorrect.", + pt: "E-mail ou palavra-passe incorretos.", + }, + po_abgelaufen: { + de: "Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.", + "de-CH": "Dini Sitzig isch abgloffe. Bitte mäld dich nomol aa.", + en: "Your session has expired. Please sign in again.", + fr: "Ta session a expiré. Merci de te reconnecter.", + pt: "A tua sessão expirou. Inicia sessão novamente.", + }, +}; diff --git a/assets/js/wd-portal.js b/assets/js/wd-portal.js new file mode 100644 index 00000000..2d120d99 --- /dev/null +++ b/assets/js/wd-portal.js @@ -0,0 +1,456 @@ +/* ===================================================================== + wd-portal.js — Kundenportal, Oberfläche (Masterplan S.12) + + Vier Ansichten in einer Seite: Anmeldung, Passwort festlegen, + Übersicht, Projektdetail. + + Wichtig zum Verständnis: Diese Datei stellt nur DAR. Sie entscheidet + nichts darüber, wer was sehen darf — das macht ausschließlich der + Server (routes/webdesign-portal.js), weil dieser Code hier im Browser + des Kunden läuft und dort beliebig veränderbar ist. Was hier ankommt, + ist bereits gefiltert. + ===================================================================== */ +(function () { + "use strict"; + + var TOKEN = "wd-portal-token"; + var token = null; + var s = window.WD.sicher; + var meldung = document.getElementById("po-meldung"); + + /* Sitzungsspeicher, nicht dauerhaft: derselbe Grundsatz wie überall in + diesem Bereich — beim Schließen ist der Zugang weg. */ + function tokenLesen() { try { return sessionStorage.getItem(TOKEN); } catch (e) { return null; } } + function tokenSchreiben(t) { + try { if (t) sessionStorage.setItem(TOKEN, t); else sessionStorage.removeItem(TOKEN); } 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); + } + + function zeige(welche) { + ["po-anmeldung", "po-setzen", "po-uebersicht", "po-detail"].forEach(function (id) { + document.getElementById(id).hidden = id !== welche; + }); + } + + /* Sitzung abgelaufen: zurück zur Anmeldung mit klarer Ansage. Eine leere + Seite würde aussehen wie "du hast keine Projekte" — eine stille + Falschaussage. */ + function behandleFehler(fehler) { + if (fehler && fehler.status === 401) { + token = null; tokenSchreiben(null); + zeige("po-anmeldung"); + window.WD.meldung(meldung, window.WD.t("po_abgelaufen"), "fehler"); + return true; + } + return false; + } + + /* ---------------- Anmeldung ---------------- */ + document.getElementById("po-login-form").addEventListener("submit", async function (e) { + e.preventDefault(); + var feld = document.getElementById("po-feld-passwort"); + var knopf = e.target.querySelector("button"); + feld.classList.remove("wd-feld--fehler"); + window.WD.meldungAus(meldung); + + var email = document.getElementById("po-email").value.trim(); + var passwort = document.getElementById("po-passwort").value; + if (!email || !passwort) { + feld.classList.add("wd-feld--fehler"); + feld.querySelector(".wd-feld-fehler").textContent = window.WD.t("po_login_leer"); + return; + } + + window.WD.knopfSperren(knopf, true, window.WD.t("po_anmelden_laeuft")); + try { + var antwort = await window.WD.api("/webdesign/portal/anmelden", { + method: "POST", body: { email: email, passwort: passwort }, + }); + token = antwort.token; + tokenSchreiben(token); + /* Sprache des Kunden übernehmen, wenn er eine hinterlegt hat — + so kommt er in seiner Sprache an, ohne umschalten zu müssen. */ + if (antwort.kunde && antwort.kunde.sprache) window.WD.setSprache(antwort.kunde.sprache); + ladeUebersicht(); + } catch (fehler) { + feld.classList.add("wd-feld--fehler"); + feld.querySelector(".wd-feld-fehler").textContent = fehler.message || window.WD.t("po_fehler_login"); + document.getElementById("po-passwort").select(); + } finally { + window.WD.knopfSperren(knopf, false); + } + }); + + document.getElementById("po-abmelden").addEventListener("click", async function () { + try { await api("/webdesign/portal/abmelden", { method: "POST", body: {} }); } catch (e) { /* egal */ } + token = null; tokenSchreiben(null); + location.href = "/webdesign/portal.html"; + }); + + /* ---------------- Passwort setzen (Einladungslink) ---------------- */ + var einladungsToken = new URLSearchParams(location.search).get("einladung"); + + document.getElementById("po-setz-form").addEventListener("submit", async function (e) { + e.preventDefault(); + var feld = document.getElementById("po-feld-neu"); + var knopf = e.target.querySelector("button"); + feld.classList.remove("wd-feld--fehler"); + + var passwort = document.getElementById("po-neu").value; + window.WD.knopfSperren(knopf, true, window.WD.t("po_anmelden_laeuft")); + try { + await window.WD.api("/webdesign/portal/passwort", { + method: "POST", body: { token: einladungsToken, passwort: passwort }, + }); + zeige("po-anmeldung"); + window.WD.meldung(meldung, window.WD.t("po_setz_fertig"), "ok"); + /* Adresszeile säubern: der Einladungslink ist verbraucht und hat in + der Chronik nichts mehr verloren. */ + history.replaceState(null, "", "/webdesign/portal.html"); + } catch (fehler) { + feld.classList.add("wd-feld--fehler"); + feld.querySelector(".wd-feld-fehler").textContent = fehler.message; + } finally { + window.WD.knopfSperren(knopf, false); + } + }); + + /* ---------------- Übersicht ---------------- */ + var PHASEN = ["briefing", "angebot", "design", "entwicklung", "tests", "abnahme", "uebergeben"]; + + function statusText(st) { return window.WD.t("po_st_" + st); } + + function phasenLeiste(status) { + var jetzt = PHASEN.indexOf(status); + /* Pausiert oder abgebrochen: keine Leiste. Ein Fortschrittsbalken bei + einem gestoppten Projekt wäre irreführend. */ + if (jetzt < 0) return ""; + return '" + + '
' + s(statusText(status)) + " · " + + (jetzt + 1) + "/" + PHASEN.length + "
"; + } + + function wartetMarke(wartet) { + if (wartet !== "kunde" && wartet !== "dogfather") return ""; + return '' + + s(window.WD.t(wartet === "kunde" ? "po_wartet_kunde" : "po_wartet_dogfather")) + ""; + } + + async function ladeUebersicht() { + zeige("po-uebersicht"); + var ziel = document.getElementById("po-inhalt"); + ziel.innerHTML = '
' + s(window.WD.t("laedt")) + "
"; + + try { + var d = await api("/webdesign/portal/uebersicht"); + document.getElementById("po-name").textContent = d.kunde.name || ""; + window.WD.meldungAus(meldung); + + var html = ""; + + /* Offene Zahlungen ganz oben — das ist das Einzige, was den Kunden + zum Handeln auffordert. Gold, weil es wirklich wichtig ist. */ + if (d.offeneZahlungen.length) { + html += '
' + + '

' + s(window.WD.t("po_offene_zahlungen")) + "

" + + d.offeneZahlungen.map(function (z) { + return "

" + s(z.betrag_text) + " · " + s(z.zweck_text) + "

"; + }).join("") + "
"; + } + + html += "

" + s(window.WD.t("po_projekte")) + "

"; + if (!d.projekte.length) { + html += '

' + s(window.WD.t("po_keine_projekte")) + "

"; + } else { + html += d.projekte.map(function (p) { + return '"; + }).join(""); + } + + if (d.abos.length) { + html += '

' + s(window.WD.t("po_betreuung")) + "

" + + '
"; + } + + ziel.innerHTML = html; + ziel.querySelectorAll("[data-projekt]").forEach(function (b) { + b.addEventListener("click", function () { ladeProjekt(b.dataset.projekt); }); + }); + } catch (fehler) { + if (behandleFehler(fehler)) return; + ziel.innerHTML = ""; + window.WD.meldung(meldung, fehler.message, "fehler"); + } + } + + /* ---------------- Projektdetail ---------------- */ + var aktuellesProjekt = null; + + document.getElementById("po-zurueck").addEventListener("click", function () { + aktuellesProjekt = null; + ladeUebersicht(); + }); + + async function ladeProjekt(id) { + aktuellesProjekt = id; + zeige("po-detail"); + var ziel = document.getElementById("po-detail-inhalt"); + ziel.innerHTML = '
' + s(window.WD.t("laedt")) + "
"; + + try { + var d = await api("/webdesign/portal/projekt/" + encodeURIComponent(id)); + var p = d.projekt; + var html = ""; + + html += '
' + + '' + s(p.nummer) + "" + + "

" + s(p.titel) + "

" + + wartetMarke(p.wartet_auf) + + phasenLeiste(p.status) + + (p.naechster_schritt + ? '

' + s(window.WD.t("po_naechster_schritt")) + + ": " + s(p.naechster_schritt) + "

" + : "") + + "
"; + + /* --- Zahlungen --- */ + html += '

' + s(window.WD.t("po_zahlungen")) + "

"; + if (!d.zahlungen.length) { + html += '

' + s(window.WD.t("po_keine_zahlungen")) + "

"; + } else { + html += '"; + } + html += "
"; + + /* --- Dateien --- */ + html += '

' + s(window.WD.t("po_dateien")) + "

"; + if (!d.dateien.length) { + html += '

' + s(window.WD.t("po_keine_dateien")) + "

"; + } else { + html += '"; + } + html += "
"; + + /* --- Ideen & Änderungswünsche --- */ + html += '

' + s(window.WD.t("po_ideen")) + "

" + + '

' + s(window.WD.t("po_ideen_text")) + "

"; + + if (d.aenderungen.length) { + html += d.aenderungen.map(function (a) { + var entscheidbar = a.status === "angeboten"; + return '
' + + '

' + s(a.titel) + "

" + + '' + + s(window.WD.t("po_ae_" + a.status)) + "
" + + '

' + s(a.beschreibung) + "

" + + (a.aufwand_text ? '

' + s(a.aufwand_text) + "

" : "") + + (a.zusatzpreis_text + ? '

' + s(window.WD.t("po_zusatzpreis")) + + ": " + s(a.zusatzpreis_text) + "" + + (a.zusatzdauer_text ? " · " + s(window.WD.t("po_zusatzdauer")) + ": " + s(a.zusatzdauer_text) : "") + + "

" + : "") + + (entscheidbar + ? '
' + + '" + + '
" + : "") + + "
"; + }).join(""); + } else { + html += '

' + s(window.WD.t("po_keine_ideen")) + "

"; + } + + html += '
' + + "" + + '' + + "
" + + '
' + + "" + + '' + + '

' + + '
"; + + /* --- Nachrichten --- */ + html += '

' + s(window.WD.t("po_nachrichten")) + "

"; + if (!d.nachrichten.length) { + html += '

' + s(window.WD.t("po_keine_nachrichten")) + "

"; + } else { + html += d.nachrichten.map(function (n) { + var vonMir = n.autor === "kunde"; + return '
' + + '
' + (vonMir ? "" : "Dogfather · ") + + window.WD.datum(n.erstellt_am) + "

" + s(n.text) + "

"; + }).join(""); + } + html += '
' + + '
' + + '
"; + + ziel.innerHTML = html; + + /* --- Verknüpfungen --- */ + ziel.querySelectorAll("[data-ja], [data-nein]").forEach(function (b) { + b.addEventListener("click", async function () { + var annehmen = !!b.dataset.ja; + var aeId = b.dataset.ja || b.dataset.nein; + /* Nur beim Annehmen nachfragen — das erzeugt eine Zahlungspflicht. + Ablehnen ist folgenlos und braucht keine Hürde. */ + if (annehmen && !confirm(window.WD.t("po_annehmen_frage"))) return; + window.WD.knopfSperren(b, true, "…"); + try { + await api("/webdesign/portal/aenderung/" + encodeURIComponent(aeId) + "/entscheiden", + { method: "POST", body: { annehmen: annehmen } }); + 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) { + var titel = ziel.querySelector("#po-idee-titel").value.trim(); + var beschreibung = ziel.querySelector("#po-idee-text").value.trim(); + var feld = ziel.querySelector("#po-feld-idee"); + feld.classList.remove("wd-feld--fehler"); + if (!titel || !beschreibung) { + feld.classList.add("wd-feld--fehler"); + feld.querySelector(".wd-feld-fehler").textContent = window.WD.t("po_idee_leer"); + return; + } + window.WD.knopfSperren(e.target, true, "…"); + try { + await api("/webdesign/portal/aenderung", + { method: "POST", body: { projektId: id, titel: titel, beschreibung: beschreibung } }); + ladeProjekt(id); + } catch (f) { + if (behandleFehler(f)) return; + window.WD.knopfSperren(e.target, false); + window.WD.meldung(meldung, f.message, "fehler"); + } + }); + + ziel.querySelector("#po-nachricht-senden").addEventListener("click", async function (e) { + var feld = ziel.querySelector("#po-nachricht-text"); + var inhalt = feld.value.trim(); + if (!inhalt) { feld.focus(); return; } + window.WD.knopfSperren(e.target, true, "…"); + try { + await api("/webdesign/portal/nachricht", + { method: "POST", body: { projektId: id, text: inhalt } }); + ladeProjekt(id); + } catch (f) { + if (behandleFehler(f)) return; + window.WD.knopfSperren(e.target, false); + window.WD.meldung(meldung, f.message, "fehler"); + } + }); + + /* Zahlungsknöpfe: die eigentliche PayPal-Anbindung folgt. Bis dahin + ein ehrlicher Hinweis statt eines Knopfes, der nichts tut. */ + ziel.querySelectorAll("[data-zahlung]").forEach(function (b) { + b.addEventListener("click", function () { + window.WD.meldung(meldung, + "Die Zahlung über PayPal wird gerade eingerichtet. Bitte melde dich kurz bei Dogfather.", "fehler"); + }); + }); + + } catch (fehler) { + if (behandleFehler(fehler)) return; + ziel.innerHTML = ""; + window.WD.meldung(meldung, fehler.message, "fehler"); + } + } + + /* ---------------- Start ---------------- */ + async function start() { + /* Einladungslink hat Vorrang: wer gerade sein Passwort setzen soll, + will nicht zuerst eine Anmeldemaske sehen. */ + if (einladungsToken) { + try { + var info = await window.WD.api("/webdesign/portal/einladung?token=" + encodeURIComponent(einladungsToken)); + document.getElementById("po-setz-fuer").textContent = info.email || ""; + zeige("po-setzen"); + return; + } catch (fehler) { + zeige("po-anmeldung"); + window.WD.meldung(meldung, fehler.message, "fehler"); + return; + } + } + + token = tokenLesen(); + if (!token) { zeige("po-anmeldung"); return; } + ladeUebersicht(); + } + + /* Nach einem Sprachwechsel die per JS gebauten Teile neu zeichnen. */ + document.addEventListener("wd-sprache-geaendert", function () { + if (!token) return; + if (aktuellesProjekt) ladeProjekt(aktuellesProjekt); + else if (!document.getElementById("po-uebersicht").hidden) ladeUebersicht(); + }); + + if (window.WD) start(); + else document.addEventListener("wd-bereit", start); +})(); diff --git a/server/pruefe-webdesign-i18n.mjs b/server/pruefe-webdesign-i18n.mjs index 58abe0dc..9f2dbb5b 100644 --- a/server/pruefe-webdesign-i18n.mjs +++ b/server/pruefe-webdesign-i18n.mjs @@ -42,7 +42,13 @@ for (const datei of seiten) { einen einzigen Nutzer, der sie braucht. Genau wie der bestehende interne Bereich des Universe, der ebenfalls nur Deutsch spricht. Die Tag-Balance wird trotzdem geprüft (siehe unten). */ - const NUR_DEUTSCH = ["verwaltung", "portal"]; + /* NUR die Verwaltung ist einsprachig. Das Kundenportal stand hier + zunaechst auch drin -- das war ein Denkfehler: die Verwaltung nutzen + Dogfather und VanVan, das Portal nutzen KUNDEN. Und Kunden koennen + Franzosen oder Portugiesen sein. Ein Portal, das die Sprachwahl der + oeffentlichen Seite ignoriert, waere genau der Bruch, den ein Kunde + sofort bemerkt. */ + const NUR_DEUTSCH = ["verwaltung"]; const einsprachig = NUR_DEUTSCH.includes(name); if (!existsSync(i18nDatei) && !einsprachig) { @@ -87,8 +93,12 @@ for (const datei of seiten) { if (existsSync(seitenSkript)) quellen.push(readFileSync(seitenSkript, "utf8")); for (const quelle of quellen) { for (const m of quelle.matchAll(/WD\.t\("([a-z0-9_]+)"\)/g)) benutzt.add(m[1]); - // Schlüssel aus Nachschlagetabellen, z.B. { ja: "af3_ja", ... } - for (const m of quelle.matchAll(/["']((?:af|pk|zg|pf|ue|fq|ab|ls|pr|s\d)[a-z0-9_]*)["']\s*[,}\]]/g)) { + /* Schlüssel aus Nachschlagetabellen, z.B. { ja: "af3_ja", ... }. + Der Unterstrich im Muster ist wichtig: ohne ihn hielt die Prüfung + auch gewöhnliche Wörter wie "abnahme" oder "uebergeben" (aus der + Phasenliste im Portal) für Wörterbuch-Schlüssel und meldete sie als + fehlend. Alle echten Schlüssel haben die Form praefix_name. */ + for (const m of quelle.matchAll(/["']([a-z]{2}\d?_[a-z0-9_]+)["']\s*[,}\]]/g)) { benutzt.add(m[1]); } } @@ -108,6 +118,11 @@ for (const datei of seiten) { const ohneI18n = []; for (const m of html.matchAll(/<(h[1-4])\b([^>]*)>([\s\S]*?)<\/\1>/g)) { if (m[2].includes("data-i18n")) continue; + /* Auch übersetzt, wenn die Übersetzung INNEN sitzt — etwa +

Hallo

, + wo ein Teil fest und ein Teil dynamisch ist. Ohne diese Ausnahme + meldet die Prüfung eine korrekt übersetzte Überschrift als Fehler. */ + if (m[3].includes("data-i18n")) continue; const text = m[3].replace(/<[^>]*>/g, "").trim(); if (!text) continue; // rein dekorativ if (/^\$\{/.test(text)) continue; // dynamisch aus JS diff --git a/webdesign/portal.html b/webdesign/portal.html new file mode 100644 index 00000000..966d7599 --- /dev/null +++ b/webdesign/portal.html @@ -0,0 +1,141 @@ + + + + + +Kundenportal — Dogfather Webdesign + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + + + + + + + + + + + +
+
+
+ +
+ + + + + +