Projektdetails und Fortschritt kundenverständlich
Der Server bereitet Commits, Tickets und Arbeitseinträge für Kunden auf (server/services/customerProjectView.js), die Rohdaten bleiben intern: - keine Co-Authored-By-/Signed-off-by-Zeilen und KI-Hinweise, keine Commit-Kürzel, Kontonamen, Git-Links oder Branch - technische Commits (Preview-Config, Merges) ausgeblendet, bekannte Automatik-Texte übersetzt, "fix"/"upload" -> "Website aktualisiert" - Ticketstatus und Arbeitsarten auf Deutsch (Open -> Eingegangen, Remote -> Online erledigt, ...), Fortschritt in 4 Schritten - Akquise-Tickets auch in den Projektdetails verborgen - /api/projects ohne Gitea-URL, Repo- und Vorlagennamen Darstellung: Auftragskarte mit Status und Fortschrittsleiste, Zeitleiste "Letzte Änderungen" nach Tagen, Arbeiten mit Farbe/Symbol je Art und Arbeitszeit; der Tab "Fortschritt" nutzt dieselben Bausteine. In der App-Ansicht öffnen sich die Projektdetails als eigene Seite mit Zurück zu "Projekte". Tests: npm test (node --test, 22 Fälle). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e54f97144c
commit
ba9916cfe6
9 files changed
+932
-138
No files matched your search
+224
-79
@@ -75,7 +75,8 @@ function fmtMinutes(min) {
|
||||
if (!n) return ''
|
||||
const h = Math.floor(n / 60)
|
||||
const m = n % 60
|
||||
return h ? `${h} h ${m} min` : `${m} min`
|
||||
if (!h) return `${m} min`
|
||||
return m ? `${h} h ${m} min` : `${h} h`
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- Projekte --- */
|
||||
@@ -162,7 +163,10 @@ function renderProjectCard(project, features) {
|
||||
<button type="button" class="kb-chip" data-view="invoices">Rechnungen</button>
|
||||
${extraChips.map((label) => `<span class="kb-chip">${escapeHtml(label)}</span>`).join('')}
|
||||
</div>
|
||||
<button type="button" class="kb-btn-outline" data-details-btn>Projektdetails anzeigen</button>
|
||||
<button type="button" class="kb-btn-outline kb-details-btn" data-details-btn aria-expanded="false">
|
||||
<span data-details-label>Projektdetails anzeigen</span>
|
||||
<svg class="kb-details-chev" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>
|
||||
</button>
|
||||
<div class="kb-details hidden" data-details-box></div>
|
||||
</div>
|
||||
`
|
||||
@@ -170,44 +174,178 @@ function renderProjectCard(project, features) {
|
||||
const frame = li.querySelector('[data-preview-frame]')
|
||||
if (frame) setupPreviewFrame(frame)
|
||||
|
||||
// Am Desktop klappen die Details in der Karte auf; in der App-Ansicht (Handy) oeffnen sie sich
|
||||
// als eigene Seite mit Zurueck-Pfeil (openProjectPage)
|
||||
const btn = li.querySelector('[data-details-btn]')
|
||||
const label = li.querySelector('[data-details-label]')
|
||||
const box = li.querySelector('[data-details-box]')
|
||||
let loaded = false
|
||||
btn.addEventListener('click', async () => {
|
||||
const isHidden = box.classList.contains('hidden')
|
||||
box.classList.toggle('hidden', !isHidden)
|
||||
btn.textContent = isHidden ? 'Projektdetails ausblenden' : 'Projektdetails anzeigen'
|
||||
if (isHidden && !loaded) {
|
||||
btn.addEventListener('click', () => {
|
||||
if (isAppLayout()) {
|
||||
openProjectPage(project)
|
||||
return
|
||||
}
|
||||
const opening = box.classList.contains('hidden')
|
||||
box.classList.toggle('hidden', !opening)
|
||||
btn.setAttribute('aria-expanded', String(opening))
|
||||
label.textContent = opening ? 'Projektdetails ausblenden' : 'Projektdetails anzeigen'
|
||||
if (opening && !loaded) {
|
||||
loaded = true
|
||||
await loadProjectDetails(project, box)
|
||||
loadProjectDetails(project, box)
|
||||
}
|
||||
})
|
||||
|
||||
return li
|
||||
}
|
||||
|
||||
function renderCommits(git) {
|
||||
const sizeInfo = git.repo
|
||||
? `<p class="muted">Projektgröße: ${fmtBytes((git.repo.sizeKb || 0) * 1024)} · Branch: ${escapeHtml(git.repo.defaultBranch || 'main')}</p>`
|
||||
/* --------------------------------------------------------- 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).
|
||||
|
||||
const PD_ICONS = {
|
||||
update: '<path d="M4 20h4L19 9l-4-4L4 16z"/><path d="M13 7l4 4"/>',
|
||||
online: '<rect x="4" y="5" width="16" height="11" rx="2"/><path d="M2 19h20"/>',
|
||||
onsite: '<path d="M12 21s7-6.2 7-11.5a7 7 0 0 0-14 0C5 14.8 12 21 12 21z"/><circle cx="12" cy="9.5" r="2.5"/>',
|
||||
office: '<path d="M5 21V5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v16"/><path d="M16 10h2a2 2 0 0 1 2 2v9M9 7h3M9 11h3M9 15h3M3 21h18"/>',
|
||||
work: '<path d="M14.7 6.3a4 4 0 0 0-5.4 5.4L3 18l3 3 6.3-6.3a4 4 0 0 0 5.4-5.4l-2.5 2.5-2.4-.6-.6-2.4z"/>',
|
||||
clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
||||
changes: '<path d="M3 12a9 9 0 1 0 2.6-6.4L3 8"/><path d="M3 3v5h5M12 7.5V12l3 2"/>',
|
||||
files: '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
|
||||
}
|
||||
const ORDER_STEPS = ['Eingegangen', 'In Arbeit', 'Prüfung', 'Erledigt']
|
||||
const PD_CHANGES_SHOWN = 5
|
||||
const PD_ENTRIES_SHOWN = 4
|
||||
|
||||
function pdIcon(name, cls = 'pd-ico') {
|
||||
return `<svg class="${cls}" viewBox="0 0 24 24" aria-hidden="true">${PD_ICONS[name] || PD_ICONS.work}</svg>`
|
||||
}
|
||||
|
||||
// "2026-09-20" -> "20.09.2026" (reines Datum, ohne Zeitzonen-Umrechnung)
|
||||
function fmtIsoDay(day) {
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(day || ''))
|
||||
return m ? `${m[3]}.${m[2]}.${m[1]}` : ''
|
||||
}
|
||||
|
||||
// Ueberschrift je Tag in der Zeitleiste: "Heute", "Gestern", sonst "Mittwoch, 24. September"
|
||||
function changeDayLabel(date) {
|
||||
const today = new Date()
|
||||
const dayStart = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()
|
||||
const diff = Math.round((dayStart(today) - dayStart(date)) / 86400000)
|
||||
if (diff === 0) return 'Heute'
|
||||
if (diff === 1) return 'Gestern'
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
weekday: 'long', day: 'numeric', month: 'long', ...(date.getFullYear() === today.getFullYear() ? {} : { year: 'numeric' }),
|
||||
})
|
||||
}
|
||||
|
||||
// Auftrag: Nummer, Betreff, Status und Fortschrittsleiste Eingegangen -> In Arbeit -> Prüfung -> Erledigt
|
||||
function renderOrder(order) {
|
||||
const { status } = order
|
||||
const steps = status.step > 0
|
||||
? `<ol class="pd-steps" aria-label="Fortschritt des Auftrags">${ORDER_STEPS.map((label, i) => {
|
||||
const n = i + 1
|
||||
const state = n < status.step ? 'is-done' : n === status.step ? 'is-current' : ''
|
||||
return `<li class="${state}"${n === status.step ? ' aria-current="step"' : ''}>${label}</li>`
|
||||
}).join('')}</ol>`
|
||||
: ''
|
||||
if (!git.commits || !git.commits.length) {
|
||||
return `${sizeInfo}<p class="muted">Noch keine Änderungen vorhanden.</p>`
|
||||
}
|
||||
return sizeInfo + git.commits.map((c) => {
|
||||
const [title, ...body] = String(c.message || '').trim().split('\n')
|
||||
const bodyText = body.map((l) => l.trim()).filter(Boolean).join('\n')
|
||||
return `
|
||||
<div class="commit-item">
|
||||
<strong>${escapeHtml(title)}</strong>
|
||||
${bodyText ? `<div class="commit-body">${escapeHtml(bodyText)}</div>` : ''}
|
||||
<div class="commit-meta">${escapeHtml(c.shortSha)} · ${escapeHtml(c.authorName)} · ${fmtDateTime(c.date)}</div>
|
||||
</div>`
|
||||
}).join('')
|
||||
const hint = status.tone === 'waiting'
|
||||
? '<p class="pd-order-hint">Wir brauchen noch eine Info von dir. <button type="button" class="kb-link" data-view="chat">Im Chat antworten</button></p>'
|
||||
: ''
|
||||
return `
|
||||
<div class="pd-order-head">
|
||||
<div class="pd-order-name">
|
||||
${order.number ? `<span class="pd-order-no">Auftrag ${escapeHtml(order.number)}</span>` : ''}
|
||||
<h3>${escapeHtml(order.topic || 'Dein Auftrag')}</h3>
|
||||
</div>
|
||||
<span class="pd-status">${escapeHtml(status.label)}</span>
|
||||
</div>
|
||||
${steps}${hint}`
|
||||
}
|
||||
|
||||
// Lange Listen: nur die ersten Eintraege zeigen, "3 weitere … anzeigen" klappt den Rest auf (initPdToggles)
|
||||
function pdMoreButton(total, shown, [one, many]) {
|
||||
const rest = total - shown
|
||||
if (rest <= 0) return ''
|
||||
const text = `${rest} ${rest === 1 ? one : many} anzeigen`
|
||||
return `<button type="button" class="pd-more" data-pd-more data-more-label="${escapeAttr(text)}">${escapeHtml(text)}</button>`
|
||||
}
|
||||
|
||||
function renderChange(c, extra) {
|
||||
const d = new Date(c.date)
|
||||
const time = Number.isNaN(d.getTime()) ? '' : d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
|
||||
return `
|
||||
<li class="pd-change"${extra ? ' data-extra' : ''}>
|
||||
<div class="pd-change-head">
|
||||
<strong>${escapeHtml(c.title)}</strong>
|
||||
${time ? `<time datetime="${escapeAttr(c.date)}">${time}</time>` : ''}
|
||||
</div>
|
||||
${c.details?.length ? `<ul class="pd-change-details">${c.details.map((t) => `<li>${escapeHtml(t)}</li>`).join('')}</ul>` : ''}
|
||||
</li>`
|
||||
}
|
||||
|
||||
// Zeitleiste der letzten Aenderungen, nach Tagen gruppiert
|
||||
function renderChanges(changes) {
|
||||
if (!changes.length) return '<p class="pd-empty">Noch keine Änderungen an der Website.</p>'
|
||||
const groups = []
|
||||
changes.forEach((c, i) => {
|
||||
const d = new Date(c.date)
|
||||
const day = Number.isNaN(d.getTime()) ? '' : changeDayLabel(d)
|
||||
if (!groups.length || groups[groups.length - 1].day !== day) groups.push({ day, first: i, items: [] })
|
||||
groups[groups.length - 1].items.push(renderChange(c, i >= PD_CHANGES_SHOWN))
|
||||
})
|
||||
return `
|
||||
<div class="pd-collapsible">
|
||||
<div class="pd-timeline">
|
||||
${groups.map((g) => `
|
||||
<div class="pd-day"${g.first >= PD_CHANGES_SHOWN ? ' data-extra' : ''}>
|
||||
${g.day ? `<p class="pd-day-label">${escapeHtml(g.day)}</p>` : ''}
|
||||
<ul class="pd-changes">${g.items.join('')}</ul>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
${pdMoreButton(changes.length, PD_CHANGES_SHOWN, ['weitere Änderung', 'weitere Änderungen'])}
|
||||
</div>`
|
||||
}
|
||||
|
||||
function renderWorkEntry(e, extra) {
|
||||
const meta = [fmtIsoDay(e.date), e.person].filter(Boolean).map(escapeHtml).join(' · ')
|
||||
const body = e.changes?.length
|
||||
? `<ul class="pd-entry-changes">${e.changes.map((t) => `<li>${escapeHtml(t)}</li>`).join('')}</ul>`
|
||||
: e.text ? `<p class="pd-entry-text">${escapeHtml(e.text)}</p>` : ''
|
||||
return `
|
||||
<li class="pd-entry pd-kind-${escapeAttr(e.kind)}"${extra ? ' data-extra' : ''}>
|
||||
<span class="pd-entry-ico">${pdIcon(e.kind)}</span>
|
||||
<div class="pd-entry-main">
|
||||
<div class="pd-entry-head">
|
||||
<strong>${escapeHtml(e.label)}</strong>
|
||||
${meta ? `<span class="pd-entry-meta">${meta}</span>` : ''}
|
||||
</div>
|
||||
${body}
|
||||
</div>
|
||||
${e.minutes ? `<span class="pd-time" title="Arbeitszeit">${pdIcon('clock', 'pd-time-ico')}${fmtMinutes(e.minutes)}</span>` : ''}
|
||||
</li>`
|
||||
}
|
||||
|
||||
function renderWorkEntries(entries, emptyText) {
|
||||
if (!entries.length) return `<p class="pd-empty">${emptyText}</p>`
|
||||
return `
|
||||
<div class="pd-collapsible">
|
||||
<ul class="pd-work">${entries.map((e, i) => renderWorkEntry(e, i >= PD_ENTRIES_SHOWN)).join('')}</ul>
|
||||
${pdMoreButton(entries.length, PD_ENTRIES_SHOWN, ['weiteren Eintrag', 'weitere Einträge'])}
|
||||
</div>`
|
||||
}
|
||||
|
||||
function initPdToggles(root) {
|
||||
root.querySelectorAll('[data-pd-more]').forEach((btn) => btn.addEventListener('click', () => {
|
||||
const wrap = btn.closest('.pd-collapsible')
|
||||
const open = wrap.classList.toggle('is-open')
|
||||
btn.textContent = open ? 'Weniger anzeigen' : btn.dataset.moreLabel
|
||||
}))
|
||||
}
|
||||
|
||||
function renderFileOverview(files) {
|
||||
if (!files || !files.totalFiles) {
|
||||
return '<p class="muted">Keine Datei-Informationen verfügbar.</p>'
|
||||
return '<p class="pd-empty">Keine Datei-Informationen verfügbar.</p>'
|
||||
}
|
||||
const rows = (files.topLevel || []).map((entry) => `
|
||||
<tr>
|
||||
@@ -216,50 +354,53 @@ function renderFileOverview(files) {
|
||||
<td>${fmtBytes(entry.sizeBytes)}</td>
|
||||
</tr>`).join('')
|
||||
return `
|
||||
<p class="muted">${files.totalFiles} Dateien · ${fmtBytes(files.totalSizeBytes)}${files.truncated ? ' (Übersicht unvollständig – sehr großes Projekt)' : ''}</p>
|
||||
<table class="simple-table">
|
||||
<p class="pd-note">${files.totalFiles} Dateien · ${fmtBytes(files.totalSizeBytes)}${files.truncated ? ' (Übersicht unvollständig – sehr großes Projekt)' : ''}</p>
|
||||
<div class="kb-table-wrap"><table class="simple-table pd-files">
|
||||
<thead><tr><th>Ordner / Datei</th><th>Dateien</th><th>Größe</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>`
|
||||
</table></div>`
|
||||
}
|
||||
|
||||
function renderWorkEntries(work) {
|
||||
if (!work.ticket) {
|
||||
return '<p class="muted">Für dieses Projekt gibt es noch kein Ticket.</p>'
|
||||
}
|
||||
const head = `<p class="muted">Ticket #${escapeHtml(work.ticket.woid)} „${escapeHtml(work.ticket.topic)}" · Status: ${escapeHtml(work.ticket.status)}</p>`
|
||||
if (!work.worksheets.length) {
|
||||
return `${head}<p class="muted">Noch keine dokumentierten Arbeiten.</p>`
|
||||
}
|
||||
return head + work.worksheets.map((ws) => `
|
||||
<div class="activity-item">
|
||||
<div>
|
||||
${ws.isGit ? '<span class="pill pill-git">Git-Push</span>' : `<span class="pill pill-muted">${escapeHtml(ws.serviceType || 'Arbeit')}</span>`}
|
||||
<strong> ${escapeHtml(ws.employeeName)}</strong>
|
||||
<span class="muted"> · ${fmtDateShort(ws.date)}</span>
|
||||
${ws.totalTime ? `<span class="pill pill-ok">${fmtMinutes(ws.totalTime)}</span>` : ''}
|
||||
${ws.pending ? '<span class="pill pill-muted">Zeiterfassung folgt</span>' : ''}
|
||||
</div>
|
||||
<div class="activity-ws">${escapeHtml(ws.details)}</div>
|
||||
</div>`).join('')
|
||||
function pdSection(icon, title, body) {
|
||||
return `<section class="pd-section"><h3 class="pd-title">${pdIcon(icon)}${title}</h3>${body}</section>`
|
||||
}
|
||||
|
||||
async function loadProjectDetails(project, box) {
|
||||
box.innerHTML = '<p class="muted">Lade Details…</p>'
|
||||
// stillWanted: die Handy-Seite teilt sich einen Container fuer alle Projekte; eine spaete Antwort fuer
|
||||
// ein inzwischen verlassenes Projekt darf die Seite des naechsten nicht ueberschreiben
|
||||
async function loadProjectDetails(project, box, { stillWanted = () => true } = {}) {
|
||||
box.innerHTML = '<p class="pd-empty">Details werden geladen…</p>'
|
||||
const [git, files, work] = await Promise.all([
|
||||
api(`/api/projects/${encodeURIComponent(project.id)}/git`).catch(() => null),
|
||||
api(`/api/projects/${encodeURIComponent(project.id)}/files`).catch(() => null),
|
||||
api(`/api/projects/${encodeURIComponent(project.id)}/work`).catch(() => null),
|
||||
])
|
||||
if (!stillWanted()) return
|
||||
const order = work?.order
|
||||
|
||||
box.innerHTML = `
|
||||
<h3>Letzte Änderungen</h3>
|
||||
${git ? renderCommits(git) : '<p class="muted">Git-Daten derzeit nicht verfügbar.</p>'}
|
||||
<h3>Datei-Übersicht</h3>
|
||||
${files ? renderFileOverview(files) : '<p class="muted">Datei-Übersicht derzeit nicht verfügbar.</p>'}
|
||||
<h3>Arbeiten am Projekt</h3>
|
||||
${work ? renderWorkEntries(work) : '<p class="muted">Keine Ticket-Daten verfügbar.</p>'}
|
||||
`
|
||||
${order ? `<section class="pd-order pd-tone-${escapeAttr(order.status.tone)}">${renderOrder(order)}</section>` : ''}
|
||||
${pdSection('changes', 'Letzte Änderungen', git
|
||||
? renderChanges(git.changes || [])
|
||||
: '<p class="pd-empty">Die Änderungen können gerade nicht geladen werden.</p>')}
|
||||
${order || !work ? pdSection('work', 'Arbeiten am Projekt', work
|
||||
? renderWorkEntries(work.entries || [], 'Noch keine Arbeitszeit eingetragen. Was sich an der Website geändert hat, steht unter „Letzte Änderungen“.')
|
||||
: '<p class="pd-empty">Die Arbeiten können gerade nicht geladen werden.</p>') : ''}
|
||||
${pdSection('files', 'Dateien', files ? renderFileOverview(files) : '<p class="pd-empty">Die Datei-Übersicht kann gerade nicht geladen werden.</p>')}`
|
||||
initPdToggles(box)
|
||||
}
|
||||
|
||||
// App-Ansicht: Projektdetails als Unterseite von "Projekte" (Zurueck-Pfeil, eigener Verlaufseintrag).
|
||||
// Dasselbe Projekt erneut oeffnen zeigt die schon geladenen Details.
|
||||
const projectPage = { project: null }
|
||||
|
||||
function openProjectPage(project) {
|
||||
const view = document.getElementById('view-projekt')
|
||||
if (!view) return
|
||||
const reload = projectPage.project !== project
|
||||
projectPage.project = project
|
||||
view.querySelector('.kb-head h1').textContent = project.projectName || project.subdomain || 'Projekt'
|
||||
showView('projekt')
|
||||
if (reload) loadProjectDetails(project, view.querySelector('.kb-details'), { stillWanted: () => projectPage.project === project })
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------- Login-Redirects --- */
|
||||
@@ -479,6 +620,8 @@ async function loadCustomerInvoices() {
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- Aktivität --- */
|
||||
// Tab "Fortschritt": je Auftrag eine Karte mit Status, Fortschrittsleiste und Arbeiten
|
||||
// (Bausteine der Projektdetails; Texte kommen aufbereitet vom Server)
|
||||
async function loadCustomerActivity() {
|
||||
const body = document.getElementById('activity-body')
|
||||
if (!body) return
|
||||
@@ -489,23 +632,11 @@ async function loadCustomerActivity() {
|
||||
return
|
||||
}
|
||||
body.innerHTML = tickets.map((t) => `
|
||||
<div class="activity-item">
|
||||
<div>
|
||||
<strong>${escapeHtml(t.woid ? `#${t.woid} ` : '')}${escapeHtml(t.topic || '–')}</strong>
|
||||
<span class="activity-status">${escapeHtml(t.status || '–')}</span>
|
||||
<span class="muted"> · ${fmtDateShort(t.createdAt)}</span>
|
||||
</div>
|
||||
${(t.worksheets || []).map((w) => `
|
||||
<div style="margin: 6px 0 0 14px;">
|
||||
${w.isGit ? '<span class="pill pill-git">Git-Push</span>' : `<span class="pill pill-muted">${escapeHtml(w.serviceType || 'Arbeit')}</span>`}
|
||||
${w.employeeName ? ` <strong>${escapeHtml(w.employeeName)}</strong>` : ''}
|
||||
<span class="muted"> · ${fmtDateShort(w.date)}</span>
|
||||
${Number(w.totalTime) ? `<span class="pill pill-ok">${fmtMinutes(w.totalTime)}</span>` : ''}
|
||||
${w.pending ? '<span class="pill pill-muted">Zeiterfassung folgt</span>' : ''}
|
||||
${w.details ? `<div class="activity-ws">${escapeHtml(w.details)}</div>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>`).join('')
|
||||
<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)
|
||||
} catch (err) {
|
||||
body.innerHTML = `<p class="muted">Aktivität konnte nicht geladen werden: ${escapeHtml(err.message)}</p>`
|
||||
}
|
||||
@@ -1143,7 +1274,8 @@ async function loadTakeoverCard({ abo, notice = '' } = {}) {
|
||||
/* ---------------------------------------------------------- Navigation --- */
|
||||
// Ansichten im Inhaltsbereich; Hash fuer Direktlinks (z. B. aus Mails: #abo, #gutschrift).
|
||||
// "wartung" gibt es nur in der App-Ansicht (Tab), am Desktop zeigt sie das Wartungs-Abo.
|
||||
const VIEW_HASHES = { projects: '', activity: 'fortschritt', invoices: 'rechnungen', chat: 'chat', wartung: 'wartung', abo: 'abo', takeover: 'gutschrift' }
|
||||
// "projekt" (Projektdetails als eigene Seite) gibt es ebenfalls nur in der App-Ansicht.
|
||||
const VIEW_HASHES = { projects: '', activity: 'fortschritt', invoices: 'rechnungen', chat: 'chat', wartung: 'wartung', abo: 'abo', takeover: 'gutschrift', projekt: 'projekt' }
|
||||
const viewLoaders = { activity: loadCustomerActivity, chat: initChat }
|
||||
const loadedViews = new Set()
|
||||
|
||||
@@ -1291,8 +1423,9 @@ async function initDashboardPage() {
|
||||
// Gleiche Bedingung wie der Abschnitt "App-Ansicht" in kundenbereich.css (auch Handys quer)
|
||||
const appLayout = window.matchMedia('(max-width: 880px), (pointer: coarse) and (max-height: 500px)')
|
||||
const coarsePointer = window.matchMedia('(pointer: coarse)')
|
||||
// Unterseite -> Tab, zu dem sie gehoert
|
||||
const APP_PARENT = { abo: 'wartung', takeover: 'wartung' }
|
||||
// Unterseite -> Tab, zu dem sie gehoert (und Text neben dem Zurueck-Pfeil)
|
||||
const APP_PARENT = { abo: 'wartung', takeover: 'wartung', projekt: 'projects' }
|
||||
const APP_PARENT_LABEL = { wartung: 'Wartung', projects: 'Projekte' }
|
||||
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])'
|
||||
|
||||
@@ -1339,7 +1472,13 @@ function updateBarState() {
|
||||
|
||||
function updateAppBar() {
|
||||
const sub = isAppLayout() && Boolean(APP_PARENT[appShell.view])
|
||||
document.getElementById('kb-back')?.classList.toggle('hidden', !sub)
|
||||
const back = document.getElementById('kb-back')
|
||||
back?.classList.toggle('hidden', !sub)
|
||||
if (back && sub) {
|
||||
const label = APP_PARENT_LABEL[APP_PARENT[appShell.view]] || 'Zurück'
|
||||
back.querySelector('span').textContent = label
|
||||
back.setAttribute('aria-label', `Zurück zu ${label}`)
|
||||
}
|
||||
document.body.classList.toggle('kb-subpage', sub)
|
||||
const title = document.getElementById('kb-app-title')
|
||||
if (title) title.textContent = currentViewTitle()?.textContent.trim() || ''
|
||||
@@ -1352,6 +1491,11 @@ function onAppViewShown(name) {
|
||||
showView('abo', { updateHash: !appShell.fromHistory })
|
||||
return
|
||||
}
|
||||
// Projektdetails als Seite nur in der App-Ansicht und nur mit gewaehltem Projekt (nicht per Direktlink)
|
||||
if (name === 'projekt' && (!isAppLayout() || !projectPage.project)) {
|
||||
showView('projects', { updateHash: !appShell.fromHistory })
|
||||
return
|
||||
}
|
||||
const prev = appShell.view
|
||||
const parent = APP_PARENT[name]
|
||||
const nav = name === prev ? 'same' : parent === prev ? 'push' : APP_PARENT[prev] === name ? 'back' : 'tab'
|
||||
@@ -1368,9 +1512,9 @@ function onAppViewShown(name) {
|
||||
})
|
||||
|
||||
if (isAppLayout()) {
|
||||
// Unterseite mit eigenem Verlaufseintrag: "Zurück" (auch die Android-Geste) führt zur Wartung
|
||||
// Unterseite mit eigenem Verlaufseintrag: "Zurück" (auch die Android-Geste) führt zum Tab darüber (Wartung bzw. Projekte)
|
||||
if (nav === 'push' && !appShell.fromHistory) {
|
||||
window.history.replaceState(null, '', `#${VIEW_HASHES[parent]}`)
|
||||
window.history.replaceState(null, '', VIEW_HASHES[parent] ? `#${VIEW_HASHES[parent]}` : window.location.pathname)
|
||||
window.history.pushState({ kbSub: name }, '', `#${VIEW_HASHES[name]}`)
|
||||
}
|
||||
// Tabs merken sich ihre Scrollposition, Unterseiten beginnen oben, erneutes Tippen scrollt nach oben
|
||||
@@ -1671,6 +1815,7 @@ function initAppShell() {
|
||||
onMediaChange(appLayout, () => {
|
||||
placeSideCards()
|
||||
if (!isAppLayout() && appShell.view === 'wartung') showView('abo')
|
||||
if (!isAppLayout() && appShell.view === 'projekt') showView('projects')
|
||||
updateAppBar()
|
||||
renderInstallOffers()
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user