9cf3e72033
Signals are now in their own feed section with an explanation banner so users understand they are not confirmed properties. Each signal card shows WHY it appears in their search (e.g. 'Fläche könnte frei werden'). Source pills are now clickable links. FutureAvailabilityContextPanel redesigned with full AI summary, source button, and relevance bridge. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
160 lines
5.1 KiB
TypeScript
160 lines
5.1 KiB
TypeScript
import { Alert, Box, Chip, Divider, Typography } from '@mui/material'
|
||
import { Zap } from 'lucide-react'
|
||
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
|
||
import { UnifiedResultCard } from './UnifiedResultCard'
|
||
import { SCORE_THEME } from '../shared/scoreTheme'
|
||
|
||
interface Props {
|
||
results: UnifiedMatchResult[]
|
||
view?: 'list' | 'grid'
|
||
}
|
||
|
||
const TIER_CONFIG = [
|
||
{
|
||
key: 'gold' as const,
|
||
label: 'Top Matches',
|
||
sublabel: '90–100 Punkte',
|
||
filter: (r: UnifiedMatchResult) => r.matchScore >= 90,
|
||
},
|
||
{
|
||
key: 'silver' as const,
|
||
label: 'Starke Matches',
|
||
sublabel: '80–89 Punkte',
|
||
filter: (r: UnifiedMatchResult) => r.matchScore >= 80 && r.matchScore < 90,
|
||
},
|
||
{
|
||
key: 'bronze' as const,
|
||
label: 'Weitere Treffer',
|
||
sublabel: 'unter 80 Punkte',
|
||
filter: (r: UnifiedMatchResult) => r.matchScore < 80,
|
||
},
|
||
]
|
||
|
||
interface TierHeaderProps {
|
||
label: string
|
||
sublabel: string
|
||
tierKey: 'gold' | 'silver' | 'bronze'
|
||
count: number
|
||
isFirst: boolean
|
||
}
|
||
|
||
function TierHeader({ label, sublabel, tierKey, count, isFirst }: TierHeaderProps) {
|
||
const theme = SCORE_THEME[tierKey]
|
||
return (
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mt: isFirst ? 0 : 3, mb: 1.5 }}>
|
||
<Box
|
||
sx={{
|
||
width: 4,
|
||
height: 28,
|
||
borderRadius: 2,
|
||
background: theme.gradient,
|
||
flexShrink: 0,
|
||
}}
|
||
/>
|
||
<Box>
|
||
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.2, color: theme.text === '#7a4f00' ? '#92400e' : theme.text === '#334155' ? '#1e293b' : '#7c3d12' }}>
|
||
{label}
|
||
</Typography>
|
||
<Typography variant="caption" color="text.secondary">{sublabel}</Typography>
|
||
</Box>
|
||
<Chip
|
||
label={count}
|
||
size="small"
|
||
sx={{
|
||
fontWeight: 700,
|
||
fontSize: '0.75rem',
|
||
background: theme.gradient,
|
||
color: theme.text,
|
||
border: `1px solid ${theme.border}`,
|
||
}}
|
||
/>
|
||
<Divider sx={{ flex: 1 }} />
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
function SchattenmarktSectionHeader({ count }: { count: number }) {
|
||
return (
|
||
<Box sx={{ mt: 4, mb: 0 }}>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 1.5 }}>
|
||
<Box sx={{ width: 4, height: 28, borderRadius: 2, background: 'linear-gradient(180deg, #7c3aed, #4c1d95)', flexShrink: 0 }} />
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||
<Zap size={15} color="#7c3aed" />
|
||
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.2, color: '#4c1d95' }}>
|
||
Schattenmarkt
|
||
</Typography>
|
||
<Chip
|
||
label={count}
|
||
size="small"
|
||
sx={{ fontWeight: 700, fontSize: '0.75rem', bgcolor: '#ede9fe', color: '#6d28d9', border: '1px solid #c4b5fd' }}
|
||
/>
|
||
</Box>
|
||
<Divider sx={{ flex: 1 }} />
|
||
</Box>
|
||
<Alert
|
||
severity="info"
|
||
icon={<Zap size={16} />}
|
||
sx={{
|
||
mb: 2,
|
||
bgcolor: '#faf5ff',
|
||
border: '1px solid #e9d5ff',
|
||
color: '#4c1d95',
|
||
'& .MuiAlert-icon': { color: '#7c3aed' },
|
||
}}
|
||
>
|
||
<strong>Was sind Schattenmarkt-Signale?</strong> Die KI hat aus öffentlichen Quellen (Stelleninseraten, Pressemeldungen, Baubewilligungen etc.) Hinweise gefunden, dass in Ihrer Zielregion in absehbarer Zeit Flächen frei werden könnten. Dies sind <strong>keine bestätigten Angebote</strong> — aber frühzeitige Hinweise, die Sie im Auge behalten sollten. Klicken Sie auf Details für Quellen und die vollständige KI-Analyse.
|
||
</Alert>
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
export function UnifiedResultFeed({ results, view = 'list' }: Props) {
|
||
const realResults = results.filter(r => r.resultType !== 'FUTURE_AVAILABILITY')
|
||
const signalResults = results.filter(r => r.resultType === 'FUTURE_AVAILABILITY')
|
||
|
||
const tiers = TIER_CONFIG.map(t => ({ ...t, items: realResults.filter(t.filter) }))
|
||
.filter(t => t.items.length > 0)
|
||
|
||
function renderCards(items: UnifiedMatchResult[]) {
|
||
return view === 'grid' ? (
|
||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: 2 }}>
|
||
{items.map(result => (
|
||
<UnifiedResultCard key={result.matchId} result={result} view="grid" />
|
||
))}
|
||
</Box>
|
||
) : (
|
||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||
{items.map(result => (
|
||
<UnifiedResultCard key={result.matchId} result={result} view="list" />
|
||
))}
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<Box>
|
||
{/* Real properties — scored tiers */}
|
||
{tiers.map((tier, idx) => (
|
||
<Box key={tier.key}>
|
||
<TierHeader
|
||
label={tier.label}
|
||
sublabel={tier.sublabel}
|
||
tierKey={tier.key}
|
||
count={tier.items.length}
|
||
isFirst={idx === 0}
|
||
/>
|
||
{renderCards(tier.items)}
|
||
</Box>
|
||
))}
|
||
|
||
{/* Schattenmarkt signals — dedicated section */}
|
||
{signalResults.length > 0 && (
|
||
<Box>
|
||
<SchattenmarktSectionHeader count={signalResults.length} />
|
||
{renderCards(signalResults)}
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
)
|
||
}
|