feat: F012 match detail view

Full decision analysis page at /demand/results/:matchId:
- MatchDetailHeader: score, badges, CTA (compare/shortlist), back button
- ExecutiveSummaryPanel: 4-row summary (fit, top reason, tradeoff, next step)
- PropertyOverviewPanel: key facts grid; signal-aware for FUTURE_AVAILABILITY
- NeedAlignmentPanel: comparison table with fit indicators (MATCH/PARTIAL/NO_MATCH)
- ScoreBreakdownPanel: hard/soft scores + modifiers + total (sidebar)
- TradeoffPanel: severity-coded list with mitigation hints
- RiskPanel: risks sorted CRITICAL→LOW with category chips
- MissingInformationPanel: priority-grouped with per-item CTAs
- SourceProvenancePanel: source type, label, URL, freshness
- FutureAvailabilityContextPanel: mandatory disclaimer + signal metadata
- NextActionsPanel: engine-ranked actions + standard actions (sidebar)
- Details button on result cards now navigates to match detail

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-16 13:55:46 +02:00
parent ea221def74
commit 7cf8d8ba72
16 changed files with 1115 additions and 2 deletions
+11
View File
@@ -6,6 +6,7 @@ import type { Match } from '../domain/match'
import type { Need } from '../domain/need'
import type { Property } from '../domain/property'
import type { StrongMatchItem } from '../domain/dashboard'
import type { ScoreBreakdown } from '../domain/match'
import type { ListResponse, ItemResponse } from './types'
import { buildFullMatch, computeRankedMatches } from '../features/matching/rankingEngine'
@@ -38,6 +39,16 @@ export const matchService = {
return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } }
},
async getMatchDetail(id: string): Promise<ItemResponse<Match | null>> {
const data = await provider.getById(id)
return { data }
},
async getScoreBreakdown(matchId: string): Promise<ItemResponse<ScoreBreakdown | null>> {
const match = await provider.getById(matchId)
return { data: match?.scoreBreakdown ?? null }
},
// ── Engine-based methods ──────────────────────────────────────────────────
computeMatch(need: Need, property: Property): Match {