Finalizing DB structure mapping and removing migrations
Deploy TrassenPlaner / deploy (push) Waiting to run Details

This commit is contained in:
Johannes Baumeister 2026-04-19 22:27:17 +02:00
parent 6d152ad3c9
commit 86f2e4c8ff
4 changed files with 395 additions and 429 deletions

View File

@ -31,7 +31,7 @@ jobs:
echo "DB_USER=${{ secrets.USER }}" >> .env
echo "DB_PASSWORD='${{ secrets.PASSWORD }}'" >> .env
echo "DB_NAME=${{ secrets.NAME }}" >> .env
echo "DB_SCHEMA=bw_scheddebrock" >> .env
echo "DB_SCHEMA=wind_projekt_bwscheddebrock" >> .env
docker compose up -d --build --force-recreate
docker image prune -f

View File

@ -11,7 +11,7 @@ services:
- DB_USER=authentik
- DB_PASSWORD=WX1t1cgP1qK09
- DB_NAME=authentik
- DB_SCHEMA=bw_scheddebrock
- DB_SCHEMA=wind_projekt_bwscheddebrock
networks:
- proxy
labels:

View File

@ -6,15 +6,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TrassenPlaner Pro</title>
<!-- Libraries (CDN via jsDelivr) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6.5.0/turf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/shpjs@latest/dist/shp.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lucide@latest/dist/umd/lucide.js"></script>
<script src="https://cdn.jsdelivr.net/npm/leaflet-editable@1.2.0/src/Leaflet.Editable.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/proj4@2.11.0/dist/proj4.js"></script>
<!-- Libraries (CDN via unpkg) -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
<script src="https://unpkg.com/shpjs@latest/dist/shp.js"></script>
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
<script src="https://unpkg.com/leaflet-editable@1.2.0/src/Leaflet.Editable.js"></script>
<script src="https://unpkg.com/jszip@3.10.1/dist/jszip.min.js"></script>
<script src="https://unpkg.com/proj4@2.11.0/dist/proj4.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script>
@ -837,8 +837,12 @@
doc.setFontSize(9);
const footerText = "Dieses Dokument wurde vom Auftragnehmer erstellt und aus dem Planungstool automatisch generiert.";
const latLngs = getFlattenedCoords(v.routes);
const lineStr = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
const nested = getNestedCoords(v.routes);
if (nested.length === 0) return;
const lines = nested.filter(s => s.length >= 2).map(s => turf.lineString(s.map(ll => [ll.lng, ll.lat])));
if (lines.length === 0) return;
const lineStr = lines.length === 1 ? lines[0] : turf.multiLineString(lines.map(l => l.geometry.coordinates));
const lineBbox = turf.bbox(lineStr);
const tableData = [];
@ -1128,21 +1132,24 @@
const variant = state.variants.find(v => routeLayers[v.id] === layer);
if (variant) {
const liveCoords = getFlattenedCoords(layer.getLatLngs());
const tempPt = (e.type === 'editable:drawing:move') ? e.latlng : null;
const isContinuous = ['editable:vertex:drag', 'editable:drawing:move', 'editable:drag', 'editable:editing'].includes(e.type);
// Always make sure variant.routes is up to date during dragging to allow calculateStats and labels to function properly
// Single-path update
if (isContinuous) {
renderSegmentLabels(variant, liveCoords, tempPt);
try {
variant.routes = layer.getLatLngs();
renderSegmentLabels(variant, variant.routes, tempPt);
calculateStats(variant);
updateVariantStatsUI(variant);
} catch (err) { console.warn("Live render error:", err); }
} else {
variant.routes = liveCoords;
variant.routes = layer.getLatLngs();
calculateStats(variant);
updateVariantStatsUI(variant);
renderVariants();
if (variant.active) updateRequiredPlots(variant);
const _activeV = state.variants.find(v => v.active); if (_activeV) saveVariantToDB(routeLayers[_activeV.id] || _activeV);
saveVariantToDB(variant);
}
}
} catch (err) {
@ -1710,6 +1717,7 @@
weight: 6,
opacity: 1,
editable: true,
middleMarkers: true,
pane: 'trassenPane',
lineOptions: { color: '#cca300', weight: 6 },
vertexOptions: { color: '#cca300', radius: 5 },
@ -1741,15 +1749,16 @@
// Z-Index: Active variant to front
if (v.active) {
layer.bringToFront();
if (layer.bringToFront) layer.bringToFront();
if (drillingLayers[v.id]) drillingLayers[v.id].bringToFront();
if (labelLayers[v.id]) labelLayers[v.id].bringToFront();
}
// Sync geometry if not actively being dragged/edited
if (!v.active) {
// Direct sync for single LineString
const isDrawing = v.active && map.editTools && map.editTools.drawing();
if (!isDrawing) {
try {
layer.setLatLngs(v.routes);
layer.setLatLngs(v.routes || []);
} catch (e) {
console.warn("Geometrie-Sync Warnung:", e);
}
@ -1764,41 +1773,27 @@
// Labels aktualisieren (Name in der Mitte der Linie auf weißem Hintergrund)
labelLayer.clearLayers();
if (v.routes && v.routes.length > 1) {
// Find geometric middle of the polyline
const points = layer.getLatLngs();
let totalDist = 0;
const distances = [];
for (let i = 0; i < points.length - 1; i++) {
const d = map.distance(points[i], points[i+1]);
distances.push(d);
totalDist += d;
}
const latLngs = (v.routes || []).map(ll => { try { return L.latLng(ll); } catch(e) { return null; } }).filter(ll => !!ll && typeof ll.lat === 'number');
if (latLngs.length >= 2) {
try {
const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
const totalKm = turf.length(line, { units: 'kilometers' });
const midPoint = turf.along(line, totalKm / 2, { units: 'kilometers' });
const midPos = L.latLng(midPoint.geometry.coordinates[1], midPoint.geometry.coordinates[0]);
let currentDist = 0;
let midPos = points[0];
for (let i = 0; i < distances.length; i++) {
if (currentDist + distances[i] >= totalDist / 2) {
// Midpoint is on this segment
const ratio = (totalDist / 2 - currentDist) / distances[i];
const lat = points[i].lat + (points[i+1].lat - points[i].lat) * ratio;
const lng = points[i].lng + (points[i+1].lng - points[i].lng) * ratio;
midPos = L.latLng(lat, lng);
break;
}
currentDist += distances[i];
L.marker(midPos, {
pane: 'labelPane',
icon: L.divIcon({
className: 'variant-map-label',
html: `<div style="background: white; border: 1px solid #cca300; color: #444; padding: 2px 8px; border-radius: 4px; font-weight: bold; font-size: 11px; box-shadow: 0 1px 4px rgba(0,0,0,0.2); white-space: nowrap; opacity: ${v.active ? 1 : 0.4}; transition: opacity 0.3s;">${v.name}</div>`,
iconSize: [0, 0],
iconAnchor: [0, 0]
}),
interactive: false
}).addTo(labelLayer);
} catch (err) {
console.warn("Label positioning failed:", err);
}
L.marker(midPos, {
pane: 'labelPane',
icon: L.divIcon({
className: 'variant-map-label',
html: `<div style="background: white; border: 1px solid #cca300; color: #444; padding: 2px 8px; border-radius: 4px; font-weight: bold; font-size: 11px; box-shadow: 0 1px 4px rgba(0,0,0,0.2); white-space: nowrap; opacity: ${v.active ? 1 : 0.4}; transition: opacity 0.3s;">${v.name}</div>`,
iconSize: [0, 0],
iconAnchor: [0, 0]
}),
interactive: false
}).addTo(labelLayer);
}
// Sync edit state
@ -1853,6 +1848,22 @@
renderVariants();
});
function isClickOnUI(e) {
if (!e || !e.originalEvent) return false;
const sidebar = document.getElementById('sidebar');
const rightPanel = document.getElementById('right-panel');
const target = e.originalEvent.target;
const isMarkerLabel = target.closest && (target.closest('.variant-map-label') || target.closest('.segment-label'));
return ((sidebar && sidebar.contains(target)) || (rightPanel && rightPanel.contains(target))) && !isMarkerLabel;
}
map.on('click', (e) => {
if (isClickOnUI(e)) return;
if (state.isMeasuring) {
addMeasurementPoint(e.latlng);
}
});
function updateToolCursors() {
const container = map.getContainer();
if (state.isDrawing) container.style.cursor = 'crosshair';
@ -1863,59 +1874,48 @@
document.getElementById('btn-measure').className = state.isMeasuring ? 'btn btn-primary' : 'btn btn-outline';
}
map.on('click', (e) => {
if (state.isMeasuring) {
addMeasurementPoint(e.latlng);
}
});
// Capture changes (drawing, dragging, inserting, deleting)
map.on('editable:drawing:clicked editable:drawing:move editable:drawing:end editable:created editable:vertex:drag editable:vertex:dragend editable:vertex:deleted editable:vertex:inserted', (e) => {
const activeV = state.variants.find(v => v.active);
if (!activeV) return;
// Enforce color on everything newly created or edited
// Enforce color
if (e.layer && e.layer.setStyle) {
e.layer.setStyle({ color: '#cca300', weight: 6 });
}
// If a new layer was created (via startPolyline or startNewPath), merge it into our existing layer
if (e.type === 'editable:created' && activeV && routeLayers[activeV.id] && e.layer !== routeLayers[activeV.id]) {
const current = getNestedCoords(routeLayers[activeV.id].getLatLngs());
const addition = e.layer.getLatLngs();
const merged = [...current, addition];
routeLayers[activeV.id].setLatLngs(merged);
activeV.routes = merged;
map.removeLayer(e.layer); // Clean up temp layer
routeLayers[activeV.id].enableEdit();
} else if (routeLayers[activeV.id] && e.layer === routeLayers[activeV.id]) {
// Sync logic for single line
if (activeV && routeLayers[activeV.id] && e.layer === routeLayers[activeV.id]) {
activeV.routes = e.layer.getLatLngs();
}
// Live updates for labels, drillings and stats
calculateStats(activeV);
updateVariantStatsUI(activeV);
if (e.type.includes('end') || e.type === 'editable:created') {
updateVariantStatsUI(activeV);
if (e.type.includes('end') || e.type === 'editable:created' || e.type.includes('dragend')) {
renderVariants();
const _activeV = state.variants.find(v => v.active); if (_activeV) saveVariantToDB(_activeV);
const _activeV = state.variants.find(v => v.active);
if (_activeV) saveVariantToDB(_activeV);
}
});
function getFlattenedCoords(raw) {
if (!raw) return [];
// If it's a flat array of LatLngs
if (raw.length > 0 && (raw[0].lat !== undefined || (Array.isArray(raw[0]) && typeof raw[0][0] === 'number'))) {
// If it's a flat array of LatLngs already
if (raw.length > 0 && raw[0] && (raw[0].lat !== undefined || (Array.isArray(raw[0]) && typeof raw[0][0] === 'number'))) {
return raw.map(ll => L.latLng(ll));
}
// If it's nested
// If it's nested [[LatLng...], [LatLng...]]
const flattened = [];
raw.forEach(section => {
if (Array.isArray(section)) {
section.forEach(ll => flattened.push(L.latLng(ll)));
}
});
if (Array.isArray(raw)) {
raw.forEach(section => {
if (Array.isArray(section)) {
section.forEach(ll => {
if (ll) flattened.push(L.latLng(ll));
});
}
});
}
return flattened;
}
@ -1923,10 +1923,20 @@
if (!raw) return [];
// Check if it's already nested
if (raw.length > 0 && Array.isArray(raw[0]) && raw[0].length > 0 && (raw[0][0].lat !== undefined || Array.isArray(raw[0][0]))) {
return raw.map(section => section.map(ll => L.latLng(ll)));
return raw.map(section => {
if (!Array.isArray(section)) return [];
return section.map(ll => {
try { return L.latLng(ll); } catch(e) { return null; }
}).filter(ll => !!ll && typeof ll.lat === 'number');
}).filter(s => s.length > 0);
}
// If it's flat, wrap it
if (raw.length > 0) return [raw.map(ll => L.latLng(ll))];
if (raw.length > 0) {
const flat = raw.map(ll => {
try { return L.latLng(ll); } catch(e) { return null; }
}).filter(ll => !!ll && typeof ll.lat === 'number');
return flat.length > 0 ? [flat] : [];
}
return [];
}
@ -1935,47 +1945,49 @@
if (!vLabel) return;
if (!variant.visible) { vLabel.clearLayers(); return; }
const nested = coordsInput ? [coordsInput] : getNestedCoords(variant.routes);
const latLngs = (coordsInput || variant.routes || []).map(ll => {
try { return L.latLng(ll); } catch(e) { return null; }
}).filter(p => !!p && typeof p.lat === 'number');
if (tempPt) latLngs.push(L.latLng(tempPt));
const currentMarkers = vLabel.getLayers();
let markerIdx = 0;
nested.forEach((latLngs, sectionIdx) => {
const drawLatLngs = [...latLngs];
// Only add temp point to the very LAST section if we are actively drawing
if (tempPt && sectionIdx === nested.length - 1) drawLatLngs.push(L.latLng(tempPt));
if (latLngs.length < 2) {
vLabel.clearLayers();
return;
}
if (drawLatLngs.length < 2) return;
for (let i = 0; i < latLngs.length - 1; i++) {
const p1 = latLngs[i];
const p2 = latLngs[i + 1];
const dist = map.distance(p1, p2);
const mid = L.latLng((p1.lat + p2.lat) / 2, (p1.lng + p2.lng) / 2);
const labelText = `${dist.toFixed(0)}m`;
for (let i = 0; i < drawLatLngs.length - 1; i++) {
const p1 = drawLatLngs[i];
const p2 = drawLatLngs[i + 1];
const dist = map.distance(p1, p2);
const mid = L.latLng((p1.lat + p2.lat) / 2, (p1.lng + p2.lng) / 2);
const labelText = `${dist.toFixed(0)}m`;
if (markerIdx < currentMarkers.length) {
const m = currentMarkers[markerIdx];
m.setLatLng(mid);
const el = m.getElement();
if (el) {
const span = el.querySelector('.segment-label-content');
if (span) span.textContent = labelText;
}
} else {
L.marker(mid, {
interactive: false,
pane: 'labelPane',
icon: L.divIcon({
className: 'segment-label',
html: `<span class="segment-label-content">${labelText}</span>`,
iconSize: [46, 20],
iconAnchor: [23, 10]
})
}).addTo(vLabel);
if (markerIdx < currentMarkers.length) {
const m = currentMarkers[markerIdx];
m.setLatLng(mid);
const el = m.getElement();
if (el) {
const span = el.querySelector('.segment-label-content');
if (span) span.textContent = labelText;
}
markerIdx++;
} else {
L.marker(mid, {
interactive: false,
pane: 'labelPane',
icon: L.divIcon({
className: 'segment-label',
html: `<span class="segment-label-content">${labelText}</span>`,
iconSize: [46, 20],
iconAnchor: [23, 10]
})
}).addTo(vLabel);
}
});
markerIdx++;
}
while (markerIdx < currentMarkers.length) {
vLabel.removeLayer(currentMarkers[markerIdx++]);
@ -1990,18 +2002,24 @@
const vDrill = drillingLayers[variant.id];
if (vDrill) vDrill.clearLayers();
const nested = getNestedCoords(variant.routes);
const latLngs = (variant.routes || []).map(ll => {
try { return L.latLng(ll); } catch(e) { return null; }
}).filter(p => !!p && typeof p.lat === 'number');
variant.stats.total = 0;
variant.stats.drilling = 0;
variant.stats.muffen = 0;
variant.stats.hasTooLongDrilling = false;
variant.drillingSegments = [];
if (nested.length === 0) return;
if (latLngs.length < 2) {
renderSegmentLabels(variant);
return;
}
renderSegmentLabels(variant);
// Pre-filter obstacles once
// Pre-filter obstacles
if (!cachedObstacles && state.usage.features) {
const keywords = ['bahn', 'gewässer', 'wasser', 'straße', 'verkehr', 'gehölz', 'baufläche', 'wald', 'forst', 'hecke', 'weg', 'pfad', 'graben', 'bach', 'fluss'];
cachedObstacles = state.usage.features.filter(f => {
@ -2011,66 +2029,61 @@
}
const currentObstacles = cachedObstacles || [];
nested.forEach(latLngs => {
if (latLngs.length < 2) return;
const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
variant.stats.total = turf.length(line, { units: 'kilometers' }) * 1000;
const startPoint = turf.point([latLngs[0].lng, latLngs[0].lat]);
const drillingRanges = [];
const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
const sectionLength = turf.length(line, { units: 'meters' });
variant.stats.total += sectionLength;
const startPoint = turf.point([latLngs[0].lng, latLngs[0].lat]);
const drillingRanges = [];
if (currentObstacles.length > 0) {
const lineBbox = turf.bbox(line);
currentObstacles.forEach(obs => {
try {
const obsBbox = turf.bbox(obs);
if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] || lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return;
if (turf.booleanIntersects(line, obs)) {
const intersect = turf.lineIntersect(line, obs);
let distances = [0, sectionLength];
intersect.features.forEach(f => {
distances.push(turf.length(turf.lineSlice(startPoint, f, line), { units: 'meters' }));
});
distances = [...new Set(distances)].sort((a, b) => a - b);
for (let i = 0; i < distances.length - 1; i++) {
const dStart = distances[i];
const dEnd = distances[i + 1];
const midPt = turf.along(line, (dStart + dEnd) / 2 / 1000, { units: 'kilometers' });
if (turf.booleanPointInPolygon(midPt, obs)) {
drillingRanges.push([Math.max(0, dStart - 20), Math.min(sectionLength, dEnd + 20)]);
}
if (currentObstacles.length > 0) {
const lineBbox = turf.bbox(line);
currentObstacles.forEach(obs => {
try {
const obsBbox = turf.bbox(obs);
if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] || lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return;
if (turf.booleanIntersects(line, obs)) {
const intersect = turf.lineIntersect(line, obs);
let distances = [0, variant.stats.total];
intersect.features.forEach(f => {
distances.push(turf.length(turf.lineSlice(startPoint, f, line), { units: 'meters' }));
});
distances = [...new Set(distances)].sort((a, b) => a - b);
for (let i = 0; i < distances.length - 1; i++) {
const dStart = distances[i];
const dEnd = distances[i + 1];
const midPt = turf.along(line, (dStart + dEnd) / 2 / 1000, { units: 'kilometers' });
if (turf.booleanPointInPolygon(midPt, obs)) {
drillingRanges.push([Math.max(0, dStart - 20), Math.min(variant.stats.total, dEnd + 20)]);
}
}
} catch (e) {}
});
}
} catch (e) {}
});
}
let mergedRanges = [];
if (drillingRanges.length > 0) {
drillingRanges.sort((a, b) => a[0] - b[0]);
let cur = drillingRanges[0];
for (let i = 1; i < drillingRanges.length; i++) {
if (drillingRanges[i][0] <= cur[1]) cur[1] = Math.max(cur[1], drillingRanges[i][1]);
else { mergedRanges.push(cur); cur = drillingRanges[i]; }
}
mergedRanges.push(cur);
}
let mergedRanges = [];
if (drillingRanges.length > 0) {
drillingRanges.sort((a, b) => a[0] - b[0]);
let cur = drillingRanges[0];
for (let i = 1; i < drillingRanges.length; i++) {
if (drillingRanges[i][0] <= cur[1]) cur[1] = Math.max(cur[1], drillingRanges[i][1]);
else { mergedRanges.push(cur); cur = drillingRanges[i]; }
}
mergedRanges.push(cur);
}
variant.stats.drilling += mergedRanges.reduce((sum, r) => sum + (r[1] - r[0]), 0);
variant.stats.muffen += mergedRanges.length * 2;
variant.stats.drilling += mergedRanges.reduce((sum, r) => sum + (r[1] - r[0]), 0);
variant.stats.muffen += mergedRanges.length * 2;
mergedRanges.forEach(range => {
const lengthM = range[1] - range[0];
if (lengthM > 180) variant.stats.hasTooLongDrilling = true;
const s = turf.along(line, range[0] / 1000, { units: 'kilometers' });
const e = turf.along(line, range[1] / 1000, { units: 'kilometers' });
variant.drillingSegments.push({
path: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]],
length: lengthM,
muffen: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]]
});
mergedRanges.forEach(range => {
const lengthM = range[1] - range[0];
if (lengthM > 180) variant.stats.hasTooLongDrilling = true;
const s = turf.along(line, range[0] / 1000, { units: 'kilometers' });
const e = turf.along(line, range[1] / 1000, { units: 'kilometers' });
variant.drillingSegments.push({
path: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]],
length: lengthM,
muffen: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]]
});
});
@ -2129,21 +2142,17 @@
if (!container) return;
container.innerHTML = '<h4 style="font-size: 13px; margin-bottom: 10px; color: #64748b; display: flex; align-items: center; gap: 8px;"><i data-lucide="layers" style="width: 14px;"></i> Benötigte Flurstücke</h4>';
const latLngs = getFlattenedCoords(variant ? variant.routes : []);
if (!variant || latLngs.length < 2 || !state.owners || !state.owners.features || state.owners.features.length === 0) {
const latLngs = (variant.routes || []).map(ll => { try { return L.latLng(ll); } catch(e) { return null; } }).filter(p => !!p && typeof p.lat === 'number');
const hasData = latLngs.length >= 2;
if (!variant || !hasData || !state.owners || !state.owners.features || state.owners.features.length === 0) {
container.innerHTML += '<p style="font-size: 11px; color: #94a3b8; text-align: center; margin-top: 20px;">Keine Daten oder Trasse vorhanden</p>';
return;
}
try {
const nested = getNestedCoords(variant.routes);
if (nested.length === 0) return;
const lines = nested.filter(s => s.length >= 2).map(s => turf.lineString(s.map(ll => [ll.lng, ll.lat])));
if (lines.length === 0) return;
const multiLine = lines.length === 1 ? lines[0] : turf.multiLineString(lines.map(l => l.geometry.coordinates));
const lineBbox = turf.bbox(multiLine);
const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
const lineBbox = turf.bbox(line);
const intersectingPlots = state.owners.features.filter(f => {
try {
@ -2151,7 +2160,7 @@
if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] ||
lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return false;
return turf.booleanIntersects(multiLine, f);
return turf.booleanIntersects(line, f);
} catch (e) { return false; }
});
@ -2206,6 +2215,10 @@
// --- Variant Management UI ---
window.startNewPath = (id) => {
if (map.editTools) {
const v = state.variants.find(v => v.id == id);
if (v && !v.visible) {
toggleVariantVisibility(v.id, true);
}
state.isDrawing = true;
state.isMeasuring = false;
updateToolCursors();
@ -2281,7 +2294,6 @@
<input type="checkbox" ${v.visible ? 'checked' : ''} onclick="toggleVariantVisibility(${v.id}, this.checked)">
<i data-lucide="eye" style="width: 14px;"></i>
</label>
<i data-lucide="plus-circle" class="action-icon" title="Weiteren Abschnitt hinzufügen" style="cursor: pointer; width: 15px; color: #10b981;" onclick="startNewPath(${v.id})"></i>
<i data-lucide="table" class="action-icon" title="Tabelle herunterladen" style="cursor: pointer; width: 15px; color: var(--corporate-teal);" onclick="downloadVariantTable(${v.id})"></i>
<i data-lucide="copy" class="action-icon" title="Duplizieren" style="cursor: pointer; width: 15px; color: #64748b;" onclick="duplicateVariant(${v.id})"></i>
<i data-lucide="trash-2" class="action-icon" title="Inhalt löschen" style="cursor: pointer; width: 15px; color: var(--danger);" onclick="clearVariant(${v.id})"></i>
@ -2344,7 +2356,7 @@
if (newName && newName.trim()) {
v.name = newName.trim();
renderVariants();
if (activeV) saveVariantToDB(activeV);
if (v) saveVariantToDB(v);
}
};
@ -2519,6 +2531,7 @@
// Global Map Click for generous vertex insertion ("Click anywhere near the line")
const handleVertexInsertion = (e) => {
if (isClickOnUI(e)) return;
if (state.isMeasuring) return;
// VERY IMPORTANT: Don't interfere if we are actively drawing a path
if (map.editTools && map.editTools.drawing()) return;
@ -2529,24 +2542,14 @@
const layer = routeLayers[variant.id];
if (!layer) return;
// If the variant is empty, we don't return here anymore,
// but we only attempt insertion if we have at least one segment (2 points).
const latlngs = getFlattenedCoords(layer.getLatLngs());
// If drawing and clicking far away, we let startPolyline/continueForward handle it
if (latlngs.length < 2) return;
if (!layer.editor) {
layer.enableEdit();
}
const allPoints = (variant.routes || []).map(ll => L.latLng(ll));
let minIdx = -1;
let minDist = Infinity;
const pt = map.latLngToLayerPoint(e.latlng);
for (let i = 0; i < latlngs.length - 1; i++) {
const p1 = map.latLngToLayerPoint(latlngs[i]);
const p2 = map.latLngToLayerPoint(latlngs[i + 1]);
for (let i = 0; i < allPoints.length - 1; i++) {
const p1 = map.latLngToLayerPoint(allPoints[i]);
const p2 = map.latLngToLayerPoint(allPoints[i + 1]);
const dist = L.LineUtil.pointToSegmentDistance(pt, p1, p2);
if (dist < minDist) {
minDist = dist;
@ -2554,29 +2557,27 @@
}
}
// CRITICAL FIX: Check if we are clicking directly on or very near an existing vertex
// If we are closer than 10 pixels to any vertex, we assume the user wants
// to move the point, not insert a new one.
const isNearVertex = latlngs.some(ll => map.latLngToLayerPoint(ll).distanceTo(pt) < 12);
const isNearVertex = allPoints.some(ll => map.latLngToLayerPoint(ll).distanceTo(pt) < 12);
if (isNearVertex) return;
// High tolerance hit-testing: 35 pixels (approx. the width of a finger/mouse inaccuracy)
// High tolerance hit-testing: 35 pixels
const tolerance = state.isDrawing ? 15 : 35;
if (minIdx !== -1 && minDist < tolerance) {
latlngs.splice(minIdx + 1, 0, e.latlng);
layer.setLatLngs(latlngs);
allPoints.splice(minIdx + 1, 0, e.latlng);
variant.routes = allPoints;
layer.setLatLngs(allPoints);
if (layer.editor && layer.editor.reset) {
layer.editor.reset();
// FORCE REFRESH of editor markers
if (layer.editor) {
layer.disableEdit();
layer.enableEdit();
}
// Force synchronization
variant.routes = getFlattenedCoords(layer.getLatLngs());
calculateStats(variant);
updateVariantStatsUI(variant);
renderVariants();
const _activeV = state.variants.find(v => v.active); if (_activeV) saveVariantToDB(_activeV);
saveVariantToDB(variant);
}
};
@ -2691,46 +2692,37 @@
}
async function saveVariantToDB(variantDef) {
if (!variantDef) return;
const indicator = document.getElementById('db-status-indicator');
if (indicator) indicator.classList.add('active');
try {
// Identify the true state variant regardless of what parameter was passed
const v = variantDef.id ? variantDef : state.variants.find(vx => routeLayers[vx.id] === variantDef);
// Ensure we have a valid variant object and points
const v = variantDef.id ? variantDef : state.variants.find(vx => vx.id === variantDef.id);
if (!v) {
console.warn("Save skipped: Could not map to a valid variant.");
return;
}
const name = v.name || "Neue Trasse";
let rawRoutes = v.routes || [];
// Fallback attempt if routes is somehow empty but layer exists
if (rawRoutes.length === 0 && routeLayers[v.id] && typeof routeLayers[v.id].getLatLngs === 'function') {
rawRoutes = routeLayers[v.id].getLatLngs();
if (indicator) indicator.classList.remove('active');
return;
}
if (!rawRoutes || rawRoutes.length === 0) {
console.warn("Save skipped: No route data available.");
// Simple flat coordinates from LineString
const latLngs = (v.routes || []).map(ll => {
try { return L.latLng(ll); } catch(e) { return null; }
}).filter(p => !!p && typeof p.lat === 'number');
if (latLngs.length < 2) {
if (indicator) indicator.classList.remove('active');
return;
}
// Leaflet-Punkte in GeoJSON-Format umwandeln [lng, lat]
let geoJsonCoords = [];
const isMulti = routeLayers[v.id] && Array.isArray(rawRoutes[0]);
if (isMulti || Array.isArray(rawRoutes[0])) {
geoJsonCoords = rawRoutes.map(line => line.map(p => [p.lng, p.lat]));
} else {
geoJsonCoords = rawRoutes.map(p => [p.lng, p.lat]);
}
const geomType = (isMulti || Array.isArray(rawRoutes[0])) ? "MultiLineString" : "LineString";
const payload = { id: v.id, geometry: {
type: geoJsonCoords.length ? geomType : "LineString",
coordinates: geoJsonCoords
const payload = {
id: v.id,
geometry: {
type: "LineString",
coordinates: latLngs.map(p => [p.lng, p.lat])
},
properties: {
name: name,
Variante: name.replace('Variante ', '')
name: v.name,
Variante: v.name.replace('Variante ', '')
}
};
@ -2748,113 +2740,98 @@
throw new Error('Server-Fehler: ' + errorText);
}
const result = await response.json();
if (result.success && result.id) {
v.id = result.id; // Sync the database ID back to the state
}
console.log("Speichern in Datenbank erfolgreich!");
} catch (err) {
console.error("DB Save failed:", err.message);
// alert("Fehler beim Speichern: " + err.message); // Commented to prevent spam during map moves
} finally {
if (indicator) indicator.classList.remove('active');
}
}
async function loadFromDatabase() {
if (isLoading) return;
isLoading = true;
const indicator = document.getElementById('db-status-indicator');
if (indicator) indicator.classList.add('active');
console.log("[V3-Sync] Starte Daten-Abruf vom Server...");
const sanitize = (gj) => {
if (!gj || !gj.features) return { type: "FeatureCollection", features: [] };
return gj;
};
// 1. Varianten (JETZT ALS ERSTES)
try {
console.log("Starte Daten-Abruf vom Server...");
const isVal = (c) => Array.isArray(c) && c.length >= 2 && !isNaN(c[0]) && !isNaN(c[1]) && c[0] !== null && c[1] !== null;
const sanitize = (gj) => {
if (!gj || !gj.features) return { type: "FeatureCollection", features: [] };
return {
...gj,
features: gj.features.filter(f => {
if (!f || !f.geometry || !f.geometry.coordinates) return false;
// Recursive coordinate check for simple structures
if (f.geometry.type === 'Point') return isVal(f.geometry.coordinates);
return true; // Polygons are harder to check deeply, but usually safer
})
};
};
const res = await fetch(`${API_BASE}/variants`);
if (res.ok) {
const rawData = await res.json();
let features = Array.isArray(rawData) ? rawData.map(item => ({
type: "Feature",
id: item.id,
geometry: {
type: "LineString",
coordinates: (Array.isArray(item.routes[0]) ? item.routes[0] : item.routes)
.map(p => [(p.lng || p[0]), (p.lat || p[1])])
},
properties: { Variante: item.name ? item.name.replace('Variante ', '') : 'A', name: item.name }
})) : (rawData.features || []);
// 1. Eigentümer
try {
const res = await fetch(`${API_BASE}/owners`);
if (res.ok) {
let gj = sanitize(await res.json());
if (gj.features.length > 0) {
// Check for UTM transform
const c = gj.features[0].geometry.coordinates;
const test = Array.isArray(c[0]) ? (Array.isArray(c[0][0]) ? c[0][0][0] : c[0][0]) : c[0];
if (Math.abs(test) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326");
console.log(`[V3-Sync] ${features.length} Trassen geladen.`);
features.forEach(f => {
const p = f.properties || {};
const varName = p.Variante ? `Variante ${p.Variante}` : (p.name || "Variante A");
const slot = state.variants.find(lv => lv.name === varName);
if (slot && f.geometry && f.geometry.coordinates) {
slot.id = f.id || p.id || slot.id;
slot.routes = f.geometry.coordinates.map(c => ({ lat: c[1], lng: c[0] }));
console.log(`[V3-Sync] ${slot.name} erfolgreich gemappt.`);
}
state.owners = gj;
updateOwnerLayer();
if (layers.owners.getBounds().isValid()) map.fitBounds(layers.owners.getBounds());
}
} catch (e) { console.error("Owners load error:", e); }
});
// 2. Varianten
try {
const res = await fetch(`${API_BASE}/variants`);
if (res.ok) {
const v = await res.json();
v.forEach(sv => {
const l = state.variants.find(lv => lv.id === sv.id);
if (l) { l.routes = sv.routes || []; l.name = sv.name || l.name; }
});
updateRouteLayers();
}
} catch (e) { console.error("Variants load error:", e); }
state.variants.forEach(v => { calculateStats(v); updateVariantStatsUI(v); });
const varA = state.variants.find(v => v.name === "Variante A");
if (varA) setActiveVariant(varA.id);
renderVariants();
updateRouteLayers();
}
} catch (e) { console.error("[V3-Sync] Variants Error:", e); }
// 3. Nutzungen
try {
const res = await fetch(`${API_BASE}/usage`);
if (res.ok) {
let gj = sanitize(await res.json());
// Transform usage if UTM
if (gj.features.length > 0) {
const c = gj.features[0].geometry.coordinates;
const test = Array.isArray(c[0]) ? (Array.isArray(c[0][0]) ? c[0][0][0] : c[0][0]) : c[0];
if (Math.abs(test) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326");
}
state.usage = gj;
updateUsageLayer();
// 2. Eigentümer (SCHWERER LAYER)
try {
const res = await fetch(`${API_BASE}/owners`);
if (res.ok) {
let gj = sanitize(await res.json());
if (gj.features.length > 0) {
const c = gj.features[0].geometry.coordinates;
if (Math.abs(Array.isArray(c[0]) ? c[0][0] : c[0]) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326");
}
} catch (e) { console.error("Usage load error:", e); }
state.owners = gj;
updateOwnerLayer();
}
} catch (e) { console.error("[V3-Sync] Owners Error:", e); }
// 4. WEA
try {
const res = await fetch(`${API_BASE}/wea`);
if (res.ok) {
let gj = sanitize(await res.json());
if (gj.features.length > 0) {
if (Math.abs(gj.features[0].geometry.coordinates[0]) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326");
}
state.wea = gj;
updateWEALayer();
// 3. Nutzungen
try {
const res = await fetch(`${API_BASE}/usage`);
if (res.ok) {
let gj = sanitize(await res.json());
if (gj.features.length > 0) {
const c = gj.features[0].geometry.coordinates;
if (Math.abs(Array.isArray(c[0]) ? c[0][0] : c[0]) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326");
}
} catch (e) { console.error("WEA load error:", e); }
updateUsageLayer(gj);
}
} catch (e) { console.error("[V3-Sync] Usage Error:", e); }
// 5. Infrastruktur
try {
const res = await fetch(`${API_BASE}/infrastructure`);
if (res.ok) {
let gj = sanitize(await res.json());
if (gj.features.length > 0) {
if (Math.abs(gj.features[0].geometry.coordinates[0]) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326");
}
state.infrastructure = gj;
updateInfrastructureLayer();
}
} catch (e) { console.error("Infra load error:", e); }
console.log("Daten-Ladevorgang abgeschlossen.");
} catch (err) {
console.error("Kritischer Fehler beim Laden der Datenbank:", err);
} finally {
isLoading = false;
renderVariants();
}
console.log("[V3-Sync] Daten-Ladevorgang abgeschlossen.");
if (indicator) indicator.classList.remove('active');
isLoading = false;
renderVariants();
}
let isSaving = false;
async function saveToFolder() {
if (isSaving || !state.directoryHandle) return;
@ -3117,6 +3094,26 @@
input.click();
});
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
if (map.editTools) {
if (map.editTools.drawing()) {
map.editTools.stopDrawing();
}
state.isDrawing = false;
state.isMeasuring = false;
updateToolCursors();
renderVariants();
const activeV = state.variants.find(v => v.active);
if (activeV && routeLayers[activeV.id]) {
routeLayers[activeV.id].enableEdit();
saveVariantToDB(activeV);
}
}
}
});
window.addEventListener('load', async () => {
try {
initApp();

149
server.js
View File

@ -28,32 +28,33 @@ app.use(express.static(__dirname));
// Helper to run the schema isolation command
async function setSchema(client) {
await client.query(`SET search_path TO ${process.env.DB_SCHEMA}, public;`);
const schema = process.env.DB_SCHEMA || 'wind_projekt_bwscheddebrock';
await client.query(`SET search_path TO "${schema}", public;`);
}
// Routes
// Database Initial Setup / Migration Logic Removed
// (Now treating existing tables as the read-only 'Source of Truth')
// --- API Routes ---
app.get('/api/health', (req, res) => {
res.json({ status: 'OK', time: new Date().toISOString() });
res.json({ status: 'OK', schema: process.env.DB_SCHEMA, time: new Date().toISOString() });
});
// 1. Get Owner Data
app.get('/api/owners', async (req, res) => {
console.log("Anfrage erhalten: /api/owners");
const client = await pool.connect();
try {
await setSchema(client);
// 1. Log actual count in DB
const countRes = await client.query('SELECT count(*) FROM bw_scheddebrock."Eigentuemerdaten"');
console.log(`Datenbank-Check: ${countRes.rows[0].count} Eigentümer in bw_scheddebrock."Eigentuemerdaten" gefunden.`);
const countRes = await client.query('SELECT count(*) FROM "Eigentuemerdaten"');
console.log(`Datenbank-Check: ${countRes.rows[0].count} Einträge in "Eigentuemerdaten".`);
const query = `
SELECT
*,
ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry
FROM bw_scheddebrock."Eigentuemerdaten"
FROM "Eigentuemerdaten"
`;
const result = await client.query(query);
console.log(`Abfrage erfolgreich: ${result.rowCount} Zeilen für Frontend geladen.`);
const geojson = {
type: "FeatureCollection",
@ -62,7 +63,7 @@ app.get('/api/owners', async (req, res) => {
const geomObj = typeof geometry === 'string' ? JSON.parse(geometry) : geometry;
return {
type: "Feature",
id: row.id || row.id_0,
id: row.id,
geometry: geomObj,
properties: properties
};
@ -82,18 +83,13 @@ app.get('/api/usage', async (req, res) => {
const client = await pool.connect();
try {
await setSchema(client);
// 1. Log actual count in DB
const countRes = await client.query('SELECT count(*) FROM bw_scheddebrock."Nutzung"');
console.log(`Datenbank-Check: ${countRes.rows[0].count} Nutzungs-Flächen in bw_scheddebrock."Nutzung" gefunden.`);
const query = `
SELECT
*,
ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry
FROM bw_scheddebrock."Nutzung"
FROM "Nutzung"
`;
const result = await client.query(query);
console.log(`Abfrage erfolgreich: ${result.rowCount} Zeilen für Frontend geladen.`);
const geojson = {
type: "FeatureCollection",
@ -116,7 +112,7 @@ app.get('/api/usage', async (req, res) => {
}
});
// 2.5 Get WEA Data (Windturbinen)
// 2.5 Get WEA Data
app.get('/api/wea', async (req, res) => {
const client = await pool.connect();
try {
@ -127,10 +123,9 @@ app.get('/api/wea', async (req, res) => {
SELECT
*,
ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry
FROM bw_scheddebrock."WEA"
FROM "WEA"
`);
} catch (dbErr) {
console.log("WEA Tabelle fehlt oder nicht abrufbar.");
result = { rows: [], rowCount: 0 };
}
@ -155,7 +150,7 @@ app.get('/api/wea', async (req, res) => {
}
});
// 2.7 Get Infrastructure Data (UW, etc)
// 2.7 Get Infrastructure Data
app.get('/api/infrastructure', async (req, res) => {
const client = await pool.connect();
try {
@ -166,10 +161,9 @@ app.get('/api/infrastructure', async (req, res) => {
SELECT
*,
ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry
FROM bw_scheddebrock."Infrastruktur"
FROM "Infrastruktur"
`);
} catch (dbErr) {
console.log("Infrastruktur Tabelle fehlt oder nicht abrufbar.");
result = { rows: [], rowCount: 0 };
}
@ -199,41 +193,32 @@ app.get('/api/variants', async (req, res) => {
const client = await pool.connect();
try {
await setSchema(client);
// Log count
const countRes = await client.query('SELECT count(*) FROM bw_scheddebrock."Kabeltrasse"');
console.log(`Datenbank-Check: ${countRes.rows[0].count} Kabeltrassen in bw_scheddebrock."Kabeltrasse" gefunden.`);
const query = `
SELECT
id_0 as id, name, "Variante",
id, name, "Variante",
ST_AsGeoJSON(ST_Transform(ST_SetSRID(geom, 25832), 4326)) as geometry
FROM bw_scheddebrock."Kabeltrasse"
ORDER BY id_0 DESC
FROM "Kabeltrasse"
ORDER BY id DESC
`;
const result = await client.query(query);
console.log(`Abfrage erfolgreich: ${result.rowCount} Trassen geladen.`);
const variants = result.rows.map(row => {
let routes = [];
const geomObj = typeof row.geometry === 'string' ? JSON.parse(row.geometry) : row.geometry;
if (geomObj && geomObj.coordinates) {
if (geomObj.type === 'MultiLineString') {
routes = geomObj.coordinates.map(line => line.map(c => ({ lat: c[1], lng: c[0] })));
} else if (geomObj.type === 'LineString') {
routes = geomObj.coordinates.map(c => ({ lat: c[1], lng: c[0] }));
}
}
return {
id: row.id,
name: row.name || row.Variante || 'Trasse',
active: false,
visible: true,
stats: { total: 0, drilling: 0, open: 0, muffen: 0 },
routes: routes
};
});
res.json(variants);
const featureCollection = {
type: "FeatureCollection",
features: result.rows.map(row => {
const geomObj = JSON.parse(row.geometry || 'null');
return {
type: "Feature",
id: row.id,
geometry: geomObj,
properties: {
id: row.id,
name: row.name || (row.Variante ? `Variante ${row.Variante}` : 'Neue Trasse'),
Variante: row.Variante
}
};
})
};
res.json(featureCollection);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to fetch variants' });
@ -244,52 +229,36 @@ app.get('/api/variants', async (req, res) => {
// 4. Save/Update Variant
app.post('/api/variants', async (req, res) => {
console.log("Empfangene Daten (Payload):", JSON.stringify(req.body).substring(0, 200) + "...");
const { geometry, properties } = req.body;
const client = await pool.connect();
try {
await setSchema(client);
console.log("Starte PostgreSQL-Query...");
// Clean UPSERT by Database ID.
const routeId = req.body.id;
const routeName = properties.name || 'Neue Trasse';
const variante = properties.Variante || properties.name || 'A';
const variante = properties.Variante || (properties.name ? properties.name.replace('Variante ', '') : 'A');
const geoJsonStr = JSON.stringify(geometry);
const upsertQuery = `
INSERT INTO "Kabeltrasse" (geom, name, "Variante")
VALUES (
ST_MakeValid(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON($1), 4326), 25832)),
$2,
$3
)
ON CONFLICT ("Variante")
DO UPDATE SET
geom = EXCLUDED.geom,
name = EXCLUDED.name
RETURNING id
`;
// Check if the passed ID is a valid database ID (small int) vs a frontend dummy Date.now() timestamp
// Or try to match it initially just in case it's a known database row.
const existing = await client.query('SELECT id_0 FROM bw_scheddebrock."Kabeltrasse" WHERE id_0 = $1', [Number(routeId) || 0]);
const upsertRes = await client.query(upsertQuery, [geoJsonStr, routeName, variante]);
const finalId = upsertRes.rows[0].id;
let result;
if (existing.rowCount > 0) {
// EXACT match found physically in DB, do an UPDATE
const updateQuery = `
UPDATE bw_scheddebrock."Kabeltrasse"
SET geom = ST_MakeValid(ST_SetSRID(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON($1), 4326), 25832), 25832)),
name = $2,
"Variante" = $3
WHERE id_0 = $4
RETURNING id_0 as id;
`;
result = await client.query(updateQuery, [geoJsonStr, routeName, variante, existing.rows[0].id_0]);
console.log(`PostgreSQL-Ergebnis: Zeile ${existing.rows[0].id_0} aktualisiert (Variante: ${routeName})!`);
} else {
// INSERT new
const insertQuery = `
INSERT INTO bw_scheddebrock."Kabeltrasse" (geom, name, "Variante")
VALUES (ST_MakeValid(ST_SetSRID(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON($1), 4326), 25832), 25832)), $2, $3)
RETURNING id_0 as id;
`;
result = await client.query(insertQuery, [geoJsonStr, routeName, variante]);
console.log(`PostgreSQL-Ergebnis: Zeile eingefügt! ID: ${result.rows[0].id}`);
}
res.json({ success: true, id: result.rows[0].id });
res.json({ success: true, id: finalId });
} catch (err) {
console.error("KRITISCHER SQL-FEHLER:", err.message);
res.status(500).json({ error: 'Failed to save variant: ' + err.message });
console.error("SQL-FEHLER (SAVE):", err.message);
res.status(500).json({ error: 'Failed to save variant' });
} finally {
client.release();
}
@ -303,18 +272,18 @@ app.patch('/api/owners/:id', async (req, res) => {
try {
await setSchema(client);
const query = `
UPDATE bw_scheddebrock."Eigentuemerdaten"
UPDATE "Eigentuemerdaten"
SET status = $1, notiz = $2
WHERE id = $3
RETURNING id;
`;
const result = await client.query(query, [status, notiz, id]);
if (result.rowCount === 0) {
return res.status(404).json({ error: 'Owner not found in database' });
return res.status(404).json({ error: 'Owner not found' });
}
res.json({ success: true, id: result.rows[0].id });
} catch (err) {
console.error("Owner Update Error:", err);
console.error(err);
res.status(500).json({ error: 'Failed to update owner' });
} finally {
client.release();