From 2f873737c2d79f842e438cd4699692e4953a3753 Mon Sep 17 00:00:00 2001 From: ah-admin Date: Sun, 4 Oct 2026 19:13:36 +0200 Subject: [PATCH] v1.33: Eintraege in allen Listen loeschbar --- kimi.md | 17 ++++++ server/src/index.js | 97 ++++++++++++++++++++++++++++++++ src/pages/BenutzerVerwaltung.tsx | 17 +++++- src/pages/Bestand.tsx | 60 ++++++++++++++++++-- src/pages/Kosten.tsx | 17 +++++- src/pages/Tickets.tsx | 21 ++++++- src/pages/Zaehler.tsx | 36 +++++++++++- src/store/store.tsx | 70 +++++++++++++++++++++++ 8 files changed, 325 insertions(+), 10 deletions(-) diff --git a/kimi.md b/kimi.md index 3c384ff..86d9f77 100644 --- a/kimi.md +++ b/kimi.md @@ -189,6 +189,23 @@ tar -czf mp-dist.tar.gz -C dist . ## 8. Verlauf / offene Punkte +**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), +/api/belege/:id, /api/buildings/:name und /api/units/:id – letztere zwei +verweigern mit 400 + deutscher Blocker-Liste (Haus: Wohnungen/Zähler/Kosten; +Wohnung: Zähler/Dokumente/Kosten/Mieter), /api/users/:id (eigenes Konto und +letzten aktiven Verwalter geschützt, sessions vorher gelöscht). Store: +deleteTicket, deleteZaehler, deleteHaus, deleteWohnung, deleteBeleg, +loescheBenutzer (apiDelete + lokaler State-Filter, threads bei Ticket-Delete +mitgefiltert). Frontend: rote Trash2-Löschbuttons mit window.confirm + +alert(err) in Bestand (Haus Karte+Liste, Wohnungszeile), Zaehler (Karte+Liste), +Kosten BelegeListe, Tickets (admin, warnt vor Mitlöschung der Kommunikation), +BenutzerVerwaltung (eigenes Konto deaktiviert); Buttons nur bei role=admin, +in Mieter-sichtbaren Seiten (Zaehler, Tickets) zusätzlich gegated. E2E: 400- +Blocker beim Haus mit Wohnung/Zähler, Kaskaden-Löschung bis Haus weg, +Ticket anlegen+löschen, fremden Testbenutzer löschen, eigenes Konto → 400. + **Erledigt 04.10. (v1.32):** Deutsche Datumsformate in allen Modulen. NEU src/lib/format.ts: fmtDatum (ISO YYYY-MM-DD → TT.MM.JJJJ, deutsche Werte durchreichen) + fmtDatumZeit (mit Uhrzeit) – eingesetzt in Zaehler (Karte, diff --git a/server/src/index.js b/server/src/index.js index e85db40..676d580 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -553,6 +553,19 @@ app.patch('/api/tickets/:id', auth, adminOnly, async (req, res, next) => { } catch (e) { next(e) } }) +// Schadensmeldung löschen inkl. verknüpfter Unterhaltung und Nachrichten (v1.33) +app.delete('/api/tickets/:id', auth, adminOnly, async (req, res, next) => { + 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]) + for (const th of threads) { + await pool.query('DELETE FROM messages WHERE thread_id = $1', [th.id]) + } + res.json({ ok: true }) + } catch (e) { next(e) } +}) + // ── Zählerstände (Strom / Wasser / Gas) ───────────────────── const ZAEHLER_ARTEN = ['Strom', 'Wasser', 'Gas', 'Wärme', 'Sonstiges'] // Gängige Heizungsformen (v1.29) – Dropdown in Haus-/Wohnungsformular @@ -707,6 +720,16 @@ app.delete('/api/meters/:id/readings/:rid', auth, adminOnly, async (req, res, ne res.json({ ok: true }) } catch (e) { next(e) } }) + +// Zähler komplett löschen inkl. Verlauf (v1.33) +app.delete('/api/meters/:id', auth, adminOnly, async (req, res, next) => { + try { + const { rows } = await pool.query('DELETE FROM meters WHERE id = $1 RETURNING id', [req.params.id]) + if (!rows.length) return res.status(404).json({ fehler: 'Zähler nicht gefunden.' }) + await pool.query('DELETE FROM meter_readings WHERE meter_id = $1', [req.params.id]) + res.json({ ok: true }) + } catch (e) { next(e) } +}) // ── Dokumente (Verwaltung) ─────────────────────────────────── const DOKUMENT_KATEGORIEN = ['Mietvertrag', 'Nebenkostenabrechnung', 'Hausordnung', 'Protokoll', 'Sonstiges'] @@ -1135,6 +1158,15 @@ app.post('/api/belege/:id/analysieren', auth, adminOnly, async (req, res, next) } catch (e) { next(e) } }) +// Beleg löschen (v1.33) +app.delete('/api/belege/:id', auth, adminOnly, async (req, res, next) => { + try { + const { rows } = await pool.query('DELETE FROM belege WHERE id = $1 RETURNING id', [req.params.id]) + if (!rows.length) return res.status(404).json({ fehler: 'Beleg nicht gefunden.' }) + res.json({ ok: true }) + } catch (e) { next(e) } +}) + // ── Bestand: Häuser & Wohnungen (Verwaltung) ──────────────── /** Name/Anzeigeadresse werden aus den Einzelfeldern zusammengesetzt (v1.26). */ function hausNameAus(b) { @@ -1237,6 +1269,28 @@ app.patch('/api/buildings/:name', auth, adminOnly, async (req, res, next) => { } catch (e) { next(e) } }) +// Haus löschen – nur wenn keine Wohnungen, Zähler oder Kosten mehr darauf verweisen (v1.33) +app.delete('/api/buildings/:name', auth, adminOnly, async (req, res, next) => { + try { + const { rows } = await pool.query('SELECT 1 FROM buildings WHERE name = $1', [req.params.name]) + if (!rows.length) return res.status(404).json({ fehler: 'Haus nicht gefunden.' }) + const blocker = [] + for (const [tabelle, spalte, label] of [ + ['units', 'building', 'Wohnungen'], + ['meters', 'building', 'Zähler'], + ['costs', 'building', 'Kostenpositionen'], + ]) { + const { rows: r } = await pool.query(`SELECT COUNT(*)::int AS n FROM ${tabelle} WHERE ${spalte} = $1`, [req.params.name]) + if (r[0].n > 0) blocker.push(`${r[0].n} ${label}`) + } + if (blocker.length) { + return res.status(400).json({ fehler: `Haus kann nicht gelöscht werden – bitte zuerst entfernen: ${blocker.join(', ')}.` }) + } + await pool.query('DELETE FROM buildings WHERE name = $1', [req.params.name]) + res.json({ ok: true }) + } catch (e) { next(e) } +}) + app.post('/api/units', auth, adminOnly, async (req, res, next) => { try { const u = req.body || {} @@ -1297,6 +1351,29 @@ app.patch('/api/units/:id', auth, adminOnly, async (req, res, next) => { } catch (e) { next(e) } }) +// Wohnung löschen – nur wenn keine Zähler, Dokumente, Kosten oder Mieter sie referenzieren (v1.33) +app.delete('/api/units/:id', auth, adminOnly, async (req, res, next) => { + try { + const { rows } = await pool.query('SELECT 1 FROM units WHERE id = $1', [req.params.id]) + if (!rows.length) return res.status(404).json({ fehler: 'Wohnung nicht gefunden.' }) + const blocker = [] + for (const [tabelle, spalte, label] of [ + ['meters', 'unit_id', 'Zähler'], + ['documents', 'unit_id', 'Dokumente'], + ['costs', 'unit_id', 'Kostenpositionen'], + ['users', 'einheit_id', 'zugeordnete Benutzer'], + ]) { + 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}`) + } + if (blocker.length) { + return res.status(400).json({ fehler: `Wohnung kann nicht gelöscht werden – bitte zuerst entfernen/zuordnen: ${blocker.join(', ')}.` }) + } + await pool.query('DELETE FROM units WHERE id = $1', [req.params.id]) + res.json({ ok: true }) + } catch (e) { next(e) } +}) + // ── Nachrichten (Threads) ──────────────────────────────────── app.post('/api/threads', auth, async (req, res, next) => { try { @@ -1458,6 +1535,26 @@ app.patch('/api/users/:id', auth, adminOnly, async (req, res, next) => { } catch (e) { next(e) } }) +// Benutzer löschen: eigenes Konto und den letzten aktiven Verwalter schützen (v1.33) +app.delete('/api/users/:id', auth, adminOnly, async (req, res, next) => { + try { + if (req.params.id === req.user.id) + return res.status(400).json({ fehler: 'Das eigene Konto kann nicht gelöscht werden.' }) + const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id]) + if (!rows.length) return res.status(404).json({ fehler: 'Benutzer nicht gefunden.' }) + if (rows[0].rolle === 'verwaltung' && rows[0].aktiv) { + const { rows: rest } = await pool.query( + `SELECT COUNT(*)::int AS n FROM users WHERE rolle = 'verwaltung' AND aktiv AND id <> $1`, [req.params.id], + ) + if (rest[0].n === 0) + return res.status(400).json({ fehler: 'Der letzte aktive Verwaltungsbenutzer kann nicht gelöscht werden.' }) + } + await pool.query('DELETE FROM sessions WHERE user_id = $1', [req.params.id]) + await pool.query('DELETE FROM users WHERE id = $1', [req.params.id]) + res.json({ ok: true }) + } catch (e) { next(e) } +}) + // Direkte E-Mail der Verwaltung an einen Mieter (immer mit CC an die // Verwaltung, damit die Kommunikation dort vollständig nachvollziehbar ist) app.post('/api/users/:id/email', auth, adminOnly, async (req, res, next) => { diff --git a/src/pages/BenutzerVerwaltung.tsx b/src/pages/BenutzerVerwaltung.tsx index a4443a9..1865e6e 100644 --- a/src/pages/BenutzerVerwaltung.tsx +++ b/src/pages/BenutzerVerwaltung.tsx @@ -229,7 +229,7 @@ function EmailDialog({ benutzer: b, einheitLabel }: { benutzer: Benutzer; einhei } export default function BenutzerVerwaltung() { - const { benutzer, codes, units, setAktiv, setPasswort, weiseEinheitZu, setzeBenutzerEmail, generiereCodes } = useStore() + const { benutzer, codes, units, setAktiv, setPasswort, weiseEinheitZu, setzeBenutzerEmail, generiereCodes, loescheBenutzer, me } = useStore() const [suche, setSuche] = useState('') const [gruppe, setGruppe] = useState<'alle' | BenutzerRolle>('alle') const [anzahl, setAnzahl] = useState(5) @@ -256,6 +256,12 @@ export default function BenutzerVerwaltung() { await setPasswort(id, '0815') } + const benutzerLoeschen = async (id: string, name: string) => { + if (!window.confirm(`Benutzer „${name}" wirklich löschen? Sitzungen werden entwertet, die Aktion kann nicht rückgängig gemacht werden.`)) return + const fehler = await loescheBenutzer(id) + if (fehler) window.alert(fehler) + } + const kopieren = (text: string) => { navigator.clipboard?.writeText(text).catch(() => {}) setKopiert(text) @@ -474,6 +480,15 @@ export default function BenutzerVerwaltung() { > {b.aktiv ? 'Deaktivieren' : 'Aktivieren'} + ) diff --git a/src/pages/Bestand.tsx b/src/pages/Bestand.tsx index d968ef8..8fcc20b 100644 --- a/src/pages/Bestand.tsx +++ b/src/pages/Bestand.tsx @@ -1,7 +1,7 @@ import { Fragment, useMemo, useState } from 'react' import { Building2, Home, MapPin, CalendarDays, ArrowUpDown, LayoutGrid, Ruler, Pencil, - Plus, Users, DoorOpen, Search, List, ChevronDown, ChevronRight, Flame, + Plus, Users, DoorOpen, Search, List, ChevronDown, ChevronRight, Flame, Trash2, } from 'lucide-react' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' @@ -321,6 +321,12 @@ function sortiereWohnungen(liste: Unit[]): Unit[] { /** Wohnungszeile, in Karten- und Listenansicht gleich. */ function WohnungsZeile({ u, gebaeude, festesHaus }: { u: Unit; gebaeude: string[]; festesHaus: string }) { + const { deleteWohnung, role } = useStore() + const loesche = async () => { + if (!window.confirm(`Wohnung ${u.building}, Whg. ${u.number} wirklich löschen?`)) return + const fehler = await deleteWohnung(u.id) + if (fehler) window.alert(fehler) + } return (
  • @@ -344,6 +350,17 @@ function WohnungsZeile({ u, gebaeude, festesHaus }: { u: Unit; gebaeude: string[ {fmtEUR.format(u.warmRent)} + {role === 'admin' && ( + + )}
  • @@ -378,11 +395,18 @@ function BelegungsFortschritt({ angelegt, soll }: { angelegt: number; soll: numb } export default function Bestand() { - const { buildings, units } = useStore() + const { buildings, units, deleteHaus, role } = useStore() const [suche, setSuche] = useState('') const [ansicht, setAnsicht] = useState<'karten' | 'liste'>('liste') const [offen, setOffen] = useState(null) + const loescheHaus = async (name: string) => { + if (!window.confirm(`Haus „${name}" wirklich löschen?`)) return + const fehler = await deleteHaus(name) + if (fehler) window.alert(fehler) + if (offen === name) setOffen(null) + } + const gebaeudeNamen = useMemo( () => buildings.map((b) => b.name).sort(), [buildings], @@ -509,7 +533,21 @@ export default function Bestand() { - + + + {role === 'admin' && ( + + )} +
    @@ -618,7 +656,21 @@ export default function Bestand() { e.stopPropagation()}> - + + + {role === 'admin' && ( + + )} + {aufgeklappt && ( diff --git a/src/pages/Kosten.tsx b/src/pages/Kosten.tsx index ff7a299..94591cc 100644 --- a/src/pages/Kosten.tsx +++ b/src/pages/Kosten.tsx @@ -316,10 +316,16 @@ function AbrechnungDialog() { /** Eingegangene SparkReceipt-Belege (nur Verwaltung). */ function BelegeListe() { - const { belege, setBelegZugeordnet, analysiereBeleg, units } = useStore() + const { belege, setBelegZugeordnet, analysiereBeleg, deleteBeleg, units } = useStore() const [nurOffene, setNurOffene] = useState(false) const [analysiere, setAnalysiere] = useState(null) + const loesche = async (b: Beleg) => { + if (!window.confirm(`Beleg „${b.haendler || b.externeId}" wirklich löschen?`)) return + const fehler = await deleteBeleg(b.id) + if (fehler) window.alert(fehler) + } + const zeilen = useMemo( () => (nurOffene ? belege.filter((b) => !b.zugeordnet) : belege), [belege, nurOffene], @@ -477,6 +483,15 @@ function BelegeListe() { Beleg )} +
    diff --git a/src/pages/Tickets.tsx b/src/pages/Tickets.tsx index 8d2f74e..6050ef5 100644 --- a/src/pages/Tickets.tsx +++ b/src/pages/Tickets.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react' import { Link } from 'react-router' -import { Plus, Wrench, CalendarDays, MapPin, MessagesSquare } from 'lucide-react' +import { Plus, Wrench, CalendarDays, MapPin, MessagesSquare, Trash2 } from 'lucide-react' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { @@ -143,9 +143,15 @@ function NewTicketDialog() { } export default function Tickets() { - const { tickets, units, role, setTicketStatus, meineEinheit, threads } = useStore() + const { tickets, units, role, setTicketStatus, meineEinheit, threads, deleteTicket } = useStore() const [filter, setFilter] = useState<'alle' | TicketStatus>('alle') + const loescheTicket = async (id: string, titel: string) => { + if (!window.confirm(`Schadensmeldung „${titel}" wirklich löschen? Die zugehörige Kommunikation wird mitgelöscht.`)) return + const fehler = await deleteTicket(id) + if (fehler) window.alert(fehler) + } + const threadFuerTicket = (ticketId: string) => threads.find((th) => th.ticketId === ticketId) const visible = useMemo( @@ -242,6 +248,17 @@ export default function Tickets() { ) : ( {t.status} )} + {role === 'admin' && ( + + )} diff --git a/src/pages/Zaehler.tsx b/src/pages/Zaehler.tsx index a1ed7f0..394c427 100644 --- a/src/pages/Zaehler.tsx +++ b/src/pages/Zaehler.tsx @@ -388,7 +388,7 @@ function StandEintragenDialog({ meter }: { meter: Meter }) { } export default function Zaehler() { - const { meters, units, role } = useStore() + const { meters, units, role, deleteZaehler } = useStore() const [filterHaus, setFilterHaus] = useState('alle') const [filterArt, setFilterArt] = useState<'alle' | ZaehlerArt>('alle') const [suche, setSuche] = useState('') @@ -405,6 +405,12 @@ export default function Zaehler() { return u ? `Whg. ${u.number}` : m.unitId } + const loescheZaehler = async (m: Meter) => { + if (!window.confirm(`Zähler ${m.nummer} (${m.art}) wirklich löschen? Der gesamte Verlauf wird mitgelöscht.`)) return + const fehler = await deleteZaehler(m.id) + if (fehler) window.alert(fehler) + } + const visible = useMemo(() => { const q = suche.trim().toLowerCase() return meters.filter( @@ -514,7 +520,21 @@ export default function Zaehler() { )} - {role === 'admin' && } + {role === 'admin' && ( + + + + + )}
    {fmtStand(m.stand)} @@ -587,6 +607,18 @@ export default function Zaehler() { {role === 'admin' && } + {role === 'admin' && ( + + )}
    diff --git a/src/store/store.tsx b/src/store/store.tsx index e664b64..61f8691 100644 --- a/src/store/store.tsx +++ b/src/store/store.tsx @@ -99,6 +99,8 @@ interface Store extends Bootstrap { ) => Promise addTicket: (t: { title: string; description: string; category: string; priority: string; unitId?: string }) => Promise setTicketStatus: (id: string, status: TicketStatus) => Promise + /** Schadensmeldung inkl. verknüpfter Kommunikation löschen (Verwaltung, v1.33) */ + deleteTicket: (id: string) => Promise newThread: (subject: string, text: string, unitId?: string) => Promise addMessage: (threadId: string, text: string) => Promise markThreadRead: (threadId: string) => void @@ -111,6 +113,8 @@ interface Store extends Bootstrap { einheitId: string | null, ) => Promise setAktiv: (id: string, aktiv: boolean) => Promise + /** Benutzer endgültig löschen (Verwaltung, v1.33; eigenes Konto/geschützter Letzter ausgenommen) */ + loescheBenutzer: (id: string) => Promise setPasswort: (id: string, passwort: string) => Promise weiseEinheitZu: (id: string, einheitId: string | null) => Promise /** Direkt-E-Mail der Verwaltung an einen Mieter (immer mit CC an die Verwaltung, v1.22) */ @@ -131,6 +135,8 @@ interface Store extends Bootstrap { id: string, z: Partial<{ nummer: string; art: ZaehlerArt; stand: number; unitId: string | null; building: string }>, ) => Promise + /** Zähler inkl. Verlauf löschen (Verwaltung, v1.33) */ + deleteZaehler: (id: string) => Promise // ── Zählerverlauf (v1.25) ───────────────────────────────── /** Verlauf eines Zählers laden (älteste zuerst) */ zaehlerVerlauf: (meterId: string) => Promise @@ -153,6 +159,10 @@ interface Store extends Bootstrap { heizungsart: string }) => Promise updateWohnung: (id: string, u: Partial) => Promise + /** Haus löschen (nur ohne Wohnungen/Zähler/Kosten; v1.33) */ + deleteHaus: (name: string) => Promise + /** Wohnung löschen (nur ohne Referenzen; v1.33) */ + deleteWohnung: (id: string) => Promise // ── Dokumente (Verwaltung pflegt, Mieter lesen) ───────── addDokument: (d: { title: string @@ -185,6 +195,8 @@ interface Store extends Bootstrap { deleteKosten: (id: string) => Promise // ── Belege (SparkReceipt-Import; Verwaltung ordnet zu) ── setBelegZugeordnet: (id: string, zugeordnet: boolean) => Promise + /** Beleg endgültig löschen (Verwaltung, v1.33) */ + deleteBeleg: (id: string) => Promise // ── Nebenkostenabrechnung als PDF (v1.23) ───────────────── erzeugeAbrechnung: (unitId: string, jahr: number) => Promise<{ fehler: string | null @@ -310,6 +322,19 @@ export function StoreProvider({ children }: { children: ReactNode }) { const { ticket } = await apiPatch<{ ticket: Ticket }>(`/api/tickets/${id}`, { status }) setDaten((d) => ({ ...d, tickets: d.tickets.map((t) => (t.id === id ? ticket : t)) })) }, + async deleteTicket(id) { + try { + await apiDelete(`/api/tickets/${id}`) + setDaten((d) => ({ + ...d, + tickets: d.tickets.filter((t) => t.id !== id), + threads: d.threads.filter((t) => t.ticketId !== id), + })) + return null + } catch (e) { + return fehlerNachricht(e) + } + }, // ── Nachrichten ───────────────────────────────────────── async newThread(subject, text, unitId) { try { @@ -360,6 +385,15 @@ export function StoreProvider({ children }: { children: ReactNode }) { const { benutzer } = await apiPatch<{ benutzer: Benutzer }>(`/api/users/${id}`, { aktiv }) setDaten((d) => ({ ...d, benutzer: d.benutzer.map((b) => (b.id === id ? benutzer : b)) })) }, + async loescheBenutzer(id) { + try { + await apiDelete(`/api/users/${id}`) + setDaten((d) => ({ ...d, benutzer: d.benutzer.filter((b) => b.id !== id) })) + return null + } catch (e) { + return fehlerNachricht(e) + } + }, async setPasswort(id, passwort) { const { benutzer } = await apiPatch<{ benutzer: Benutzer }>(`/api/users/${id}`, { passwortHash: await sha256(passwort), @@ -411,6 +445,15 @@ export function StoreProvider({ children }: { children: ReactNode }) { return fehlerNachricht(e) } }, + async deleteZaehler(id) { + try { + await apiDelete(`/api/meters/${id}`) + setDaten((d) => ({ ...d, meters: d.meters.filter((m) => m.id !== id) })) + return null + } catch (e) { + return fehlerNachricht(e) + } + }, // ── Zählerverlauf (v1.25) ─────────────────────────────── async zaehlerVerlauf(meterId) { const { readings } = await apiGet<{ readings: MeterReading[] }>(`/api/meters/${meterId}/readings`) @@ -454,6 +497,15 @@ export function StoreProvider({ children }: { children: ReactNode }) { return fehlerNachricht(e) } }, + async deleteHaus(name) { + try { + await apiDelete(`/api/buildings/${encodeURIComponent(name)}`) + setDaten((d) => ({ ...d, buildings: d.buildings.filter((b) => b.name !== name) })) + return null + } catch (e) { + return fehlerNachricht(e) + } + }, async addWohnung(u) { try { const { unit } = await apiPost<{ unit: Unit }>('/api/units', u) @@ -472,6 +524,15 @@ export function StoreProvider({ children }: { children: ReactNode }) { return fehlerNachricht(e) } }, + async deleteWohnung(id) { + try { + await apiDelete(`/api/units/${id}`) + setDaten((d) => ({ ...d, units: d.units.filter((x) => x.id !== id) })) + return null + } catch (e) { + return fehlerNachricht(e) + } + }, // ── Dokumente ─────────────────────────────────────────── async addDokument(d) { try { @@ -555,6 +616,15 @@ export function StoreProvider({ children }: { children: ReactNode }) { return fehlerNachricht(e) } }, + async deleteBeleg(id) { + try { + await apiDelete(`/api/belege/${id}`) + setDaten((x) => ({ ...x, belege: x.belege.filter((b) => b.id !== id) })) + return null + } catch (e) { + return fehlerNachricht(e) + } + }, // ── Nebenkostenabrechnung (v1.23) ───────────────────────── async erzeugeAbrechnung(unitId, jahr) { try {