Implement Flächensicherung directly with ALKIS DB and added Notiz field
Deploy Bürgerwind / deploy (push) Successful in 16s
Details
Deploy Bürgerwind / deploy (push) Successful in 16s
Details
This commit is contained in:
parent
f55f70c0eb
commit
3172ee1939
122
app.js
122
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 = `<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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
127
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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue