From 3172ee19396dbbb2733a796b63295f4a875cd9d4 Mon Sep 17 00:00:00 2001 From: Johannes Baumeister Date: Thu, 30 Apr 2026 14:30:51 +0200 Subject: [PATCH] =?UTF-8?q?Implement=20Fl=C3=A4chensicherung=20directly=20?= =?UTF-8?q?with=20ALKIS=20DB=20and=20added=20Notiz=20field?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 122 ++++++++++++++++++++++++++++++++++++++------------ index.html | 9 ++-- server.js | 127 ++++++++++++++++++++++++++++++----------------------- 3 files changed, 169 insertions(+), 89 deletions(-) diff --git a/app.js b/app.js index 9ab7d1a..b1976ae 100644 --- a/app.js +++ b/app.js @@ -1192,12 +1192,65 @@ document.addEventListener('DOMContentLoaded', async () => { await loadShapefileLayer(l); } } + + // NEU: ALKIS aus Datenbank laden, falls kein Eigentümer-Layer da ist + const hasOwnerLayer = Object.keys(overlays).some(k => k.toLowerCase().includes('eigentümer')); + if (!hasOwnerLayer) { + console.log("Kein lokaler Eigentümer-Layer. Lade ALKIS aus Datenbank..."); + const resp = await fetch('/api/layers/alkis').catch(() => null); + if (resp && resp.ok) { + const data = await resp.json(); + await processALKISData(data, "Eigentümer (ALKIS DB)"); + } + } + statusEl.innerText = "Alle konfigurierten Layer geladen."; } catch (e) { if (!isLocalFile) console.error("Layer-Init fehlgeschlagen:", e); } } + async function processALKISData(geojson, layerName) { + const style = getDynamicStyle(layerName) || { color: '#000', weight: 1, fillOpacity: 0.1 }; + + const layer = L.geoJSON(geojson, { + style: (feature) => { + const statusRaw = feature.properties.status; + const status = statusRaw ? statusRaw.toLowerCase() : ""; + let fillColor = 'transparent'; + + if (status === 'gbr' || status === 'gesichert') fillColor = '#2ecc71'; + else if (status === 'external' || status === 'fremdplanung') fillColor = '#e74c3c'; + else if (status === 'declined' || status === 'ablehnend' || status === 'negative') fillColor = '#e74c3c'; + else if (status === 'undecided' || status === 'unentschlossen') fillColor = '#95a5a6'; + else if (status === 'positive' || status === 'positiv') fillColor = '#5efd9c'; + else if (status === 'in verhandlung') fillColor = '#f1c40f'; + + return { + color: '#000', + weight: 1, + fillOpacity: fillColor === 'transparent' ? 0.1 : 0.7, + fillColor: fillColor + }; + }, + onEachFeature: (feature, layer) => { + if (feature.properties) { + let popup = `${layerName}

