171 lines
10 KiB
JavaScript
171 lines
10 KiB
JavaScript
/* 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), modell ('abo': kein
|
||
* Preis und keine Umsatzsteuer, kein Feld „unternehmer“, statt des Hakens „keine ausgeschlossenen Waren“ ein Satz über dem Knopf; gesendet wird
|
||
* keineAusgeschlossenen: true, unternehmer entfällt – die Kundenart kennt der Server).
|
||
* 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, modell } = {}) {
|
||
const abo = modell === 'abo' // Abo-Modell (strikt): ohne Preis, ohne Unternehmer-Haken, der Satz S2 statt des Hakens
|
||
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')),
|
||
abo ? null : hakenFeld('unternehmer', unternehmer, 'Ich verkaufe als Unternehmer (nicht privat).'),
|
||
abo ? null : hakenFeld('keineAusgeschlossenen', ausgeschlossen, `Ich verkaufe keine ausgeschlossenen Waren (${AUSGESCHLOSSEN}).`),
|
||
feld('nachricht', 'Noch etwas, das wir wissen sollten? (freiwillig)', nachricht),
|
||
allgemeinerFehler(),
|
||
abo ? h('p', { class: 'sh-hinweis', 'data-satz': 'ausgeschlossen' }, `Mit „Anfrage senden“ bestätigst du, dass du keine ausgeschlossenen Waren verkaufst (${AUSGESCHLOSSEN}).`) : null,
|
||
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,
|
||
...(abo ? { keineAusgeschlossenen: true } : { 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, abo ? 'Im Wartungs-Abo enthalten.' : `${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, abo ? 'Wir sehen uns deine Anfrage kurz an. Erst nach unserer Zusage kannst du einschalten.' : 'Wir sehen uns deine Anfrage kurz an. Erst nach unserer Zusage kannst du bestellen.')),
|
||
ustHinweis && !abo ? 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')))
|
||
}
|