Öffnungszeiten: Kachel und Bestell-/Einschaltweg im Portal
Kachel „Öffnungszeiten live“ inline in funktionen.js (keine neuen Importe, keine Zusatzabfrage), Ansicht #zeiten (zeiten.js, zeiten-bestellen.js, zeiten.css) mit Einschalten im Wartungs-Abo, Bestellen für 30 € (Widerrufsbelehrung vor dem Knopf, Ruling R32), Behalten nach dem Abo-Ende, Statuskarte je Zustand, Rückkehr von Stripe und Platz für den Editor (Aufgabe 29). GET /api/funktionen liefert dafür zeiten.belehrung (Haken belehrung der Funktion). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f20f056c60
commit
7bb918390a
10 files changed
+1878
-8
No files matched your search
+113
-1
@@ -92,6 +92,7 @@ const ICONS = {
|
||||
news: '<path d="M12 17v5"/><path d="M9 10.8a2 2 0 0 1-1.1 1.8l-1.8.9A2 2 0 0 0 5 15.2V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.8a2 2 0 0 0-1.1-1.8l-1.8-.9A2 2 0 0 1 15 10.8V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/>',
|
||||
calendar: '<rect x="3" y="5" width="18" height="16" rx="3"/><path d="M8 3v4M16 3v4M3 10h18"/>',
|
||||
chart: '<path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/>',
|
||||
clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
||||
}
|
||||
const svg = (name) => `<svg class="fn-ico" viewBox="0 0 24 24" aria-hidden="true">${ICONS[name]}</svg>`
|
||||
|
||||
@@ -238,6 +239,107 @@ async function showTermineCount(root, projects) {
|
||||
el.hidden = false
|
||||
}
|
||||
|
||||
/* Oeffnungszeiten live: Kachel nur, wenn der Schalter es erlaubt (zeiten.verfuegbar); sonst gibt es sie gar nicht (auch kein "Kommt bald").
|
||||
Wie bei den anderen Funktionen steht sie inline, die Ansicht #zeiten (zeiten.js) laedt app.js. Alles, was sie zeigt, kommt mit der Liste
|
||||
(GET /api/funktionen): zeiten (Angebot) mit inklusive (true: im laufenden, aktiven Wartungs-Abo enthalten, einschaltbar | false: sonst, dann
|
||||
sagt grund warum: kein_abo | gekuendigt | nicht_aktiv | null: Abo nicht lesbar) und preis; je Projekt features.zeiten (state, einbau, ticketWoid,
|
||||
aboEndeAt, disabledReason, loeschungAm), zeitenStatus ({ offen, kurz, text } des Hauptplans) und zeitenLeer (keine Plaene gespeichert).
|
||||
Ruling R20: nicht_aktiv (Probezeit, Zahlung offen) ist einschaltbar, sobald das Abo aktiv ist, und bietet kein Bezahlen an. */
|
||||
const ZEITEN_TEXT = 'Deine Website zeigt live, ob du gerade geöffnet hast – mit Feiertagen, Urlaub und Hinweisen.'
|
||||
const ZEITEN_ABGESCHALTET = 'Öffnungszeiten live wurde für diese Website abgeschaltet. Schreib uns im Chat, wenn du die Funktion wieder möchtest.'
|
||||
const ZEITEN_NICHT_PRUEFBAR = 'Gerade nicht prüfbar – bitte später erneut.'
|
||||
const zeitenOf = (project) => project.features?.zeiten || { state: 'off' }
|
||||
|
||||
// Zielprojekt: aktiv, dann in Einrichtung/Zahlung, dann abgeschaltet, sonst das erste
|
||||
function pickZeitenProject(projects) {
|
||||
return projects.find((p) => zeitenOf(p).state === 'active')
|
||||
|| projects.find((p) => isSetup(zeitenOf(p).state))
|
||||
|| projects.find((p) => zeitenOf(p).state === 'disabled')
|
||||
|| projects[0]
|
||||
}
|
||||
|
||||
// Frist zum Behalten: Berliner Kalendertag des erkannten Abo-Endes plus 30 Tage (wie fristTag in server/services/zeiten/aboAbgleich.js und die Mail K26);
|
||||
// ein unlesbares Datum gibt '' (dann steht kein Hinweis da, nie ein erfundenes Datum). Doppelt zu zeiten.js (funktionen.js importiert dort nichts).
|
||||
function zeitenFrist(aboEndeAt) {
|
||||
const ms = typeof aboEndeAt === 'string' ? Date.parse(aboEndeAt) : NaN
|
||||
if (Number.isNaN(ms)) return ''
|
||||
const [y, m, d] = new Date(ms).toLocaleDateString('sv-SE', { timeZone: 'Europe/Berlin' }).split('-').map(Number)
|
||||
return new Date(Date.UTC(y, m - 1, d + 30)).toLocaleDateString('de-DE', { timeZone: 'UTC', day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
}
|
||||
|
||||
// 'JJJJ-MM-TT' als TT.MM.JJJJ, sonst ''
|
||||
const zeitenTag = (iso) => {
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(typeof iso === 'string' ? iso : '')
|
||||
return m ? `${m[3]}.${m[2]}.${m[1]}` : ''
|
||||
}
|
||||
|
||||
function zeitenTile(projects, angebot = {}) {
|
||||
const target = pickZeitenProject(projects)
|
||||
const f = target ? zeitenOf(target) : { state: 'off' }
|
||||
const preis = textOr(angebot.preis, PRICE)
|
||||
const knopf = (text, { weg = '', klasse = 'kb-btn-primary fn-tile-cta' } = {}) => `<button type="button" class="${klasse}" data-view="zeiten" data-project="${esc(target.projectId)}"${weg ? ` data-zeiten-weg="${weg}"` : ''}>${text}</button>`
|
||||
const chat = '<button type="button" class="kb-btn-primary fn-tile-cta" data-view="chat">Chat öffnen</button>'
|
||||
let pill = ['Aus', 'off']
|
||||
let extra = ''
|
||||
let action = ''
|
||||
if (!target) {
|
||||
extra = '<p class="fn-count">Sobald deine Website angelegt ist, kannst du Öffnungszeiten live einrichten.</p>'
|
||||
} else if (f.state === 'active' || f.state === 'setup') {
|
||||
const aktiv = f.state === 'active'
|
||||
pill = aktiv ? ['Aktiv', 'on'] : ['In Einrichtung', 'setup']
|
||||
const status = typeof target.zeitenStatus?.text === 'string' ? target.zeitenStatus.text : ''
|
||||
if (aktiv && status) extra += `<p class="fn-count" data-zeiten-status>${esc(status)}</p>`
|
||||
if (!aktiv) {
|
||||
extra += f.einbau === 'angestossen'
|
||||
? '<p>Hinweis- und Kontakt-Leiste erscheinen nach dem nächsten Veröffentlichen (meist in wenigen Minuten).</p>'
|
||||
: `<p>Unser Team baut die Funktion ein${f.ticketWoid ? ` (Ticket #${esc(f.ticketWoid)})` : ''}.</p>`
|
||||
}
|
||||
if (target.zeitenLeer === true) extra += `<p>${aktiv ? 'Trag jetzt deine Zeiten ein.' : 'Trag jetzt deine Zeiten ein – erst dann kann das Team die Tabelle einbauen.'}</p>`
|
||||
action = knopf('Zeiten bearbeiten')
|
||||
// Wartungs-Abo beendet (aboEndeAt): fuer einmalig 30 € behalten; nur wenn sicher ist, dass das Abo nicht wieder laeuft (inklusive false)
|
||||
const frist = f.aboEndeAt && angebot.inklusive === false ? zeitenFrist(f.aboEndeAt) : ''
|
||||
if (frist) {
|
||||
extra += `<p class="fn-count">Dein Wartungs-Abo ist beendet – behalte die Funktion bis ${frist} für einmalig ${esc(preis)}.</p>`
|
||||
action += knopf(`Für ${esc(preis)} behalten`, { weg: 'behalten', klasse: 'kb-btn-outline' })
|
||||
}
|
||||
} else if (f.state === 'ordered') {
|
||||
pill = ['Zahlung offen', 'setup']
|
||||
extra = '<p>Zahlung wird bestätigt …</p>'
|
||||
action = knopf('Status ansehen')
|
||||
} else if (f.state === 'disabled') {
|
||||
pill = ['Abgeschaltet', 'off']
|
||||
const aboEnde = f.disabledReason === 'abo_ende'
|
||||
// Daten da: Plaene gespeichert (zeitenLeer false) und die Loeschfrist (loeschungAm, Berliner Tag) ist nicht vorbei
|
||||
const daten = aboEnde && target.zeitenLeer === false && !(typeof f.loeschungAm === 'string' && f.loeschungAm < berlinIsoDay())
|
||||
extra = `<p>${aboEnde ? 'Öffnungszeiten live ist abgeschaltet, weil dein Wartungs-Abo beendet ist.' : ZEITEN_ABGESCHALTET}</p>`
|
||||
if (daten && zeitenTag(f.loeschungAm)) extra += `<p class="fn-count">Deine Eingaben bleiben bis ${zeitenTag(f.loeschungAm)} gespeichert.</p>`
|
||||
if (daten && angebot.inklusive === true) extra += '<p class="fn-count">Dein Wartungs-Abo läuft wieder – wir schalten Öffnungszeiten live automatisch wieder ein (meist innerhalb eines Tages).</p>'
|
||||
else if (daten && angebot.inklusive !== false) extra += `<p class="fn-count">${ZEITEN_NICHT_PRUEFBAR}</p>`
|
||||
else action = daten ? knopf(`Für ${esc(preis)} wieder einschalten`, { weg: 'behalten' }) : chat
|
||||
} else if (angebot.inklusive === true) {
|
||||
extra = '<p>Im Wartungs-Abo enthalten.</p>'
|
||||
action = knopf('Einschalten')
|
||||
} else if (angebot.inklusive === false && angebot.grund === 'nicht_aktiv') {
|
||||
extra = '<p class="fn-count">Im Wartungs-Abo enthalten – einschaltbar, sobald dein Abo aktiv ist.</p>'
|
||||
} else if (angebot.inklusive === false) {
|
||||
extra = `<p>${esc(preis)} einmalig</p>`
|
||||
action = knopf(`Öffnungszeiten einrichten · ${esc(preis)}`)
|
||||
} else {
|
||||
extra = `<p class="fn-count">${ZEITEN_NICHT_PRUEFBAR}</p>`
|
||||
}
|
||||
const aktiveAnzahl = projects.filter((p) => zeitenOf(p).state === 'active').length
|
||||
const count = projects.length > 1 && aktiveAnzahl ? `<p class="fn-count">aktiv für ${aktiveAnzahl} von ${projects.length} Websites</p>` : ''
|
||||
return `
|
||||
<article class="fn-tile" data-fn-tile="zeiten">
|
||||
<div class="fn-tile-head">${svg('clock')}<span class="fn-pill fn-pill-${pill[1]}">${pill[0]}</span></div>
|
||||
<h3>Öffnungszeiten live</h3>
|
||||
<p>${ZEITEN_TEXT}</p>
|
||||
${extra}
|
||||
${count}
|
||||
${action}
|
||||
</article>`
|
||||
}
|
||||
|
||||
const soonTile = (icon, title, text) => `
|
||||
<article class="fn-tile fn-tile-soon">
|
||||
<div class="fn-tile-head">${svg(icon)}<span class="fn-pill fn-pill-soon">Kommt bald</span></div>
|
||||
@@ -247,11 +349,12 @@ const soonTile = (icon, title, text) => `
|
||||
|
||||
export async function initFunktionen(root) {
|
||||
if (!root) return
|
||||
const { projects = [], termine, features } = await request('/api/funktionen')
|
||||
const { projects = [], termine, zeiten, features } = await request('/api/funktionen')
|
||||
root.innerHTML = `
|
||||
<div class="fn-tiles">
|
||||
${newsTile(projects, newsOffer(features).price)}
|
||||
${termine?.verfuegbar ? termineTile(projects, termine) : soonTile('calendar', 'Terminplaner', TERMINE_TEXT)}
|
||||
${zeiten?.verfuegbar === true ? zeitenTile(projects, zeiten) : ''}
|
||||
${analyseTile(projects)}
|
||||
</div>`
|
||||
root.querySelector('[data-fn-tile="news"] .fn-tile-cta')?.addEventListener('click', (ev) => { selection.projectId = ev.currentTarget.dataset.project })
|
||||
@@ -261,6 +364,15 @@ export async function initFunktionen(root) {
|
||||
const body = document.getElementById('termine-body')
|
||||
if (body) body.dataset.wunschProjekt = ev.currentTarget.dataset.project
|
||||
})
|
||||
// Die Zeiten-Ansicht liest Projekt und Weg (Behalten) aus ihrem Behaelter (zeiten.js, initZeiten); der Klick steigt danach zum Navigationshandler auf
|
||||
root.querySelector('[data-fn-tile="zeiten"]')?.addEventListener('click', (ev) => {
|
||||
const knopf = ev.target?.closest?.('button[data-view="zeiten"]')
|
||||
const body = knopf && document.getElementById('zeiten-body')
|
||||
if (!body) return
|
||||
body.dataset.wunschProjekt = knopf.dataset.project
|
||||
if (knopf.dataset.zeitenWeg === 'behalten') body.dataset.wunschWeg = 'behalten'
|
||||
else delete body.dataset.wunschWeg
|
||||
})
|
||||
if (termine?.verfuegbar) await showTermineCount(root, projects)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user