v1.33: Eintraege in allen Listen loeschbar
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => benutzerLoeschen(b.id, b.name)}
|
||||
disabled={me?.id === b.id}
|
||||
title={me?.id === b.id ? 'Das eigene Konto kann nicht gelöscht werden' : 'Benutzer löschen'}
|
||||
aria-label={`Benutzer ${b.name} löschen`}
|
||||
className="rounded-lg bg-slate-100 px-2.5 py-1.5 text-xs font-medium text-slate-500 transition hover:bg-red-100 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-red-950 dark:hover:text-red-300"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+54
-2
@@ -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 (
|
||||
<li className="rounded-xl border bg-slate-50/60 px-3 py-2 dark:border-slate-700 dark:bg-slate-800/60">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
@@ -344,6 +350,17 @@ function WohnungsZeile({ u, gebaeude, festesHaus }: { u: Unit; gebaeude: string[
|
||||
<span className="flex shrink-0 items-center gap-2">
|
||||
<span className="font-medium text-slate-700 dark:text-slate-200">{fmtEUR.format(u.warmRent)}</span>
|
||||
<WohnungDialog wohnung={u} gebaeude={gebaeude} festesHaus={festesHaus} />
|
||||
{role === 'admin' && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-slate-400 transition-colors hover:text-red-600 dark:text-slate-500 dark:hover:text-red-400"
|
||||
onClick={loesche}
|
||||
aria-label={`Wohnung ${u.number} löschen`}
|
||||
title="Wohnung löschen"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
@@ -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<string | null>(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() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="flex shrink-0 items-center gap-1">
|
||||
<HausDialog haus={h} />
|
||||
{role === 'admin' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-red-600 dark:text-slate-500 dark:hover:text-red-400"
|
||||
onClick={() => loescheHaus(h.name)}
|
||||
aria-label={`Haus ${h.adresse} löschen`}
|
||||
title="Haus löschen"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<dl className="mt-4 grid grid-cols-2 gap-x-4 gap-y-2 text-xs">
|
||||
<div className="flex items-center gap-1.5 text-slate-500 dark:text-slate-400">
|
||||
@@ -618,7 +656,21 @@ export default function Bestand() {
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-right" onClick={(e) => e.stopPropagation()}>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<HausDialog haus={h} />
|
||||
{role === 'admin' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-red-600 dark:text-slate-500 dark:hover:text-red-400"
|
||||
onClick={() => loescheHaus(h.name)}
|
||||
aria-label={`Haus ${h.adresse} löschen`}
|
||||
title="Haus löschen"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{aufgeklappt && (
|
||||
|
||||
+16
-1
@@ -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<string | null>(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() {
|
||||
<ExternalLink className="h-3.5 w-3.5" /> Beleg
|
||||
</a>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loesche(b)}
|
||||
title="Beleg löschen"
|
||||
aria-label={`Beleg ${b.haendler || b.externeId} löschen`}
|
||||
className="text-slate-400 transition-colors hover:text-red-600 dark:text-slate-500 dark:hover:text-red-400"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
+19
-2
@@ -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() {
|
||||
) : (
|
||||
<Badge className={statusStyle[t.status]} variant="secondary">{t.status}</Badge>
|
||||
)}
|
||||
{role === 'admin' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loescheTicket(t.id, t.title)}
|
||||
title="Schadensmeldung löschen"
|
||||
aria-label={`Schadensmeldung ${t.title} löschen`}
|
||||
className="text-slate-400 transition-colors hover:text-red-600 dark:text-slate-500 dark:hover:text-red-400"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+34
-2
@@ -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() {
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{role === 'admin' && <ZaehlerDialog meter={m} />}
|
||||
{role === 'admin' && (
|
||||
<span className="flex shrink-0 items-center gap-1">
|
||||
<ZaehlerDialog meter={m} />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-red-600 dark:text-slate-500 dark:hover:text-red-400"
|
||||
onClick={() => loescheZaehler(m)}
|
||||
aria-label={`Zähler ${m.nummer} löschen`}
|
||||
title="Zähler löschen"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 text-3xl font-bold tracking-tight tabular-nums">
|
||||
{fmtStand(m.stand)}
|
||||
@@ -587,6 +607,18 @@ export default function Zaehler() {
|
||||
<VerlaufDialog meter={m} />
|
||||
<StandEintragenDialog meter={m} />
|
||||
{role === 'admin' && <ZaehlerDialog meter={m} />}
|
||||
{role === 'admin' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-red-600 dark:text-slate-500 dark:hover:text-red-400"
|
||||
onClick={() => loescheZaehler(m)}
|
||||
aria-label={`Zähler ${m.nummer} löschen`}
|
||||
title="Zähler löschen"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -99,6 +99,8 @@ interface Store extends Bootstrap {
|
||||
) => Promise<string | null>
|
||||
addTicket: (t: { title: string; description: string; category: string; priority: string; unitId?: string }) => Promise<string | null>
|
||||
setTicketStatus: (id: string, status: TicketStatus) => Promise<void>
|
||||
/** Schadensmeldung inkl. verknüpfter Kommunikation löschen (Verwaltung, v1.33) */
|
||||
deleteTicket: (id: string) => Promise<string | null>
|
||||
newThread: (subject: string, text: string, unitId?: string) => Promise<string | null>
|
||||
addMessage: (threadId: string, text: string) => Promise<void>
|
||||
markThreadRead: (threadId: string) => void
|
||||
@@ -111,6 +113,8 @@ interface Store extends Bootstrap {
|
||||
einheitId: string | null,
|
||||
) => Promise<string | null>
|
||||
setAktiv: (id: string, aktiv: boolean) => Promise<void>
|
||||
/** Benutzer endgültig löschen (Verwaltung, v1.33; eigenes Konto/geschützter Letzter ausgenommen) */
|
||||
loescheBenutzer: (id: string) => Promise<string | null>
|
||||
setPasswort: (id: string, passwort: string) => Promise<void>
|
||||
weiseEinheitZu: (id: string, einheitId: string | null) => Promise<void>
|
||||
/** 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<string | null>
|
||||
/** Zähler inkl. Verlauf löschen (Verwaltung, v1.33) */
|
||||
deleteZaehler: (id: string) => Promise<string | null>
|
||||
// ── Zählerverlauf (v1.25) ─────────────────────────────────
|
||||
/** Verlauf eines Zählers laden (älteste zuerst) */
|
||||
zaehlerVerlauf: (meterId: string) => Promise<MeterReading[]>
|
||||
@@ -153,6 +159,10 @@ interface Store extends Bootstrap {
|
||||
heizungsart: string
|
||||
}) => Promise<string | null>
|
||||
updateWohnung: (id: string, u: Partial<Unit>) => Promise<string | null>
|
||||
/** Haus löschen (nur ohne Wohnungen/Zähler/Kosten; v1.33) */
|
||||
deleteHaus: (name: string) => Promise<string | null>
|
||||
/** Wohnung löschen (nur ohne Referenzen; v1.33) */
|
||||
deleteWohnung: (id: string) => Promise<string | null>
|
||||
// ── Dokumente (Verwaltung pflegt, Mieter lesen) ─────────
|
||||
addDokument: (d: {
|
||||
title: string
|
||||
@@ -185,6 +195,8 @@ interface Store extends Bootstrap {
|
||||
deleteKosten: (id: string) => Promise<string | null>
|
||||
// ── Belege (SparkReceipt-Import; Verwaltung ordnet zu) ──
|
||||
setBelegZugeordnet: (id: string, zugeordnet: boolean) => Promise<string | null>
|
||||
/** Beleg endgültig löschen (Verwaltung, v1.33) */
|
||||
deleteBeleg: (id: string) => Promise<string | null>
|
||||
// ── 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 {
|
||||
|
||||
Reference in New Issue
Block a user