Je Auftrag eine Überschrift (Titel fett, Status-Pille), ein schmaler Balken mit vier Abschnitten (Stufennamen als Text für Screenreader), bei Rückfrage die Zeile "Wir brauchen eine Info von dir · Im Chat antworten" und eine Zeitleiste mit Haarlinie (Datum TT.MM. und Kurztext, drei sichtbar, "mehr anzeigen"). Fünf Aufträge offen, der Rest hinter "Ältere Aufträge anzeigen". Die Auftragsnummer steht nicht mehr im Text (ruhiger), bleibt als data-auftrag. Die alte baueAktivitaet und ihre toten CSS-Regeln entfallen; die Projektdetails (baueAuftrag, baueArbeiten) bleiben unverändert. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
135 lines
7.3 KiB
JavaScript
135 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 { 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' })
|
||
}
|