refactor: split AppShell, match-detail panels, extract useCompareData

AppShell.tsx: 627→116 lines
- appShellConfig.ts: NavItem/WorkspaceConfig types, WORKSPACE_CONFIG, nav helpers
- AppShellSidebar.tsx: Sidebar component with visual constants
- AppShellTopBar.tsx: TopBar component

Match-detail panels:
- ScoreBreakdownPanel: 377→303 lines (scoreBreakdownConstants.ts + CriterionRow.tsx extracted)
- LocationIntelligencePanel: 383→333 lines (SoftFactorBar.tsx extracted)
- FutureAvailabilityContextPanel: 386→351 lines (futureAvailabilityConstants.tsx extracted)

Compare.tsx: 485→441 lines
- useCompareData hook: all queries and derived state extracted to hooks/useCompareData.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 01:00:38 +02:00
parent ae82d0e6a0
commit 0bacd188d6
13 changed files with 792 additions and 741 deletions
+81
View File
@@ -0,0 +1,81 @@
import { useQuery } from '@tanstack/react-query'
import type { UnifiedMatchResult } from '../domain/unifiedResult'
import { WEIGHTING_KEYS, WEIGHTING_LABELS } from '../domain/needBuilder'
import type { WeightingKey } from '../domain/needBuilder'
import { aiService } from '../services/aiService'
import { needService } from '../services/needService'
import { CRITERION_ALIASES, getProp } from '../components/compare/compareUtils'
export function useCompareData(compareItems: UnifiedMatchResult[]) {
const { data: aiSummary, isLoading: aiLoading } = useQuery({
queryKey: ['ai-compare', compareItems.map(i => i.matchId)],
queryFn: () => aiService.summarizeComparison(compareItems),
enabled: compareItems.length >= 2,
select: r => r.data,
staleTime: Infinity,
})
const { data: needsData } = useQuery({
queryKey: ['needs'],
queryFn: () => needService.getAll(),
select: r => r.data,
})
const activeNeed = needsData
? [...needsData].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0]
: undefined
const relevantCriteria = activeNeed
? WEIGHTING_KEYS
.map(key => ({ key, label: WEIGHTING_LABELS[key], weight: activeNeed.weightingProfile[key] ?? 0 }))
.filter(c => c.weight > 0)
.sort((a, b) => b.weight - a.weight)
: []
const weightedTotals = compareItems.map(item =>
relevantCriteria.reduce((sum, { key, weight }) => {
const factor = [...item.match.positiveFactors, ...item.match.negativeFactors]
.find(f => CRITERION_ALIASES[key as WeightingKey].some(a => a.toLowerCase() === f.criterion.toLowerCase()))
return sum + (factor?.score ?? 50) * weight
}, 0)
)
const maxWeightedTotal = Math.max(...weightedTotals)
const overallWinnerIdx = compareItems.length > 1 && weightedTotals.filter(t => t === maxWeightedTotal).length === 1
? weightedTotals.indexOf(maxWeightedTotal)
: -1
const bestScoreIdx = compareItems.length > 0
? compareItems.reduce(
(best, item, i) => item.matchScore > compareItems[best].matchScore ? i : best, 0
)
: -1
const worstConfIdx = compareItems.length > 0
? compareItems.reduce(
(worst, item, i) => item.match.confidenceLevel < compareItems[worst].match.confidenceLevel ? i : worst, 0
)
: -1
const dqScores = compareItems.map(item => getProp(item)?.dataQuality.score ?? 1)
const worstDQIdx = dqScores.length > 0 ? dqScores.indexOf(Math.min(...dqScores)) : -1
const missingCriticalCounts = compareItems.map(
item => item.match.missingData?.filter(m => m.importance === 'CRITICAL').length ?? 0
)
const maxMissingCritical = missingCriticalCounts.length > 0 ? Math.max(...missingCriticalCounts) : 0
return {
aiSummary,
aiLoading,
activeNeed,
relevantCriteria,
weightedTotals,
overallWinnerIdx,
bestScoreIdx,
worstConfIdx,
dqScores,
worstDQIdx,
missingCriticalCounts,
maxMissingCritical,
}
}