cc5582b7fd
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>
201 lines
9.2 KiB
TypeScript
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>
|
|
)
|
|
}
|