/* ================================================ 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 (