Files
Webklar-Kundenbereich/public/neu/ansicht-website.js
T
ClaudeandClaude Sonnet 5.5 2064bddba6 Kundenansicht neu (Aufträge): "Was wir zuletzt erledigt haben" als Zeitleiste auf der Website-Seite
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>
2026-10-17 15:07:34 +00:00

135 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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' })
}