Files
property-match/src/pages/supply/SupplyDashboard.tsx
T
Benjamin Sutter cc5582b7fd refactor: dritte Bereinigungsrunde Property On
Kopfzeile: Der zusaetzliche Titelbalken mit dem Chip Verwaltung und dem
wiederholten Seitennamen entfaellt auf allen Hauptseiten, ebenso der
Einstieg AI Assistent. Ohne Trennlinie geht die Zeile bruchlos in den
Seiteninhalt ueber; Glocke und Profil bleiben rechts stehen. Der
Pilotkunde heisst sichtbar Beispiel Immobilien AG, die technische Kennung
org-wincasa bleibt unangetastet.

Flaechensuche und Anfragencenter sind aus Navigation, Routing und Branch
entfernt. Verweise, die dadurch ins Leere zeigten, haengen neu am Match
Center; der Quicklink ins Anfragencenter im Vorgangsdetail ist entfallen.

Meine Objekte: feste vier Karten je Zeile auf Desktop, zwei auf Tablet,
eine auf Mobil. Die Dichteauswahl 3/5/10 und der Datenpflege-Hinweis sind
weg; die Datenpflege bleibt als eigener Hauptreiter erreichbar.

Objektbilder liegen erstmals lokal. Zuvor zeigten 98 Objekte auf Unsplash
und teilten sich 29 Fotos, verschiedene Adressen trugen also dasselbe
Bild. Neu traegt jede Objekt-ID genau eine eigene Datei; die Zuordnung
laeuft ueber die stabile ID, nicht ueber den Titel. Die Dateien sind
erzeugte Vektorgrafiken, keine Fotografien - echte Aufnahmen ersetzen sie
spaeter unter demselben Dateinamen ohne Codeaenderung. Vite bettet sie
nicht mehr als Base64 ein, damit alle Objekte gleich behandelt sind.

Teamuebersicht zeigt nur noch das Kernteam; die Stufenfilter sind
entfallen und die aeusseren Ringe werden nicht mehr gezeichnet statt nur
durchsichtig zu sein - unsichtbare, aber anspringbare Schaltflaechen sind
eine Falle. Die Nabe nennt daher die Zahl der gezeigten Mitarbeitenden
statt der Gesamtzahl. Die Personalverwaltung hat nur noch die
Dossieransicht, der Umschalter und das drehbare Organigramm sind weg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 10:37:36 +02:00

201 lines
9.2 KiB
TypeScript

