From 5a09d060a12bfe01d51bfa60fe745f718d699ef5 Mon Sep 17 00:00:00 2001 From: knso Date: Wed, 30 Sep 2026 17:06:02 +0000 Subject: [PATCH] Portal: Fortschritt unter Projekte, neuer Tab Funktionen mit News-Editor Co-Authored-By: Claude Opus 5.5 --- public/app.js | 66 ++++- public/dashboard.html | 32 ++- public/funktionen.css | 139 ++++++++++ public/funktionen.js | 500 ++++++++++++++++++++++++++++++++++ server/routes/funktionen.js | 6 +- test/funktionenRoutes.test.js | 1 + test/ui/funktionen.ui.mjs | 217 +++++++++++++++ 7 files changed, 937 insertions(+), 24 deletions(-) create mode 100644 public/funktionen.css create mode 100644 public/funktionen.js create mode 100644 test/ui/funktionen.ui.mjs diff --git a/public/app.js b/public/app.js index 7233675..f7d2bf9 100644 --- a/public/app.js +++ b/public/app.js @@ -210,7 +210,7 @@ function renderProjectCard(project, features) { /* --------------------------------------------------------- Projektdetails --- */ // Die Daten kommen fertig fuer Kunden aufbereitet vom Server (customerProjectView.js): keine // Commit-Kuerzel, Kontonamen oder KI-Hinweise, Status und Arbeitsarten auf Deutsch. -// Dieselben Bausteine nutzt der Tab "Fortschritt" (loadCustomerActivity). +// Dieselben Bausteine nutzt der Abschnitt "Was wir zuletzt erledigt haben" unter Projekte (loadCustomerActivity). const PD_ICONS = { update: '', @@ -690,23 +690,34 @@ async function loadCustomerInvoices() { } /* -------------------------------------------------------------- Aktivität --- */ -// Tab "Fortschritt": je Auftrag eine Karte mit Status, Fortschrittsleiste und Arbeiten -// (Bausteine der Projektdetails; Texte kommen aufbereitet vom Server) +// Abschnitt "Was wir zuletzt erledigt haben" unter Projekte: je Auftrag eine Karte mit Status, Fortschrittsleiste +// und Arbeiten (Bausteine der Projektdetails; Texte kommen aufbereitet vom Server). Zuerst die drei neuesten, +// "Alle anzeigen" zeigt den Rest. +const ACTIVITY_SHOWN = 3 async function loadCustomerActivity() { const body = document.getElementById('activity-body') if (!body) return try { - const { tickets } = await api('/api/activity') - if (!tickets || !tickets.length) { + const { tickets: all } = await api('/api/activity') + if (!all || !all.length) { body.innerHTML = '

Es wurden noch keine Arbeiten dokumentiert.

' return } - body.innerHTML = tickets.map((t) => ` + const tickets = [...all].sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || ''))) + const card = (t) => `
${renderOrder(t)} ${renderWorkEntries(t.entries || [], 'Noch keine Arbeiten dokumentiert.')} -
`).join('') - initPdToggles(body) + ` + const draw = (showAll) => { + body.innerHTML = tickets.slice(0, showAll ? tickets.length : ACTIVITY_SHOWN).map(card).join('') + + (tickets.length > ACTIVITY_SHOWN + ? `` + : '') + initPdToggles(body) + body.querySelector('[data-activity-all]')?.addEventListener('click', () => draw(!showAll)) + } + draw(false) } catch (err) { body.innerHTML = `

Aktivität konnte nicht geladen werden: ${escapeHtml(err.message)}

` } @@ -1948,14 +1959,32 @@ function initPasswordView() { // Ansichten im Inhaltsbereich; Hash fuer Direktlinks (z. B. aus Mails: #vertraege, #gutschrift). // Die alten Hashes #wartung und #abo fuehren zu #vertraege (HASH_ALIASES). // "projekt" (Projektdetails als eigene Seite) gibt es ebenfalls nur in der App-Ansicht. -// "passwort" oeffnet das Konto-Menue (Profilbild). -const VIEW_HASHES = { projects: '', activity: 'fortschritt', invoices: 'rechnungen', chat: 'chat', vertraege: 'vertraege', stunden: 'stunden', 'bestellen-hosting': 'bestellen-hosting', 'bestellen-wartung': 'bestellen-wartung', wechsel: 'wechsel', takeover: 'gutschrift', projekt: 'projekt', passwort: 'passwort' } +// "passwort" oeffnet das Konto-Menue (Profilbild). #fortschritt fuehrt zu Projekte und scrollt zum Abschnitt +// "Was wir zuletzt erledigt haben" (#pj-activity); hinter dem Hash darf ein Query stehen (#news?bestellt=1, Rueckkehr von Stripe). +const VIEW_HASHES = { projects: '', funktionen: 'funktionen', news: 'news', invoices: 'rechnungen', chat: 'chat', vertraege: 'vertraege', stunden: 'stunden', 'bestellen-hosting': 'bestellen-hosting', 'bestellen-wartung': 'bestellen-wartung', wechsel: 'wechsel', takeover: 'gutschrift', projekt: 'projekt', passwort: 'passwort' } const HASH_ALIASES = { wartung: 'vertraege', abo: 'vertraege' } -const viewLoaders = { activity: loadCustomerActivity, chat: initChat } +const viewLoaders = { chat: initChat } +// Funktionen und News laden bei jedem Oeffnen neu (Zustand aendert sich durch Bestellung und Freischaltung); Modul erst dann laden +const viewRefreshers = { + funktionen: () => import('/funktionen.js').then((m) => m.initFunktionen(document.getElementById('funktionen-body'))), + news: () => import('/funktionen.js').then((m) => m.initNews(document.getElementById('news-body'))), +} const loadedViews = new Set() +function currentHash() { + return window.location.hash.replace(/^#/, '').split('?')[0] +} + +// Direktlink #fortschritt: nach dem Laden der Projekte zum Abschnitt scrollen +let activityScrollPending = false +function scrollToActivity() { + if (!activityScrollPending) return + document.getElementById('pj-activity')?.scrollIntoView({ block: 'start' }) +} + function viewFromHash() { - const hash = window.location.hash.replace(/^#/, '') + const hash = currentHash() + if (hash === 'fortschritt') return 'projects' if (HASH_ALIASES[hash]) return HASH_ALIASES[hash] return Object.keys(VIEW_HASHES).find((v) => VIEW_HASHES[v] && VIEW_HASHES[v] === hash) || '' } @@ -1969,6 +1998,10 @@ function showView(name, { updateHash = true } = {}) { loadedViews.add(name) viewLoaders[name]() } + viewRefreshers[name]?.().catch((err) => { + const box = document.getElementById(`${name}-body`) + if (box) box.innerHTML = `

${escapeHtml(err.message || 'Konnte nicht geladen werden.')}

` + }) if (name === 'chat') { api('/api/chat/read', { method: 'POST' }).catch(() => {}) updateChatBadge(0) @@ -2073,10 +2106,12 @@ async function initDashboardPage() { if (aboParam) window.history.replaceState(null, '', `/dashboard.html${window.location.hash}`) loadContracts({ afterCheckout: aboParam === 'success' }) loadCustomerInvoices() + activityScrollPending = currentHash() === 'fortschritt' + loadCustomerActivity().then(scrollToActivity) const initialView = viewFromHash() // Alte Hashes (#wartung, #abo) gleich auf den neuen (#vertraege) umschreiben - if (initialView) showView(initialView, { updateHash: Boolean(HASH_ALIASES[window.location.hash.replace(/^#/, '')]) }) + if (initialView) showView(initialView, { updateHash: Boolean(HASH_ALIASES[currentHash()]) }) const [{ projects }, { features }] = await Promise.all([ api('/api/projects'), @@ -2098,6 +2133,7 @@ async function initDashboardPage() { } else { empty.classList.remove('hidden') } + scrollToActivity() // Chat-Badge global pollen (auch wenn der Chat nicht offen ist) pollChatBadge() @@ -2121,8 +2157,8 @@ const appLayout = window.matchMedia('(max-width: 880px), (pointer: coarse) and ( const coarsePointer = window.matchMedia('(pointer: coarse)') // Unterseite -> Tab, zu dem sie gehoert (und Text neben dem Zurueck-Pfeil). // "passwort" setzt onAppViewShown: die Ansicht, aus der das Konto-Menue geoeffnet wurde. -const APP_PARENT = { stunden: 'vertraege', 'bestellen-hosting': 'vertraege', 'bestellen-wartung': 'vertraege', wechsel: 'vertraege', takeover: 'vertraege', projekt: 'projects' } -const APP_PARENT_LABEL = { vertraege: 'Verträge', projects: 'Projekte', activity: 'Fortschritt', invoices: 'Rechnungen', chat: 'Chat' } +const APP_PARENT = { news: 'funktionen', stunden: 'vertraege', 'bestellen-hosting': 'vertraege', 'bestellen-wartung': 'vertraege', wechsel: 'vertraege', takeover: 'vertraege', projekt: 'projects' } +const APP_PARENT_LABEL = { vertraege: 'Verträge', projects: 'Projekte', funktionen: 'Funktionen', invoices: 'Rechnungen', chat: 'Chat' } const appShell = { view: 'projects', scroll: {}, fromHistory: false, sideCards: [], installPrompt: null } const TEXT_INPUT = 'textarea, input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=date]):not([type=button]):not([type=submit]):not([type=range]):not([type=color])' diff --git a/public/dashboard.html b/public/dashboard.html index 45fb90d..95baacc 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -21,6 +21,7 @@ +