// 5-Schritt-Konfigurator + Bestätigungsseite const { useState, useEffect, useMemo } = React; const { Btn, navigate, PH, WFNote } = window.UI; // Werte werden zur Render-Zeit aus window.SITE_DATA gelesen (vom Server geladen) // Werte werden zur Render-Zeit aus window.SITE_DATA gelesen — Bootstrap // (app.jsx) füllt SITE_DATA vor dem Render mit Server-Konfig. const getSD = () => window.SITE_DATA || {}; const getANL = () => getSD().ANLAESSE || []; const getBAU = () => getSD().BAUSTEINE || {}; const getTLBau = () => getSD().TIMELINE_BAUSTEINE || []; const getTLVorlage = (anlassId) => { const v = getSD().TIMELINE_VORLAGEN || {}; return (anlassId && v[anlassId]) || v.default || getSD().TIMELINE_VORLAGE || []; }; const T = (k, fb='') => (getSD().TEXTE || {})[k] || fb; // ============ State Hook (URL + localStorage persistent) ============ function useConfigState() { const initial = () => { // URL-Parameter (?anlass=hochzeit etc.) hat höchste Priorität — überschreibt localStorage let urlAnlass = ''; try { const sp = new URLSearchParams(window.location.search); const v = (sp.get('anlass') || '').toLowerCase().trim(); const validIds = (getANL() || []).map(a => a.id); if (v && validIds.includes(v)) urlAnlass = v; } catch {} try { const saved = JSON.parse(localStorage.getItem('onkelfinke_config') || 'null'); if (saved) return urlAnlass ? { ...saved, anlass: urlAnlass } : saved; } catch {} return { anlass: urlAnlass, location: '', personen: 50, datum: '', plz: '', catering: '', getraenke: '', service: [], extras: [], beratung: 'unverbindlich', timeline: [...getTLVorlage(urlAnlass)], kontakt: { name: '', email: '', telefon: '', notiz: '' }, }; }; const [state, setState] = useState(initial); useEffect(() => { localStorage.setItem('onkelfinke_config', JSON.stringify(state)); }, [state]); const update = (patch) => setState(s => ({ ...s, ...patch })); const reset = () => { localStorage.removeItem('onkelfinke_config'); setState(initial); }; return [state, update, reset]; } // ============ Preisberechnung ============ function calc(state) { const p = Math.max(1, Number(state.personen) || 0); let total = 0; const lines = []; const find = (key, id) => (getBAU()[key]?.options || []).find(o => o.id === id); if (state.location) { const o = find('location', state.location); if (o) { const sum = (o.unit === 'p.P.') ? o.price * p : o.price; if (sum > 0) { total += sum; lines.push({ titel: 'Location · ' + o.label, sub: o.unit === 'p.P.' ? `${p} × ${o.price} €` : 'pauschal', sum }); } else lines.push({ titel: 'Location · ' + o.label, sub: 'inkl.', sum: 0 }); } } if (state.catering) { const o = find('catering', state.catering); if (o) { const sum = o.price * p; total += sum; lines.push({ titel: 'Catering · ' + o.label, sub: `${p} × ${o.price} €`, sum }); } } if (state.getraenke) { const o = find('getraenke', state.getraenke); if (o && o.price > 0) { const sum = o.price * p; total += sum; lines.push({ titel: 'Getränke · ' + o.label, sub: `${p} × ${o.price} €`, sum }); } else if (o) { lines.push({ titel: 'Getränke · ' + o.label, sub: 'auf Verbrauch / pauschal', sum: 0 }); } } state.service.forEach(id => { const o = find('service', id); if (o) { total += o.price; lines.push({ titel: 'Service · ' + o.label, sub: o.unit, sum: o.price }); } }); state.extras.forEach(id => { const o = find('extras', id); if (o) { const sum = o.unit === 'p.P.' ? o.price * p : o.price; total += sum; lines.push({ titel: 'Extra · ' + o.label, sub: o.unit === 'p.P.' ? `${p} × ${o.price} €` : 'pauschal', sum }); } }); if (state.beratung && state.beratung !== 'unverbindlich') { const o = find('beratung', state.beratung); if (o && o.price > 0) { total += o.price; lines.push({ titel: 'Beratung · ' + o.label, sub: 'pauschal', sum: o.price }); } } return { total, lines }; } // ============ Schritt-Header ============ const STEPS = [ { n: 1, t: 'Anlass' }, { n: 2, t: 'Location' }, { n: 3, t: 'Catering' }, { n: 4, t: 'Service' }, { n: 5, t: 'Timeline' }, { n: 6, t: 'Übersicht' }, ]; function Stepper({ step, onJump }) { const ref = React.useRef(null); React.useEffect(() => { const el = ref.current; if (!el) return; const active = el.querySelector('[data-active="true"]'); if (active && el.scrollWidth > el.clientWidth) { const target = active.offsetLeft - 16; el.scrollTo({ left: Math.max(0, target), behavior: 'smooth' }); } }, [step]); return (
{STEPS.map((s, i) => ( ))}
); } // ============ Option Card ============ function OptionCard({ active, onClick, title, desc, price, unit, multi }) { return ( ); } // ============ Schritt 1: Anlass + Eckdaten ============ function Step1({ state, update }) { return (

{T("step1_h", "Was feierst du?")}

Wir richten alles auf deinen Anlass aus — sag uns als erstes, worum's geht.

