feat: Future Availability Intelligence — rebrand & card redesign

Replaces "Schattenmarkt" branding with professional Future Availability
Intelligence throughout the UI. Two distinct card types: Pre-Market
(LEASE_EXPIRY + verified, green accent) and Market Signal (probabilistic,
blue accent). Professioneller Data-Header replaces dark purple gradient.
New detail panel sections: Erkannte Marktindikatoren, Strategische
Interpretation, Bestätigt/Nicht bestätigt. All 15 mock signals enriched
with concrete Swiss CRE market indicators and strategic interpretations.
Signal quality shown as 4-dot indicator (Hoch/Mittel/Niedrig).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-20 13:06:16 +02:00
parent d5a4862a28
commit 5fe15bac62
12 changed files with 531 additions and 358 deletions
@@ -1,4 +1,4 @@
import { Alert, Box, Button, Chip, Divider, LinearProgress, Tooltip, Typography } from '@mui/material'
import { Box, Button, Chip, Divider, Typography } from '@mui/material'
import {
BarChart2,
Briefcase,
@@ -9,9 +9,7 @@ import {
FileText,
Newspaper,
ShieldCheck,
TrendingUp,
User,
Zap,
} from 'lucide-react'
import { LocationPreview } from '../shared/LocationPreview'
import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme'
@@ -36,210 +34,211 @@ const SOURCE_META: Record<string, { label: string; icon: React.ReactNode }> = {
LEASE_CONTRACT: { label: 'Vertrag verifiziert', icon: <ShieldCheck size={11} /> },
}
const SIGNAL_TYPE_LABELS: Record<string, string> = {
EXPANSION: 'Expansion',
POSSIBLE_MOVE_OUT: 'Möglicher Auszug',
CONSTRUCTION_PROJECT:'Bauprojekt',
RESTRUCTURING: 'Restrukturierung',
PROJECT_DEVELOPMENT: 'Projektentwicklung',
SPACE_CONSOLIDATION: 'Flächenkonsolidierung',
LEASE_EXPIRY: 'Vertragsende',
const ASSET_TYPE_LABELS: Record<string, string> = {
OFFICE: 'Bürofläche',
LOGISTICS: 'Lagerfläche',
RETAIL: 'Retailfläche',
PRODUCTION: 'Produktionsfläche',
MIXED: 'Gewerbefläche',
}
// Human-readable explanation of WHY a signal type is relevant to a space search
const SIGNAL_RELEVANCE_BRIDGE: Record<string, string> = {
EXPANSION: 'Neuer Flächenbedarf entsteht — Fläche wird gesucht',
POSSIBLE_MOVE_OUT: 'Fläche könnte frei werden — frühzeitig beobachten',
CONSTRUCTION_PROJECT:'Neubau geplant — zukünftige Verfügbarkeit möglich',
RESTRUCTURING: 'Flächenveränderung durch Restrukturierung möglich',
PROJECT_DEVELOPMENT: 'Projektentwicklung — neue Flächen in Planung',
SPACE_CONSOLIDATION: 'Konsolidierung — Teilflächen könnten verfügbar werden',
// Signal type → opportunity headline (with asset type)
function getOpportunityHeadline(signalType: string | undefined, assetTypeLabel: string): string {
switch (signalType) {
case 'LEASE_EXPIRY': return `${assetTypeLabel} wird verfügbar`
case 'POSSIBLE_MOVE_OUT': return `Mögliche ${assetTypeLabel} erkannt`
case 'EXPANSION': return `Unternehmen sucht ${assetTypeLabel}`
case 'CONSTRUCTION_PROJECT': return `Neubau: ${assetTypeLabel} in Planung`
case 'RESTRUCTURING': return `Mögliche Flächenfreigabe erkannt`
case 'SPACE_CONSOLIDATION': return `Mögliche Teilfläche erkannt`
case 'PROJECT_DEVELOPMENT': return `Neue Fläche in Projektentwicklung`
default: return `Potenzielle ${assetTypeLabel} erkannt`
}
}
const CREDIBILITY_META: Record<string, { label: string; color: string }> = {
HIGH: { label: 'Hohe Quellenqualität', color: '#1a7a4a' },
MEDIUM: { label: 'Mittlere Quellenqualität', color: '#d97706' },
LOW: { label: 'Niedrige Quellenqualität', color: '#c0392b' },
}
// ── Schattenmarkt hero zone ───────────────────────────────────────────────────
function SignalHero({ vm }: { vm: MatchCardViewModel }) {
const tier = getScoreTier(vm.matchScore)
const theme = SCORE_THEME[tier]
const signalLabel = vm.signalType ? (SIGNAL_TYPE_LABELS[vm.signalType] ?? vm.signalType) : 'Signal'
const relevanceBridge = vm.signalType ? (SIGNAL_RELEVANCE_BRIDGE[vm.signalType] ?? null) : null
const initials = (vm.title ?? '?')
.split(/\s+/)
.slice(0, 2)
.map(w => w[0])
.join('')
.toUpperCase()
// Signal quality dots display
function SignalQualityDots({ quality }: { quality: 'HIGH' | 'MEDIUM' | 'LOW' | undefined }) {
const config = {
HIGH: { dots: [1, 1, 1, 1], color: '#1a7a4a', label: 'Hohe Signalqualität' },
MEDIUM: { dots: [1, 1, 1, 0], color: '#d97706', label: 'Mittlere Signalqualität' },
LOW: { dots: [1, 1, 0, 0], color: '#c0392b', label: 'Niedrige Signalqualität' },
}
const c = quality ? config[quality] : config.LOW
return (
<Box sx={{ position: 'relative' }}>
{/* Gradient hero background */}
<Box
sx={{
height: 140,
background: 'linear-gradient(135deg, #1a0a3a 0%, #3b0a6e 50%, #6d28d9 100%)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 0.5,
position: 'relative',
overflow: 'hidden',
px: 7,
}}
>
{/* Subtle pattern overlay */}
<Box sx={{
position: 'absolute', inset: 0, opacity: 0.08,
backgroundImage: 'radial-gradient(circle at 20% 50%, white 1px, transparent 1px), radial-gradient(circle at 80% 20%, white 1px, transparent 1px)',
backgroundSize: '40px 40px',
}} />
{/* Company initials */}
<Box sx={{
width: 40, height: 40, borderRadius: '50%',
bgcolor: 'rgba(255,255,255,0.15)',
border: '1.5px solid rgba(255,255,255,0.3)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 1, flexShrink: 0,
}}>
<Typography sx={{ color: 'white', fontWeight: 800, fontSize: '0.9rem', letterSpacing: 1 }}>
{initials}
</Typography>
</Box>
{/* Signal type label */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, zIndex: 1 }}>
<Zap size={10} color="#c4b5fd" />
<Typography sx={{ color: '#c4b5fd', fontSize: '0.63rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.8 }}>
{signalLabel}
</Typography>
</Box>
{/* Relevance bridge — why this appears in results */}
{relevanceBridge && (
<Typography sx={{ color: 'rgba(255,255,255,0.85)', fontSize: '0.67rem', textAlign: 'center', lineHeight: 1.3, zIndex: 1 }}>
{relevanceBridge}
</Typography>
)}
{/* Verified badge */}
{vm.signalIsVerified && (
<Box sx={{ position: 'absolute', bottom: 8, right: 8, display: 'flex', alignItems: 'center', gap: 0.4, bgcolor: 'rgba(26,122,74,0.9)', borderRadius: 1, px: 0.75, py: 0.25 }}>
<ShieldCheck size={10} color="white" />
<Typography sx={{ color: 'white', fontSize: '0.6rem', fontWeight: 700 }}>Verifiziert</Typography>
</Box>
)}
</Box>
{/* Score badge — overlaid top-left */}
<Box sx={{
position: 'absolute', top: 10, left: 10,
background: theme.gradient, borderRadius: '10px',
px: 1.5, py: 0.5,
boxShadow: `0 4px 16px ${theme.glow}`,
border: `1px solid ${theme.border}`,
minWidth: 52, textAlign: 'center',
}}>
<Typography sx={{ fontWeight: 900, fontSize: '1.5rem', color: theme.text, lineHeight: 1 }}>
{vm.matchScore}%
</Typography>
{tier !== 'bronze' && (
<Typography sx={{ fontSize: '0.575rem', color: theme.text, opacity: 0.8, textTransform: 'uppercase', letterSpacing: 0.8, lineHeight: 1.2 }}>
{theme.label}
</Typography>
)}
</Box>
{/* Schattenmarkt chip — top-right */}
<Box sx={{ position: 'absolute', top: 10, right: 10 }}>
<Chip label="Schattenmarkt" size="small" sx={{ bgcolor: '#7c3aed', color: 'white', fontWeight: 700, fontSize: 10, height: 20 }} />
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
{c.dots.map((filled, i) => (
<Box key={i} sx={{ width: 7, height: 7, borderRadius: '50%', bgcolor: filled ? c.color : '#e2e8f0' }} />
))}
<Typography sx={{ fontSize: '0.68rem', color: c.color, fontWeight: 600, ml: 0.25 }}>
{c.label}
</Typography>
</Box>
)
}
// ── Signal metadata strip ─────────────────────────────────────────────────────
// ── Future Availability Card ──────────────────────────────────────────────────
function SignalMetaStrip({ vm }: { vm: MatchCardViewModel }) {
const sourceMeta = vm.signalSourceType ? SOURCE_META[vm.signalSourceType] : null
const credMeta = vm.signalSourceCredibility ? CREDIBILITY_META[vm.signalSourceCredibility] : null
const probPct = vm.signalProbability ? Math.round(vm.signalProbability * 100) : null
function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
const isControlled = vm.signalIsControlled ?? false
const accentColor = isControlled ? '#1a7a4a' : '#1d4ed8'
const headerBg = isControlled ? '#f0fdf4' : '#f8fafc'
const badge = isControlled ? 'Pre-Market' : 'Market Signal'
const badgeBg = isControlled ? '#dcfce7' : '#dbeafe'
const badgeColor = isControlled ? '#1a7a4a' : '#1d4ed8'
const assetLabel = vm.assetType ? (ASSET_TYPE_LABELS[vm.assetType] ?? vm.assetType) : 'Fläche'
const headline = getOpportunityHeadline(vm.signalType, assetLabel)
const tier = getScoreTier(vm.matchScore)
const theme = SCORE_THEME[tier]
return (
<Box sx={{ px: 1.75, pt: 1.5, pb: 0, display: 'flex', flexDirection: 'column', gap: 1 }}>
{/* Source + credibility row */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flexWrap: 'wrap' }}>
{sourceMeta && (
vm.signalSourceUrl ? (
<Box
component="a"
href={vm.signalSourceUrl}
target="_blank"
rel="noopener noreferrer"
sx={{ display: 'flex', alignItems: 'center', gap: 0.4, bgcolor: '#ede9fe', color: '#6d28d9', borderRadius: 1, px: 0.75, py: 0.25, textDecoration: 'none', '&:hover': { bgcolor: '#ddd6fe' } }}
>
{sourceMeta.icon}
<Typography sx={{ fontSize: '0.68rem', fontWeight: 600 }}>{sourceMeta.label}</Typography>
<ExternalLink size={9} style={{ opacity: 0.7 }} />
</Box>
) : (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.4, bgcolor: '#ede9fe', color: '#6d28d9', borderRadius: 1, px: 0.75, py: 0.25 }}>
{sourceMeta.icon}
<Typography sx={{ fontSize: '0.68rem', fontWeight: 600 }}>{sourceMeta.label}</Typography>
</Box>
)
)}
{credMeta && (
<Tooltip title={credMeta.label} arrow>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.3, bgcolor: '#f8fafc', border: `1px solid ${credMeta.color}30`, borderRadius: 1, px: 0.6, py: 0.2, cursor: 'default' }}>
<Box sx={{ width: 6, height: 6, borderRadius: '50%', bgcolor: credMeta.color }} />
<Typography sx={{ fontSize: '0.63rem', color: credMeta.color, fontWeight: 600 }}>
{vm.signalSourceCredibility === 'HIGH' ? 'Hoch' : vm.signalSourceCredibility === 'MEDIUM' ? 'Mittel' : 'Niedrig'}
</Typography>
</Box>
</Tooltip>
<Box sx={{
borderRadius: 2,
overflow: 'hidden',
border: `1px solid ${isControlled ? '#bbf7d0' : '#bfdbfe'}`,
borderLeft: `3px solid ${accentColor}`,
background: 'white',
display: 'flex', flexDirection: 'column',
boxShadow: '0 1px 8px rgba(0,0,0,0.06)',
transition: 'box-shadow 0.15s, transform 0.1s',
'&:hover': { boxShadow: '0 4px 20px rgba(0,0,0,0.10)', transform: 'translateY(-1px)' },
}}>
{/* Data Header */}
<Box sx={{ bgcolor: headerBg, px: 2, pt: 1.75, pb: 1.5, borderBottom: `1px solid ${isControlled ? '#bbf7d0' : '#bfdbfe'}` }}>
{/* Asset type + location */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.5 }}>
<Typography sx={{ fontSize: '0.63rem', fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: 0.8 }}>
{assetLabel.toUpperCase()} · {vm.locationLabel?.split(',')[0]?.toUpperCase() ?? ''}
</Typography>
<Box sx={{ bgcolor: badgeBg, color: badgeColor, px: 0.875, py: 0.2, borderRadius: 1, fontSize: '0.63rem', fontWeight: 700, letterSpacing: 0.3 }}>
{badge}
</Box>
</Box>
{/* Opportunity headline */}
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.3, color: '#1e293b', mb: 0.75 }}>
{headline}
</Typography>
{/* Key facts row */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25, flexWrap: 'wrap' }}>
{vm.signalAreaSqmEstimate ? (
<Typography sx={{ fontSize: '0.72rem', color: '#475569' }}>
~{vm.signalAreaSqmEstimate.toLocaleString('de-CH')} m²
</Typography>
) : null}
{vm.availabilityLabel && (
<Typography sx={{ fontSize: '0.72rem', color: '#475569' }}>
{vm.availabilityLabel}
</Typography>
)}
</Box>
</Box>
{/* Score + signal quality strip */}
<Box sx={{ px: 2, py: 1.25, display: 'flex', alignItems: 'center', gap: 1.5, borderBottom: '1px solid #f1f5f9', bgcolor: 'white' }}>
<Box sx={{
background: theme.gradient, borderRadius: '8px',
px: 1.25, py: 0.4, border: `1px solid ${theme.border}`,
boxShadow: `0 2px 8px ${theme.glow}`,
}}>
<Typography sx={{ fontWeight: 900, fontSize: '1.25rem', color: theme.text, lineHeight: 1 }}>
{vm.matchScore}%
</Typography>
</Box>
<SignalQualityDots quality={vm.signalQuality} />
{isControlled && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.35, ml: 'auto', bgcolor: '#f0fdf4', border: '1px solid #86efac', borderRadius: 1, px: 0.625, py: 0.2 }}>
<ShieldCheck size={10} color="#1a7a4a" />
<Typography sx={{ fontSize: '0.6rem', color: '#1a7a4a', fontWeight: 700 }}>Verifiziert</Typography>
</Box>
)}
</Box>
{/* Probability bar */}
{probPct !== null && (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.3 }}>
<Typography sx={{ fontSize: '0.63rem', color: '#64748b', fontWeight: 600 }}>Eintretenswahrscheinlichkeit</Typography>
<Typography sx={{ fontSize: '0.63rem', fontWeight: 700, color: probPct >= 70 ? '#1a7a4a' : probPct >= 50 ? '#d97706' : '#c0392b' }}>
{probPct}%
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={probPct}
sx={{
height: 4, borderRadius: 2,
bgcolor: '#e2e8f0',
'& .MuiLinearProgress-bar': {
bgcolor: probPct >= 70 ? '#1a7a4a' : probPct >= 50 ? '#d97706' : '#c0392b',
borderRadius: 2,
},
}}
/>
</Box>
)}
{/* Card body */}
<Box sx={{ px: 2, pt: 1.25, pb: 1.5, flex: 1, display: 'flex', flexDirection: 'column', gap: 0 }}>
{/* Market indicator */}
{vm.signalMarketIndicator && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, bgcolor: '#f0fdf4', borderRadius: 1, px: 0.75, py: 0.4 }}>
<TrendingUp size={11} color="#1a7a4a" style={{ flexShrink: 0 }} />
<Typography sx={{ fontSize: '0.68rem', color: '#1a7a4a', fontWeight: 500 }}>
{vm.signalMarketIndicator}
</Typography>
{/* CONTROLLED: Confirmed facts */}
{isControlled && (vm.signalConfirmedFacts?.length ?? 0) > 0 && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.4, mb: 1.25 }}>
{(vm.signalConfirmedFacts ?? []).slice(0, 2).map((fact, i) => (
<Box key={i} sx={{ display: 'flex', alignItems: 'center', gap: 0.6 }}>
<CheckCircle2 size={12} color="#1a7a4a" style={{ flexShrink: 0 }} />
<Typography sx={{ fontSize: '0.72rem', color: '#1a7a4a', fontWeight: 500 }}>{fact}</Typography>
</Box>
))}
</Box>
)}
{/* MARKET SIGNAL: Market indicators */}
{!isControlled && (vm.signalMarketIndicators?.length ?? 0) > 0 && (
<>
<Typography sx={{ fontSize: '0.63rem', fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: 0.6, mb: 0.5 }}>
Erkannte Marktindikatoren
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.35, mb: 1.25 }}>
{(vm.signalMarketIndicators ?? []).slice(0, 3).map((ind, i) => (
<Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.6 }}>
<Box sx={{ width: 4, height: 4, borderRadius: '50%', bgcolor: '#1d4ed8', mt: '5px', flexShrink: 0 }} />
<Typography sx={{ fontSize: '0.72rem', color: '#475569', lineHeight: 1.3 }}>{ind}</Typography>
</Box>
))}
</Box>
</>
)}
{/* Why relevant (match reasons) */}
{vm.reasons.length > 0 && (
<>
<Divider sx={{ mb: 1 }} />
<Typography sx={{ fontSize: '0.63rem', fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: 0.6, mb: 0.6 }}>
Warum relevant für Ihre Suche?
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{vm.reasons.slice(0, 3).map((r, i) => (
<Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.6 }}>
<CheckCircle2 size={12} color="#1a7a4a" style={{ marginTop: 2, flexShrink: 0 }} />
<Box>
<Typography sx={{ fontSize: '0.72rem', fontWeight: 600, color: '#1e293b', lineHeight: 1.3 }}>{r.label}</Typography>
<Typography sx={{ fontSize: '0.69rem', color: '#64748b', lineHeight: 1.3 }}>{r.explanation}</Typography>
</Box>
</Box>
))}
</Box>
</>
)}
{/* Actions */}
<Box sx={{ display: 'flex', gap: 0.75, mt: 1.5, pt: 1.25, borderTop: '1px solid rgba(0,0,0,0.06)' }}>
{vm.actions.map(a => (
<Button
key={a.id}
size="small"
variant={a.variant === 'primary' ? 'contained' : 'outlined'}
onClick={a.onClick}
disabled={a.disabled}
sx={{
textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1,
...(a.variant === 'primary' && {
bgcolor: accentColor,
'&:hover': { bgcolor: isControlled ? '#166534' : '#1e40af' },
}),
}}
>
{a.label}
</Button>
))}
</Box>
)}
{/* Disclaimer footnote */}
{vm.disclaimer && (
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1, fontSize: '0.65rem', lineHeight: 1.4, borderTop: '1px solid #f1f5f9', pt: 0.75 }}>
{vm.disclaimer}
</Typography>
)}
</Box>
</Box>
)
}
@@ -260,67 +259,61 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro
const rt = RESULT_TYPE_META[vm.resultType] ?? { label: vm.resultType, color: '#64748b' }
const isFuture = vm.resultType === 'FUTURE_AVAILABILITY'
// Future availability cards use the new design
if (isFuture) {
return <FutureAvailabilityCard vm={vm} />
}
return (
<Box sx={{
borderRadius: 2,
overflow: 'hidden',
boxShadow: isFuture
? '0 2px 16px rgba(109,40,217,0.15), 0 0 0 2px #7c3aed'
: `0 2px 16px rgba(0,0,0,0.07), 0 0 0 1px ${theme.cardBorder}`,
boxShadow: `0 2px 16px rgba(0,0,0,0.07), 0 0 0 1px ${theme.cardBorder}`,
background: theme.cardBg,
display: 'flex',
flexDirection: 'column',
transition: 'box-shadow 0.15s, transform 0.1s',
'&:hover': {
boxShadow: isFuture
? '0 6px 28px rgba(109,40,217,0.2), 0 0 0 2px #7c3aed'
: `0 6px 28px rgba(0,0,0,0.12), 0 0 0 1px ${theme.cardBorder}`,
boxShadow: `0 6px 28px rgba(0,0,0,0.12), 0 0 0 1px ${theme.cardBorder}`,
transform: 'translateY(-1px)',
},
}}>
{/* ── Hero zone ── */}
{isFuture ? (
<SignalHero vm={vm} />
) : (
<Box sx={{ position: 'relative' }}>
<LocationPreview imageUrl={imageUrl} lat={lat} lng={lng} cityLabel={cityLabel} height={175} />
<Box sx={{
position: 'absolute', top: 10, left: 10,
background: theme.gradient, borderRadius: '10px',
px: 1.5, py: 0.5,
boxShadow: `0 4px 16px ${theme.glow}`,
border: `1px solid ${theme.border}`,
minWidth: 52, textAlign: 'center',
}}>
<Typography sx={{ fontWeight: 900, fontSize: '1.5rem', color: theme.text, lineHeight: 1 }}>
{vm.matchScore}%
<Box sx={{ position: 'relative' }}>
<LocationPreview imageUrl={imageUrl} lat={lat} lng={lng} cityLabel={cityLabel} height={175} />
<Box sx={{
position: 'absolute', top: 10, left: 10,
background: theme.gradient, borderRadius: '10px',
px: 1.5, py: 0.5,
boxShadow: `0 4px 16px ${theme.glow}`,
border: `1px solid ${theme.border}`,
minWidth: 52, textAlign: 'center',
}}>
<Typography sx={{ fontWeight: 900, fontSize: '1.5rem', color: theme.text, lineHeight: 1 }}>
{vm.matchScore}%
</Typography>
{tier !== 'bronze' && (
<Typography sx={{ fontSize: '0.575rem', color: theme.text, opacity: 0.8, textTransform: 'uppercase', letterSpacing: 0.8, lineHeight: 1.2 }}>
{theme.label}
</Typography>
{tier !== 'bronze' && (
<Typography sx={{ fontSize: '0.575rem', color: theme.text, opacity: 0.8, textTransform: 'uppercase', letterSpacing: 0.8, lineHeight: 1.2 }}>
{theme.label}
</Typography>
)}
</Box>
<Box sx={{ position: 'absolute', top: 10, right: 44, display: 'flex', flexDirection: 'column', gap: 0.5, alignItems: 'flex-end' }}>
<Chip label={rt.label} size="small" sx={{ bgcolor: rt.color, color: 'white', fontWeight: 600, fontSize: 10, height: 20 }} />
{vm.resultType === 'VERIFIED_PORTFOLIO' && (
<Chip
icon={<Building2 size={9} color="#1e3a5f" />}
label="Ihr Objekt"
size="small"
sx={{ bgcolor: 'rgba(30,58,95,0.85)', color: 'white', fontWeight: 700, fontSize: 9, height: 18, '& .MuiChip-icon': { ml: 0.5 } }}
/>
)}
</Box>
)}
</Box>
)}
{/* ── Signal metadata (Schattenmarkt only) ── */}
{isFuture && <SignalMetaStrip vm={vm} />}
<Box sx={{ position: 'absolute', top: 10, right: 44, display: 'flex', flexDirection: 'column', gap: 0.5, alignItems: 'flex-end' }}>
<Chip label={rt.label} size="small" sx={{ bgcolor: rt.color, color: 'white', fontWeight: 600, fontSize: 10, height: 20 }} />
{vm.resultType === 'VERIFIED_PORTFOLIO' && (
<Chip
icon={<Building2 size={9} color="#1e3a5f" />}
label="Ihr Objekt"
size="small"
sx={{ bgcolor: 'rgba(30,58,95,0.85)', color: 'white', fontWeight: 700, fontSize: 9, height: 18, '& .MuiChip-icon': { ml: 0.5 } }}
/>
)}
</Box>
</Box>
{/* ── Card body ── */}
<Box sx={{ p: isFuture ? 1.75 : 2, pt: isFuture ? 1 : 2, flex: 1, display: 'flex', flexDirection: 'column', gap: 0 }}>
<Box sx={{ p: 2, flex: 1, display: 'flex', flexDirection: 'column', gap: 0 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.3 }} noWrap>
{vm.title}
</Typography>
@@ -328,25 +321,8 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro
{[vm.locationLabel, vm.availabilityLabel].filter(Boolean).join(' · ')}
</Typography>
{/* AI Signal Summary (Schattenmarkt) */}
{isFuture && vm.aiSignalSummary && (
<>
<Divider sx={{ my: 1 }} />
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.75, bgcolor: '#faf5ff', borderRadius: 1, p: 1 }}>
<Zap size={12} color="#7c3aed" style={{ flexShrink: 0, marginTop: 2 }} />
<Typography variant="body2" sx={{
fontSize: '0.72rem', color: '#4c1d95', lineHeight: 1.5,
overflow: 'hidden', display: '-webkit-box',
WebkitLineClamp: 4, WebkitBoxOrient: 'vertical',
}}>
{vm.aiSignalSummary}
</Typography>
</Box>
</>
)}
{/* Regular explainability summary (non-future) */}
{!isFuture && vm.explainabilitySummary && (
{/* Regular explainability summary */}
{vm.explainabilitySummary && (
<>
<Divider sx={{ my: 1.25 }} />
<Typography variant="body2" sx={{
@@ -392,8 +368,8 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro
sx={{
textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1,
...(a.variant === 'primary' && {
bgcolor: isFuture ? '#7c3aed' : '#1e3a5f',
'&:hover': { bgcolor: isFuture ? '#6d28d9' : '#162d4a' },
bgcolor: '#1e3a5f',
'&:hover': { bgcolor: '#162d4a' },
}),
}}
>
@@ -402,13 +378,6 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro
))}
</Box>
</Box>
{/* Disclaimer */}
{vm.disclaimer && (
<Alert severity="warning" sx={{ borderRadius: 0, py: 0.25, '& .MuiAlert-message': { fontSize: '0.7rem' } }}>
{vm.disclaimer}
</Alert>
)}
</Box>
)
}
@@ -58,6 +58,13 @@ export interface MatchCardViewModel {
signalType?: string
signalIsVerified?: boolean
aiSignalSummary?: string
signalQuality?: 'HIGH' | 'MEDIUM' | 'LOW'
signalMarketIndicators?: string[]
signalStrategicInterpretation?: string
signalConfirmedFacts?: string[]
signalUnconfirmedFacts?: string[]
signalIsControlled?: boolean
signalAreaSqmEstimate?: number
// States
isSelected?: boolean