Portal: Fortschritt unter Projekte, neuer Tab Funktionen mit News-Editor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-09-30 17:06:02 +00:00
1 parent 6dac6aa073
commit 5a09d060a1
7 files changed
+937 -24

No files matched your search

+51 -15
View File
@@ -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: '<path d="M4 20h4L19 9l-4-4L4 16z"/><path d="M13 7l4 4"/>',
@@ -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 = '<p class="muted">Es wurden noch keine Arbeiten dokumentiert.</p>'
return
}
body.innerHTML = tickets.map((t) => `
const tickets = [...all].sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || '')))
const card = (t) => `
<article class="pd-order pd-order-card pd-tone-${escapeAttr(t.status.tone)}">
${renderOrder(t)}
${renderWorkEntries(t.entries || [], 'Noch keine Arbeiten dokumentiert.')}
</article>`).join('')
initPdToggles(body)
</article>`
const draw = (showAll) => {
body.innerHTML = tickets.slice(0, showAll ? tickets.length : ACTIVITY_SHOWN).map(card).join('') +
(tickets.length > ACTIVITY_SHOWN
? `<button type="button" class="kb-btn-outline kb-activity-all" data-activity-all>${showAll ? 'Weniger anzeigen' : `Alle anzeigen (${tickets.length})`}</button>`
: '')
initPdToggles(body)
body.querySelector('[data-activity-all]')?.addEventListener('click', () => draw(!showAll))
}
draw(false)
} catch (err) {
body.innerHTML = `<p class="muted">Aktivität konnte nicht geladen werden: ${escapeHtml(err.message)}</p>`
}
@@ -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 = `<p class="muted">${escapeHtml(err.message || 'Konnte nicht geladen werden.')}</p>`
})
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])'