{getANL().map(a => ( ))}

Eckdaten

update({ datum: e.target.value })} style={inputStyle} />
update({ personen: Number(e.target.value) })} style={{ ...inputStyle, textAlign: 'center', flex: 1, minWidth: 0 }} />
update({ plz: e.target.value })} style={inputStyle} />
); } // ============ Schritt 2: Location ============ function StepLocation({ state, update }) { const cat = getBAU().location; const options = (cat && cat.options) || [ { id: 'eigene', label: 'Ich habe eine eigene Location', price: 0, unit: 'inkl.', desc: 'Du hast bereits einen Saal, Garten oder eine Halle.' }, { id: 'suchen', label: 'Onkelfinke sucht eine Location', price: 180, unit: 'pauschal', desc: 'Wir finden die passende Location nach deinen Wünschen.' }, { id: 'komplett', label: 'Komplettpaket mit Location', price: 0, unit: 'auf Anfrage', desc: 'Location + Catering + Service — alles aus einer Hand.' }, ]; return (

{T("step_location_h", "Wo soll's stattfinden?")}

{T("step_location_p", "Hast du schon eine Location — oder sollen wir uns darum kümmern?")}

{options.map(o => ( update({ location: state.location === o.id ? '' : o.id })} title={o.label} desc={o.desc} price={o.price} unit={o.unit} /> ))}
); } // ============ Schritt 3: Catering + Getränke ============ function Step2({ state, update }) { return (

{T("step2_h", "Was kommt auf den Tisch?")}

Catering und Getränke — wähle je eine Variante. Du kannst es später jederzeit ändern.

{['catering', 'getraenke'].map(key => { const cat = getBAU()[key]; if (!cat) return null; return (
{cat.title}
{cat.options.map(o => ( update({ [key]: state[key] === o.id ? '' : o.id })} title={o.label} desc={o.desc} price={o.price} unit={o.unit} /> ))}
); })}
); } // ============ Schritt 3: Service + Extras ============ function Step3({ state, update }) { const toggle = (key, id) => { const list = state[key]; update({ [key]: list.includes(id) ? list.filter(x => x !== id) : [...list, id] }); }; return (

{T("step3_h", "Wer macht's?")}

Service und Extras — alles optional, alles dazubuchbar.

{['service', 'extras', 'beratung'].map(key => { const cat = getBAU()[key]; if (!cat) return null; return (
{cat.title}
{cat.options.map(o => { const active = cat.multi ? state[key].includes(o.id) : state[key] === o.id; return ( cat.multi ? toggle(key, o.id) : update({ [key]: state[key] === o.id ? '' : o.id })} title={o.label} desc={o.desc} price={o.price} unit={o.unit} /> ); })}
); })}
); } // ============ Schritt 4: Timeline (drag & drop) ============ function Step4({ state, update }) { const [dragId, setDragId] = useState(null); const onDragStart = (id) => (e) => { setDragId(id); e.dataTransfer.effectAllowed = 'move'; }; const onDragOver = (idx) => (e) => { e.preventDefault(); }; const onDrop = (idx) => (e) => { e.preventDefault(); if (!dragId) return; const list = [...state.timeline]; const fromIdx = list.findIndex(x => x.id === dragId); if (fromIdx < 0) { // Aus Bausteinen ziehen const tpl = getTLBau().find(x => x.id === dragId); if (tpl) { list.splice(idx, 0, { ...tpl, id: tpl.id + '-' + Date.now(), zeit: '' }); } } else { const [item] = list.splice(fromIdx, 1); list.splice(idx, 0, item); } update({ timeline: list }); setDragId(null); }; const removeItem = (id) => update({ timeline: state.timeline.filter(x => x.id !== id) }); const updateItem = (id, patch) => update({ timeline: state.timeline.map(x => x.id === id ? { ...x, ...patch } : x) }); return (

{T("step4_h", "Wie soll der Tag laufen?")}

Ein grober Plan — du kannst Bausteine reinziehen, Zeiten ändern, Reihenfolge anpassen. Wir machen daraus einen Ablauf.

{/* Timeline */}
Dein Ablauf
{state.timeline.map((item, idx) => (
⋮⋮ updateItem(item.id, { zeit: e.target.value })} className="mono of-timeline-time" style={{ ...inputStyle, padding: '6px 8px', fontSize: 11, textAlign: 'center' }} /> {item.icon} updateItem(item.id, { label: e.target.value })} style={{ ...inputStyle, padding: '6px 8px', fontSize: 14 }} />
))}
Hier Baustein ablegen ↓
{/* Bausteine */}
Bausteine zum Ziehen
{getTLBau().map(b => (
{b.icon} {b.label} ziehen ⋮⋮
))}
); } // ============ Schritt 5: Übersicht + Kontakt ============ function Step5({ state, update }) { const { total, lines } = calc(state); return (

{T("step5_h", "Sieht das gut aus?")}

Prüf deine Auswahl, sag uns wie wir dich erreichen — wir melden uns innerhalb von 24h mit einem persönlichen Angebot.

{/* Kontaktformular */}
Deine Kontaktdaten
update({ kontakt: { ...state.kontakt, name: e.target.value } })} style={inputStyle} /> update({ kontakt: { ...state.kontakt, email: e.target.value } })} style={inputStyle} /> update({ kontakt: { ...state.kontakt, telefon: e.target.value } })} style={inputStyle} />