diff --git a/kimi.md b/kimi.md index 86d9f77..28c15fe 100644 --- a/kimi.md +++ b/kimi.md @@ -189,6 +189,30 @@ tar -czf mp-dist.tar.gz -C dist . ## 8. Verlauf / offene Punkte +**Erledigt 04.10. (v1.34):** SparkReceipt-Zuordnung nutzt Mietobjekt-Adresse. +Problem: auf eingehenden Rechnungen steht als Adresse der Rechnungsempfänger +(Rott 250 = eigene Verwaltungsadresse, als Haus gepflegt) – die sekundäre +Adress-Zuordnung matchte damit immer das falsche Haus. Lösung: Gebäude +bekommen das Kennzeichen eigenobjekt (DB: buildings.eigenobjekt BOOLEAN, +Default FALSE); belegAutoZuordnung schließt solche Häuser aus +(WHERE eigenobjekt IS NOT TRUE) – primäre Stromzählernummer-Matchung +bleibt unverändert. POST/PATCH /api/buildings und mapBuilding liefern/ +speichern eigenobjekt; PATCH merged via mapBuilding-Altbestand, bleibt +also erhalten wenn nicht mitsendet. Bestand.tsx: neuer Abschnitt +„Zuordnung Belege" mit Checkbox „Eigenobjekt / Rechnungsempfänger" +(Erklärung im Hint), Badge „Eigenobjekt" in Haus-Karte und Listenzeile. +Verwaltung muss Rott 250 einmalig als Eigenobjekt markieren; bereits +falsch zugeordnete Belege lassen sich einzeln per „Analysieren" neu +zuordnen (Schutz nur für 'manuell'). E2E: Webhook mit nur Empfängeradresse +→ keine Zuordnung statt Rott 250; mit Mietobjekt-Adresse im Text → +Zuordnung aufs Mietobjekt. +Fix in v1.33 mitnachgetragen: DELETE /api/tickets/:id löscht jetzt in +einer Transaktion zuerst messages, dann threads (threads.ticket_id hat +KEIN ON DELETE CASCADE), dann das Ticket – vorher 500 durch FK-Verletzung. +Blocker-Liste von DELETE /api/units/:id um transactions, tickets, +threads, registration_codes und belege erweitert (alle FKs auf units.id +ohne Cascade, vorher möglicher 500 statt 400er-Hinweis). + **Erledigt 04.10. (v1.33):** Einträge in allen Listen löschbar. Neue adminOnly-DELETE-Endpunkte in server/src/index.js: /api/tickets/:id (kaskadiert threads + messages), /api/meters/:id (kaskadiert meter_readings), diff --git a/server/src/db.js b/server/src/db.js index 20e7b3d..8d66869 100644 --- a/server/src/db.js +++ b/server/src/db.js @@ -175,6 +175,9 @@ ALTER TABLE buildings ADD COLUMN IF NOT EXISTS grundstuecksflaeche NUMERIC(9,1); -- v1.29: Heizungsart (Dropdown) in Haus- und Wohnungsverwaltung ALTER TABLE buildings ADD COLUMN IF NOT EXISTS heizungsart TEXT NOT NULL DEFAULT ''; ALTER TABLE units ADD COLUMN IF NOT EXISTS heizungsart TEXT NOT NULL DEFAULT ''; +-- v1.34: Eigenobjekt (z. B. Rechnungsempfänger-Adresse der Verwaltung) – +-- wird von der automatischen Beleg-Zuordnung ausgeschlossen +ALTER TABLE buildings ADD COLUMN IF NOT EXISTS eigenobjekt BOOLEAN NOT NULL DEFAULT FALSE; CREATE TABLE IF NOT EXISTS costs ( id TEXT PRIMARY KEY, diff --git a/server/src/index.js b/server/src/index.js index 676d580..e8fff61 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -195,6 +195,7 @@ const mapBuilding = (b) => ({ grundstuecksflaeche: b.grundstuecksflaeche === null || b.grundstuecksflaeche === undefined ? null : Number(b.grundstuecksflaeche), heizungsart: b.heizungsart ?? '', + eigenobjekt: Boolean(b.eigenobjekt), }) const mapKosten = (k) => ({ id: k.id, building: k.building, unitId: k.unit_id, jahr: k.jahr, @@ -555,15 +556,26 @@ app.patch('/api/tickets/:id', auth, adminOnly, async (req, res, next) => { // Schadensmeldung löschen inkl. verknüpfter Unterhaltung und Nachrichten (v1.33) app.delete('/api/tickets/:id', auth, adminOnly, async (req, res, next) => { + const client = await pool.connect() try { - const { rows } = await pool.query('DELETE FROM tickets WHERE id = $1 RETURNING id', [req.params.id]) - if (!rows.length) return res.status(404).json({ fehler: 'Schadensmeldung nicht gefunden.' }) - const { rows: threads } = await pool.query('DELETE FROM threads WHERE ticket_id = $1 RETURNING id', [req.params.id]) + await client.query('BEGIN') + const { rows: threads } = await client.query('DELETE FROM threads WHERE ticket_id = $1 RETURNING id', [req.params.id]) for (const th of threads) { - await pool.query('DELETE FROM messages WHERE thread_id = $1', [th.id]) + await client.query('DELETE FROM messages WHERE thread_id = $1', [th.id]) } + const { rows } = await client.query('DELETE FROM tickets WHERE id = $1 RETURNING id', [req.params.id]) + if (!rows.length) { + await client.query('ROLLBACK') + return res.status(404).json({ fehler: 'Schadensmeldung nicht gefunden.' }) + } + await client.query('COMMIT') res.json({ ok: true }) - } catch (e) { next(e) } + } catch (e) { + await client.query('ROLLBACK').catch(() => {}) + next(e) + } finally { + client.release() + } }) // ── Zählerstände (Strom / Wasser / Gas) ───────────────────── @@ -1041,8 +1053,12 @@ async function belegAutoZuordnung(text) { return fund } } - // Sekundär: Hausname/Adresse im Volltext - const { rows: haeuser } = await pool.query('SELECT name, adresse FROM buildings') + // Sekundär: Hausname/Adresse im Volltext. Eigenobjekte (z. B. die + // Rechnungsempfänger-Adresse der Verwaltung) werden übersprungen – sonst + // landet jeder Beleg, auf dem die Empfängeradresse steht, am falschen Haus. + const { rows: haeuser } = await pool.query( + 'SELECT name, adresse FROM buildings WHERE eigenobjekt IS NOT TRUE', + ) for (const h of haeuser) { const name = normalisieren((h.name || '').trim()) const adr = normalisieren((h.adresse || '').trim()) @@ -1211,12 +1227,14 @@ app.post('/api/buildings', auth, adminOnly, async (req, res, next) => { const heizung = String(b.heizungsart || '').trim() const { rows } = await pool.query( `INSERT INTO buildings (name, adresse, strasse, hausnummer, plz, ort, lage, - baujahr, stockwerke, etagenhoehe, wohneinheiten, wohnflaeche, grundstuecksflaeche, heizungsart) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING *`, + baujahr, stockwerke, etagenhoehe, wohneinheiten, wohnflaeche, grundstuecksflaeche, heizungsart, + eigenobjekt) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) 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, heizung], + Number(b.wohneinheiten), Number(b.wohnflaeche), grund, heizung, + Boolean(b.eigenobjekt)], ) res.json({ building: mapBuilding(rows[0]) }) } catch (e) { next(e) } @@ -1245,12 +1263,13 @@ app.patch('/api/buildings/:name', auth, adminOnly, async (req, res, next) => { await client.query( `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, heizungsart = $14 - WHERE name = $15`, + wohnflaeche = $12, grundstuecksflaeche = $13, heizungsart = $14, + eigenobjekt = $15 + WHERE name = $16`, [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, heizung, req.params.name], + Number(b.wohnflaeche), grund, heizung, Boolean(b.eigenobjekt), req.params.name], ) // Haus umbenannt → Bezüge in Wohnungen und Zählern mitziehen if (neuName !== alt.name) { @@ -1362,6 +1381,11 @@ app.delete('/api/units/:id', auth, adminOnly, async (req, res, next) => { ['documents', 'unit_id', 'Dokumente'], ['costs', 'unit_id', 'Kostenpositionen'], ['users', 'einheit_id', 'zugeordnete Benutzer'], + ['transactions', 'unit_id', 'Zahlungen'], + ['tickets', 'unit_id', 'Schadensmeldungen'], + ['threads', 'unit_id', 'Unterhaltungen'], + ['registration_codes', 'einheit_id', 'Registrierungscodes'], + ['belege', 'unit_id', 'Belege'], ]) { const { rows: r } = await pool.query(`SELECT COUNT(*)::int AS n FROM ${tabelle} WHERE ${spalte} = $1`, [req.params.id]) if (r[0].n > 0) blocker.push(`${r[0].n} ${label}`) diff --git a/src/pages/Bestand.tsx b/src/pages/Bestand.tsx index 8fcc20b..6861e85 100644 --- a/src/pages/Bestand.tsx +++ b/src/pages/Bestand.tsx @@ -73,6 +73,7 @@ function HausDialog({ haus }: { haus?: Building }) { haus?.grundstuecksflaeche != null ? String(haus.grundstuecksflaeche).replace('.', ',') : '', ) const [heizungsart, setHeizungsart] = useState(haus?.heizungsart ?? '') + const [eigenobjekt, setEigenobjekt] = useState(haus?.eigenobjekt ?? false) const [sende, setSende] = useState(false) const [fehler, setFehler] = useState(null) @@ -97,6 +98,7 @@ function HausDialog({ haus }: { haus?: Building }) { wohnflaeche: zahl(wohnflaeche), grundstuecksflaeche: grundstuecksflaeche.trim() ? zahl(grundstuecksflaeche) : null, heizungsart, + eigenobjekt, } const err = bearbeiten ? await updateHaus(haus!.name, payload) : await addHaus(payload) setSende(false) @@ -157,6 +159,25 @@ function HausDialog({ haus }: { haus?: Building }) { setWohnflaeche(e.target.value)} /> setGrundstuecksflaeche(e.target.value)} /> + + + {fehler && (
{fehler} @@ -530,6 +551,11 @@ export default function Bestand() { · Grundstück {h.grundstuecksflaeche.toLocaleString('de-DE')} m² )} + {h.eigenobjekt && ( + + Eigenobjekt + + )}
@@ -635,7 +661,14 @@ export default function Bestand() { {aufgeklappt ? : } - {h.adresse} + + {h.adresse} + {h.eigenobjekt && ( + + Eigenobjekt + + )} + {h.lage || '–'} {h.baujahr} {h.stockwerke} diff --git a/src/types/index.ts b/src/types/index.ts index a033281..b9a2d29 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -142,6 +142,9 @@ export interface Building { grundstuecksflaeche: number | null // in m², optional /** Heizungsart des Hauses (Dropdown, '' = nicht angegeben) */ heizungsart: string + /** Eigenobjekt (z. B. Rechnungsempfänger-Adresse der Verwaltung) – + * wird von der automatischen Beleg-Zuordnung ausgeschlossen */ + eigenobjekt: boolean } /** Formular-Daten für Neuanlage/Bearbeitung (Name/Adresse entfallen). */