134 lines
7.3 KiB
JavaScript
134 lines
7.3 KiB
JavaScript
/* 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 { api } from './api.js'
|
||
import { h, leeren } from './dom.js'
|
||
import { K, ersetze, zuAnsicht, meldeTitel, fmtDatumZeit, sichereAdresse } 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'
|
||
|
||
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' })
|
||
}
|