Shop G2: Hülle #webshop mit Anfrage, Bestellseite, Statuskarte und fünf Reitern
Eigene Helfer in shop-dom.js (inkl. euroEingabe, datumText), Anfrageformular und -karten in shop-anfrage.js, Reiter als lazy geladene Platzhalter-Module für G3 bis G6. Die Bestellseite zeigt den Hinweis zu „Ändern“ nur noch, wenn es Schritte gibt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7464a4ef55
commit
738afa2d0e
11 files changed
+1407
-16
No files matched your search
@@ -0,0 +1,167 @@
|
||||
/* Webshop im Kundenbereich: die Anfrage („auf Anfrage“, Zustand off). Das Formular (renderAnfrageFormular), die Karte einer offenen oder
|
||||
abgelehnten Anfrage (anfrageKarte) und die Zusammenfassung der Angaben (anfrageZeilen, auch fuer die Bestellseite).
|
||||
Schnittstelle (server/routes/shop.js): POST /api/shop/anfrage { projectId, anfrage: { warenart, beschreibung, artikelzahl, versand, abholung,
|
||||
unternehmer, keineAusgeschlossenen, nachricht } } und POST /api/shop/anfrage/zurueckziehen { projectId }. Geprueft wird nur auf dem Server
|
||||
(Feldfehler kommen als err.fehler zurueck und stehen am Feld). Text kommt nur ueber textContent in die Seite (h). */
|
||||
|
||||
import { allgemeinerFehler, datumText, feld, h, hakenFeld, request, setzeFehler, zeigeFehler } from './shop-dom.js'
|
||||
|
||||
export const WARENARTEN = [['waren', 'Waren'], ['lebensmittel', 'Lebensmittel'], ['kleidung', 'Kleidung'], ['kunst', 'Kunst'], ['sonstiges', 'Sonstiges']]
|
||||
export const ARTIKELZAHLEN = [['bis20', 'bis 20'], ['bis100', 'bis 100'], ['mehr', 'mehr als 100']]
|
||||
export const AUSGESCHLOSSEN = 'Alkohol, Tabak/E-Zigaretten, Arzneimittel, Waffen/Feuerwerk, Erotik, lebende Tiere, digitale Inhalte, Gutscheine'
|
||||
|
||||
const labelVon = (liste, key) => liste.find(([k]) => k === key)?.[1] || ''
|
||||
const text = (wert) => (typeof wert === 'string' ? wert : '')
|
||||
|
||||
/** Angaben der Anfrage als Zeilen [{ name, wert }] (Gruppe der Bestellseite und Zusammenfassung der Karte). */
|
||||
export function anfrageZeilen(anfrage) {
|
||||
const a = anfrage && typeof anfrage === 'object' ? anfrage : {}
|
||||
const wege = [a.versand === true ? 'Versand' : '', a.abholung === true ? 'Abholung' : ''].filter(Boolean).join(' und ')
|
||||
return [
|
||||
['Was Du verkaufst', labelVon(WARENARTEN, a.warenart)],
|
||||
['Beschreibung', text(a.beschreibung)],
|
||||
['Artikel', labelVon(ARTIKELZAHLEN, a.artikelzahl)],
|
||||
['Lieferung', wege],
|
||||
['Nachricht', text(a.nachricht)],
|
||||
].filter(([, wert]) => wert !== '').map(([name, wert]) => ({ name, wert }))
|
||||
}
|
||||
|
||||
/** Die Angaben als Definitionsliste (nur Text). */
|
||||
export const anfrageListe = (anfrage) => h('dl', { class: 'sh-fakten' }, anfrageZeilen(anfrage).map(({ name, wert }) => h('div', null, h('dt', null, name), h('dd', null, wert))))
|
||||
|
||||
/**
|
||||
* Formular „Webshop anfragen“ in root. optionen: projectId, vorgabe (frueherer Stand der Anfrage zum Vorausfuellen), preis ('299 €'),
|
||||
* ustHinweis (Satz zur Umsatzsteuer), nachSenden(anfrage) (die Anfrage ist gespeichert: die Ansicht zeichnet sich neu).
|
||||
* Der Server prueft alles; ein 409 (z. B. laeuft schon eine Anfrage) zeigt seinen Text und laesst die Ansicht neu laden.
|
||||
*/
|
||||
export function renderAnfrageFormular(root, { projectId, vorgabe = null, preis = '299 €', ustHinweis = '', nachSenden } = {}) {
|
||||
const v = vorgabe && typeof vorgabe === 'object' ? vorgabe : {}
|
||||
const auswahl = (liste, aktuell) => {
|
||||
const optionen = [h('option', { value: '' }, 'Bitte wählen …'), ...liste.map(([wert, name]) => h('option', { value: wert }, name))]
|
||||
const select = h('select', null, optionen)
|
||||
select.value = aktuell || ''
|
||||
return select
|
||||
}
|
||||
const warenart = auswahl(WARENARTEN, v.warenart)
|
||||
const artikelzahl = auswahl(ARTIKELZAHLEN, v.artikelzahl)
|
||||
const beschreibung = h('textarea', { rows: 4, maxlength: 1500 })
|
||||
beschreibung.value = text(v.beschreibung)
|
||||
const versand = h('input', { type: 'checkbox' })
|
||||
versand.checked = v.versand === true
|
||||
const abholung = h('input', { type: 'checkbox' })
|
||||
abholung.checked = v.abholung === true
|
||||
const unternehmer = h('input', { type: 'checkbox' })
|
||||
const ausgeschlossen = h('input', { type: 'checkbox' })
|
||||
const nachricht = h('textarea', { rows: 3, maxlength: 1000 })
|
||||
nachricht.value = text(v.nachricht)
|
||||
const absenden = h('button', { type: 'submit', class: 'kb-btn-primary', 'data-aktion': 'anfrage-senden' }, 'Anfrage senden')
|
||||
|
||||
const form = h('form', { class: 'sh-form', novalidate: true, 'aria-label': 'Webshop anfragen' },
|
||||
feld('warenart', 'Was möchtest Du verkaufen?', warenart),
|
||||
feld('beschreibung', 'Beschreibe kurz, was Du verkaufst', beschreibung, 'Zum Beispiel: handgemachte Kerzen, Bio-Honig oder Fanartikel eines Vereins.'),
|
||||
feld('artikelzahl', 'Wie viele Artikel möchtest Du etwa anbieten?', artikelzahl),
|
||||
h('fieldset', { class: 'sh-gruppe' },
|
||||
h('legend', null, 'Wie bekommen Deine Kunden die Ware?'),
|
||||
hakenFeld('versand', versand, 'Versand'),
|
||||
hakenFeld('abholung', abholung, 'Abholung')),
|
||||
hakenFeld('unternehmer', unternehmer, 'Ich verkaufe als Unternehmer (nicht privat).'),
|
||||
hakenFeld('keineAusgeschlossenen', ausgeschlossen, `Ich verkaufe keine ausgeschlossenen Waren (${AUSGESCHLOSSEN}).`),
|
||||
feld('nachricht', 'Noch etwas, das wir wissen sollten? (freiwillig)', nachricht),
|
||||
allgemeinerFehler(),
|
||||
h('div', { class: 'sh-aktionen' }, absenden))
|
||||
|
||||
let laeuft = false
|
||||
form.addEventListener('submit', async (ev) => {
|
||||
ev.preventDefault()
|
||||
if (laeuft) return
|
||||
laeuft = true
|
||||
absenden.disabled = true
|
||||
setzeFehler(form, {})
|
||||
try {
|
||||
const antwort = await request('/api/shop/anfrage', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
projectId,
|
||||
anfrage: {
|
||||
warenart: warenart.value,
|
||||
beschreibung: beschreibung.value.trim(),
|
||||
artikelzahl: artikelzahl.value,
|
||||
versand: versand.checked,
|
||||
abholung: abholung.checked,
|
||||
unternehmer: unternehmer.checked,
|
||||
keineAusgeschlossenen: ausgeschlossen.checked,
|
||||
nachricht: nachricht.value.trim(),
|
||||
},
|
||||
},
|
||||
})
|
||||
if (typeof nachSenden === 'function') await nachSenden(antwort?.anfrage ?? null)
|
||||
return
|
||||
} catch (err) {
|
||||
zeigeFehler(form, err)
|
||||
if (err?.status === 409 && typeof nachSenden === 'function') {
|
||||
// der Stand hat sich geaendert (z. B. laeuft schon eine Anfrage): Ansicht neu laden, die Meldung des Servers bleibt am Formular stehen
|
||||
try { await nachSenden(null) } catch { /* der Fehler steht schon am Formular */ }
|
||||
return
|
||||
}
|
||||
}
|
||||
laeuft = false
|
||||
absenden.disabled = false
|
||||
})
|
||||
|
||||
const erklaerung = h('div', { class: 'sh-erklaerung' },
|
||||
h('h3', null, 'Webshop anfragen'),
|
||||
h('p', null, 'Mit dem Webshop verkaufst Du direkt auf Deiner Website: Deine Kunden legen Artikel in den Warenkorb und bezahlen online.'),
|
||||
h('ul', null,
|
||||
h('li', null, `${preis} einmalig für den Einbau in Deine Website, keine laufende Gebühr von WEBklar.`),
|
||||
h('li', null, 'Die Zahlungen gehen direkt auf Dein eigenes Stripe-Konto. Die Gebühren von Stripe zahlst Du direkt an Stripe.'),
|
||||
h('li', null, 'Deine Kunden zahlen mit Karte, Apple Pay, Google Pay, Klarna oder SEPA-Lastschrift.'),
|
||||
h('li', null, 'PayPal ist über Stripe leider nicht möglich.'),
|
||||
h('li', null, 'Wir sehen uns Deine Anfrage kurz an. Erst nach unserer Zusage kannst Du bestellen.')),
|
||||
ustHinweis ? h('p', { class: 'sh-hinweis' }, ustHinweis) : null)
|
||||
|
||||
const karte = h('section', { class: 'kb-panel sh-anfrage' }, erklaerung, form)
|
||||
root.replaceChildren(karte)
|
||||
return karte
|
||||
}
|
||||
|
||||
/**
|
||||
* Karte einer Anfrage im Stand offen (wird geprueft, mit „Anfrage zurückziehen“) oder abgelehnt (Begruendung, „Neue Anfrage stellen“).
|
||||
* ansicht = status.anfrage (anfrageAnsicht des Servers), optionen: projectId, neu() (Formular zeigen), neuLaden() (Stand holen),
|
||||
* kundenansicht (sperrt das Zurueckziehen).
|
||||
*/
|
||||
export function anfrageKarte(ansicht, { projectId, neu, neuLaden } = {}) {
|
||||
const offen = ansicht?.status === 'offen'
|
||||
const meldung = h('p', { class: 'sh-fehler', role: 'alert', 'data-meldung': 'anfrage' })
|
||||
const datum = datumText(ansicht?.at)
|
||||
let aktion
|
||||
if (offen) {
|
||||
aktion = h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'zurueckziehen' }, 'Anfrage zurückziehen')
|
||||
aktion.addEventListener('click', async () => {
|
||||
if (aktion.disabled) return
|
||||
aktion.disabled = true
|
||||
meldung.textContent = ''
|
||||
try {
|
||||
await request('/api/shop/anfrage/zurueckziehen', { method: 'POST', body: { projectId } })
|
||||
if (typeof neuLaden === 'function') await neuLaden()
|
||||
return
|
||||
} catch (err) {
|
||||
meldung.textContent = err.message
|
||||
// 409: die Anfrage ist inzwischen entschieden; der neue Stand erscheint
|
||||
if (err?.status === 409 && typeof neuLaden === 'function') { try { await neuLaden() } catch { /* Meldung bleibt */ } return }
|
||||
}
|
||||
aktion.disabled = false
|
||||
})
|
||||
} else {
|
||||
aktion = h('button', { type: 'button', class: 'kb-btn-primary', 'data-aktion': 'neue-anfrage', onclick: () => { if (typeof neu === 'function') neu() } }, 'Neue Anfrage stellen')
|
||||
}
|
||||
return h('section', { class: 'kb-panel sh-info sh-anfrage-karte', 'data-anfrage': offen ? 'offen' : 'abgelehnt' },
|
||||
h('span', { class: `sh-pille ${offen ? 'sh-pille-wait' : 'sh-pille-bad'}` }, offen ? 'In Prüfung' : 'Abgelehnt'),
|
||||
h('h3', null, offen ? 'Deine Anfrage wird geprüft' : 'Deine Anfrage wurde leider abgelehnt'),
|
||||
offen
|
||||
? h('p', null, `${datum ? `Du hast Deine Anfrage am ${datum} Uhr gesendet. ` : ''}Wir melden uns, sobald wir sie angesehen haben. Bis dahin kannst Du sie zurückziehen und eine neue stellen.`)
|
||||
: h('p', null, 'Unsere Begründung:'),
|
||||
!offen ? h('blockquote', { class: 'sh-begruendung' }, text(ansicht?.begruendung) || 'Dazu liegt keine Begründung vor.') : null,
|
||||
ansicht?.anfrage ? h('div', { class: 'sh-zusammenfassung' }, h('h4', null, 'Deine Angaben'), anfrageListe(ansicht.anfrage)) : null,
|
||||
meldung,
|
||||
h('div', { class: 'sh-aktionen' }, aktion, h('button', { type: 'button', class: 'kb-btn-outline', 'data-view': 'chat' }, 'Frage dazu? Chat öffnen')))
|
||||
}
|
||||
Reference in new issue
Block a user