Implement Flächensicherung directly with ALKIS DB and added Notiz field
Deploy Bürgerwind / deploy (push) Successful in 16s Details

This commit is contained in:
Johannes Baumeister 2026-04-30 14:30:51 +02:00
parent f55f70c0eb
commit 3172ee1939
3 changed files with 169 additions and 89 deletions

122
app.js
View File

@ -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 = `<b>${layerName}</b><br><hr style="margin: 5px 0; border: 0; border-top: 1px solid #444;">`;
for (let key in feature.properties) {
const val = feature.properties[key];
if (val !== null && val !== undefined) popup += `<b>${key}:</b> ${val}<br>`;
}
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 = `
<td><b>${name}</b></td>
@ -1379,7 +1435,10 @@ document.addEventListener('DOMContentLoaded', async () => {
</select>
</td>
<td>
<button class="btn-secure" data-first="${data.first}" data-last="${data.last}" style="padding: 4px 8px; font-size: 0.75rem; background: var(--primary-color); color: white; border: none; border-radius: 4px; cursor: pointer;">Sichern</button>
<input type="text" class="notiz-input" data-owner="${name}" value="${notiz}" placeholder="Notiz..." style="width: 100%; font-size: 0.75rem; padding: 4px; border: 1px solid var(--border-color); border-radius: 4px; background: transparent; color: white;">
</td>
<td>
<button class="btn-secure" data-first="${data.first}" data-last="${data.last}" data-owner="${name}" style="padding: 4px 8px; font-size: 0.75rem; background: var(--primary-color); color: white; border: none; border-radius: 4px; cursor: pointer;">Speichern</button>
</td>
`;
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);
}

View File

@ -232,10 +232,11 @@
<table id="ownerTable">
<thead>
<tr>
<th>Name</th>
<th>Flächen</th>
<th>Status</th>
<th>Aktion</th>
<th>Name</th>
<th>Flächen</th>
<th>Status</th>
<th>Notiz</th>
<th>Aktion</th>
</tr>
</thead>
<tbody></tbody>

127
server.js
View File

@ -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;