Kundenansicht neu P2: Ansicht Website mit Projektdetails als Etage

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 16:55:16 +00:00
1 parent e036d24ae5
commit a3831040ba
4 files changed
+208 -11

No files matched your search

+130 -4
View File
@@ -1,7 +1,133 @@
/* Kundenansicht neu: Platzhalter, den das nächste Paket füllt (Stub aus P1). */
/* Kundenansicht neu (P2): Bereich Website. Projektliste mit Vorschau, offene Rechnung, Installationshinweis und Aktivität;
als Etage "projekt" die Projektdetails (#projekt?projekt=<id>). Gezeichnet wird mit h() und Textknoten. */
import { h } from './dom.js'
import { api } from './api.js'
import { h, leeren } from './dom.js'
import { K, zuAnsicht, meldeTitel, fmtDatumZeit, safeUrl } from './ansicht-hilfen.js'
import { baueAktivitaet, baueProjektDetails, ladeProjektDetails } from './website-details.js'
import { ladeOffeneRechnungen, rechnungOffenZeile } from './rechnung-offen.js'
import { starteInstallQuelle, installAngebot, baueInstallHinweis, istAusgeblendet, merkeAusgeblendet } from './install-hinweis.js'
import { HANDY_ABFRAGE } from './leiste.js'
export async function zeigeWebsite(wurzel, { route, me, nav }) {
wurzel.replaceChildren(h('p', { class: 'kn-leer' }, 'Website (folgt)'))
const VORSCHAU_BASISBREITE = 1280
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
/** Vorschau fertig: Status ready oder deployed und eine Adresse (wie app.js isProjectPreviewReady). */
export function vorschauBereit(project) {
const status = String(project?.provisioningStatus || project?.status || '').toLowerCase()
return ['ready', 'deployed'].includes(status) && Boolean(project?.previewUrl)
}
/** Ton und Text der Status-Pille eines Projekts. */
export function projektStatus(project) {
if (vorschauBereit(project)) return { ton: 'gruen', text: 'Vorschau online' }
const status = String(project?.provisioningStatus || project?.status || '').toLowerCase()
if (status.includes('fail')) return { ton: 'rot', text: 'Vorschau gestört' }
if (project?.previewUrl) return { ton: 'orange', text: 'Vorschau wird vorbereitet' }
return { ton: 'grau', text: 'Noch keine Website' }
}
// Der Frame zeigt die Seite in 1280 px Breite und wird auf die Breite des Rahmens verkleinert
function skaliereVorschau(rahmen) {
const frame = rahmen.querySelector('iframe')
if (!frame) return
const anwenden = () => {
const breite = rahmen.clientWidth
if (breite && frame.style) frame.style.transform = `scale(${breite / VORSCHAU_BASISBREITE})`
}
if (typeof ResizeObserver !== 'undefined') new ResizeObserver(anwenden).observe(rahmen)
anwenden()
}
function hostVon(url) {
try { return new URL(url).hostname || '' } catch { return '' }
}
const fakt = (titel, wert) => h('div', null, h('dt', null, titel), h('dd', null, wert))
function projektZeile(projekt) {
const bereit = vorschauBereit(projekt)
const url = safeUrl(projekt.previewUrl)
const status = projektStatus(projekt)
const rahmen = bereit && url
? h('div', { class: 'kn-vorschau', 'data-vorschau': true }, h('iframe', { src: url, loading: 'lazy', scrolling: 'no', tabindex: '-1', 'aria-hidden': 'true', title: 'Vorschau deiner Website' }))
: h('div', { class: 'kn-vorschau kn-vorschau-leer' }, projekt.previewUrl ? 'Vorschau wird vorbereitet …' : 'Noch keine Website veröffentlicht')
if (bereit && url) skaliereVorschau(rahmen)
const host = hostVon(url)
return h('li', { class: 'kn-projekt', 'data-projekt': projekt.id },
rahmen,
h('div', { class: 'kn-projekt-koerper' },
h('div', { class: 'kn-projekt-kopf' }, h('h2', { class: 'kn-projekt-name' }, projekt.projectName || projekt.subdomain || 'Projekt'), h('span', { class: K.pille[status.ton] }, status.text)),
h('dl', { class: 'kn-fakten' },
fakt('Vorschau-Adresse', host || '–'),
fakt('Live-Domain', projekt.liveDomain || 'noch nicht verbunden'),
fakt('Letzte Änderung', projekt.updatedAt ? fmtDatumZeit(projekt.updatedAt) : '–')),
h('div', { class: 'kn-projekt-links' },
bereit && url ? h('a', { class: K.knopf, href: url, target: '_blank', rel: 'noopener noreferrer' }, 'Website öffnen') : null,
h('a', { class: K.knopf, ...zuAnsicht('projekt', projekt.id) }, 'Projektdetails'))))
}
// Installationshinweis: ersetzt sich selbst, wenn sich Installierbarkeit oder Auswahl ändern
function installPlatz() {
const platz = h('div', { class: 'kn-install-platz' })
const win = window
const quelle = starteInstallQuelle(win)
const handy = () => (typeof win.matchMedia === 'function' ? Boolean(win.matchMedia(HANDY_ABFRAGE)?.matches) : false)
const zeichne = () => {
const angebot = installAngebot({ appLayout: handy(), standalone: quelle.standalone(), canPrompt: quelle.kannPrompt(), ios: quelle.ios, dismissed: istAusgeblendet() })
leeren(platz)
if (angebot.zeigen) platz.append(baueInstallHinweis({ art: angebot.art, onInstall: () => quelle.installiere(), onDismiss: () => { merkeAusgeblendet(); zeichne() } }))
}
zeichne()
aktualisierer.set(quelle, zeichne)
if (!angemeldet.has(quelle)) { angemeldet.add(quelle); quelle.beiAenderung(() => aktualisierer.get(quelle)?.()) }
return platz
}
const aktualisierer = new WeakMap()
const angemeldet = new WeakSet()
function aktivitaetAbschnitt(antwort, jetzt) {
const inhalt = antwort.fehler
? h('p', { class: 'kn-hinweis' }, `Aktivität konnte nicht geladen werden: ${antwort.fehler}`)
: baueAktivitaet(antwort.tickets, jetzt)
return h('section', { class: 'kn-sektion', 'data-abschnitt': 'aktivitaet' }, h('h2', { class: K.abschnitt }, 'Was wir zuletzt erledigt haben'), inhalt)
}
const projekteLaden = () => api('/api/projects').then((a) => ({ liste: Array.isArray(a?.projects) ? a.projects : [] }), (f) => ({ fehler: f.message }))
const aktivitaetLaden = () => api('/api/activity').then((a) => ({ tickets: Array.isArray(a?.tickets) ? a.tickets : [] }), (f) => ({ fehler: f.message }))
const projektId = (query) => { const p = new URLSearchParams(query || ''); return p.get('projekt') || p.get('id') || '' }
export async function zeigeWebsite(wurzel, { route, me, nav } = {}) {
const nr = (laeufe.get(wurzel) ?? 0) + 1
laeufe.set(wurzel, nr)
const aktuell = () => laeufe.get(wurzel) === nr
leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …'))
const jetzt = new Date()
if (route?.etage === 'projekt') {
const projekte = await projekteLaden()
if (!aktuell()) return
if (projekte.fehler) { leeren(wurzel).append(h('p', { class: K.fehler }, `Projekte konnten nicht geladen werden: ${projekte.fehler}`)); return }
const wunsch = projektId(route.query)
const projekt = wunsch ? projekte.liste.find((p) => p.id === wunsch) : projekte.liste[0]
if (!projekt) { leeren(wurzel).append(h('p', { class: K.leer }, projekte.liste.length ? 'Dieses Projekt wurde nicht gefunden.' : 'Noch keine Website-Projekte zugewiesen.')); return }
meldeTitel(wurzel, projekt.projectName || projekt.subdomain || 'Projekt')
const details = await ladeProjektDetails(projekt.id)
if (!aktuell()) return
leeren(wurzel).append(baueProjektDetails(details, jetzt))
return
}
const [projekte, aktivitaet, offen] = await Promise.all([projekteLaden(), aktivitaetLaden(), ladeOffeneRechnungen()])
if (!aktuell()) return
const zeile = rechnungOffenZeile(offen)
const liste = projekte.fehler
? h('p', { class: K.fehler }, `Projekte konnten nicht geladen werden: ${projekte.fehler}`)
: projekte.liste.length
? h('ul', { class: 'kn-projekte' }, projekte.liste.map(projektZeile))
: h('p', { class: K.leer }, 'Noch keine Website-Projekte zugewiesen.')
const aktivitaetSektion = aktivitaetAbschnitt(aktivitaet, jetzt)
leeren(wurzel).append(zeile, installPlatz(), liste, aktivitaetSektion)
if (route?.scroll === 'aktivitaet') aktivitaetSektion.scrollIntoView({ block: 'start' })
}
+4 -5
View File
@@ -169,10 +169,9 @@ export function baueProjektDetails({ git, files, work }, jetzt = new Date()) {
return h('div', { class: 'kn-details' },
order ? h('section', { class: 'kn-sektion' }, baueAuftrag(order, work?.totalMinutes || 0)) : null,
abschnitt('Letzte Änderungen', git ? baueAenderungen(git.changes || [], jetzt) : leerText('Die Änderungen können gerade nicht geladen werden.')),
order || !work
? abschnitt('Arbeiten am Projekt', work
? baueArbeiten(work.entries || [], 'Noch keine Arbeitszeit eingetragen. Was sich an der Website geändert hat, steht unter „Letzte Änderungen“.')
: leerText('Die Arbeiten können gerade nicht geladen werden.'))
: null,
// Der Abschnitt steht immer da (Test Etage projekt): ohne Auftrag zeigt er den Leertext, bei Ausfall den Fehlertext
abschnitt('Arbeiten am Projekt', work
? baueArbeiten(work.entries || [], 'Noch keine Arbeitszeit eingetragen. Was sich an der Website geändert hat, steht unter „Letzte Änderungen“.')
: leerText('Die Arbeiten können gerade nicht geladen werden.')),
abschnitt('Dateien', files ? baueDateien(files) : leerText('Die Datei-Übersicht kann gerade nicht geladen werden.')))
}