/* Kundenansicht neu (P2): Bereich Website. Projektliste mit Vorschau, offene Rechnung, Installationshinweis und Aktivität; als Etage "projekt" die Projektdetails (#projekt?projekt=). Gezeichnet wird mit h() und Textknoten. */ import { api } from './api.js' import { h, leeren } from './dom.js' import { K, ersetze, zuAnsicht, meldeTitel, fmtDatumZeit, sichereAdresse } from './ansicht-hilfen.js' import { baueProjektDetails, ladeProjektDetails } from './website-details.js' import { baueAktivitaet } from './auftrags-zeitleiste.js' import { ladeOffeneRechnungen, rechnungOffenZeile } from './rechnung-offen.js' import { starteInstallQuelle, installAngebot, baueInstallHinweis, istAusgeblendet, merkeAusgeblendet } from './install-hinweis.js' import { HANDY_ABFRAGE } from './leiste.js' 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 = sichereAdresse(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) ersetze(wurzel, zeile, installPlatz(), liste, aktivitaetSektion) if (route?.scroll === 'aktivitaet') aktivitaetSektion.scrollIntoView({ block: 'start' }) }