Files
Mieterportal/src/pages/Payments.tsx
T
ah-admin fbe033ebd6 v1.1: Rebrand Hinterstein Immobilien + echtes Auth-System
- Rebrand: Kunden-Logo, Joseph-Hinterstein-Portrait, Kontakt-Banner, Texte
- Login-Pflicht auf /login (Route-Guard), Gruppen Verwaltung/Mieter
- Mieter-Registrierung per einmaligem Registrierungscode
- Benutzerverwaltung als 6. Modul (/benutzer, nur Verwaltung)
- Demo-Zugänge mit Passwort 0815 (j.hinterstein, a.berger, m.wolf)
- Mail-Server-API: Token-Auth (/send, X-Api-Token), Mail-Stack live auf Server
- Store-Key hinterstein-mieterportal-v2, Doku (kimi.md, README) nachgeführt
2026-10-02 12:11:43 +02:00

238 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo, useState } from 'react'
import {
BadgeEuro,
CalendarDays,
CircleCheck,
CircleAlert,
Clock,
Download,
ReceiptText,
} from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { useStore, fmtEUR } from '../store/store'
const statusStyle: Record<string, string> = {
bezahlt: 'bg-emerald-100 text-emerald-800',
offen: 'bg-amber-100 text-amber-800',
'überfällig': 'bg-red-100 text-red-700',
}
function TenantPayments() {
const { meineEinheit: demoUnit, transactions } = useStore()
const myTx = useMemo(
() =>
transactions
.filter((t) => t.unitId === demoUnit.id)
.slice()
.reverse(),
[transactions, demoUnit],
)
const [filter, setFilter] = useState('alle')
const rows = filter === 'alle' ? myTx : myTx.filter((t) => t.status === filter)
const openSum = myTx.filter((t) => t.status !== 'bezahlt').reduce((s, t) => s + t.amount, 0)
const nk2025 = 86.4 // Guthaben laut Abrechnung
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">Mietkonto & Zahlungen</h1>
<p className="text-sm text-slate-500">
{demoUnit.building} · Whg. {demoUnit.number} – alle Buchungen auf einen Blick
</p>
</div>
<div className="grid gap-4 sm:grid-cols-3">
<div className="rounded-2xl border bg-white p-5 shadow-sm">
<div className="mb-2 flex items-center gap-2 text-sm text-slate-500">
<CalendarDays className="h-4 w-4" /> Fällig am 03.10.2026
</div>
<div className="text-xl font-bold">{fmtEUR.format(demoUnit.warmRent)}</div>
<div className="mt-0.5 text-xs text-slate-500">Warmmiete Oktober (überwiesen ausstehend)</div>
</div>
<div className="rounded-2xl border bg-white p-5 shadow-sm">
<div className="mb-2 flex items-center gap-2 text-sm text-slate-500">
<Clock className="h-4 w-4" /> Offen gesamt
</div>
<div className={`text-xl font-bold ${openSum > 0 ? 'text-amber-600' : ''}`}>
{fmtEUR.format(openSum)}
</div>
<div className="mt-0.5 text-xs text-slate-500">{myTx.filter((t) => t.status !== 'bezahlt').length} offene Posten</div>
</div>
<div className="rounded-2xl border bg-white p-5 shadow-sm">
<div className="mb-2 flex items-center gap-2 text-sm text-slate-500">
<ReceiptText className="h-4 w-4" /> Nebenkosten 2025
</div>
<div className="text-xl font-bold text-emerald-600">+ {fmtEUR.format(nk2025)} Guthaben</div>
<div className="mt-0.5 text-xs text-slate-500">Ausgeglichen mit der Oktober-Miete</div>
</div>
</div>
<div className="rounded-2xl border bg-white p-5 shadow-sm">
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<h2 className="font-semibold">Buchungsverlauf 2026</h2>
<Tabs value={filter} onValueChange={setFilter}>
<TabsList>
<TabsTrigger value="alle">Alle</TabsTrigger>
<TabsTrigger value="bezahlt">Bezahlt</TabsTrigger>
<TabsTrigger value="offen">Offen</TabsTrigger>
<TabsTrigger value="überfällig">Überfällig</TabsTrigger>
</TabsList>
</Tabs>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Monat</TableHead>
<TableHead>Vorgang</TableHead>
<TableHead>Betrag</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Beleg</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.map((t) => (
<TableRow key={t.id}>
<TableCell className="font-medium">{t.month} {t.year}</TableCell>
<TableCell>{t.type}</TableCell>
<TableCell>{fmtEUR.format(t.amount)}</TableCell>
<TableCell>
<Badge className={statusStyle[t.status]} variant="secondary">
{t.status === 'bezahlt' && <CircleCheck className="mr-1 h-3 w-3" />}
{t.status === 'überfällig' && <CircleAlert className="mr-1 h-3 w-3" />}
{t.status}
</Badge>
</TableCell>
<TableCell className="text-right">
{t.status === 'bezahlt' && (
<button className="inline-flex items-center gap-1 text-xs font-medium text-teal-700 hover:underline">
<Download className="h-3.5 w-3.5" /> PDF
</button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="flex flex-wrap items-center gap-3 rounded-2xl border border-teal-200 bg-teal-50 p-5 text-sm text-teal-900">
<BadgeEuro className="h-5 w-5 shrink-0" />
<div>
<span className="font-semibold">Zahlungsdaten:</span> IBAN DE44 5001 0517 5407 3249 31 ·
Verwendungszweck: „Miete {demoUnit.number}/HS 10-2026“ · Zahlungsziel jeweils bis zum 3. des Monats.
</div>
</div>
</div>
)
}
function AdminPayments() {
const { units, transactions } = useStore()
const [building, setBuilding] = useState('alle')
const list = units
.filter((u) => u.status === 'vermietet')
.filter((u) => building === 'alle' || u.building === building)
.map((u) => {
const tx = transactions.filter((t) => t.unitId === u.id)
const overdue = tx.filter((t) => t.status === 'überfällig')
const lastPaid = tx.filter((t) => t.status === 'bezahlt').slice(-1)[0]
return { unit: u, overdue, lastPaid }
})
const totals = {
soll: list.reduce((s, r) => s + r.unit.warmRent, 0),
rueckstand: list.reduce((s, r) => s + r.overdue.reduce((a, t) => a + t.amount, 0), 0),
}
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">Mietkonto & Zahlungen</h1>
<p className="text-sm text-slate-500">Forderungsmanagement für 50 Wohnungen</p>
</div>
<Select value={building} onValueChange={setBuilding}>
<SelectTrigger className="w-56">
<SelectValue placeholder="Haus wählen" />
</SelectTrigger>
<SelectContent>
<SelectItem value="alle">Alle Häuser</SelectItem>
<SelectItem value="Hauptstraße 12">Hauptstraße 12</SelectItem>
<SelectItem value="Ringweg 3">Ringweg 3</SelectItem>
<SelectItem value="Am Park 5">Am Park 5</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="rounded-2xl border bg-white p-5 shadow-sm">
<div className="text-sm text-slate-500">Monatlicher Miet-Soll</div>
<div className="mt-1 text-2xl font-bold">{fmtEUR.format(totals.soll)}</div>
</div>
<div className="rounded-2xl border bg-white p-5 shadow-sm">
<div className="text-sm text-slate-500">Gesamtrückstand (überfällig)</div>
<div className={`mt-1 text-2xl font-bold ${totals.rueckstand > 0 ? 'text-red-600' : ''}`}>
{fmtEUR.format(totals.rueckstand)}
</div>
</div>
</div>
<div className="rounded-2xl border bg-white p-5 shadow-sm">
<h2 className="mb-4 font-semibold">Zahlungsstatus der Mieter</h2>
<Table>
<TableHeader>
<TableRow>
<TableHead>Wohnung</TableHead>
<TableHead>Mieter</TableHead>
<TableHead>Warmmiete</TableHead>
<TableHead>Letzte Zahlung</TableHead>
<TableHead>Rückstand</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{list.map(({ unit, overdue, lastPaid }) => (
<TableRow key={unit.id}>
<TableCell className="font-medium">
{unit.building}, Whg. {unit.number}
</TableCell>
<TableCell>{unit.tenant}</TableCell>
<TableCell>{fmtEUR.format(unit.warmRent)}</TableCell>
<TableCell>{lastPaid ? `${lastPaid.month} (${lastPaid.paidAt})` : '–'}</TableCell>
<TableCell className={overdue.length ? 'font-semibold text-red-600' : 'text-slate-400'}>
{overdue.length ? fmtEUR.format(overdue.reduce((a, t) => a + t.amount, 0)) : '–'}
</TableCell>
<TableCell>
<Badge
className={overdue.length ? statusStyle['überfällig'] : statusStyle['bezahlt']}
variant="secondary"
>
{overdue.length ? 'Mahnung prüfen' : 'im Soll'}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)
}
export default function Payments() {
const { role } = useStore()
return role === 'tenant' ? <TenantPayments /> : <AdminPayments />
}