`; + for (let key in feature.properties) { + const val = feature.properties[key]; + if (val !== null && val !== undefined) popup += `${key}: ${val}
`; + } + layer.bindPopup(popup); + } + } + }); + + overlays[layerName] = layer; + state.map.addLayer(layer); + layerControl.addOverlay(layer, layerName); + layer.bringToBack(); + } + // Manual Import & Bundling const btnManualImport = document.getElementById('btnManualImport'); const manualShpInput = document.getElementById('manualShpInput'); @@ -1363,7 +1416,10 @@ document.addEventListener('DOMContentLoaded', async () => { ownerTableBody.innerHTML = ''; Object.keys(owners).sort().forEach(name => { const data = owners[name]; - const status = state.ownerStatuses[name] || 'none'; + const stored = state.ownerStatuses[name.toLowerCase()] || { status: 'none', notiz: '' }; + const status = typeof stored === 'string' ? stored : (stored.status || 'none'); + const notiz = typeof stored === 'string' ? '' : (stored.notiz || ''); + const row = document.createElement('tr'); row.innerHTML = ` ${name} @@ -1379,7 +1435,10 @@ document.addEventListener('DOMContentLoaded', async () => { - + + + + `; ownerTableBody.appendChild(row); @@ -1388,21 +1447,20 @@ document.addEventListener('DOMContentLoaded', async () => { // Add event listeners to dropdowns document.querySelectorAll('.status-select').forEach(sel => { sel.onchange = async (e) => { - const name = e.target.dataset.owner; // original name for display + const name = e.target.dataset.owner; const status = e.target.value; - state.ownerStatuses[name.toLowerCase()] = status; + const notizInput = document.querySelector(`.notiz-input[data-owner="${name}"]`); + const notiz = notizInput ? notizInput.value : ""; + + state.ownerStatuses[name.toLowerCase()] = { status, notiz }; // Sync with DB const data = owners[name]; if (data) { - await secureOwner(data.first, data.last, e.target, status); + await secureOwner(data.first, data.last, e.target, status, notiz); } - // Refresh map style - const ownerLayerName = Object.keys(overlays).find(k => k.toLowerCase().includes('eigentümer')); - if (ownerLayerName) { - overlays[ownerLayerName].setStyle(overlays[ownerLayerName].options.style); - } + refreshOwnerLayerStyle(); }; }); @@ -1411,12 +1469,18 @@ document.addEventListener('DOMContentLoaded', async () => { btn.onclick = async (e) => { const first = e.target.dataset.first; const last = e.target.dataset.last; - await secureOwner(first, last, e.target); + const name = e.target.dataset.owner; + const sel = document.querySelector(`.status-select[data-owner="${name}"]`); + const status = sel ? sel.value : 'Gesichert'; + const notizInput = document.querySelector(`.notiz-input[data-owner="${name}"]`); + const notiz = notizInput ? notizInput.value : ""; + + await secureOwner(first, last, e.target, status, notiz); }; }); } - async function secureOwner(vorname, nachname, element, status = 'Gesichert') { + async function secureOwner(vorname, nachname, element, status = 'Gesichert', notiz = '') { const isButton = element.tagName === 'BUTTON'; const originalText = isButton ? element.innerText : ""; if (isButton) { @@ -1430,26 +1494,28 @@ document.addEventListener('DOMContentLoaded', async () => { const response = await fetch('/api/sicherung', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ vorname, nachname, projekt_id, status }) + body: JSON.stringify({ vorname, nachname, projekt_id, status, notiz }) }); const result = await response.json(); if (response.ok) { if (isButton) { element.style.background = '#2ecc71'; - element.innerText = "✓ Gesichert"; + element.innerText = "✓ Gespeichert"; + setTimeout(() => { + element.style.background = ''; + element.innerText = "Speichern"; + element.disabled = false; + }, 2000); } console.log(result.message); const fullName = `${vorname || ''} ${nachname || ''}`.trim(); if (fullName) { - state.ownerStatuses[fullName.toLowerCase()] = status; + state.ownerStatuses[fullName.toLowerCase()] = { status, notiz }; } - const ownerLayerName = Object.keys(overlays).find(k => k.toLowerCase().includes('eigentümer')); - if (ownerLayerName) { - overlays[ownerLayerName].setStyle(overlays[ownerLayerName].options.style); - } + refreshOwnerLayerStyle(); const select = document.querySelector(`.status-select[data-owner="${fullName}"]`); if (select) select.value = status; @@ -1527,22 +1593,20 @@ document.addEventListener('DOMContentLoaded', async () => { try { const response = await fetch(`/api/sicherung/${projekt_id}`); if (response.ok) { - const statuses = await response.json(); - statuses.forEach(s => { + const entries = await response.json(); + entries.forEach(s => { const first = s.vorname || ''; const last = s.nachname || ''; const fullName = `${first} ${last}`.trim().toLowerCase(); if (fullName) { - state.ownerStatuses[fullName] = s.status; + state.ownerStatuses[fullName] = { + status: s.status, + notiz: s.notiz + }; } }); - console.log(`${statuses.length} Eigentümer-Status geladen.`); - - // Refresh map style if owner layer exists - const ownerLayerName = Object.keys(overlays).find(k => k.toLowerCase().includes('eigentümer')); - if (ownerLayerName && overlays[ownerLayerName]) { - overlays[ownerLayerName].setStyle(overlays[ownerLayerName].options.style); - } + console.log(`${entries.length} Eigentümer-Status geladen.`); + refreshOwnerLayerStyle(); } else { console.warn("API Fehler beim Laden der Status:", response.status); } diff --git a/index.html b/index.html index e39ca7b..9613964 100644 --- a/index.html +++ b/index.html @@ -232,10 +232,11 @@ - - - - + + + + + diff --git a/server.js b/server.js index f093f9e..2679832 100644 --- a/server.js +++ b/server.js @@ -133,71 +133,55 @@ async function resolveProjectId(client, input) { // API zur Flächensicherung von Eigentümern app.post('/api/sicherung', async (req, res) => { - const { nachname, vorname, projekt_id, status } = req.body; + const { nachname, vorname, projekt_id, status, notiz } = req.body; const targetStatus = status || 'Gesichert'; const schema = process.env.DB_SCHEMA || 'geodaten'; - log(`Sicherungs-Request: ${vorname} ${nachname} Status='${targetStatus}' für Projekt ${projekt_id}`); + log(`Sicherungs-Request: ${vorname} ${nachname} Status='${targetStatus}' Notiz='${notiz}'`); const client = await pool.connect(); try { await client.query('BEGIN'); - // 1. Projekt-ID auflösen - const resolvedPid = await resolveProjectId(client, projekt_id); - if (!resolvedPid) { - throw new Error(`Projekt '${projekt_id}' konnte nicht gefunden werden.`); - } + // Suche nach dem Eigentümer in flaecheneigentuemer_alkis und aktualisiere status und notiz + const searchNachname = (nachname || '').trim(); + const searchVorname = (vorname || '').trim(); - // 2. FSKs für den Namen finden (Behandlung von NULL vs leerem String) - // Wir suchen flexibel: entweder exakter Match oder der Name ist Teil eines kombinierten Feldes - const searchNachname = (nachname || '').trim().toLowerCase(); - const searchVorname = (vorname || '').trim().toLowerCase(); - const searchFull = `${searchVorname} ${searchNachname}`.trim().toLowerCase(); - - const ownerRes = await client.query( - `SELECT "FSK" FROM ${schema}.flaecheneigentuemer_alkis + // Wir aktualisieren alle Einträge, die auf diesen Namen passen + const updateRes = await client.query( + `UPDATE ${schema}.flaecheneigentuemer_alkis + SET status = $1, notiz = $2 WHERE - (LOWER("GNA") = $1 AND LOWER("VNA") = $2) OR - (LOWER("VNA") = $3 AND "GNA" IS NULL) OR - (LOWER("GNA") = $3 AND "VNA" IS NULL) OR - (LOWER("VNA") LIKE '%' || $1 || '%' AND LOWER("VNA") LIKE '%' || $2 || '%')`, - [searchNachname, searchVorname, searchFull] + ("GNA" = $3 AND "VNA" = $4) OR + ("VNA" = $5 AND "GNA" IS NULL) OR + ("GNA" = $5 AND "VNA" IS NULL)`, + [targetStatus, notiz, searchNachname, searchVorname, `${searchVorname} ${searchNachname}`.trim()] ); - - const fsks = ownerRes.rows.map(r => r.FSK); - if (fsks.length === 0) { - log(`Keine Flurstücke für ${vorname} ${nachname} gefunden.`); - await client.query('ROLLBACK'); - return res.status(404).json({ error: 'Keine Flurstücke für diesen Namen gefunden.' }); - } - log(`Gefunden: ${fsks.length} Flurstücke für ${vorname} ${nachname}. Prüfe Zuweisung...`); - - // 3. Zuweisung prüfen und Status schreiben - let securedCount = 0; - for (const fsk of fsks) { - const assignmentRes = await client.query( - `SELECT 1 FROM ${schema}.flaecheneigentuemer_alkis_zuweisung WHERE fsk = $1 AND projekt_id = $2`, - [fsk, resolvedPid] + if (updateRes.rowCount === 0) { + log(`Kein Eigentümer für ${vorname} ${nachname} zum Aktualisieren gefunden.`); + // Optional: Fallback Suche mit LOWER + const updateResLower = await client.query( + `UPDATE ${schema}.flaecheneigentuemer_alkis + SET status = $1, notiz = $2 + WHERE + (LOWER("GNA") = LOWER($3) AND LOWER("VNA") = LOWER($4)) OR + (LOWER("VNA") = LOWER($5) AND "GNA" IS NULL) OR + (LOWER("GNA") = LOWER($5) AND "VNA" IS NULL)`, + [targetStatus, notiz, searchNachname, searchVorname, `${searchVorname} ${searchNachname}`.trim()] ); - if (assignmentRes.rowCount > 0) { - await client.query( - `INSERT INTO ${schema}.flaecheneigentuemer_status (id, fsk, projekt_id, status, datum) - VALUES (gen_random_uuid(), $1, $2, $3, NOW())`, - [fsk, resolvedPid, targetStatus] - ); - securedCount++; + if (updateResLower.rowCount === 0) { + await client.query('ROLLBACK'); + return res.status(404).json({ error: 'Eigentümer nicht in der Datenbank gefunden.' }); } } await client.query('COMMIT'); - log(`Erfolgreich: ${securedCount} von ${fsks.length} Flurstücken als '${targetStatus}' markiert.`); + log(`Erfolgreich: ${updateRes.rowCount || 1} Einträge in ALKIS aktualisiert.`); res.json({ - message: `${securedCount} Flurstücke für ${vorname} ${nachname} erfolgreich als '${targetStatus}' markiert.`, - count: securedCount, - total_found: fsks.length + message: `Eigentümer ${vorname} ${nachname} erfolgreich aktualisiert.`, + count: updateRes.rowCount }); } catch (err) { await client.query('ROLLBACK'); @@ -215,19 +199,14 @@ app.get('/api/sicherung/:projekt_id', async (req, res) => { const client = await pool.connect(); try { - const resolvedPid = await resolveProjectId(client, projekt_id); - if (!resolvedPid) { - throw new Error(`Projekt '${projekt_id}' konnte nicht gefunden werden.`); - } - + // Wir laden nun direkt aus der ALKIS Tabelle, wie vom User gewünscht const result = await client.query( - `SELECT vorname, nachname, aktueller_status as status - FROM geodaten.v_projekt_sicherung - WHERE projekt_id = $1`, - [resolvedPid] + `SELECT "VNA" as vorname, "GNA" as nachname, status, notiz + FROM geodaten.flaecheneigentuemer_alkis + WHERE status IS NOT NULL OR notiz IS NOT NULL` ); - log(`Geladen: ${result.rowCount} Status-Einträge.`); + log(`Geladen: ${result.rowCount} Status-Einträge aus ALKIS.`); res.json(result.rows); } catch (e) { log(`FEHLER beim Laden der Stände: ${e.message}`); @@ -237,6 +216,42 @@ app.get('/api/sicherung/:projekt_id', async (req, res) => { } }); +// NEU: API zum Laden des ALKIS-Layers als GeoJSON +app.get('/api/layers/alkis', async (req, res) => { + log("Lade ALKIS-Layer aus Datenbank..."); + try { + const result = await pool.query( + `SELECT jsonb_build_object( + 'type', 'FeatureCollection', + 'features', jsonb_agg(features.feature) + ) + FROM ( + SELECT jsonb_build_object( + 'type', 'Feature', + 'id', id, + 'geometry', ST_AsGeoJSON(ST_Transform(geom, 4326))::jsonb, + 'properties', jsonb_build_object( + 'id', id, + 'VNA', "VNA", + 'GNA', "GNA", + 'FSK', "FSK", + 'PLZ', "PLZ", + 'ORP', "ORP", + 'STR', "STR", + 'status', status, + 'notiz', notiz + ) + ) AS feature + FROM geodaten.flaecheneigentuemer_alkis + ) features` + ); + res.json(result.rows[0].jsonb_build_object); + } catch (err) { + log(`FEHLER beim Laden des ALKIS-Layers: ${err.message}`); + res.status(500).json({ error: err.message }); + } +}); + // API für Projekt-Statistiken (Fortschrittsanzeige) app.get('/api/stats/:projekt_id', async (req, res) => { const { projekt_id } = req.params;
NameFlächenStatusAktionNameFlächenStatusNotizAktion