import { Box, Button, Chip, Divider, Paper, Typography } from '@mui/material'
import { ArrowRight, Building2, TrendingUp, Zap } from 'lucide-react'
import { useNavigate } from 'react-router'
import { useSupplyDashboard } from '../../hooks/useSupplyDashboard'
import { useSessionStore } from '../../stores/sessionStore'
import { UserRole } from '../../domain/enums'
import { DashboardHeader, ReviewTaskWidget, DashboardSkeleton } from '../../components/supply'
import { DS_BG, DS_BORDER, DS_SURFACE, DS_TEXT } from '../../lib/ds'
export default function SupplyDashboard() {
const { data, isLoading, isError, refetch } = useSupplyDashboard()
const { currentUser } = useSessionStore()
const navigate = useNavigate()
const role = currentUser?.role ?? UserRole.DEMAND_USER
const isReviewer = role === UserRole.REVIEWER
if (isLoading) return <DashboardSkeleton />
if (isError) {
return (
<Box sx={{ p: 6, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}>
<Typography variant="h6" color="error">Dashboard konnte nicht geladen werden</Typography>
<Typography variant="body2" color="text.secondary">Der Service ist vorübergehend nicht verfügbar.</Typography>
<Button variant="outlined" onClick={() => refetch()}>Erneut versuchen</Button>
</Box>
)
}
if (!data || data.totalProperties === 0) {
return (
<Box sx={{ p: 6, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}>
<Typography variant="h6">Noch keine Objekte vorhanden</Typography>
<Typography variant="body2" color="text.secondary">
Fügen Sie Ihr erstes Objekt hinzu wir matchen es sofort mit aktiven Suchanfragen.
</Typography>
<Button variant="contained" onClick={() => navigate('/supply/properties')}>
Erstes Objekt erfassen
</Button>
</Box>
)
}
const strongMatchCount = data.strongMatchCount ?? 0
const topMatches = data.strongMatches ?? []
const futureSignalCount = data.futureSignals?.total ?? 0
return (
<Box sx={{ p: 3, display: 'flex', flexDirection: 'column', gap: 3 }}>
<DashboardHeader orgName={currentUser?.organizationName} lastUpdated={data.lastUpdated} />
{/* ── Hero: Nachfrage-Intelligence ── */}
<Paper sx={{ p: 0, overflow: 'hidden', border: `1px solid ${DS_BORDER.default}` }}>
{/* KPI strip */}
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr 1fr', md: 'repeat(3, 1fr)' } }}>
{/* Starke Matches */}
<Box sx={{ p: 2.5, borderRight: `1px solid ${DS_BORDER.default}`, bgcolor: strongMatchCount > 0 ? DS_SURFACE.success.bg : 'white' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 0.5 }}>
<TrendingUp size={15} color={strongMatchCount > 0 ? DS_TEXT.success : DS_TEXT.muted} />
<Typography variant="overline" sx={{ color: strongMatchCount > 0 ? DS_TEXT.success : DS_TEXT.muted, fontWeight: 700, lineHeight: 1 }}>
Starke Match-Anfragen
</Typography>
</Box>
<Typography variant="h3" sx={{ fontWeight: 800, color: strongMatchCount > 0 ? DS_TEXT.success : DS_TEXT.disabled, lineHeight: 1 }}>
{strongMatchCount}
</Typography>
<Typography variant="caption" sx={{ color: DS_TEXT.muted }}>
qualifizierte Interessenten 80%
</Typography>
</Box>
{/* Aktive Objekte */}
<Box sx={{ p: 2.5, borderRight: `1px solid ${DS_BORDER.default}`, cursor: 'pointer', '&:hover': { bgcolor: DS_BG.subtle } }}
onClick={() => navigate('/supply/properties')}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 0.5 }}>
<Building2 size={15} color={DS_TEXT.muted} />
<Typography variant="overline" sx={{ color: DS_TEXT.muted, fontWeight: 700, lineHeight: 1 }}>
Aktive Objekte
</Typography>
</Box>
<Typography variant="h3" sx={{ fontWeight: 800, color: DS_TEXT.primary, lineHeight: 1 }}>
{data.activeProperties}
<Typography component="span" variant="h5" sx={{ color: DS_TEXT.muted, fontWeight: 400, ml: 0.5 }}>
/ {data.totalProperties}
</Typography>
</Typography>
<Typography variant="caption" sx={{ color: DS_TEXT.muted }}>verfügbar · im Bestand</Typography>
</Box>
{/* Zukunftssignale */}
<Box sx={{ p: 2.5, bgcolor: futureSignalCount > 0 ? '#faf5ff' : 'white' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 0.5 }}>
<Zap size={15} color={futureSignalCount > 0 ? '#7c3aed' : DS_TEXT.muted} />
<Typography variant="overline" sx={{ color: futureSignalCount > 0 ? '#7c3aed' : DS_TEXT.muted, fontWeight: 700, lineHeight: 1 }}>
Zukunftssignale
</Typography>
</Box>
<Typography variant="h3" sx={{ fontWeight: 800, color: futureSignalCount > 0 ? '#7c3aed' : DS_TEXT.disabled, lineHeight: 1 }}>
{futureSignalCount}
</Typography>
<Typography variant="caption" sx={{ color: DS_TEXT.muted }}>
vorqualifizierte Nachfrage-Signale
</Typography>
</Box>
</Box>
{/* ── Top matches list ── */}
{topMatches.length > 0 && (
<>
<Divider />
<Box sx={{ px: 2.5, py: 1.5 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: DS_TEXT.muted, textTransform: 'uppercase', letterSpacing: 0.6 }}>
Stärkste Interessenten
</Typography>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
{topMatches.slice(0, 4).map((m, i) => (
<Box
key={m.matchId}
sx={{
display: 'flex', alignItems: 'center', gap: 2,
px: 2.5, py: 1.25,
borderTop: i > 0 ? `1px solid ${DS_BORDER.muted}` : undefined,
cursor: 'pointer',
'&:hover': { bgcolor: DS_BG.subtle },
}}
onClick={() => navigate('/supply/match-center')}
>
<Box sx={{
minWidth: 44, height: 36, borderRadius: 1,
bgcolor: m.matchScore >= 80 ? DS_SURFACE.success.bg : DS_SURFACE.warning.bg,
border: `1px solid ${m.matchScore >= 80 ? DS_SURFACE.success.border : DS_SURFACE.warning.border}`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<Typography sx={{ fontWeight: 800, fontSize: '0.9rem', color: m.matchScore >= 80 ? DS_TEXT.success : DS_TEXT.warning }}>
{m.matchScore}%
</Typography>
</Box>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }} noWrap>{m.propertyTitle}</Typography>
<Typography variant="caption" color="text.secondary" noWrap>{m.topReason}</Typography>
</Box>
<Chip
label={m.nextBestAction}
size="small"
sx={{ fontSize: '0.65rem', height: 20, bgcolor: DS_BG.subtle, color: DS_TEXT.secondary, flexShrink: 0, maxWidth: 140 }}
/>
<ArrowRight size={14} color={DS_TEXT.disabled} style={{ flexShrink: 0 }} />
</Box>
))}
</Box>
<Divider />
<Box sx={{ px: 2.5, py: 1.25 }}>
<Button
size="small"
variant="text"
onClick={() => navigate('/supply/match-center')}
sx={{ textTransform: 'none', color: DS_TEXT.secondary, fontWeight: 500 }}
>
Alle Matches anzeigen
</Button>
</Box>
</>
)}
</Paper>
{/* ── Datenpflege (sekundär, collapsed) ── */}
{(data.dataQuality?.critical ?? 0) > 0 && (
<Paper sx={{ p: 2, border: `1px solid ${DS_BORDER.default}`, bgcolor: DS_BG.subtle }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, color: DS_TEXT.secondary }}>
{data.dataQuality!.critical} Objekte mit Datenlücken
</Typography>
<Typography variant="caption" color="text.secondary">
Vollständige Daten verbessern die Match-Qualität erheblich.
</Typography>
</Box>
<Button
size="small"
variant="outlined"
onClick={() => navigate('/supply/properties')}
sx={{ textTransform: 'none', flexShrink: 0 }}
>
Datenpflege
</Button>
</Box>
</Paper>
)}
{isReviewer && data.reviewTasks !== null && (
<ReviewTaskWidget
tasks={data.reviewTasks}
onNavigate={() => navigate('/ops/review-queue')}
/>
)}
</Box>
)
}