v1.25: Zaehlerverlauf pro Zaehler inkl. Anomalitaetserkennung mit E-Mail-Benachrichtigung
This commit is contained in:
+84
-2
@@ -15,6 +15,7 @@ const { pool, init, sessionsAufraeumen } = require('./db')
|
||||
const { seed, seedeZaehler, seedeHaeuser, seedeKosten } = require('./seed')
|
||||
const mail = require('./mail')
|
||||
const { erzeugeAbrechnung } = require('./abrechnung')
|
||||
const zaehlerVerlauf = require('./zaehlerVerlauf')
|
||||
|
||||
const PORT = Number(process.env.PORT || 3000)
|
||||
const SESSION_DAUER_MS = 8 * 60 * 60 * 1000
|
||||
@@ -181,6 +182,7 @@ const mapAnnouncement = (a) => ({ id: a.id, title: a.title, text: a.text, date:
|
||||
const mapMeter = (m) => ({
|
||||
id: m.id, unitId: m.unit_id, building: m.building, nummer: m.nummer, art: m.art,
|
||||
stand: Number(m.stand), standAm: m.stand_am, geaendertAm: m.geaendert_am,
|
||||
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,
|
||||
@@ -353,8 +355,8 @@ app.get('/api/bootstrap', auth, async (req, res, next) => {
|
||||
? pool.query('SELECT * FROM registration_codes ORDER BY erstellt_am DESC')
|
||||
: { rows: [] },
|
||||
istAdmin
|
||||
? pool.query('SELECT * FROM meters ORDER BY building, nummer')
|
||||
: pool.query('SELECT * FROM meters WHERE unit_id = $1 ORDER BY nummer', [einheit]),
|
||||
? pool.query(`SELECT *, (SELECT anomalie FROM meter_readings r WHERE r.meter_id = meters.id ORDER BY r.seq DESC LIMIT 1) AS letzte_anomalie FROM meters ORDER BY building, nummer`)
|
||||
: pool.query(`SELECT *, (SELECT anomalie FROM meter_readings r WHERE r.meter_id = meters.id ORDER BY r.seq DESC LIMIT 1) AS letzte_anomalie FROM meters WHERE unit_id = $1 ORDER BY nummer`, [einheit]),
|
||||
istAdmin
|
||||
? pool.query('SELECT * FROM buildings ORDER BY name')
|
||||
: { rows: [] },
|
||||
@@ -565,6 +567,11 @@ app.post('/api/meters', auth, adminOnly, async (req, res, next) => {
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING *`,
|
||||
[id, einheitId, haus, String(nummer).trim(), art, standZahl, deutschesDatum(), deutschesDatumZeit()],
|
||||
)
|
||||
// Anfangsstand als erster Verlaufseintrag
|
||||
await zaehlerVerlauf.standEintragen({
|
||||
meter: rows[0], stand: standZahl, standAm: deutschesDatum(),
|
||||
erfasstVon: req.user.name, quelle: 'erstbefund',
|
||||
})
|
||||
res.json({ meter: mapMeter(rows[0]) })
|
||||
} catch (e) { next(e) }
|
||||
})
|
||||
@@ -601,9 +608,83 @@ app.patch('/api/meters/:id', auth, adminOnly, async (req, res, next) => {
|
||||
[neu.nummer, neu.art, neu.stand, neu.unit_id, neu.building,
|
||||
standGeaendert ? deutschesDatum() : m.stand_am, deutschesDatumZeit(), req.params.id],
|
||||
)
|
||||
// Geänderter Stand → neuer Verlaufseintrag (inkl. Anomalieprüfung + Mail)
|
||||
if (standGeaendert) {
|
||||
await zaehlerVerlauf.standEintragen({
|
||||
meter: aktualisiert[0], stand: neu.stand, standAm: deutschesDatum(),
|
||||
erfasstVon: req.user.name, quelle: 'manuel',
|
||||
})
|
||||
}
|
||||
res.json({ meter: mapMeter(aktualisiert[0]) })
|
||||
} catch (e) { next(e) }
|
||||
})
|
||||
|
||||
// ── Zählerverlauf (v1.25): Liste pro Zähler + Stand eintragen ──
|
||||
async function meterFuer(req, res) {
|
||||
const { rows } = await pool.query('SELECT * FROM meters WHERE id = $1', [req.params.id])
|
||||
if (!rows.length) { res.status(404).json({ fehler: 'Zähler nicht gefunden.' }); return null }
|
||||
const m = rows[0]
|
||||
// Mieter dürfen nur die Zähler ihrer eigenen Wohneinheit sehen/pflegen
|
||||
if (req.user.rolle !== 'verwaltung' && m.unit_id !== req.user.einheit_id) {
|
||||
res.status(403).json({ fehler: 'Kein Zugriff auf diesen Zähler.' })
|
||||
return null
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
app.get('/api/meters/:id/readings', auth, async (req, res, next) => {
|
||||
try {
|
||||
const m = await meterFuer(req, res)
|
||||
if (!m) return
|
||||
const verlauf = await zaehlerVerlauf.verlaufLaden(m.id)
|
||||
res.json({ readings: verlauf.map(zaehlerVerlauf.mapReading) })
|
||||
} catch (e) { next(e) }
|
||||
})
|
||||
|
||||
app.post('/api/meters/:id/readings', auth, async (req, res, next) => {
|
||||
try {
|
||||
const m = await meterFuer(req, res)
|
||||
if (!m) return
|
||||
const { stand, standAm } = req.body || {}
|
||||
const standZahl = Number(stand)
|
||||
if (!Number.isFinite(standZahl) || standZahl < 0) return res.status(400).json({ fehler: 'Ungültiger Zählerstand.' })
|
||||
const datum = String(standAm || '').trim() || deutschesDatum()
|
||||
if (!zaehlerVerlauf.parseDatum(datum)) return res.status(400).json({ fehler: 'Ungültiges Ablesedatum.' })
|
||||
const { eintrag, anomalie, grund } = await zaehlerVerlauf.standEintragen({
|
||||
meter: m, stand: standZahl, standAm: datum,
|
||||
erfasstVon: req.user.name, quelle: req.user.rolle === 'verwaltung' ? 'manuel' : 'mieter',
|
||||
})
|
||||
// meters.stand bleibt der aktuellste Wert (Cache für die Übersicht)
|
||||
const { rows: aktualisiert } = await pool.query(
|
||||
`UPDATE meters SET stand = $1, stand_am = $2, geaendert_am = $3 WHERE id = $4 RETURNING *`,
|
||||
[standZahl, datum, deutschesDatumZeit(), m.id],
|
||||
)
|
||||
res.json({ eintrag, anomalie, grund, meter: mapMeter(aktualisiert[0]) })
|
||||
} catch (e) { next(e) }
|
||||
})
|
||||
|
||||
// Einzelnen Verlaufseintrag löschen (Korrektur falscher Erfassungen, Verwaltung)
|
||||
app.delete('/api/meters/:id/readings/:rid', auth, adminOnly, async (req, res, next) => {
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'DELETE FROM meter_readings WHERE id = $1 AND meter_id = $2 RETURNING *',
|
||||
[req.params.rid, req.params.id],
|
||||
)
|
||||
if (!rows.length) return res.status(404).json({ fehler: 'Eintrag nicht gefunden.' })
|
||||
// meters-Cache aus dem neuesten verbleibenden Eintrag neu aufbauen
|
||||
const { rows: letzter } = await pool.query(
|
||||
'SELECT stand, stand_am FROM meter_readings WHERE meter_id = $1 ORDER BY seq DESC LIMIT 1',
|
||||
[req.params.id],
|
||||
)
|
||||
if (letzter.length) {
|
||||
await pool.query(
|
||||
'UPDATE meters SET stand = $1, stand_am = $2, geaendert_am = $3 WHERE id = $4',
|
||||
[letzter[0].stand, letzter[0].stand_am, deutschesDatumZeit(), req.params.id],
|
||||
)
|
||||
}
|
||||
res.json({ ok: true })
|
||||
} catch (e) { next(e) }
|
||||
})
|
||||
// ── Dokumente (Verwaltung) ───────────────────────────────────
|
||||
const DOKUMENT_KATEGORIEN = ['Mietvertrag', 'Nebenkostenabrechnung', 'Hausordnung', 'Protokoll', 'Sonstiges']
|
||||
|
||||
@@ -1412,6 +1493,7 @@ async function start() {
|
||||
if (geseedet) console.log('[api] Demo-Daten eingespielt (50 Wohnungen, 552 Buchungen, …)')
|
||||
const zaehlerGeseedet = await seedeZaehler()
|
||||
if (zaehlerGeseedet) console.log('[api] Zähler-Demo-Daten eingespielt')
|
||||
await zaehlerVerlauf.erstbefundFehlende()
|
||||
const haeuserGeseedet = await seedeHaeuser()
|
||||
if (haeuserGeseedet) console.log('[api] Haus-Demo-Daten eingespielt')
|
||||
const kostenGeseedet = await seedeKosten()
|
||||
|
||||
Reference in New Issue
Block a user