v1.26: Haeuser ohne Namensfeld, Adresse in Einzelfelder, Flaechen/Lage, Suche + Listenansicht

This commit is contained in:
2026-10-04 17:08:59 +02:00
parent 6fb70e015f
commit 6e2d783e12
12 changed files with 434 additions and 141 deletions
+8
View File
@@ -164,6 +164,14 @@ CREATE TABLE IF NOT EXISTS buildings (
wohneinheiten INT NOT NULL, -- Anzahl Wohneinheiten (Soll)
quadratmeter NUMERIC(9,1) NOT NULL -- Gesamtwohnfläche in m²
);
-- v1.26: Adresse in Einzelfelder aufgeteilt, Flächen aufgeteilt, optionale Lage
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS strasse TEXT NOT NULL DEFAULT '';
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS hausnummer TEXT NOT NULL DEFAULT '';
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS plz TEXT NOT NULL DEFAULT '';
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS ort TEXT NOT NULL DEFAULT '';
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS lage TEXT NOT NULL DEFAULT '';
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS wohnflaeche NUMERIC(9,1);
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS grundstuecksflaeche NUMERIC(9,1);
CREATE TABLE IF NOT EXISTS costs (
id TEXT PRIMARY KEY,
+77 -20
View File
@@ -185,9 +185,15 @@ const mapMeter = (m) => ({
letzteAnomalie: m.letzte_anomalie === undefined ? undefined : Boolean(m.letzte_anomalie),
})
const mapBuilding = (b) => ({
name: b.name, adresse: b.adresse, baujahr: b.baujahr, stockwerke: b.stockwerke,
name: b.name, adresse: b.adresse,
strasse: b.strasse ?? '', hausnummer: b.hausnummer ?? '',
plz: b.plz ?? '', ort: b.ort ?? '', lage: b.lage ?? '',
baujahr: b.baujahr, stockwerke: b.stockwerke,
etagenhoehe: Number(b.etagenhoehe), wohneinheiten: b.wohneinheiten,
quadratmeter: Number(b.quadratmeter),
// Fallback: Bestand vor v1.26 trägt die Wohnfläche noch in quadratmeter
wohnflaeche: Number(b.wohnflaeche ?? b.quadratmeter),
grundstuecksflaeche: b.grundstuecksflaeche === null || b.grundstuecksflaeche === undefined
? null : Number(b.grundstuecksflaeche),
})
const mapKosten = (k) => ({
id: k.id, building: k.building, unitId: k.unit_id, jahr: k.jahr,
@@ -1047,20 +1053,33 @@ app.post('/api/belege/:id/analysieren', auth, adminOnly, async (req, res, next)
})
// ── Bestand: Häuser & Wohnungen (Verwaltung) ────────────────
/** Name/Anzeigeadresse werden aus den Einzelfeldern zusammengesetzt (v1.26). */
function hausNameAus(b) {
return `${String(b.strasse || '').trim()} ${String(b.hausnummer || '').trim()}`.trim()
}
function hausAdresseAus(b) {
return `${hausNameAus(b)}, ${String(b.plz || '').trim()} ${String(b.ort || '').trim()}`.trim()
}
function pruefeHaus(b) {
const fehler = []
const baujahr = Number(b.baujahr)
const stockwerke = Number(b.stockwerke)
const etagenhoehe = Number(b.etagenhoehe)
const wohneinheiten = Number(b.wohneinheiten)
const quadratmeter = Number(b.quadratmeter)
if (!String(b.name || '').trim()) fehler.push('Hausname fehlt.')
if (!String(b.adresse || '').trim()) fehler.push('Adresse fehlt.')
const wohnflaeche = Number(b.wohnflaeche)
const grund = b.grundstuecksflaeche === null || b.grundstuecksflaeche === undefined || b.grundstuecksflaeche === ''
? null : Number(b.grundstuecksflaeche)
if (!String(b.strasse || '').trim()) fehler.push('Straße fehlt.')
if (!String(b.hausnummer || '').trim()) fehler.push('Hausnummer fehlt.')
if (!/^\d{5}$/.test(String(b.plz || '').trim())) fehler.push('PLZ ungültig (5 Ziffern).')
if (!String(b.ort || '').trim()) fehler.push('Ort fehlt.')
if (!Number.isInteger(baujahr) || baujahr < 1500 || baujahr > new Date().getFullYear() + 2) fehler.push('Baujahr ungültig.')
if (!Number.isInteger(stockwerke) || stockwerke < 0 || stockwerke > 50) fehler.push('Stockwerke ungültig.')
if (!Number.isInteger(stockwerke) || stockwerke < 0 || stockwerke > 50) fehler.push('Anzahl Stockwerke ungültig.')
if (!Number.isFinite(etagenhoehe) || etagenhoehe <= 0 || etagenhoehe > 10) fehler.push('Etagenhöhe ungültig.')
if (!Number.isInteger(wohneinheiten) || wohneinheiten < 0) fehler.push('Wohneinheiten ungültig.')
if (!Number.isFinite(quadratmeter) || quadratmeter <= 0) fehler.push('Quadratmeter ungültig.')
if (!Number.isInteger(wohneinheiten) || wohneinheiten < 0) fehler.push('Anzahl Wohneinheiten ungültig.')
if (!Number.isFinite(wohnflaeche) || wohnflaeche <= 0) fehler.push('Wohnfläche ungültig.')
if (grund !== null && (!Number.isFinite(grund) || grund <= 0)) fehler.push('Grundstücksfläche ungültig.')
return fehler
}
@@ -1069,14 +1088,18 @@ app.post('/api/buildings', auth, adminOnly, async (req, res, next) => {
const b = req.body || {}
const fehler = pruefeHaus(b)
if (fehler.length) return res.status(400).json({ fehler: fehler.join(' ') })
const name = String(b.name).trim()
const name = hausNameAus(b)
const vorhanden = await pool.query('SELECT 1 FROM buildings WHERE name = $1', [name])
if (vorhanden.rows.length) return res.status(400).json({ fehler: 'Dieses Haus existiert bereits.' })
if (vorhanden.rows.length) return res.status(400).json({ fehler: 'Ein Haus mit dieser Straße und Hausnummer existiert bereits.' })
const grund = b.grundstuecksflaeche ? Number(b.grundstuecksflaeche) : null
const { rows } = await pool.query(
`INSERT INTO buildings (name, adresse, baujahr, stockwerke, etagenhoehe, wohneinheiten, quadratmeter)
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`,
[name, String(b.adresse).trim(), Number(b.baujahr), Number(b.stockwerke),
Number(b.etagenhoehe), Number(b.wohneinheiten), Number(b.quadratmeter)],
`INSERT INTO buildings (name, adresse, strasse, hausnummer, plz, ort, lage,
baujahr, stockwerke, etagenhoehe, wohneinheiten, wohnflaeche, grundstuecksflaeche)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING *`,
[name, hausAdresseAus(b), String(b.strasse).trim(), String(b.hausnummer).trim(),
String(b.plz).trim(), String(b.ort).trim(), String(b.lage || '').trim(),
Number(b.baujahr), Number(b.stockwerke), Number(b.etagenhoehe),
Number(b.wohneinheiten), Number(b.wohnflaeche), grund],
)
res.json({ building: mapBuilding(rows[0]) })
} catch (e) { next(e) }
@@ -1090,19 +1113,25 @@ app.patch('/api/buildings/:name', auth, adminOnly, async (req, res, next) => {
const b = { ...alt, ...(req.body || {}) }
const fehler = pruefeHaus(b)
if (fehler.length) return res.status(400).json({ fehler: fehler.join(' ') })
const neuName = String(b.name).trim()
// Name folgt immer Straße + Hausnummer; die Anzeigeadresse wird neu zusammengesetzt
const neuName = hausNameAus(b)
if (neuName !== alt.name) {
const vorhanden = await pool.query('SELECT 1 FROM buildings WHERE name = $1', [neuName])
if (vorhanden.rows.length) return res.status(400).json({ fehler: 'Dieser Hausname ist bereits vergeben.' })
if (vorhanden.rows.length) return res.status(400).json({ fehler: 'Ein Haus mit dieser Straße und Hausnummer existiert bereits.' })
}
const grund = b.grundstuecksflaeche ? Number(b.grundstuecksflaeche) : null
const client = await pool.connect()
try {
await client.query('BEGIN')
await client.query(
`UPDATE buildings SET name = $1, adresse = $2, baujahr = $3, stockwerke = $4,
etagenhoehe = $5, wohneinheiten = $6, quadratmeter = $7 WHERE name = $8`,
[neuName, String(b.adresse).trim(), Number(b.baujahr), Number(b.stockwerke),
Number(b.etagenhoehe), Number(b.wohneinheiten), Number(b.quadratmeter), req.params.name],
`UPDATE buildings SET name = $1, adresse = $2, strasse = $3, hausnummer = $4, plz = $5, ort = $6, lage = $7,
baujahr = $8, stockwerke = $9, etagenhoehe = $10, wohneinheiten = $11,
wohnflaeche = $12, grundstuecksflaeche = $13
WHERE name = $14`,
[neuName, hausAdresseAus(b), String(b.strasse).trim(), String(b.hausnummer).trim(),
String(b.plz).trim(), String(b.ort).trim(), String(b.lage || '').trim(),
Number(b.baujahr), Number(b.stockwerke), Number(b.etagenhoehe), Number(b.wohneinheiten),
Number(b.wohnflaeche), grund, req.params.name],
)
// Haus umbenannt → Bezüge in Wohnungen und Zählern mitziehen
if (neuName !== alt.name) {
@@ -1471,6 +1500,33 @@ app.use((err, _req, res, _next) => {
})
// ── Start ────────────────────────────────────────────────────
/** Zerlegt „Straße Nr., PLZ Ort" in Einzelfelder (Best-Effort-Migration v1.26). */
function adresseZerlegen(adresse) {
const teile = { strasse: '', hausnummer: '', plz: '', ort: '' }
const linksRechts = String(adresse).split(',')
const rechts = (linksRechts[1] || '').trim()
const plzMatch = /(\d{5})\s*(.*)/.exec(rechts)
if (plzMatch) { teile.plz = plzMatch[1]; teile.ort = plzMatch[2].trim() }
const links = (linksRechts[0] || '').trim().split(/\s+/)
teile.hausnummer = links.length > 1 ? links.pop() : ''
teile.strasse = links.join(' ')
return teile
}
async function haeuserFelderMigrieren() {
const { rows } = await pool.query("SELECT * FROM buildings WHERE strasse = '' AND adresse <> ''")
for (const b of rows) {
const t = adresseZerlegen(b.adresse)
await pool.query(
'UPDATE buildings SET strasse = $1, hausnummer = $2, plz = $3, ort = $4 WHERE name = $5',
[t.strasse, t.hausnummer, t.plz, t.ort, b.name],
)
}
// Wohnfläche aus Alt-Feld quadratmeter nachziehen
await pool.query('UPDATE buildings SET wohnflaeche = quadratmeter WHERE wohnflaeche IS NULL')
if (rows.length) console.log(`[api] Häuser: ${rows.length} Adresse(n) in Einzelfelder zerlegt`)
}
async function warteAufDatenbank() {
let letzterFehler = null
for (let versuch = 1; versuch <= 30; versuch++) {
@@ -1498,6 +1554,7 @@ async function start() {
if (haeuserGeseedet) console.log('[api] Haus-Demo-Daten eingespielt')
const kostenGeseedet = await seedeKosten()
if (kostenGeseedet) console.log('[api] Kosten-Demo-Daten eingespielt')
await haeuserFelderMigrieren()
setInterval(sessionsAufraeumen, 60 * 60 * 1000).unref()
app.listen(PORT, () => console.log(`[api] mp-api lauscht auf :${PORT}`))
}
+8 -3
View File
@@ -139,10 +139,15 @@ async function seedeHaeuser() {
const { rows } = await pool.query('SELECT COUNT(*)::int AS n FROM buildings')
if (rows[0].n > 0 || !(await demoBestandVorhanden())) return false
for (const h of buildings) {
const teile = String(h.adresse).trim().split(/\s+/)
const hausnummer = teile.length > 1 ? teile.pop() : ''
const strasse = teile.join(' ')
await pool.query(
`INSERT INTO buildings (name, adresse, baujahr, stockwerke, etagenhoehe, wohneinheiten, quadratmeter)
VALUES ($1,$2,$3,$4,$5,$6,$7)`,
[h.name, h.adresse, h.baujahr, h.stockwerke, h.etagenhoehe, h.wohneinheiten, h.quadratmeter],
`INSERT INTO buildings (name, adresse, strasse, hausnummer, plz, ort, lage,
baujahr, stockwerke, etagenhoehe, wohneinheiten, quadratmeter, wohnflaeche)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)`,
[h.name, h.adresse, strasse, hausnummer, '', '', '',
h.baujahr, h.stockwerke, h.etagenhoehe, h.wohneinheiten, h.quadratmeter, h.quadratmeter],
)
}
return true