Shop G1: Verdrahtung #webshop, Webshop-Kachel, Bestellseite mit key und optionaler Zusammenfassung
Dashboard-Abschnitt view-webshop, shop.css und Platzhalter shop.js, app.js (Hash, Refresher, Label, Elternansicht), Kachel in funktionen.js nach shop.verfuegbar, GET /api/funktionen liefert shop.verfuegbar, funktion-bestellen.js nimmt key/name/zusammenfassung (Standard: Terminplaner unverändert). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0ebd2e6663
commit
809da80c9b
11 files changed
+333
-19
No files matched your search
+4
-3
@@ -139,7 +139,7 @@ function projectState(project) {
|
||||
return ['kb-state-off', 'Noch keine Website']
|
||||
}
|
||||
|
||||
const FEATURE_LABELS = { preview: 'Vorschau', chat: 'Chat', invoices: 'Rechnungen', news: 'News-Pinnwand', termine: 'Terminplaner' }
|
||||
const FEATURE_LABELS = { preview: 'Vorschau', chat: 'Chat', invoices: 'Rechnungen', news: 'News-Pinnwand', termine: 'Terminplaner', shop: 'Webshop' }
|
||||
|
||||
function renderProjectCard(project, features) {
|
||||
const li = document.createElement('li')
|
||||
@@ -1910,7 +1910,7 @@ function initPasswordView() {
|
||||
// "projekt" (Projektdetails als eigene Seite) gibt es ebenfalls nur in der App-Ansicht.
|
||||
// "passwort" oeffnet das Konto-Menue (Profilbild). #fortschritt fuehrt zu Projekte und scrollt zum Abschnitt
|
||||
// "Was wir zuletzt erledigt haben" (#pj-activity); hinter dem Hash darf ein Query stehen (#news?bestellt=1, Rueckkehr von Stripe).
|
||||
const VIEW_HASHES = { projects: '', funktionen: 'funktionen', news: 'news', analyse: 'analyse', termine: 'termine', invoices: 'rechnungen', chat: 'chat', vertraege: 'vertraege', stunden: 'stunden', 'bestellen-hosting': 'bestellen-hosting', 'bestellen-wartung': 'bestellen-wartung', wechsel: 'wechsel', takeover: 'gutschrift', projekt: 'projekt', passwort: 'passwort' }
|
||||
const VIEW_HASHES = { projects: '', funktionen: 'funktionen', news: 'news', analyse: 'analyse', termine: 'termine', invoices: 'rechnungen', chat: 'chat', vertraege: 'vertraege', stunden: 'stunden', 'bestellen-hosting': 'bestellen-hosting', 'bestellen-wartung': 'bestellen-wartung', wechsel: 'wechsel', takeover: 'gutschrift', projekt: 'projekt', passwort: 'passwort', webshop: 'webshop' }
|
||||
const HASH_ALIASES = { wartung: 'vertraege', abo: 'vertraege' }
|
||||
const viewLoaders = { chat: initChat }
|
||||
// Funktionen und News laden bei jedem Oeffnen neu (Zustand aendert sich durch Bestellung und Freischaltung); Modul erst dann laden
|
||||
@@ -1919,6 +1919,7 @@ const viewRefreshers = {
|
||||
news: () => import('/funktionen.js').then((m) => m.initNews(document.getElementById('news-body'))),
|
||||
analyse: () => import('/analyse.js').then((m) => m.initAnalyse(document.getElementById('analyse-body'))),
|
||||
termine: () => import('/termine.js').then((m) => m.initTermine(document.getElementById('termine-body'))),
|
||||
webshop: () => import('/shop.js').then((m) => m.initShop(document.getElementById('webshop-body'))),
|
||||
}
|
||||
const loadedViews = new Set()
|
||||
|
||||
@@ -2111,7 +2112,7 @@ const appLayout = window.matchMedia('(max-width: 880px), (pointer: coarse) and (
|
||||
const coarsePointer = window.matchMedia('(pointer: coarse)')
|
||||
// Unterseite -> Tab, zu dem sie gehoert (und Text neben dem Zurueck-Pfeil).
|
||||
// "passwort" setzt onAppViewShown: die Ansicht, aus der das Konto-Menue geoeffnet wurde.
|
||||
const APP_PARENT = { news: 'funktionen', analyse: 'funktionen', termine: 'funktionen', stunden: 'vertraege', 'bestellen-hosting': 'vertraege', 'bestellen-wartung': 'vertraege', wechsel: 'vertraege', takeover: 'vertraege', projekt: 'projects' }
|
||||
const APP_PARENT = { news: 'funktionen', analyse: 'funktionen', termine: 'funktionen', stunden: 'vertraege', 'bestellen-hosting': 'vertraege', 'bestellen-wartung': 'vertraege', wechsel: 'vertraege', takeover: 'vertraege', projekt: 'projects', webshop: 'funktionen' }
|
||||
const APP_PARENT_LABEL = { vertraege: 'Verträge', projects: 'Projekte', funktionen: 'Funktionen', invoices: 'Rechnungen', chat: 'Chat' }
|
||||
const appShell = { view: 'projects', scroll: {}, fromHistory: false, sideCards: [], installPrompt: null }
|
||||
const TEXT_INPUT = 'textarea, input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=date]):not([type=button]):not([type=submit]):not([type=range]):not([type=color])'
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<link rel="stylesheet" href="/funktionen.css">
|
||||
<link rel="stylesheet" href="/analyse.css">
|
||||
<link rel="stylesheet" href="/termine.css">
|
||||
<link rel="stylesheet" href="/shop.css">
|
||||
</head>
|
||||
<body class="portal-page dashboard-page kb-page">
|
||||
<div class="dot-background" aria-hidden="true">
|
||||
@@ -196,6 +197,15 @@
|
||||
<div id="termine-body"><p class="kb-empty">Lade…</p></div>
|
||||
</section>
|
||||
|
||||
<!-- Webshop: Anfrage, Bestellung, Artikel, Bestellungen, Einstellungen (shop.js, initShop); Unterseite von Funktionen -->
|
||||
<section class="kb-view" id="view-webshop">
|
||||
<header class="kb-head">
|
||||
<h1>Webshop</h1>
|
||||
<p>Dein eigener Online-Shop auf deiner Website – Zahlungen gehen direkt auf dein Stripe-Konto.</p>
|
||||
</header>
|
||||
<div id="webshop-body"><p class="kb-empty">Lade…</p></div>
|
||||
</section>
|
||||
|
||||
<!-- Rechnungen -->
|
||||
<section class="kb-view" id="view-invoices">
|
||||
<header class="kb-head">
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
Es gibt keine Texte des Registers im Client: Knopf, Haken, Belehrung, Preis und Unterlagen kommen als `bestellung` aus
|
||||
GET /api/termine/einstellungen (Server: bestellungAnsicht), und der Knopf zeigt genau bestellung.consent.knopf, denselben Text, den der
|
||||
Server im Nachweis speichert. Fremdtext kommt nur über textContent in die Seite (h aus termine-dom.js), nie über innerHTML.
|
||||
Zusammenfassung: zusammenfassung() aus termine-view.js. Checkout: POST /api/funktionen/termine/checkout (Server: routes/funktionen.js). */
|
||||
Zusammenfassung: zusammenfassung() aus termine-view.js (Standard für key 'termine'; andere Funktionen übergeben eine eigene Funktion oder
|
||||
zeigen keine). Checkout: POST /api/funktionen/<key>/checkout (Server: routes/funktionen.js), key ist standardmäßig 'termine'. */
|
||||
|
||||
import { fehlerKarte, h, request } from './termine-dom.js'
|
||||
import { FRAGEBOGEN_SCHRITTE, zusammenfassung } from './termine-view.js'
|
||||
|
||||
const CHECKOUT = '/api/funktionen/termine/checkout'
|
||||
const STANDARD_KEY = 'termine'
|
||||
// Checkout-Pfad der Funktion; der Schlüssel ist ein Registerschlüssel (Kleinbuchstaben), alles andere fällt auf den Terminplaner zurück
|
||||
const checkoutPfad = (key) => `/api/funktionen/${typeof key === 'string' && /^[a-z][a-z0-9-]{0,31}$/.test(key) ? key : STANDARD_KEY}/checkout`
|
||||
const RECHTLICHES = '/rechtliches/'
|
||||
const KUNDENANSICHT = 'In der Kundenansicht kann nicht bestellt werden.'
|
||||
const ZAHLUNGSSEITE = 'Die Zahlungsseite konnte nicht geöffnet werden. Bitte versuch es noch einmal oder schreib uns im Chat.'
|
||||
@@ -113,12 +116,16 @@ function ladeBeiRueckkehrNeu(element) {
|
||||
* Der Knopf ist gesperrt, solange ein Haken fehlt, offene Punkte bleiben oder die Kundenansicht läuft. Ein Klick sperrt ihn, sendet
|
||||
* bestellKoerper und geht zur Zahlungsseite. Bei 409 lädt die Seite die Daten neu und setzt die Haken zurück (bestellung_offen zeigt die
|
||||
* Punkte aus der Antwort, fassung_veraltet die neue Fassung aus der Antwort, ein Abo-Code den Text der Antwort).
|
||||
* Weitere Optionen: key = Schlüssel der Funktion im Register (Standard 'termine', bestimmt den Checkout-Pfad), zusammenfassung =
|
||||
* Funktion (einstellung, fragebogen, branchen) => [{ schritt, titel, zeilen }]; ohne sie zeigt nur der Terminplaner seine Zusammenfassung,
|
||||
* andere Funktionen lassen den Block weg.
|
||||
*/
|
||||
export function renderBestellen(root, { ctx, daten, zumSchritt } = {}) {
|
||||
return baueSeite(root, { ctx, daten, zumSchritt }, {})
|
||||
export function renderBestellen(root, { ctx, daten, zumSchritt, key = STANDARD_KEY, zusammenfassung: gruppenFunktion } = {}) {
|
||||
const gruppenFn = typeof gruppenFunktion === 'function' ? gruppenFunktion : key === STANDARD_KEY ? zusammenfassung : null
|
||||
return baueSeite(root, { ctx, daten, zumSchritt, key, gruppenFn }, {})
|
||||
}
|
||||
|
||||
function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
|
||||
function baueSeite(root, { ctx, daten, zumSchritt, key, gruppenFn }, uebernahme) {
|
||||
const roh = daten?.bestellung
|
||||
if (!roh || !istConsent(roh.consent)) {
|
||||
const karte = fehlerKarte('Die Bestellseite konnte gerade nicht geladen werden. Bitte lade die Seite neu.')
|
||||
@@ -140,7 +147,7 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
|
||||
const gehe = (schritt) => { if (springt) zumSchritt(schritt) }
|
||||
|
||||
// 1 Zusammenfassung: je Gruppe die Angaben als Text und „Ändern“
|
||||
const gruppen = zusammenfassung(daten.einstellung, daten.fragebogen, daten.branchen).map((gruppe) => h('div', { class: 'tm-bestellen-gruppe', 'data-schritt': gruppe.schritt },
|
||||
const gruppen = (gruppenFn ? gruppenFn(daten.einstellung, daten.fragebogen, daten.branchen) : []).map((gruppe) => h('div', { class: 'tm-bestellen-gruppe', 'data-schritt': gruppe.schritt },
|
||||
h('div', { class: 'tm-bestellen-gruppe-kopf' },
|
||||
h('h4', null, gruppe.titel),
|
||||
springt
|
||||
@@ -200,8 +207,8 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
|
||||
|
||||
const seite = h('section', { class: 'kb-panel tm-bestellen' },
|
||||
h('h3', null, 'Prüfen und bestellen'),
|
||||
h('p', { class: 'tm-abschnitt-text tm-hinweis' }, 'Lies alles in Ruhe durch. Mit „Ändern“ kommst du zu dem Schritt zurück, in dem du die Angabe gemacht hast.'),
|
||||
h('div', { class: 'tm-bestellen-zusammenfassung' }, gruppen),
|
||||
gruppenFn ? h('p', { class: 'tm-abschnitt-text tm-hinweis' }, 'Lies alles in Ruhe durch. Mit „Ändern“ kommst du zu dem Schritt zurück, in dem du die Angabe gemacht hast.') : null,
|
||||
gruppenFn ? h('div', { class: 'tm-bestellen-zusammenfassung' }, gruppen) : null,
|
||||
offenBlock,
|
||||
preisBlock,
|
||||
belehrungBlock,
|
||||
@@ -246,7 +253,7 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
|
||||
karte.focus({ preventScroll: true })
|
||||
return
|
||||
}
|
||||
const neueSeite = baueSeite(root, { ctx, daten: neu, zumSchritt }, {
|
||||
const neueSeite = baueSeite(root, { ctx, daten: neu, zumSchritt, key, gruppenFn }, {
|
||||
offen: err.code === 'bestellung_offen' ? err.body?.offen : undefined,
|
||||
consent: err.code === 'fassung_veraltet' ? err.body?.consent : undefined,
|
||||
})
|
||||
@@ -264,7 +271,7 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
|
||||
aktualisiere()
|
||||
zeigeMeldung('')
|
||||
try {
|
||||
const antwort = await request(CHECKOUT, { method: 'POST', body: bestellKoerper({ projectId: ctx?.projectId, bestellung, gewaehlt }) })
|
||||
const antwort = await request(checkoutPfad(key), { method: 'POST', body: bestellKoerper({ projectId: ctx?.projectId, bestellung, gewaehlt }) })
|
||||
if (geheZu(antwort?.url)) return // die Seite wird verlassen, der Knopf bleibt gesperrt
|
||||
zeigeMeldung(ZAHLUNGSSEITE)
|
||||
} catch (err) {
|
||||
@@ -288,13 +295,15 @@ function baueSeite(root, { ctx, daten, zumSchritt }, uebernahme) {
|
||||
* projekt = Eintrag des Projekts aus GET /api/funktionen (projectId), bestellung = projekt.features.termine (state, orderedAt, ticketWoid,
|
||||
* invoiceUrl), einrichtung = Text der Zeiteinschätzung (Register: setupEstimate), danke = Rückkehr von Stripe, kundenansicht = Sitzung in der
|
||||
* Kundenansicht, meldung = Text, der am Knopf „Zahlung fortsetzen“ steht (z. B. der Fehler eines Versuchs vor dem Neuzeichnen).
|
||||
* key = Schlüssel der Funktion (Standard 'termine', Checkout-Pfad), name = Name der Funktion in
|
||||
* den Texten (Standard 'Terminplaner'), hinweisEinrichtung = Satz unter den Fakten im Zustand setup (Standard: Einstellungen gesperrt).
|
||||
* weiter(fall) wird gerufen, wenn „Zahlung fortsetzen“ scheitert (Ruling R110): fall = { bestellen, meldung }, bestellen ist wahr bei 409
|
||||
* zustimmung_fehlt (keine offene Sitzung mit Nachweis: die Ansicht zeigt wieder das Bestellformular), meldung der Fehlertext der Antwort.
|
||||
* weiter() lädt neu und zeichnet die Ansicht nach dem Zustand von neuem, damit keine veraltete Karte stehen bleibt (z. B. wenn die Bestellung
|
||||
* inzwischen bezahlt ist und der Server 400 antwortet); wirft es, bleibt die Karte stehen und der Fehler steht am Knopf. Fehlt weiter,
|
||||
* steht die Meldung des Servers am Knopf.
|
||||
*/
|
||||
export function statusKarte({ projekt, bestellung, einrichtung, danke, kundenansicht, meldung } = {}, { weiter } = {}) {
|
||||
export function statusKarte({ projekt, bestellung, einrichtung, danke, kundenansicht, meldung, key = STANDARD_KEY, name = 'Terminplaner', hinweisEinrichtung } = {}, { weiter } = {}) {
|
||||
const bestellt = bestellung?.state === 'ordered'
|
||||
const datum = datumTag(bestellung?.orderedAt)
|
||||
const ticket = bestellung?.ticketWoid
|
||||
@@ -317,7 +326,7 @@ export function statusKarte({ projekt, bestellung, einrichtung, danke, kundenans
|
||||
fortsetzenKnopf.disabled = true
|
||||
meldungEl.textContent = ''
|
||||
try {
|
||||
const antwort = await request(CHECKOUT, { method: 'POST', body: fortsetzenKoerper(projekt?.projectId) })
|
||||
const antwort = await request(checkoutPfad(key), { method: 'POST', body: fortsetzenKoerper(projekt?.projectId) })
|
||||
if (geheZu(antwort?.url)) return // die Seite wird verlassen
|
||||
meldungEl.textContent = ZAHLUNGSSEITE
|
||||
} catch (err) {
|
||||
@@ -341,12 +350,12 @@ export function statusKarte({ projekt, bestellung, einrichtung, danke, kundenans
|
||||
const karte = h('section', { class: 'kb-panel tm-statuskarte', 'data-state': bestellt ? 'ordered' : 'setup' },
|
||||
danke ? h('p', { class: 'tm-danke', role: 'status', 'data-danke': true }, DANKE) : null,
|
||||
h('span', { class: 'tm-pille tm-pille-wait' }, bestellt ? 'Zahlung offen' : 'In Einrichtung'),
|
||||
h('h3', null, bestellt ? 'Deine Bestellung wartet auf die Zahlung' : 'Wir richten deinen Terminplaner ein'),
|
||||
h('p', null, bestellt ? 'Sobald deine Zahlung bei uns ankommt, legen wir mit der Einrichtung los.' : 'Sobald die Einrichtung fertig ist, schalten wir deinen Terminplaner für dich frei.'),
|
||||
h('h3', null, bestellt ? 'Deine Bestellung wartet auf die Zahlung' : `Wir richten deinen ${text(name) || 'Terminplaner'} ein`),
|
||||
h('p', null, bestellt ? 'Sobald deine Zahlung bei uns ankommt, legen wir mit der Einrichtung los.' : `Sobald die Einrichtung fertig ist, schalten wir deinen ${text(name) || 'Terminplaner'} für dich frei.`),
|
||||
fakten.length > 0 ? h('dl', { class: 'tm-fakten' }, fakten) : null,
|
||||
bestellt
|
||||
? h('p', { class: 'tm-hinweis' }, 'Zahlst du per SEPA-Lastschrift, kann es einige Tage dauern, bis die Zahlung bei uns ankommt.')
|
||||
: h('p', { class: 'tm-hinweis' }, 'Deine Einstellungen sind jetzt gesperrt, Änderungen laufen über den Chat.'),
|
||||
: h('p', { class: 'tm-hinweis' }, text(hinweisEinrichtung) || 'Deine Einstellungen sind jetzt gesperrt, Änderungen laufen über den Chat.'),
|
||||
meldungEl,
|
||||
bestellt && kundenansicht === true ? h('p', { class: 'tm-hinweis' }, KUNDENANSICHT) : null,
|
||||
h('div', { class: 'tm-aktionen' },
|
||||
|
||||
+55
-1
@@ -71,6 +71,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"/>',
|
||||
shop: '<circle cx="9" cy="20" r="1.5"/><circle cx="18" cy="20" r="1.5"/><path d="M2 3h3l2.7 12.4a1 1 0 0 0 1 .8h9.1a1 1 0 0 0 1-.8L21 7H6"/>',
|
||||
}
|
||||
const svg = (name) => `<svg class="fn-ico" viewBox="0 0 24 24" aria-hidden="true">${ICONS[name]}</svg>`
|
||||
|
||||
@@ -211,6 +212,53 @@ async function showTermineCount(root, projects) {
|
||||
el.hidden = false
|
||||
}
|
||||
|
||||
/* Webshop: Kachel nur, wenn der Server den Shop fuer diesen Kunden freigibt (shop.verfuegbar der Antwort, Schalter SHOP_ENABLED); sonst
|
||||
gar keine Kachel (der Shop startet im Pilot und wird nicht vorab angekuendigt). Die Ansicht #webshop (shop.js) zeigt die Anfrage und ihren
|
||||
Stand; die Kachel kennt nur den Zustand des Funktionsdokuments (projects[].features.shop: off | ordered | setup | active | disabled) und
|
||||
baut sich inline wie die Terminplaner-Kachel, damit funktionen.js keinen weiteren Import braucht. Der Preis kommt aus dem Register
|
||||
(features.shop.priceLabel), Rueckfall 299 €. */
|
||||
const SHOP_PREIS = '299 €'
|
||||
const shopOf = (project) => project.features?.shop || { state: 'off' }
|
||||
|
||||
// Zielprojekt: aktiv, dann ordered/setup, dann abgeschaltet, dann das erste
|
||||
function pickShopProject(projects) {
|
||||
return projects.find((p) => shopOf(p).state === 'active')
|
||||
|| projects.find((p) => isSetup(shopOf(p).state))
|
||||
|| projects.find((p) => shopOf(p).state === 'disabled')
|
||||
|| projects[0]
|
||||
}
|
||||
|
||||
function shopTile(projects, features) {
|
||||
const preis = textOr(features?.shop?.priceLabel, SHOP_PREIS)
|
||||
const active = projects.filter((p) => shopOf(p).state === 'active').length
|
||||
const target = pickShopProject(projects)
|
||||
const state = target ? shopOf(target).state : 'off'
|
||||
const knopf = (text) => `<button type="button" class="kb-btn-primary fn-tile-cta" data-view="webshop" data-project="${esc(target.projectId)}">${text}</button>`
|
||||
// Zustand -> Pille und Knopf (aus: noch keine Funktion, die Anfrage selbst zeigt die Ansicht #webshop)
|
||||
const ZUSTAENDE = {
|
||||
off: [['Aus', 'off'], 'Webshop anfragen'],
|
||||
ordered: [['Zahlung offen', 'setup'], 'Zahlung offen'],
|
||||
setup: [['In Einrichtung', 'setup'], 'In Einrichtung'],
|
||||
active: [['Aktiv', 'on'], 'Webshop öffnen'],
|
||||
disabled: [['Abgeschaltet', 'off'], 'Webshop ansehen'],
|
||||
}
|
||||
const [pill, knopfText] = ZUSTAENDE[state] || ZUSTAENDE.off
|
||||
const action = target ? knopf(knopfText) : ''
|
||||
let extra = ''
|
||||
if (!target) extra = '<p class="fn-count">Sobald deine Website angelegt ist, kannst du den Webshop anfragen.</p>'
|
||||
else if (state === 'disabled') extra = '<p>Der Webshop ist für diese Website abgeschaltet. Deine Bestellungen kannst du noch ansehen.</p>'
|
||||
const count = projects.length > 1 && active ? `<p class="fn-count">aktiv für ${active} von ${projects.length} Websites</p>` : ''
|
||||
return `
|
||||
<article class="fn-tile" data-fn-tile="shop">
|
||||
<div class="fn-tile-head">${svg('shop')}<span class="fn-pill fn-pill-${pill[1]}">${pill[0]}</span></div>
|
||||
<h3>Webshop</h3>
|
||||
<p>Dein eigener Online-Shop auf Deiner Website. Zahlungen gehen direkt auf Dein Stripe-Konto. ${esc(preis)} einmalig.</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>
|
||||
@@ -220,11 +268,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, features, shop } = 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)}
|
||||
${shop?.verfuegbar ? shopTile(projects, features) : ''}
|
||||
${analyseTile(projects)}
|
||||
</div>`
|
||||
root.querySelector('[data-fn-tile="news"] .fn-tile-cta')?.addEventListener('click', (ev) => { selection.projectId = ev.currentTarget.dataset.project })
|
||||
@@ -234,6 +283,11 @@ export async function initFunktionen(root) {
|
||||
const body = document.getElementById('termine-body')
|
||||
if (body) body.dataset.wunschProjekt = ev.currentTarget.dataset.project
|
||||
})
|
||||
// Die Webshop-Ansicht liest das gewaehlte Projekt aus ihrem Behaelter (shop.js, initShop)
|
||||
root.querySelector('[data-fn-tile="shop"] .fn-tile-cta[data-project]')?.addEventListener('click', (ev) => {
|
||||
const body = document.getElementById('webshop-body')
|
||||
if (body) body.dataset.wunschProjekt = ev.currentTarget.dataset.project
|
||||
})
|
||||
if (termine?.verfuegbar) await showTermineCount(root, projects)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/* Webshop im Kundenbereich (shop.js): Farben und Abstaende aus den Tokens von kundenbereich.css (.kb-page); nur Klassen mit sh-.
|
||||
Task G1 legt nur die Verdrahtung an, die Oberflaeche (Reiter, Karten, Formulare) kommt mit G2 bis G6. */
|
||||
|
||||
#view-webshop .kb-panel { max-width: none; }
|
||||
#view-webshop button:focus-visible, #view-webshop input:focus-visible, #view-webshop select:focus-visible, #view-webshop textarea:focus-visible,
|
||||
#view-webshop a:focus-visible { outline: 3px solid #d4b4ff; outline-offset: 2px; }
|
||||
|
||||
.sh-info p { margin: 0 0 12px; line-height: 1.55; color: var(--kb-muted); }
|
||||
.sh-info p:last-child { margin-bottom: 0; }
|
||||
@@ -0,0 +1,16 @@
|
||||
/* Webshop im Kundenbereich (ES-Modul, wird von app.js erst beim Oeffnen der Ansicht #webshop geladen).
|
||||
initShop(rootEl, { projectId }): Platzhalter, bis die Huelle mit Anfrage, Bestellseite und Reitern steht (Task G2).
|
||||
Fremdtext kommt nur ueber textContent in die Seite. Styles: shop.css. */
|
||||
|
||||
export function initShop(root, { projectId } = {}) {
|
||||
if (!root) return
|
||||
const wunsch = root.dataset?.wunschProjekt || projectId || ''
|
||||
if (root.dataset && root.dataset.wunschProjekt) delete root.dataset.wunschProjekt
|
||||
const panel = document.createElement('div')
|
||||
panel.className = 'kb-panel sh-info'
|
||||
if (wunsch) panel.dataset.projekt = wunsch
|
||||
const text = document.createElement('p')
|
||||
text.textContent = 'Webshop wird geladen …'
|
||||
panel.append(text)
|
||||
root.replaceChildren(panel)
|
||||
}
|
||||
Reference in new issue
Block a user