v1.14: Steuermodul - umlegbare und nicht umlegbare Kosten nach Haus und Wohnung
This commit is contained in:
@@ -9,6 +9,7 @@ import Tickets from './pages/Tickets'
|
||||
import Documents from './pages/Documents'
|
||||
import Messages from './pages/Messages'
|
||||
import Zaehler from './pages/Zaehler'
|
||||
import Kosten from './pages/Kosten'
|
||||
import Bestand from './pages/Bestand'
|
||||
import BenutzerVerwaltung from './pages/BenutzerVerwaltung'
|
||||
|
||||
@@ -83,6 +84,7 @@ export default function App() {
|
||||
<Route path="/mietkonto" element={<Payments />} />
|
||||
<Route path="/schadensmeldungen" element={<Tickets />} />
|
||||
<Route path="/zaehler" element={<Zaehler />} />
|
||||
<Route path="/kosten" element={<Kosten />} />
|
||||
<Route path="/bestand" element={<Bestand />} />
|
||||
<Route path="/dokumente" element={<Documents />} />
|
||||
<Route path="/nachrichten" element={<Messages />} />
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
LogOut,
|
||||
Building2,
|
||||
Gauge,
|
||||
Calculator,
|
||||
} from 'lucide-react'
|
||||
import { useStore } from '../store/store'
|
||||
import ThemeToggle from './ThemeToggle'
|
||||
@@ -19,6 +20,7 @@ const navItems = [
|
||||
{ to: '/mietkonto', label: 'Mietkonto & Zahlungen', icon: Wallet },
|
||||
{ to: '/schadensmeldungen', label: 'Schadensmeldungen', icon: Wrench },
|
||||
{ to: '/zaehler', label: 'Zählerstände', icon: Gauge },
|
||||
{ to: '/kosten', label: 'Steuer & Kosten', icon: Calculator },
|
||||
{ to: '/dokumente', label: 'Dokumente', icon: FileText },
|
||||
{ to: '/nachrichten', label: 'Nachrichten', icon: MessagesSquare },
|
||||
]
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Calculator, Receipt, ReceiptText, PiggyBank, Plus, Pencil, Trash2, Coins } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { useStore, fmtEUR } from '../store/store'
|
||||
import type { KostenItem } from '../types'
|
||||
|
||||
const KOSTEN_ARTEN = [
|
||||
'Wasser/Abwasser', 'Müllabfuhr', 'Straßenreinigung/Winterdienst', 'Gartenpflege',
|
||||
'Hausreinigung', 'Hauswart', 'Aufzug', 'Versicherung', 'Grundsteuer',
|
||||
'Heizung/Warmwasser', 'Kabelanschluss', 'Verwaltung', 'Instandhaltung',
|
||||
'Mietausfall', 'Sonstiges',
|
||||
]
|
||||
|
||||
/** Komma oder Punkt als Dezimaltrenner akzeptieren. */
|
||||
const zahl = (s: string) => Number(s.replace(',', '.'))
|
||||
|
||||
function KostenDialog({ position }: { position?: KostenItem }) {
|
||||
const { addKosten, updateKosten, units, buildings } = useStore()
|
||||
const bearbeiten = Boolean(position)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [kostenart, setKostenart] = useState(position?.kostenart ?? 'Wasser/Abwasser')
|
||||
const [jahr, setJahr] = useState(String(position?.jahr ?? new Date().getFullYear()))
|
||||
const [betrag, setBetrag] = useState(position ? String(position.betrag).replace('.', ',') : '')
|
||||
const [einheit, setEinheit] = useState(position?.unitId ?? '__haus') // '__haus' = ganzes Haus
|
||||
const [haus, setHaus] = useState(position?.building ?? buildings[0]?.name ?? '')
|
||||
const [umlegbar, setUmlegbar] = useState(position?.umlegbar ?? true)
|
||||
const [notiz, setNotiz] = useState(position?.notiz ?? '')
|
||||
const [sende, setSende] = useState(false)
|
||||
const [fehler, setFehler] = useState<string | null>(null)
|
||||
|
||||
const submit = async () => {
|
||||
if (sende) return
|
||||
setSende(true)
|
||||
setFehler(null)
|
||||
const payload = {
|
||||
kostenart,
|
||||
jahr: zahl(jahr),
|
||||
betrag: zahl(betrag),
|
||||
unitId: einheit === '__haus' ? null : einheit,
|
||||
building: haus,
|
||||
umlegbar,
|
||||
notiz: notiz.trim(),
|
||||
}
|
||||
const err = bearbeiten ? await updateKosten(position!.id, payload) : await addKosten(payload)
|
||||
setSende(false)
|
||||
if (err) return setFehler(err)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{bearbeiten ? (
|
||||
<Button variant="outline" size="sm" className="gap-1.5">
|
||||
<Pencil className="h-3.5 w-3.5" /> Bearbeiten
|
||||
</Button>
|
||||
) : (
|
||||
<Button className="gap-2">
|
||||
<Plus className="h-4 w-4" /> Neue Kostenposition
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{bearbeiten ? 'Kostenposition bearbeiten' : 'Neue Kostenposition erfassen'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Kostenart</Label>
|
||||
<Select value={kostenart} onValueChange={setKostenart}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{KOSTEN_ARTEN.map((k) => (
|
||||
<SelectItem key={k} value={k}>
|
||||
{k}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>Umlegbarkeit</Label>
|
||||
<Select value={umlegbar ? 'umlegbar' : 'nicht'} onValueChange={(v) => setUmlegbar(v === 'umlegbar')}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="umlegbar">Umlegbar auf Mieter</SelectItem>
|
||||
<SelectItem value="nicht">Nicht umlegbar (Eigentümer)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="k-jahr">Jahr</Label>
|
||||
<Input id="k-jahr" inputMode="numeric" placeholder="z. B. 2026" value={jahr} onChange={(e) => setJahr(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="k-betrag">Betrag (€)</Label>
|
||||
<Input id="k-betrag" inputMode="decimal" placeholder="z. B. 1.250,00" value={betrag} onChange={(e) => setBetrag(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Haus</Label>
|
||||
<Select value={haus} onValueChange={setHaus} disabled={einheit !== '__haus'}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Haus wählen" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{buildings.map((b) => (
|
||||
<SelectItem key={b.name} value={b.name}>
|
||||
{b.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{einheit !== '__haus' && (
|
||||
<p className="text-xs text-slate-400">Das Haus folgt automatisch der gewählten Wohnung.</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>Wohnung</Label>
|
||||
<Select value={einheit} onValueChange={setEinheit}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Ganzes Haus" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__haus">Ganzes Haus</SelectItem>
|
||||
{units.map((u) => (
|
||||
<SelectItem key={u.id} value={u.id}>
|
||||
{u.building}, Whg. {u.number}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="k-notiz">Notiz (optional)</Label>
|
||||
<Input id="k-notiz" placeholder="z. B. Rechnungs-Nr., Abrechnungszeitraum …" value={notiz} onChange={(e) => setNotiz(e.target.value)} />
|
||||
</div>
|
||||
{fehler && <p className="text-sm font-medium text-red-600">{fehler}</p>}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button onClick={submit} disabled={sende || !betrag.trim() || !jahr.trim()}>
|
||||
{sende ? 'Speichere …' : bearbeiten ? 'Speichern' : 'Erfassen'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Kosten() {
|
||||
const { kosten, role, units, buildings, deleteKosten } = useStore()
|
||||
const [jahrFilter, setJahrFilter] = useState('alle')
|
||||
const [hausFilter, setHausFilter] = useState('alle')
|
||||
const [artFilter, setArtFilter] = useState<'alle' | 'umlegbar' | 'nicht'>('alle')
|
||||
|
||||
const jahre = useMemo(
|
||||
() => Array.from(new Set(kosten.map((k) => k.jahr))).sort((a, b) => b - a),
|
||||
[kosten],
|
||||
)
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return kosten.filter(
|
||||
(k) =>
|
||||
(jahrFilter === 'alle' || k.jahr === Number(jahrFilter)) &&
|
||||
(hausFilter === 'alle' || k.building === hausFilter) &&
|
||||
(artFilter === 'alle' || (artFilter === 'umlegbar' ? k.umlegbar : !k.umlegbar)),
|
||||
)
|
||||
}, [kosten, jahrFilter, hausFilter, artFilter])
|
||||
|
||||
const summeUmlegbar = filtered.filter((k) => k.umlegbar).reduce((s, k) => s + k.betrag, 0)
|
||||
const summeNicht = filtered.filter((k) => !k.umlegbar).reduce((s, k) => s + k.betrag, 0)
|
||||
|
||||
const loeschen = async (k: KostenItem) => {
|
||||
if (!window.confirm(`${k.kostenart} ${k.jahr} (${fmtEUR.format(k.betrag)}) wirklich löschen?`)) return
|
||||
await deleteKosten(k.id)
|
||||
}
|
||||
|
||||
const einheitLabel = (k: KostenItem) =>
|
||||
k.unitId ? (() => {
|
||||
const u = units.find((x) => x.id === k.unitId)
|
||||
return u ? `Whg. ${u.number}` : k.unitId
|
||||
})() : 'Ganzes Haus'
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">Steuer & Kosten</h1>
|
||||
<p className="text-sm text-slate-500">
|
||||
{role === 'tenant'
|
||||
? 'Umlegbare und nicht umlegbare Kosten Ihrer Wohnung'
|
||||
: 'Umlegbare und nicht umlegbare Kosten nach Haus und Wohnung – Grundlage für die Nebenkostenabrechnung'}
|
||||
</p>
|
||||
</div>
|
||||
{role === 'admin' && <KostenDialog />}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<div className="rounded-2xl border bg-white p-5 shadow-sm">
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500">
|
||||
<Receipt className="h-4 w-4 text-teal-600" /> Umlegbar (Filterauswahl)
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-bold text-teal-700">{fmtEUR.format(summeUmlegbar)}</div>
|
||||
<div className="text-xs text-slate-400">{filtered.filter((k) => k.umlegbar).length} Positionen</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border bg-white p-5 shadow-sm">
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500">
|
||||
<PiggyBank className="h-4 w-4 text-slate-500" /> Nicht umlegbar (Eigentümer)
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-bold text-slate-700">{fmtEUR.format(summeNicht)}</div>
|
||||
<div className="text-xs text-slate-400">{filtered.filter((k) => !k.umlegbar).length} Positionen</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border bg-white p-5 shadow-sm">
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500">
|
||||
<Coins className="h-4 w-4 text-amber-500" /> Gesamt
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-bold">{fmtEUR.format(summeUmlegbar + summeNicht)}</div>
|
||||
<div className="text-xs text-slate-400">{filtered.length} Positionen gesamt</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(['alle', 'umlegbar', 'nicht'] as const).map((a) => (
|
||||
<button
|
||||
key={a}
|
||||
onClick={() => setArtFilter(a)}
|
||||
className={`rounded-full px-4 py-1.5 text-sm font-medium transition ${
|
||||
artFilter === a
|
||||
? 'bg-teal-600 text-white'
|
||||
: 'bg-white text-slate-600 ring-1 ring-slate-200 hover:ring-teal-400'
|
||||
}`}
|
||||
>
|
||||
{a === 'alle' ? 'Alle' : a === 'umlegbar' ? 'Umlegbar' : 'Nicht umlegbar'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{role === 'admin' && (
|
||||
<>
|
||||
<Select value={jahrFilter} onValueChange={setJahrFilter}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="alle">Alle Jahre</SelectItem>
|
||||
{jahre.map((j) => (
|
||||
<SelectItem key={j} value={String(j)}>
|
||||
{j}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={hausFilter} onValueChange={setHausFilter}>
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="alle">Alle Häuser</SelectItem>
|
||||
{buildings.map((b) => (
|
||||
<SelectItem key={b.name} value={b.name}>
|
||||
{b.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border bg-white p-5 shadow-sm">
|
||||
{filtered.length === 0 ? (
|
||||
<div className="py-10 text-center text-sm text-slate-500">
|
||||
<Calculator className="mx-auto mb-3 h-8 w-8 text-slate-300" />
|
||||
Keine Kostenpositionen gefunden.
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Kostenart</TableHead>
|
||||
{role === 'admin' && <TableHead>Haus</TableHead>}
|
||||
<TableHead>Wohnung</TableHead>
|
||||
<TableHead>Jahr</TableHead>
|
||||
<TableHead>Umlegbarkeit</TableHead>
|
||||
<TableHead className="text-right">Betrag</TableHead>
|
||||
{role === 'admin' && <TableHead className="text-right">Aktion</TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filtered.map((k) => (
|
||||
<TableRow key={k.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<ReceiptText className="h-4 w-4 shrink-0 text-teal-600" />
|
||||
<div>
|
||||
{k.kostenart}
|
||||
{k.notiz && <div className="text-xs font-normal text-slate-400">{k.notiz}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
{role === 'admin' && <TableCell className="text-slate-500">{k.building}</TableCell>}
|
||||
<TableCell className="text-slate-500">{einheitLabel(k)}</TableCell>
|
||||
<TableCell>{k.jahr}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={k.umlegbar ? 'bg-teal-50 text-teal-700' : 'bg-slate-100 text-slate-600'}
|
||||
>
|
||||
{k.umlegbar ? 'Umlegbar' : 'Nicht umlegbar'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-semibold">{fmtEUR.format(k.betrag)}</TableCell>
|
||||
{role === 'admin' && (
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<KostenDialog position={k} />
|
||||
<Button variant="outline" size="sm" className="gap-1.5 text-red-600 hover:text-red-700" onClick={() => loeschen(k)}>
|
||||
<Trash2 className="h-3.5 w-3.5" /> Löschen
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-400">
|
||||
Umlegbare Kosten werden nach den Vorgaben der Betriebskostenverordnung auf die Mieter
|
||||
umgelegt; nicht umlegbare Positionen trägt die Eigentümerseite. Erfasst von der
|
||||
Verwaltung, Grundlage der jährlichen Nebenkostenabrechnung.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
Meter,
|
||||
ZaehlerArt,
|
||||
Building,
|
||||
KostenItem,
|
||||
} from '../types'
|
||||
import { apiGet, apiPost, apiPatch, apiDelete, ApiFehler } from '../lib/api'
|
||||
import { sha256, DEMO_PASSWORT } from '../lib/auth'
|
||||
@@ -51,6 +52,7 @@ interface Bootstrap {
|
||||
codes: RegistrierungsCode[]
|
||||
meters: Meter[]
|
||||
buildings: Building[]
|
||||
kosten: KostenItem[]
|
||||
}
|
||||
|
||||
const LEER: Bootstrap = {
|
||||
@@ -64,6 +66,7 @@ const LEER: Bootstrap = {
|
||||
codes: [],
|
||||
meters: [],
|
||||
buildings: [],
|
||||
kosten: [],
|
||||
}
|
||||
|
||||
interface Store extends Bootstrap {
|
||||
@@ -139,6 +142,21 @@ interface Store extends Bootstrap {
|
||||
deleteDokument: (id: string) => Promise<string | null>
|
||||
/** Ersetzt ein Dokument im State durch die Server-Fassung (z. B. nach PDF-Upload). */
|
||||
syncDokument: (dokument: DocumentItem) => void
|
||||
// ── Steuer & Kosten (Verwaltung pflegt, Mieter lesen eigene Wohnung) ──
|
||||
addKosten: (k: {
|
||||
building: string
|
||||
unitId: string | null
|
||||
jahr: number
|
||||
kostenart: string
|
||||
betrag: number
|
||||
umlegbar: boolean
|
||||
notiz: string
|
||||
}) => Promise<string | null>
|
||||
updateKosten: (
|
||||
id: string,
|
||||
k: Partial<{ building: string; unitId: string | null; jahr: number; kostenart: string; betrag: number; umlegbar: boolean; notiz: string }>,
|
||||
) => Promise<string | null>
|
||||
deleteKosten: (id: string) => Promise<string | null>
|
||||
}
|
||||
|
||||
const StoreContext = createContext<Store | null>(null)
|
||||
@@ -399,6 +417,34 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
||||
: [dokument, ...x.documents],
|
||||
}))
|
||||
},
|
||||
// ── Steuer & Kosten ─────────────────────────────────────
|
||||
async addKosten(k) {
|
||||
try {
|
||||
const { kosten: position } = await apiPost<{ kosten: KostenItem }>('/api/costs', k)
|
||||
setDaten((x) => ({ ...x, kosten: [position, ...x.kosten] }))
|
||||
return null
|
||||
} catch (e) {
|
||||
return fehlerNachricht(e)
|
||||
}
|
||||
},
|
||||
async updateKosten(id, k) {
|
||||
try {
|
||||
const { kosten: position } = await apiPatch<{ kosten: KostenItem }>(`/api/costs/${id}`, k)
|
||||
setDaten((x) => ({ ...x, kosten: x.kosten.map((m) => (m.id === id ? position : m)) }))
|
||||
return null
|
||||
} catch (e) {
|
||||
return fehlerNachricht(e)
|
||||
}
|
||||
},
|
||||
async deleteKosten(id) {
|
||||
try {
|
||||
await apiDelete(`/api/costs/${id}`)
|
||||
setDaten((x) => ({ ...x, kosten: x.kosten.filter((m) => m.id !== id) }))
|
||||
return null
|
||||
} catch (e) {
|
||||
return fehlerNachricht(e)
|
||||
}
|
||||
},
|
||||
}), [daten, bereit, me, role, meineEinheit, ladeAlles])
|
||||
|
||||
return <StoreContext.Provider value={value}>{children}</StoreContext.Provider>
|
||||
|
||||
@@ -120,3 +120,16 @@ export interface Meter {
|
||||
standAm: string
|
||||
geaendertAm: string
|
||||
}
|
||||
|
||||
/** Kostenposition des Steuermoduls (umlegbar / nicht umlegbar). */
|
||||
export interface KostenItem {
|
||||
id: string
|
||||
building: string // Haus
|
||||
unitId: string | null // null = hausweite Kostenposition
|
||||
jahr: number
|
||||
kostenart: string
|
||||
betrag: number // €
|
||||
umlegbar: boolean
|
||||
notiz: string
|
||||
erfasstAm: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user