require('dotenv').config(); const express = require('express'); const { Pool } = require('pg'); const bodyParser = require('body-parser'); const cors = require('cors'); const path = require('path'); const app = express(); const port = process.env.PORT || 3000; app.use(cors()); app.use(bodyParser.json()); // Serve static files from the root directory app.use(express.static(path.join(__dirname, './'))); const pool = new Pool({ host: process.env.DB_HOST, port: process.env.DB_PORT, database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD }); // In-memory logs for remote debugging const serverLogs = []; function log(msg) { const entry = `[${new Date().toISOString()}] ${msg}`; console.log(entry); serverLogs.push(entry); if (serverLogs.length > 50) serverLogs.shift(); } // Status-Endpunkt für Diagnose app.get('/api/status', async (req, res) => { try { const result = await pool.query('SELECT NOW()'); res.json({ status: 'online', database: 'connected', time: result.rows[0].now, schema: process.env.DB_SCHEMA || 'geodaten' }); } catch (err) { res.status(500).json({ status: 'error', database: 'disconnected', error: err.message }); } }); app.get('/api/logs', (req, res) => { res.type('text/plain').send(serverLogs.join('\n')); }); // API to save turbines app.post('/api/wea', async (req, res) => { const { projekt_id, turbines } = req.body; const targetProject = projekt_id || 'BWSamern-Ohne'; const schema = process.env.DB_SCHEMA || 'geodaten'; log(`Empfange Save-Request für Projekt: ${targetProject} (${turbines?.length || 0} WEAs)`); const client = await pool.connect(); try { await client.query('BEGIN'); // 1. Bestehende WEAs für dieses Projekt löschen const delRes = await client.query( `DELETE FROM ${schema}.wea_standorte WHERE projekt_id = $1`, [targetProject] ); log(`Gelöscht: ${delRes.rowCount} bestehende Anlagen.`); // 2. Neue WEAs einfügen if (turbines && turbines.length > 0) { for (const t of turbines) { log(`Füge WEA ein: Nr=${t.nr}, Typ=${t.type}, Pos=${t.latlng?.lat},${t.latlng?.lng}`); await client.query( `INSERT INTO ${schema}.wea_standorte (projekt_id, wea_nummer, anlagentyp, nabenhoehe, rotordurchmesser, ksf_drehung, variante, geom) VALUES ($1, $2, $3, $4, $5, $6, $7, ST_Transform(ST_SetSRID(ST_MakePoint($8, $9), 4326), 25832))`, [targetProject, t.nr, t.type, t.hh, t.rd, t.ksfAngle, t.variant, t.latlng.lng, t.latlng.lat] ); } } await client.query('COMMIT'); log(`Erfolgreich gespeichert: ${turbines?.length || 0} WEAs.`); res.json({ message: `${turbines?.length || 0} WEAs erfolgreich in Datenbank gespeichert.` }); } catch (err) { await client.query('ROLLBACK'); log(`FEHLER beim Speichern: ${err.message}`); res.status(500).json({ error: 'Datenbankfehler beim Speichern', details: err.message }); } finally { client.release(); } }); // API to load turbines app.get('/api/wea/:projekt_id', async (req, res) => { const { projekt_id } = req.params; log(`Lade WEAs für Projekt: ${projekt_id}`); try { const result = await pool.query( `SELECT wea_nummer as nr, anlagentyp as type, nabenhoehe as hh, rotordurchmesser as rd, ksf_drehung as ksfAngle, variante as variant, ST_X(ST_Transform(geom, 4326)) as lng, ST_Y(ST_Transform(geom, 4326)) as lat FROM geodaten.wea_standorte WHERE projekt_id = $1`, [projekt_id] ); log(`Geladen: ${result.rowCount} Anlagen.`); res.status(200).json(result.rows); } catch (e) { log(`FEHLER beim Laden: ${e.message}`); res.status(500).json({ error: e.message }); } }); // Hilfsfunktion zur Auflösung von Projekt-ID oder Name async function resolveProjectId(client, input) { if (!input) return null; // Prüfen, ob input eine valide UUID ist const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (uuidRegex.test(input)) return input; // Normalisierte Suche (Kleinschreibung, alle Bindestriche und Unterstriche entfernen) const normalized = input.toLowerCase().replace(/[-_]/g, ''); const res = await client.query( 'SELECT id FROM geodaten.projekte WHERE LOWER(REPLACE(REPLACE(name, \'-\', \'\'), \'_\', \'\')) = $1', [normalized] ); return res.rows.length > 0 ? res.rows[0].id : null; } // API zur Flächensicherung von Eigentümern app.post('/api/sicherung', async (req, res) => { 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}' Notiz='${notiz}'`); const client = await pool.connect(); try { await client.query('BEGIN'); // Suche nach dem Eigentümer in flaecheneigentuemer_alkis und aktualisiere status und notiz const searchNachname = (nachname || '').trim(); const searchVorname = (vorname || '').trim(); // Normalisierte Suche (TRIM und COALESCE für ALKIS-Daten) const updateRes = await client.query( `UPDATE ${schema}.flaecheneigentuemer_alkis SET status = $1, notiz = $2 WHERE (TRIM(COALESCE("GNA", '')) = TRIM(COALESCE($3, '')) AND TRIM(COALESCE("VNA", '')) = TRIM(COALESCE($4, ''))) OR (TRIM(COALESCE("VNA", '')) = TRIM(COALESCE($5, '')) AND TRIM(COALESCE("GNA", '')) = '') OR (TRIM(COALESCE("GNA", '')) = TRIM(COALESCE($5, '')) AND TRIM(COALESCE("VNA", '')) = '') OR (LOWER(TRIM(COALESCE("GNA", ''))) = LOWER(TRIM(COALESCE($3, ''))) AND LOWER(TRIM(COALESCE("VNA", ''))) = LOWER(TRIM(COALESCE($4, ''))))`, [targetStatus, notiz, searchNachname, searchVorname, `${searchVorname} ${searchNachname}`.trim()] ); if (updateRes.rowCount === 0) { log(`Kein Eigentümer für ${vorname} ${nachname} gefunden.`); await client.query('ROLLBACK'); return res.status(404).json({ error: `Eigentümer '${vorname} ${nachname}' nicht in der ALKIS-Tabelle gefunden.` }); } await client.query('COMMIT'); log(`Erfolgreich: ${updateRes.rowCount || 1} Einträge in ALKIS aktualisiert.`); res.json({ message: `Eigentümer ${vorname} ${nachname} erfolgreich aktualisiert.`, count: updateRes.rowCount }); } catch (err) { await client.query('ROLLBACK'); log(`FEHLER bei Sicherung: ${err.message}`); res.status(500).json({ error: 'Fehler bei der Flächensicherung', details: err.message }); } finally { client.release(); } }); // API zum Laden der Sicherungsstände app.get('/api/sicherung/:projekt_id', async (req, res) => { const { projekt_id } = req.params; log(`Lade Sicherungsstände für Projekt: ${projekt_id}`); const client = await pool.connect(); try { // Wir laden nun direkt aus der ALKIS Tabelle, wie vom User gewünscht const result = await client.query( `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 aus ALKIS.`); res.json(result.rows); } catch (e) { log(`FEHLER beim Laden der Stände: ${e.message}`); res.status(500).json({ error: e.message }); } finally { client.release(); } }); // 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; log(`Lade Statistiken für Projekt: ${projekt_id}`); 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.`); } // Verwende die vom User vorgeschlagene Logik, aber mit resolvedPid // Wir gruppieren nach aktueller_status (da dies in der View v_projekt_sicherung so heißt) const result = await client.query( `SELECT aktueller_status as status, count(*) as anzahl, ROUND(SUM(ST_Area(geom)) / 10000, 2) as hektar FROM geodaten.v_projekt_sicherung WHERE projekt_id = $1 GROUP BY aktueller_status`, [resolvedPid] ); log(`Statistiken berechnet: ${result.rows.length} Status-Gruppen.`); res.json(result.rows); } catch (e) { log(`FEHLER beim Laden der Statistiken: ${e.message}`); res.status(500).json({ error: e.message }); } finally { client.release(); } }); // Catch-all route to serve index.html for any other request (optional, good for SPAs) app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'index.html')); }); app.listen(port, '0.0.0.0', () => { console.log(`Server running at http://0.0.0.0:${port}`); });