feat: F006 supply dashboard — complete spec implementation

- 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>
This commit is contained in:
Benjamin Sutter
2026-05-15 16:31:54 +02:00
parent 46acca9e62
commit 7b7be0b436
12 changed files with 454 additions and 183 deletions
+30
View File
@@ -1,6 +1,8 @@
import { MockupMatchProvider } from '../provider/MockupMatchProvider'
import { MockupPropertyProvider } from '../provider/MockupPropertyProvider'
import type { MatchFilters } from '../provider/IMatchProvider'
import type { Match } from '../domain/match'
import type { StrongMatchItem } from '../domain/dashboard'
import type { ListResponse, ItemResponse } from './types'
const provider = MockupMatchProvider
@@ -26,4 +28,32 @@ export const matchService = {
const data = await provider.approve(id, reviewedBy)
return { data }
},
async getStrongMatches(minScore = 80): Promise<StrongMatchItem[]> {
const [matches, properties] = await Promise.all([
provider.getAll(),
MockupPropertyProvider.getAll(),
])
return matches
.filter(m => m.matchScore >= minScore)
.slice(0, 5)
.map(m => {
const prop = properties.find(p => p.id === m.propertyId)
const topFactor = m.positiveFactors?.[0]
const firstAction = m.nextBestActions?.[0]
return {
matchId: m.id,
propertyId: m.propertyId,
propertyTitle: prop?.title ?? 'Unbekanntes Objekt',
propertyAddress: prop?.address
? `${prop.address.street} ${prop.address.houseNumber}, ${prop.address.city}`
: '',
needSummary: m.needId,
matchScore: m.matchScore,
topReason: topFactor?.explanation ?? topFactor?.criterion ?? '',
missingDataCount: m.missingData?.length ?? 0,
nextBestAction: firstAction?.label ?? '',
} satisfies StrongMatchItem
})
},
}