Hocks Mieterportal: 5 Module (Dashboard, Mietkonto, Schadensmeldungen, Dokumente, Nachrichten), Docker-Setup, eigenes Branding

This commit is contained in:
2026-10-02 10:02:19 +02:00
commit 38d5099a2c
91 changed files with 17062 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
import { useMemo, useState } from 'react'
import { Download, FileText, FolderOpen, Scale, ReceiptText, FileSignature, FileIcon } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { useStore, DEMO_UNIT_ID } from '../store/store'
const catIcon = (cat: string) => {
switch (cat) {
case 'Mietvertrag':
return <FileSignature className="h-4.5 w-4.5" />
case 'Nebenkostenabrechnung':
return <ReceiptText className="h-4.5 w-4.5" />
case 'Hausordnung':
return <Scale className="h-4.5 w-4.5" />
case 'Protokoll':
return <FileText className="h-4.5 w-4.5" />
default:
return <FileIcon className="h-4.5 w-4.5" />
}
}
export default function Documents() {
const { documents, role, units } = useStore()
const [query, setQuery] = useState('')
const rows = useMemo(() => {
const base =
role === 'tenant'
? documents.filter((d) => d.unitId === DEMO_UNIT_ID || d.unitId === null)
: documents
const q = query.trim().toLowerCase()
return q
? base.filter(
(d) =>
d.title.toLowerCase().includes(q) ||
d.category.toLowerCase().includes(q) ||
(units.find((u) => u.id === d.unitId)?.building ?? '').toLowerCase().includes(q),
)
: base
}, [documents, role, query, units])
const cats = ['Alle', ...Array.from(new Set(rows.map((d) => d.category)))]
const [cat, setCat] = useState('Alle')
const filtered = cat === 'Alle' ? rows : rows.filter((d) => d.category === cat)
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">Dokumente</h1>
<p className="text-sm text-slate-500">
{role === 'tenant'
? 'Verträge, Abrechnungen und Unterlagen für Ihre Wohnung'
: 'Zentraler Dokumentenspeicher des gesamten Bestands'}
</p>
</div>
<Input
placeholder="Dokument suchen …"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-64"
/>
</div>
<div className="flex flex-wrap gap-2">
{cats.map((c) => (
<button
key={c}
onClick={() => setCat(c)}
className={`rounded-full px-4 py-1.5 text-sm font-medium transition ${
cat === c
? 'bg-teal-600 text-white'
: 'bg-white text-slate-600 ring-1 ring-slate-200 hover:ring-teal-400'
}`}
>
{c}
</button>
))}
</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">
<FolderOpen className="mx-auto mb-3 h-8 w-8 text-slate-300" />
Keine Dokumente gefunden.
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-10" />
<TableHead>Dokument</TableHead>
{role === 'admin' && <TableHead>Wohnung</TableHead>}
<TableHead>Kategorie</TableHead>
<TableHead>Datum</TableHead>
<TableHead>Größe</TableHead>
<TableHead className="text-right">Aktion</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((d) => (
<TableRow key={d.id}>
<TableCell>
<span className="inline-flex rounded-lg bg-teal-50 p-2 text-teal-700">
{catIcon(d.category)}
</span>
</TableCell>
<TableCell className="font-medium">{d.title}</TableCell>
{role === 'admin' && (
<TableCell className="text-slate-500">
{d.unitId
? (() => {
const u = units.find((x) => x.id === d.unitId)
return u ? `${u.building}, Whg. ${u.number}` : d.unitId
})()
: 'Alle Häuser'}
</TableCell>
)}
<TableCell>
<Badge variant="secondary" className="bg-slate-100 text-slate-700">
{d.category}
</Badge>
</TableCell>
<TableCell>{d.date}</TableCell>
<TableCell>{d.size}</TableCell>
<TableCell className="text-right">
<button className="inline-flex items-center gap-1 rounded-lg bg-teal-50 px-3 py-1.5 text-xs font-semibold text-teal-700 transition hover:bg-teal-100">
<Download className="h-3.5 w-3.5" /> PDF
</button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
<p className="text-xs text-slate-400">
Alle Dokumente werden verschlüsselt gespeichert und sind nur für berechtigte Konten
einsehbar. Die Download-Funktion ist in dieser Demo dargestellt.
</p>
</div>
)
}