/* 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'))) }