Analyse: im Abo-Modell Linkzeile und Zielgruppen-Häkchen, Hosting berechtigt, Reiter in der Etage
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c37faabcd3
commit
5534655671
2 files changed
+171
-12
No files matched your search
+36
-12
@@ -9,11 +9,18 @@ import { analyseOf, pickAnalyseProject, analyseSel, bannerColor, BANNER_PREVIEW_
|
||||
import { headerHtml, tabsHtml, overviewHtml, rankCardHtml, skeletonHtml, errorHtml, settingsHtml, chartTipText } from './analyse-cards.js'
|
||||
import { HEAT_DEVICES, deviceHtml, heatCaption } from './geraete.js'
|
||||
import { ZU_VERTRAEGE } from './vertraege-link.js'
|
||||
import { meldeReiter, reiterEintraege } from './fn-reiter.js'
|
||||
|
||||
const PRESET_COLORS = ['#111111', '#1d4ed8', '#047857', '#b91c1c', '#7c3aed', '#c2410c']
|
||||
const HEX_RE = /^#[0-9a-f]{6}$/i
|
||||
const nf = new Intl.NumberFormat('de-DE')
|
||||
const TABS = ['uebersicht', 'heatmap', 'einstellungen']
|
||||
// Abo-Modell: die Reiter zeichnet die Etage (kn:reiter, public/fn-reiter.js); Reihenfolge und Wortlaut wie tabsHtml
|
||||
const ANALYSE_REITER = [{ key: 'uebersicht', label: 'Übersicht' }, { key: 'heatmap', label: 'Heatmap' }, { key: 'einstellungen', label: 'Einstellungen' }]
|
||||
const reiterLabel = (key) => ANALYSE_REITER.find((r) => r.key === key)?.label || ANALYSE_REITER[0].label
|
||||
// Abo-Modell, Texte A1 bis A3
|
||||
const ZIELGRUPPE = 'Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren.'
|
||||
const KEIN_ABO_ANALYSE = 'Die Analyse ist im Hosting- und im Wartungs-Abo enthalten.'
|
||||
const RANGES = [7, 30, 90]
|
||||
const CLOCK = new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
|
||||
|
||||
@@ -21,10 +28,14 @@ const showError = (root) => (err) => { root.innerHTML = `<p class="muted">${esc(
|
||||
|
||||
export async function initAnalyse(root, { projectId } = {}) {
|
||||
if (!root) return
|
||||
if (projectId) analyseSel.projectId = projectId
|
||||
const { projects = [] } = await request('/api/funktionen')
|
||||
const wunsch = projectId || root.dataset?.wunschProjekt || '' // die Kachel in Funktionen legt ihr Projekt hier ab
|
||||
if (root.dataset) delete root.dataset.wunschProjekt
|
||||
if (wunsch) analyseSel.projectId = wunsch
|
||||
const { projects = [], modell } = await request('/api/funktionen')
|
||||
const abo = modell === 'abo' // strikt: nur dann Reiter in der Etage und die Texte A1 bis A3
|
||||
const usable = projects.filter((p) => !analyseOf(p).comingSoon)
|
||||
if (!usable.length) {
|
||||
if (abo) meldeReiter(root, { eintraege: [] })
|
||||
root.innerHTML = '<div class="an"><section class="an-card an-state"><p class="an-empty">Die Analyse ist für deine Websites noch nicht freigeschaltet.</p></section></div>'
|
||||
return
|
||||
}
|
||||
@@ -33,7 +44,10 @@ export async function initAnalyse(root, { projectId } = {}) {
|
||||
const a = analyseOf(project)
|
||||
const active = a.state === 'active'
|
||||
const tab = TABS.includes(analyseSel.tab) ? analyseSel.tab : 'uebersicht'
|
||||
root.innerHTML = `<div class="an">${headHtml(project, usable, { showRange: active && tab === 'uebersicht' })}${active ? tabsHtml(tab) : ''}<div id="an-stage"${active ? ` role="tabpanel" aria-labelledby="an-tab-${tab}"` : ''}></div></div>`
|
||||
if (abo && !active) meldeReiter(root, { eintraege: [] }) // ohne Cockpit keine Reiter; das Cockpit meldet seine gleich nach dem Zeichnen
|
||||
const leiste = active && !abo ? tabsHtml(tab) : ''
|
||||
const panel = !active ? '' : abo ? ` role="tabpanel" aria-label="${reiterLabel(tab)}"` : ` role="tabpanel" aria-labelledby="an-tab-${tab}"`
|
||||
root.innerHTML = `<div class="an">${headHtml(project, usable, { showRange: active && tab === 'uebersicht' })}${leiste}<div id="an-stage"${panel}></div></div>`
|
||||
const stage = root.querySelector('#an-stage')
|
||||
const reload = () => initAnalyse(root).catch(showError(root))
|
||||
// Website wechseln: der Reiter bleibt (analyseSel.tab)
|
||||
@@ -42,11 +56,11 @@ export async function initAnalyse(root, { projectId } = {}) {
|
||||
analyseSel.projectId = ev.target.value
|
||||
reload()
|
||||
})
|
||||
if (a.state === 'active') mountCockpit({ an: root.querySelector('.an'), stage, project, usable, reload })
|
||||
if (a.state === 'active') mountCockpit({ an: root.querySelector('.an'), stage, project, usable, reload, abo, root })
|
||||
else if (a.state === 'disabled') stage.innerHTML = '<section class="an-card an-state"><h3>Analyse ist abgeschaltet</h3><p>Für diese Website ist die Analyse im Moment gesperrt. <button type="button" class="kb-link" data-view="chat">Schreib uns im Chat</button>, dann klären wir das.</p></section>'
|
||||
else if (a.state === 'unknown') stage.innerHTML = '<section class="an-card an-state"><p>Die Daten konnten gerade nicht geladen werden.</p></section>'
|
||||
else if (!a.eligible) stage.innerHTML = `<section class="an-card an-state"><h3>Analyse nicht verfügbar</h3><p>${esc(a.reasonText || 'Für diese Website gerade nicht verfügbar.')}${a.reason === 'kein_abo' ? ` <button type="button" class="${ZU_VERTRAEGE.klasse}" data-view="${ZU_VERTRAEGE.view}">${ZU_VERTRAEGE.text}</button>` : ''}</p></section>`
|
||||
else renderAnalyseActivation(stage, project, reload)
|
||||
else if (!a.eligible) stage.innerHTML = `<section class="an-card an-state"><h3>Analyse nicht verfügbar</h3><p>${esc(abo && a.reason === 'kein_abo' ? KEIN_ABO_ANALYSE : a.reasonText || 'Für diese Website gerade nicht verfügbar.')}${a.reason === 'kein_abo' ? ` <button type="button" class="${ZU_VERTRAEGE.klasse}" data-view="${ZU_VERTRAEGE.view}">${ZU_VERTRAEGE.text}</button>` : ''}</p></section>`
|
||||
else renderAnalyseActivation(stage, project, reload, { modell })
|
||||
}
|
||||
|
||||
// Kopfleiste (analyse-cards.js); Zeitraum nur in der Uebersicht, "Stand" = Uhrzeit des letzten Ladens, today = "Heute bisher" aus der Statistik
|
||||
@@ -73,7 +87,8 @@ function controllerBlock(controller) {
|
||||
return `<div class="an-ctl" id="an-ctl"><p>Auf der Datenschutz-Zusatzseite Deiner Website erscheinen als Verantwortlicher:</p><p><strong>${controllerLines(controller).map(esc).join(' · ')}</strong></p><p>Stimmt etwas nicht? Schreib uns im Chat, bevor Du einschaltest.</p></div>`
|
||||
}
|
||||
|
||||
export function renderAnalyseActivation(stage, project, reload) {
|
||||
export function renderAnalyseActivation(stage, project, reload, { modell } = {}) {
|
||||
const abo = modell === 'abo' // Abo-Modell: Linkzeile statt des langen Zustimmungssatzes, Zielgruppen-Häkchen, audienceConfirmed
|
||||
const a = analyseOf(project)
|
||||
const legal = a.legal || {}
|
||||
const controller = a.controller || { ok: false }
|
||||
@@ -91,7 +106,10 @@ export function renderAnalyseActivation(stage, project, reload) {
|
||||
<p class="an-hint hidden" id="an-hint">Diese Farbe ist für weiße Schrift zu hell. Der Banner nutzt dann Schwarz (#111111).</p>
|
||||
<div id="an-preview"></div>
|
||||
${controllerBlock(controller)}
|
||||
<label class="fn-consent"><input type="checkbox" id="an-avv"><span>Ich beauftrage WEBklar mit der Website-Analyse und stimme dem <a href="${esc(legal.avv || '')}" target="_blank" rel="noopener">Auftragsverarbeitungsvertrag Website-Analyse</a> und der <a href="${esc(legal.lb || '')}" target="_blank" rel="noopener">Leistungsbeschreibung Website-Analyse</a> (Fassung ${esc(a.avvVersion)}) zu. Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren.</span></label>
|
||||
${abo
|
||||
? `<p class="an-hint" id="an-legal">Es gelten <a href="${esc(legal.lb || '')}" target="_blank" rel="noopener">Leistungsbeschreibung</a> und <a href="${esc(legal.avv || '')}" target="_blank" rel="noopener">Auftragsverarbeitung</a> (Fassung ${esc(a.avvVersion)}).</p>
|
||||
<label class="fn-consent"><input type="checkbox" id="an-zielgruppe"><span>${ZIELGRUPPE}</span></label>`
|
||||
: `<label class="fn-consent"><input type="checkbox" id="an-avv"><span>Ich beauftrage WEBklar mit der Website-Analyse und stimme dem <a href="${esc(legal.avv || '')}" target="_blank" rel="noopener">Auftragsverarbeitungsvertrag Website-Analyse</a> und der <a href="${esc(legal.lb || '')}" target="_blank" rel="noopener">Leistungsbeschreibung Website-Analyse</a> (Fassung ${esc(a.avvVersion)}) zu. Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren.</span></label>`}
|
||||
<p class="error hidden" id="an-error" role="alert"></p>
|
||||
<div class="abo-actions"><button type="button" class="abo-btn" id="an-on" disabled>Analyse einschalten</button></div>
|
||||
</div>`
|
||||
@@ -104,7 +122,7 @@ export function renderAnalyseActivation(stage, project, reload) {
|
||||
paint()
|
||||
input.addEventListener('input', () => { color = input.value; paint() })
|
||||
stage.querySelectorAll('.an-preset').forEach((b) => b.addEventListener('click', () => { color = b.dataset.color; input.value = color; paint() }))
|
||||
const box = stage.querySelector('#an-avv')
|
||||
const box = stage.querySelector(abo ? '#an-zielgruppe' : '#an-avv')
|
||||
const button = stage.querySelector('#an-on')
|
||||
const error = stage.querySelector('#an-error')
|
||||
box.addEventListener('change', () => { button.disabled = !box.checked || controller.ok !== true })
|
||||
@@ -113,7 +131,7 @@ export function renderAnalyseActivation(stage, project, reload) {
|
||||
button.disabled = true
|
||||
error.classList.add('hidden')
|
||||
try {
|
||||
await request('/api/funktionen/analyse/activate', { method: 'POST', body: { projectId: project.projectId, accentColor: color.toLowerCase(), consent: { accepted: true, avvVersion: a.avvVersion } } })
|
||||
await request('/api/funktionen/analyse/activate', { method: 'POST', body: { projectId: project.projectId, accentColor: color.toLowerCase(), consent: { accepted: true, avvVersion: a.avvVersion }, ...(abo ? { audienceConfirmed: true } : {}) } })
|
||||
reload()
|
||||
} catch (err) {
|
||||
error.textContent = err.message
|
||||
@@ -171,7 +189,7 @@ function wireChart(stage) {
|
||||
svg.addEventListener('pointercancel', hideChartTip)
|
||||
}
|
||||
|
||||
function mountCockpit({ an, stage, project, usable, reload }) {
|
||||
function mountCockpit({ an, stage, project, usable, reload, abo = false, root = null }) {
|
||||
const pid = project.projectId
|
||||
const state = { stats: null, range: 0, stamp: '', rank: { seen: {}, who: {} } }
|
||||
const tabNow = () => (TABS.includes(analyseSel.tab) ? analyseSel.tab : 'uebersicht')
|
||||
@@ -195,7 +213,12 @@ function mountCockpit({ an, stage, project, usable, reload }) {
|
||||
b.setAttribute('aria-selected', String(on))
|
||||
b.tabIndex = on ? 0 : -1
|
||||
})
|
||||
stage.setAttribute('aria-labelledby', `an-tab-${tab}`)
|
||||
if (abo) stage.setAttribute('aria-label', reiterLabel(tab))
|
||||
else stage.setAttribute('aria-labelledby', `an-tab-${tab}`)
|
||||
}
|
||||
// Abo-Modell: den aktiven Reiter an die Etage melden (bei = pickTab); der Teaser der Uebersicht ruft ebenfalls pickTab
|
||||
function melde() {
|
||||
if (abo) meldeReiter(root, { eintraege: reiterEintraege('analyse', ANALYSE_REITER), aktiv: tabNow(), bei: pickTab })
|
||||
}
|
||||
// Anfrage je Zeitraum: Uebersicht und Heatmap teilen sich eine laufende Anfrage. Sie schreibt nichts in state; uebernommen
|
||||
// (adopt) wird die Antwort erst nach current(mine), sonst ueberschriebe eine verspaetete den Stand des neueren Zeitraums.
|
||||
@@ -276,6 +299,7 @@ function mountCockpit({ an, stage, project, usable, reload }) {
|
||||
}
|
||||
function showTab() {
|
||||
paintTabs()
|
||||
melde()
|
||||
paintHead()
|
||||
const tab = tabNow()
|
||||
if (tab === 'heatmap') showHeat()
|
||||
|
||||
Reference in new issue
Block a user