7b7be0b436
- dataQualityService with getPortfolioQualitySummary() - Service methods: getDashboardPropertiesSummary, getStrongMatches, getSignalSummary, getDashboardTasks added to respective services - dashboardService uses Promise.allSettled for partial error resilience - KpiCard: onClick + Tooltip support - KpiGrid: navigate actions and tooltips per card - StrongMatchMiniCard: Match Center, Prüfung, Vergleichen action buttons - FutureSignalWidget: time horizon distribution (0–6, 6–12, 12–24 Mo.) - SupplyDashboard: DashboardHeader, empty state, error state, partial widget errors, permission-based rendering per UserRole Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
189 lines
6.2 KiB
TypeScript
189 lines
6.2 KiB
TypeScript
import { Box, Alert, Button, Typography } from '@mui/material'
|
|
import { useNavigate } from 'react-router'
|
|
import { useSupplyDashboard } from '../../hooks/useSupplyDashboard'
|
|
import { useSessionStore } from '../../stores/sessionStore'
|
|
import { UserRole } from '../../domain/enums'
|
|
import {
|
|
DashboardHeader,
|
|
KpiGrid,
|
|
StrongMatchOverview,
|
|
DataQualityWidget,
|
|
FutureSignalWidget,
|
|
ReviewTaskWidget,
|
|
QuickActionPanel,
|
|
DashboardSkeleton,
|
|
} from '../../components/supply'
|
|
|
|
function WidgetError({ label }: { label: string }) {
|
|
return (
|
|
<Alert severity="error" sx={{ height: '100%' }}>
|
|
{label} konnte nicht geladen werden.
|
|
</Alert>
|
|
)
|
|
}
|
|
|
|
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
|
|
const isOwnerViewer = role === UserRole.OWNER_VIEWER
|
|
const canSeeMatches = role !== UserRole.OWNER_VIEWER && role !== UserRole.DEMAND_USER
|
|
const canSeeOperations =
|
|
role === UserRole.SUPER_ADMIN ||
|
|
role === UserRole.ORGANIZATION_ADMIN ||
|
|
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 oder laden Sie Demo-Daten.
|
|
</Typography>
|
|
<Button variant="contained" onClick={() => navigate('/supply/properties')}>
|
|
Objekte verwalten
|
|
</Button>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ p: 3, display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
<DashboardHeader
|
|
orgName={currentUser?.organizationName}
|
|
lastUpdated={data.lastUpdated}
|
|
/>
|
|
|
|
{isOwnerViewer ? (
|
|
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 2 }}>
|
|
<Box sx={{ p: 2.5, border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
|
|
<Typography variant="overline" color="text.secondary">
|
|
Aktive Objekte
|
|
</Typography>
|
|
<Typography variant="h4" sx={{ fontWeight: 700 }}>
|
|
{data.activeProperties} / {data.totalProperties}
|
|
</Typography>
|
|
</Box>
|
|
<Box sx={{ p: 2.5, border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
|
|
<Typography variant="overline" color="text.secondary">
|
|
Ø Datenqualität
|
|
</Typography>
|
|
<Typography variant="h4" sx={{ fontWeight: 700 }}>
|
|
{data.avgDataQuality}%
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
) : (
|
|
<KpiGrid data={data} />
|
|
)}
|
|
|
|
{/* REVIEWER: Review Queue first, then Matches */}
|
|
{isReviewer && (
|
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 3 }}>
|
|
{data.reviewTasks !== null ? (
|
|
<ReviewTaskWidget
|
|
tasks={data.reviewTasks}
|
|
onNavigate={() => navigate('/ops/review-queue')}
|
|
/>
|
|
) : (
|
|
<WidgetError label="Review Tasks" />
|
|
)}
|
|
{canSeeMatches &&
|
|
(data.strongMatches !== null ? (
|
|
<StrongMatchOverview
|
|
matches={data.strongMatches}
|
|
onNavigate={() => navigate('/supply/match-center')}
|
|
/>
|
|
) : (
|
|
<WidgetError label="Starke Matches" />
|
|
))}
|
|
</Box>
|
|
)}
|
|
|
|
{/* Default: Matches + Data Quality */}
|
|
{!isReviewer && canSeeMatches && (
|
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 3 }}>
|
|
{data.strongMatches !== null ? (
|
|
<StrongMatchOverview
|
|
matches={data.strongMatches}
|
|
onNavigate={() => navigate('/supply/match-center')}
|
|
/>
|
|
) : (
|
|
<WidgetError label="Starke Matches" />
|
|
)}
|
|
{data.dataQuality !== null ? (
|
|
<DataQualityWidget
|
|
summary={data.dataQuality}
|
|
onNavigate={() => navigate('/supply/data-quality')}
|
|
/>
|
|
) : (
|
|
<WidgetError label="Datenqualität" />
|
|
)}
|
|
</Box>
|
|
)}
|
|
|
|
{canSeeMatches && data.strongMatches?.length === 0 && (
|
|
<Alert
|
|
severity="info"
|
|
action={
|
|
<Button size="small" onClick={() => navigate('/demand/ai-search')}>
|
|
Bedarfsprofil erstellen
|
|
</Button>
|
|
}
|
|
>
|
|
Noch keine Matches vorhanden. Erstellen Sie ein Bedarfsprofil, um passende Objekte zu
|
|
finden.
|
|
</Alert>
|
|
)}
|
|
|
|
{!isOwnerViewer && (
|
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 3 }}>
|
|
{data.futureSignals !== null ? (
|
|
<FutureSignalWidget summary={data.futureSignals} />
|
|
) : (
|
|
<WidgetError label="Marktsignale" />
|
|
)}
|
|
{canSeeOperations && !isReviewer ? (
|
|
data.reviewTasks !== null ? (
|
|
<ReviewTaskWidget
|
|
tasks={data.reviewTasks}
|
|
onNavigate={() => navigate('/ops/review-queue')}
|
|
/>
|
|
) : (
|
|
<WidgetError label="Review Tasks" />
|
|
)
|
|
) : !canSeeOperations && data.dataQuality !== null ? (
|
|
<DataQualityWidget
|
|
summary={data.dataQuality}
|
|
onNavigate={() => navigate('/supply/data-quality')}
|
|
/>
|
|
) : null}
|
|
</Box>
|
|
)}
|
|
|
|
<QuickActionPanel />
|
|
</Box>
|
|
)
|
|
}
|