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:
@@ -15,12 +15,7 @@ import {
|
||||
} from '@mui/material'
|
||||
import { Trophy, AlertTriangle, AlertOctagon, Zap, CheckCircle2, XCircle } from 'lucide-react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useCompareStore } from '../../stores/compareStore'
|
||||
import { aiService } from '../../services/aiService'
|
||||
import { needService } from '../../services/needService'
|
||||
import { WEIGHTING_KEYS, WEIGHTING_LABELS } from '../../domain/needBuilder'
|
||||
import type { WeightingKey } from '../../domain/needBuilder'
|
||||
import {
|
||||
CompareEmptyState,
|
||||
CompareColumnHeader,
|
||||
@@ -35,13 +30,13 @@ import {
|
||||
SCORE_COLOR,
|
||||
TYPE_META,
|
||||
RISK_LEVEL_ORDER,
|
||||
CRITERION_ALIASES,
|
||||
getProp,
|
||||
getSig,
|
||||
LABEL_SX,
|
||||
DATA_SX,
|
||||
scoreBar,
|
||||
} from '../../components/compare/compareUtils'
|
||||
import { useCompareData } from '../../hooks/useCompareData'
|
||||
|
||||
// ── Module-level helpers ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -62,42 +57,19 @@ export default function Compare() {
|
||||
const navigate = useNavigate()
|
||||
const { compareItems, removeFromCompare, clearCompare } = useCompareStore()
|
||||
|
||||
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 {
|
||||
aiSummary,
|
||||
aiLoading,
|
||||
activeNeed,
|
||||
relevantCriteria,
|
||||
overallWinnerIdx,
|
||||
bestScoreIdx,
|
||||
worstConfIdx,
|
||||
dqScores,
|
||||
worstDQIdx,
|
||||
missingCriticalCounts,
|
||||
maxMissingCritical,
|
||||
} = useCompareData(compareItems)
|
||||
|
||||
if (compareItems.length === 0) {
|
||||
return (
|
||||
@@ -110,22 +82,6 @@ export default function Compare() {
|
||||
)
|
||||
}
|
||||
|
||||
// ── Highlight indices ──────────────────────────────────────────────────────
|
||||
|
||||
const bestScoreIdx = compareItems.reduce(
|
||||
(best, item, i) => item.matchScore > compareItems[best].matchScore ? i : best, 0
|
||||
)
|
||||
const worstConfIdx = compareItems.reduce(
|
||||
(worst, item, i) => item.match.confidenceLevel < compareItems[worst].match.confidenceLevel ? i : worst, 0
|
||||
)
|
||||
const dqScores = compareItems.map(item => getProp(item)?.dataQuality.score ?? 1)
|
||||
const worstDQIdx = dqScores.indexOf(Math.min(...dqScores))
|
||||
|
||||
const missingCriticalCounts = compareItems.map(
|
||||
item => item.match.missingData?.filter(m => m.importance === 'CRITICAL').length ?? 0
|
||||
)
|
||||
const maxMissingCritical = Math.max(...missingCriticalCounts)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<AddToPipelineDialog />
|
||||
|
||||
Reference in New Issue
Block a user