diff --git a/public/app.js b/public/app.js index 8382334..c9492a0 100644 --- a/public/app.js +++ b/public/app.js @@ -85,7 +85,7 @@ function featuresForProject(features, projectId) { const PREVIEW_BASE_WIDTH = 1280 function setupPreviewFrame(frame) { - const view = frame.querySelector('.preview-frame__view') + const view = frame.querySelector('iframe') if (!view) return const apply = () => { const w = frame.clientWidth @@ -99,38 +99,72 @@ function setupPreviewFrame(frame) { apply() } +function fmtDateTimeLong(value) { + if (!value) return '–' + const d = new Date(value) + if (Number.isNaN(d.getTime())) return escapeHtml(value) + return d.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) +} + +// "autohaus-meier.project.webklar.com" -> Subdomain hervorheben, Rest gedimmt +function previewHostHtml(url) { + try { + const host = new URL(url).hostname + const dot = host.indexOf('.') + return dot > 0 + ? `${escapeHtml(host.slice(0, dot))}${escapeHtml(host.slice(dot))}` + : escapeHtml(host) + } catch { + return '–' + } +} + +function projectState(project) { + if (isProjectPreviewReady(project)) return ['kb-state-ok', 'Vorschau online'] + const s = String(project.provisioningStatus || project.status || '').toLowerCase() + if (s.includes('fail')) return ['kb-state-off', 'Vorschau gestört'] + if (project.previewUrl) return ['kb-state-wait', 'Vorschau wird vorbereitet'] + return ['kb-state-off', 'Noch keine Website'] +} + +const FEATURE_LABELS = { preview: 'Vorschau', chat: 'Chat', invoices: 'Rechnungen' } + function renderProjectCard(project, features) { const li = document.createElement('li') - li.className = 'project-card' - const projectFeatures = featuresForProject(features, project.id) - const featureHtml = projectFeatures.length - ? `
${projectFeatures.map((f) => `${escapeHtml(f.featureKey)}`).join('')}
` - : '' - + li.className = 'kb-project' const ready = isProjectPreviewReady(project) const url = project.previewUrl ? escapeAttr(project.previewUrl) : '' - const previewHtml = ready && url - ? ` - - - Webseite öffnen ↗ - ` - : `
${project.previewUrl ? 'Vorschau wird vorbereitet …' : 'Noch keine Webseite veröffentlicht'}
` + const [stateClass, stateText] = projectState(project) + const extraChips = featuresForProject(features, project.id) + .map((f) => FEATURE_LABELS[f.featureKey] || f.featureKey) + .filter((label) => label && !['Vorschau', 'Chat', 'Rechnungen'].includes(label)) li.innerHTML = ` - ${previewHtml} -

${escapeHtml(project.projectName || project.subdomain || 'Projekt')}

-
-
Subdomain
${escapeHtml(project.subdomain || '–')}
-
Live-Domain
${project.liveDomain ? escapeHtml(project.liveDomain) : '–'}
-
Status
${escapeHtml(project.status || '–')}
-
- ${ready && url ? `

Projekt-Webseite öffnen ↗

` : ''} - ${featureHtml} -
- + ${ready && url + ? ` + + Website öffnen ↗ + ` + : `
${project.previewUrl ? 'Vorschau wird vorbereitet …' : 'Noch keine Website veröffentlicht'}
`} +
+
+

${escapeHtml(project.projectName || project.subdomain || 'Projekt')}

+ ${stateText} +
+
+
Vorschau-Adresse
${project.previewUrl ? previewHostHtml(project.previewUrl) : '–'}
+
Live-Domain
${project.liveDomain ? escapeHtml(project.liveDomain) : 'noch nicht verbunden'}
+
Letzte Änderung
${project.updatedAt ? fmtDateTimeLong(project.updatedAt) : '–'}
+
+
+ ${ready && url ? `Vorschau` : ''} + + + ${extraChips.map((label) => `${escapeHtml(label)}`).join('')} +
+ +
- ` const frame = li.querySelector('[data-preview-frame]') @@ -142,7 +176,7 @@ function renderProjectCard(project, features) { btn.addEventListener('click', async () => { const isHidden = box.classList.contains('hidden') box.classList.toggle('hidden', !isHidden) - btn.textContent = isHidden ? 'Details ausblenden' : 'Details anzeigen' + btn.textContent = isHidden ? 'Projektdetails ausblenden' : 'Projektdetails anzeigen' if (isHidden && !loaded) { loaded = true await loadProjectDetails(project, box) @@ -271,7 +305,10 @@ async function canAccessPreviewUrl(url) { } async function resolvePostLoginTarget() { - const next = safeRedirectUrl(getQueryParam('next')) + // Direktlink auf eine Ansicht im Kundenbereich (z. B. /dashboard.html#gutschrift) + const raw = getQueryParam('next') + if (/^\/dashboard\.html(#[a-z]+)?$/.test(raw)) return raw + const next = safeRedirectUrl(raw) if (next && (await canAccessPreviewUrl(next))) { return next } @@ -388,7 +425,7 @@ function renderViewAsBanner(me) { banner.classList.remove('hidden') banner.innerHTML = ` Admin-Ansicht: Du siehst das Portal als ${escapeHtml(me.customer?.name || 'Kunde')}. - + ` document.getElementById('exit-viewas-btn')?.addEventListener('click', async () => { await api('/api/portal-admin/exit-view-as', { method: 'POST' }).catch(() => {}) @@ -402,6 +439,13 @@ async function loadCustomerInvoices() { if (!body) return try { const data = await api('/api/invoices') + // Zaehler in der Seitenleiste: Rechnungen mit offenem Betrag + const openCount = (data.invoices || []).filter((i) => Number(i.balance) > 0).length + const badge = document.getElementById('invoices-badge') + if (badge) { + badge.textContent = openCount + badge.classList.toggle('hidden', !openCount) + } if (!data.linked) { body.innerHTML = '

Für dein Konto sind noch keine Rechnungen hinterlegt.

' return @@ -411,7 +455,7 @@ async function loadCustomerInvoices() { return } body.innerHTML = ` - +
${data.invoices.map((i) => ` @@ -427,7 +471,7 @@ async function loadCustomerInvoices() { `).join('')} -
Nr.DatumBetragOffenStatus
` +
` } catch (err) { body.innerHTML = `

Rechnungen konnten nicht geladen werden: ${escapeHtml(err.message)}

` } @@ -515,7 +559,7 @@ async function pollChatMessages() { } function isChatTabActive() { - return document.querySelector('.portal-tab[data-tab="chat"]')?.classList.contains('active') + return Boolean(document.getElementById('view-chat')?.classList.contains('active')) } async function sendChatMessage() { @@ -581,7 +625,7 @@ async function pollChatBadge() { /* -------------------------------------------------------------- Dashboard --- */ // --------------------------------------------------------------------------- -// Wartungs-Abo (Startseite): Angebot + Bestellung bzw. Stundenkonto +// Wartungs-Abo: Karte in der Seitenleiste + Ansicht (Bestellung bzw. Stundenkonto) // --------------------------------------------------------------------------- const ABO_FEATURES = [ @@ -591,6 +635,9 @@ const ABO_FEATURES = [ ['1 Stunde Arbeitszeit pro Monat', 'nicht genutzte Zeit bleibt 12 Monate verfügbar'], ] +// Kurzfassung fuer die Karte in der Seitenleiste +const ABO_SIDE_FEATURES = ['Betrieb & Pflege, Server in Deutschland', '1 Domain inklusive', '1 E-Mail-Postfach, 5 GB', '1 Std. Arbeitszeit pro Monat'] + function fmtHours(min) { const n = Math.max(0, Number(min || 0)) const h = Math.floor(n / 60) @@ -598,6 +645,15 @@ function fmtHours(min) { return m ? `${h}:${String(m).padStart(2, '0')} h` : `${h} h` } +function euroShort(cents) { + return cents % 100 === 0 ? `${cents / 100} €` : fmtEuro(cents / 100) +} + +function fmtDayMonth(value) { + const d = new Date(value) + return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }) +} + function aboFeaturesHtml() { return `