/* ================================================ Wizard — 4 Steps ================================================ */ /* Convert a Leaflet ring ([[lat,lng],...]) to a GeoJSON polygon feature (lon/lat + closed ring). */ function ringToGeoJson(ring) { const coords = ring.map(([lat, lng]) => [lng, lat]); const first = coords[0]; const last = coords[coords.length - 1]; if (!first || !last) return null; if (first[0] !== last[0] || first[1] !== last[1]) coords.push([...first]); return { type: "Feature", geometry: { type: "Polygon", coordinates: [coords] }, properties: {} }; } /* Merge a list of Flurstück objects into a single outer ring in [lat,lng] format, suitable for storage in parcel.koordinaten and Leaflet rendering. Uses Turf.js union when available; falls back to the first ring. */ function mergeFlurstuecke(flurstuecke) { if (!flurstuecke || flurstuecke.length === 0) return []; if (flurstuecke.length === 1) return flurstuecke[0].koordinaten.slice(); const turfAvail = typeof window !== "undefined" && window.turf && typeof window.turf.union === "function"; if (!turfAvail) return flurstuecke[0].koordinaten.slice(); try { let merged = ringToGeoJson(flurstuecke[0].koordinaten); for (let i = 1; i < flurstuecke.length; i++) { const next = ringToGeoJson(flurstuecke[i].koordinaten); if (!next) continue; // Turf 7 expects a FeatureCollection for union() const fc = { type: "FeatureCollection", features: [merged, next] }; const u = window.turf.union(fc); if (u && u.geometry) merged = u; } const g = merged.geometry; let ring; if (g.type === "Polygon") { ring = g.coordinates[0]; } else if (g.type === "MultiPolygon") { // pick the biggest polygon by point count let best = g.coordinates[0][0]; for (const poly of g.coordinates) { if (poly[0].length > best.length) best = poly[0]; } ring = best; } else { return flurstuecke[0].koordinaten.slice(); } const out = ring.map(([lng, lat]) => [lat, lng]); if (out.length > 1) { const a = out[0], b = out[out.length - 1]; if (a[0] === b[0] && a[1] === b[1]) out.pop(); } return out; } catch (e) { console.warn("mergeFlurstuecke failed:", e); return flurstuecke[0].koordinaten.slice(); } } // Standard-Zentrum: Achkarren (Ortsteil von Vogtsburg im Kaiserstuhl) const DEFAULT_CENTER = [48.0712, 7.6612]; const DEFAULT_PLZ = "79235"; function Wizard({ onClose, onSave, onDelete, mapStyle, existingParcels, editParcel }) { const isEdit = !!editParcel; const [step, setStep] = React.useState(0); const [confirmDelete, setConfirmDelete] = React.useState(false); const [pickerStatus, setPickerStatus] = React.useState({ loading: false, message: null, count: 0, truncated: false }); const [plz, setPlz] = React.useState(DEFAULT_PLZ); const [plzStatus, setPlzStatus] = React.useState({ loading: false, error: null, info: null }); // flyTo-Signal für den Picker: {lat, lng, zoom, ts} – ts triggert den Effect. const [flyTo, setFlyTo] = React.useState(null); const [data, setData] = React.useState(() => editParcel ? { lagebuch: editParcel.lagebuch, lage: editParcel.lage, region: "", flaeche: editParcel.flaeche || 0, center: editParcel.koordinaten[0] || DEFAULT_CENTER, flurstuecke: editParcel.flurstuecke || [], prevKoordinaten: editParcel.koordinaten || [], sorte: editParcel.sorte, pflanzjahr: editParcel.pflanzjahr, erziehung: editParcel.erziehung, notizen: editParcel.notizen === "—" ? "" : (editParcel.notizen || ""), } : { lagebuch: `LB-${new Date().getFullYear()}-${String(existingParcels.length + 1).padStart(3, "0")}`, lage: "", region: "", flaeche: 0, center: DEFAULT_CENTER, flurstuecke: [], prevKoordinaten: [], sorte: "", pflanzjahr: new Date().getFullYear(), erziehung: "Drahtrahmen", notizen: "", }); // "Touched"-Flags: sobald der Benutzer manuell eintippt, hört die Auto- // Befüllung auf. Beim Edit starten wir als touched, damit vorhandene Werte // bestehen bleiben. const [lageTouched, setLageTouched] = React.useState(isEdit); const [regionTouched, setRegionTouched] = React.useState(isEdit); const [flaecheTouched, setFlaecheTouched] = React.useState(isEdit); const flaecheHa = React.useMemo( () => data.flurstuecke.reduce((s, f) => s + (f.flaeche_ha || 0), 0), [data.flurstuecke] ); // Auto-Befüllung (Lage / Region / Fläche) aus dem ersten Flurstück. // Region = gemarkung_name (so vom Benutzer gewünscht). React.useEffect(() => { if (data.flurstuecke.length === 0) return; const f = data.flurstuecke[0]; setData(d => ({ ...d, lage: lageTouched ? d.lage : (f.gemarkung_name ? `${f.gemarkung_name} ${f.flurstueckstext || ""}`.trim() : d.lage), region: regionTouched ? d.region : (f.gemarkung_name || d.region), flaeche: flaecheTouched ? d.flaeche : Number(flaecheHa.toFixed(2)), })); }, [data.flurstuecke.map(f => f.kennzeichen).join("|"), flaecheHa]); // Merged polygon (outer ring, in [lat,lng]) for saving + preview const mergedPolygon = React.useMemo(() => mergeFlurstuecke(data.flurstuecke), [data.flurstuecke]); // Step-Validierung: 0 = Flurstücke, 1 = Stammdaten, 2 = Sorte, 3 = Summary const canAdvance = { 0: data.flurstuecke.length >= 1 && mergedPolygon.length >= 3, 1: !!String(data.lagebuch || "").trim() && !!String(data.lage || "").trim() && Number(data.flaeche) > 0, 2: !!data.sorte && !!data.pflanzjahr, 3: true, }[step]; const steps = [ { n: 1, label: "Flurstücke wählen", sub: "Auf der Karte auswählen" }, { n: 2, label: "Stammdaten", sub: "Lagebuch, Lage, Fläche" }, { n: 3, label: "Sorte & Details", sub: "Rebsorte und Angaben" }, { n: 4, label: "Zusammenfassung", sub: "Überprüfen & speichern" }, ]; async function searchPlz() { const clean = String(plz || "").replace(/\D+/g, ""); if (clean.length !== 5) { setPlzStatus({ loading: false, error: "PLZ muss 5 Ziffern enthalten.", info: null }); return; } setPlzStatus({ loading: true, error: null, info: null }); try { const r = await fetch(`/api/geocode/plz?plz=${clean}`, { headers: { Accept: "application/json" } }); const j = await r.json(); if (!r.ok) { setPlzStatus({ loading: false, error: j.error || "Nicht gefunden.", info: null }); return; } setFlyTo({ lat: j.lat, lng: j.lon, zoom: 17, ts: Date.now() }); setPlzStatus({ loading: false, error: null, info: j.display_name ? j.display_name.split(",").slice(0, 3).join(",") : `PLZ ${clean}`, }); } catch (e) { setPlzStatus({ loading: false, error: "Netzwerkfehler.", info: null }); } } function handleSave() { onSave({ // Lagebuchnummer ist die ID – keine separate p-xxx-ID mehr. id: data.lagebuch, lagebuch: data.lagebuch, lage: data.lage, sorte: data.sorte, pflanzjahr: Number(data.pflanzjahr), erziehung: data.erziehung, notizen: data.notizen || "—", flaeche: Number(Number(data.flaeche).toFixed(2)), koordinaten: mergedPolygon, flurstuecke: data.flurstuecke.map(f => ({ kennzeichen: f.kennzeichen, gemarkung_name: f.gemarkung_name, gemeinde_name: f.gemeinde_name, flurnummer: f.flurnummer, zaehler: f.zaehler, nenner: f.nenner, flurstueckstext: f.flurstueckstext, flaeche_ha: f.flaeche_ha, koordinaten: f.koordinaten, })), letzteBearbeitung: new Date().toISOString().slice(0, 10), historie: editParcel ? [{ datum: new Date().toISOString().slice(0, 10), text: "Fläche bearbeitet" }, ...editParcel.historie] : [{ datum: new Date().toISOString().slice(0, 10), text: "Fläche angelegt" }], farbe: window.getSorteFarbe(data.sorte), }); } return (
e.stopPropagation()}>

Neue Fläche anlegen

{data.lagebuch}
{steps.map((s, i) => (
{i < step ? : s.n}
{s.label}
{s.sub}
))}
{step === 0 && (
setPlz(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); searchPlz(); } }} placeholder="79235" maxLength={5} inputMode="numeric" /> {plzStatus.error && {plzStatus.error}} {plzStatus.info && !plzStatus.error && {plzStatus.info}}
setData(d => ({ ...d, flurstuecke: fs }))} mapStyle={mapStyle} center={data.center} onStatus={setPickerStatus} initialOutline={data.prevKoordinaten} flyTo={flyTo} />
So gehts: Über die PLZ kannst du die Karte schnell in deine Region bringen. Zoome dann weiter hinein (ab Zoom 16 werden ALKIS-Flurstücke eingeblendet) und klicke die gewünschten Flurstücke an. Auch mehrere benachbarte sind möglich — sie werden zu einer Fläche zusammengefasst.
{pickerStatus.loading && ⏳ Flurstücke werden geladen…} {!pickerStatus.loading && pickerStatus.message && ℹ️ {pickerStatus.message}} {!pickerStatus.loading && !pickerStatus.message && pickerStatus.count > 0 && ( {pickerStatus.count} Flurstücke im Sichtbereich{pickerStatus.truncated ? " (abgeschnitten)" : ""} )}
Ausgewählte Flurstücke{data.flurstuecke.length}
Amtliche Fläche{data.flurstuecke.length ? `${fmtNum(flaecheHa)} ha` : "—"}
Auswahl
{data.flurstuecke.length > 0 && ( )}
{data.flurstuecke.length === 0 && (
Noch keine Flurstücke ausgewählt.
)} {data.flurstuecke.map((f) => (
{f.gemarkung_name || "?"} {fmtNum(f.flaeche_ha)} ha
Flur {f.flurnummer} · Flst. {f.flurstueckstext || f.zaehler} {f.nenner ? `/${f.nenner}` : ""} · {f.gemeinde_name || ""}
))}
{data.flurstuecke.length > 1 && (
Die gewählten Flurstücke werden zu einer Fläche zusammengefasst. Bei nicht zusammenhängender Auswahl wird der größte Teil übernommen.
)}
)} {step === 1 && (
setData({ ...data, lagebuch: e.target.value })} /> Wird automatisch vorgeschlagen, manuell anpassbar. Dient als eindeutige ID.
{ setFlaecheTouched(true); setData({ ...data, flaeche: e.target.value }); }} /> {flaecheTouched ? <>Manuell überschrieben. : <>Automatisch aus der Summe der gewählten Flurstücke.}
{ setLageTouched(true); setData({ ...data, lage: e.target.value }); }} placeholder="z.B. Achkarren Schlossberg" /> {lageTouched ? "Manuell überschrieben." : "Vorschlag aus Gemarkung + Flurstücksnummer."}
{ setRegionTouched(true); setData({ ...data, region: e.target.value }); }} placeholder="z.B. Kaiserstuhl" /> {regionTouched ? <>Manuell überschrieben.{data.flurstuecke[0]?.gemarkung_name ? <> : null} : <>Automatisch aus der Gemarkung des ersten Flurstücks.}
)} {step === 2 && (
{SORTEN.map((s) => ( ))}
setData({ ...data, pflanzjahr: e.target.value })} min="1950" max={new Date().getFullYear()} />