import type { ReactNode } from 'react' import { Alert, Box, Button, Card, Chip, LinearProgress, Stack, Table, TableBody, TableCell, TableHead, TableRow, Typography, } 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 { CompareEmptyState, CompareColumnHeader, CompareCell, MissingDataCell, AICompareSummary, } from '../../components/compare' import type { UnifiedMatchResult } from '../../domain/unifiedResult' import type { VerifiedPortfolioResult, ExternalMarketResult, FutureAvailabilityResult } from '../../domain/unifiedResult' // ── Helpers ─────────────────────────────────────────────────────────────────── const HARD_CRITERIA = new Set(['area', 'location', 'budget', 'timing']) const SCORE_COLOR = (s: number) => s >= 78 ? '#1a7a4a' : s >= 52 ? '#d97706' : '#c0392b' function getProp(item: UnifiedMatchResult) { return item.resultType !== 'FUTURE_AVAILABILITY' ? (item as VerifiedPortfolioResult | ExternalMarketResult).property : null } function getSig(item: UnifiedMatchResult) { return item.resultType === 'FUTURE_AVAILABILITY' ? (item as FutureAvailabilityResult).signal : null } const TYPE_META: Record = { VERIFIED_PORTFOLIO: { label: 'Verified Portfolio', color: '#1e3a5f' }, EXTERNAL_MARKET: { label: 'Marktinserat', color: '#d97706' }, FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#7c3aed' }, } const RISK_LEVEL_ORDER: Record = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 } // ── Row label cell ──────────────────────────────────────────────────────────── const LABEL_SX = { position: 'sticky' as const, left: 0, bgcolor: 'white', zIndex: 1, width: 200, minWidth: 200, color: '#64748b', fontSize: 13, fontWeight: 600, borderRight: '1px solid #e2e8f0', verticalAlign: 'top', py: 1.5, } const DATA_SX = { borderLeft: '1px solid #f1f5f9', minWidth: 220, verticalAlign: 'top', py: 1.5, } // ── Main component ──────────────────────────────────────────────────────────── 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, }) if (compareItems.length === 0) { return ( Vergleich ) } // ── 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) // ── Shared render helpers ───────────────────────────────────────────────── function row(label: string, cells: ReactNode[]) { return ( {label} {cells.map((cell, i) => ( {cell} ))} ) } function scoreBar(value: number, label?: string) { const color = value >= 0.8 ? '#1a7a4a' : value >= 0.6 ? '#d97706' : '#c0392b' return ( {label ?? `${Math.round(value * 100)}%`} ) } return ( {/* Page header */} Vergleich {/* Mobile notice */} Die Vergleichsansicht ist für Desktop optimiert. Für beste Erfahrung auf einem grösseren Bildschirm öffnen. {/* Desktop table */} {/* AI Summary */} = 2} /> {/* Column headers */} Kriterium {compareItems.map(item => ( removeFromCompare(item.matchId)} /> ))} {/* 1. Result Type */} {row('1. Result-Typ', compareItems.map(item => { const m = TYPE_META[item.resultType] ?? { label: item.resultType, color: '#64748b' } return }))} {/* 2. Source / Provenance */} {row('2. Quelle / Provenienz', compareItems.map(item => { const prop = getProp(item) const sig = getSig(item) const label = prop?.sourceLabel ?? sig?.source?.type ?? null return label ? {label} : }))} {/* 3. Match Score */} {row('3. Match Score', compareItems.map((item, idx) => ( : undefined} iconTooltip="Höchster Match Score" > {item.matchScore} /100 )))} {/* 4. Confidence Score */} {row('4. Konfidenz', compareItems.map((item, idx) => ( : undefined} iconTooltip="Niedrigste Konfidenz" > {scoreBar(item.match.confidenceLevel)} )))} {/* 5. Data Quality Score */} {row('5. Datenqualität', compareItems.map((item, idx) => { const prop = getProp(item) const dq = prop?.dataQuality.score ?? null if (dq === null) return return ( : undefined} iconTooltip="Niedrigste Datenqualität" > {scoreBar(dq)} ) }))} {/* 6. Asset Type */} {row('6. Nutzungstyp', compareItems.map(item => { const prop = getProp(item) const label = prop?.assetType ?? null return label ? : }))} {/* 7. Location */} {row('7. Standort', compareItems.map(item => { const prop = getProp(item) const sig = getSig(item) const city = prop?.location?.city ?? sig?.locationHint ?? null const district = prop?.location?.district return city ? {city}{district ? `, ${district}` : ''} : }))} {/* 8. Area */} {row('8. Fläche', compareItems.map(item => { const prop = getProp(item) const sig = getSig(item) const area = prop?.areaSqm ?? sig?.areaSqmEstimate ?? null return area !== null ? {area.toLocaleString('de-CH')} m²{sig ? ' (Schätzung)' : ''} : }))} {/* 9. Rent / Budget Fit */} {row('9. Miete / Budget', compareItems.map(item => { const prop = getProp(item) if (!prop) return return ( CHF {prop.rentPricePerSqm}/m² {prop.totalRentMonthly && ( {prop.totalRentMonthly.toLocaleString('de-CH')} CHF/Monat )} ) }))} {/* 10. Availability / Time Horizon */} {row('10. Verfügbarkeit', compareItems.map(item => { const prop = getProp(item) const sig = getSig(item) if (prop) return {prop.availabilityDate} if (sig) return ( } iconTooltip="Probabilistisches Signal — keine bestätigte Verfügbarkeit"> ~{sig.timeHorizonMonths} Monate {Math.round(sig.probability * 100)}% Wahrscheinlichkeit ) return }))} {/* 11. Hard Criteria Fit */} {row('11. Hardkriterien', compareItems.map(item => { const hardMatches = item.match.positiveFactors.filter(f => HARD_CRITERIA.has(f.criterion)) const total = 4 const count = hardMatches.length const color = count >= 3 ? '#1a7a4a' : count >= 2 ? '#d97706' : '#c0392b' return ( {count}/{total} erfüllt {hardMatches.map(f => ( } sx={{ fontSize: 10, bgcolor: '#f0fdf4', color: '#166534', '& .MuiChip-icon': { color: '#1a7a4a' } }} /> ))} ) }))} {/* 12. Top Soft Factors */} {row('12. Soft Factors', compareItems.map(item => { const softFactors = item.match.positiveFactors .filter(f => !HARD_CRITERIA.has(f.criterion)) .slice(0, 3) if (softFactors.length === 0) return return ( {softFactors.map(f => ( ))} ) }))} {/* 13. Main Strengths */} {row('13. Stärken', compareItems.map(item => { const top = item.match.positiveFactors.slice(0, 2) if (top.length === 0) return return ( {top.map((f, i) => ( {f.explanation} ))} ) }))} {/* 14. Main Tradeoffs */} {row('14. Abwägungen', compareItems.map(item => { const tradeoffs = item.match.tradeoffs?.slice(0, 2) ?? [] if (tradeoffs.length === 0) return ( Keine signifikanten Abwägungen ) return ( {tradeoffs.map((t, i) => ( {t.criterion}: {t.concern} ))} ) }))} {/* 15. Main Risks */} {row('15. Risiken', compareItems.map(item => { const risks = [...(item.match.risks ?? [])].sort( (a, b) => (RISK_LEVEL_ORDER[a.level] ?? 4) - (RISK_LEVEL_ORDER[b.level] ?? 4) ).slice(0, 2) if (risks.length === 0) return ( Keine identifizierten Risiken ) return ( {risks.map((r, i) => ( {r.description} ))} ) }))} {/* 16. Missing Data */} {row('16. Fehlende Daten', compareItems.map((item, idx) => { const total = item.match.missingData?.length ?? 0 const critical = missingCriticalCounts[idx] if (total === 0) return ( Vollständig ) return ( 0 && critical === maxMissingCritical ? 'critical' : critical > 0 ? 'worst' : 'none'} icon={critical > 0 ? : } iconTooltip={critical > 0 ? 'Kritische Pflichtfelder fehlen' : 'Optionale Felder fehlen'} > {total} fehlend {critical > 0 && ( {critical} kritisch )} ) }))} {/* 17. Future Availability Context */} {row('17. Zukunftskontext', compareItems.map(item => { const sig = getSig(item) if (!sig) return ( Nicht anwendbar ) return ( } iconTooltip="Probabilistisches Zukunftssignal"> {Math.round(sig.probability * 100)}% Wahrscheinlichkeit Sensitivität: {sig.sensitivityLevel} {sig.disclaimer} ) }))} {/* 18. Recommended Next Action */} {row('18. Nächste Aktion', compareItems.map(item => { const action = item.match.nextBestActions?.[0] if (!action) return return ( {action.label} {action.description && ( {action.description} )} ) }))}
{/* Add more prompt */} {compareItems.length < 4 && ( Weiteres Ergebnis hinzufügen Bis zu {4 - compareItems.length} weitere{4 - compareItems.length === 1 ? 's' : ''} Ergebnis{4 - compareItems.length === 1 ? '' : 'se'} möglich )}
) }