feat: Schattenmarkt cards — source pills, probability bar, AI summary
Complete IntelligenceMatchCard redesign for FUTURE_AVAILABILITY signals: purple gradient hero, source type pills, credibility indicators, probability LinearProgress bar, market indicator badge, AI-generated signal summary block. Added aiSummary field to FutureSignal domain and rich German AI summaries to mock signals 001–005. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,214 @@
|
|||||||
import { Alert, Box, Button, Chip, Divider, Typography } from '@mui/material'
|
import { Alert, Box, Button, Chip, Divider, LinearProgress, Tooltip, Typography } from '@mui/material'
|
||||||
import { CheckCircle2 } from 'lucide-react'
|
import {
|
||||||
|
BarChart2,
|
||||||
|
Briefcase,
|
||||||
|
Building2,
|
||||||
|
CheckCircle2,
|
||||||
|
FileCheck,
|
||||||
|
FileText,
|
||||||
|
Newspaper,
|
||||||
|
ShieldCheck,
|
||||||
|
TrendingUp,
|
||||||
|
User,
|
||||||
|
Zap,
|
||||||
|
} from 'lucide-react'
|
||||||
import { LocationPreview } from '../shared/LocationPreview'
|
import { LocationPreview } from '../shared/LocationPreview'
|
||||||
import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme'
|
import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme'
|
||||||
import type { MatchCardViewModel } from './MatchCardViewModel'
|
import type { MatchCardViewModel } from './MatchCardViewModel'
|
||||||
|
|
||||||
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const RESULT_TYPE_META: Record<string, { label: string; color: string }> = {
|
const RESULT_TYPE_META: Record<string, { label: string; color: string }> = {
|
||||||
VERIFIED_PORTFOLIO: { label: 'Verified Portfolio', color: '#1e3a5f' },
|
VERIFIED_PORTFOLIO: { label: 'Verified Portfolio', color: '#1e3a5f' },
|
||||||
EXTERNAL_MARKET: { label: 'Marktinserat', color: '#d97706' },
|
EXTERNAL_MARKET: { label: 'Marktinserat', color: '#d97706' },
|
||||||
FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#7c3aed' },
|
FUTURE_AVAILABILITY: { label: 'Schattenmarkt', color: '#7c3aed' },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SOURCE_META: Record<string, { label: string; icon: React.ReactNode }> = {
|
||||||
|
JOB_POSTING: { label: 'Stelleninserate', icon: <Briefcase size={11} /> },
|
||||||
|
PRESS: { label: 'Pressebericht', icon: <Newspaper size={11} /> },
|
||||||
|
CONSTRUCTION_PERMIT:{ label: 'Baubewilligung', icon: <FileCheck size={11} /> },
|
||||||
|
COMPANY_REPORT: { label: 'Geschäftsbericht',icon: <FileText size={11} /> },
|
||||||
|
MARKET_DATA: { label: 'Marktdaten', icon: <BarChart2 size={11} /> },
|
||||||
|
MANUAL: { label: 'Analyst', icon: <User 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',
|
||||||
|
}
|
||||||
|
|
||||||
|
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 initials = (vm.title ?? '?')
|
||||||
|
.split(/\s+/)
|
||||||
|
.slice(0, 2)
|
||||||
|
.map(w => w[0])
|
||||||
|
.join('')
|
||||||
|
.toUpperCase()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ position: 'relative' }}>
|
||||||
|
{/* Gradient hero background */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: 130,
|
||||||
|
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',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 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: 44, height: 44, 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,
|
||||||
|
}}>
|
||||||
|
<Typography sx={{ color: 'white', fontWeight: 800, fontSize: '1rem', letterSpacing: 1 }}>
|
||||||
|
{initials}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Signal type label */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, zIndex: 1 }}>
|
||||||
|
<Zap size={11} color="#c4b5fd" />
|
||||||
|
<Typography sx={{ color: '#c4b5fd', fontSize: '0.68rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.8 }}>
|
||||||
|
{signalLabel}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Signal metadata strip ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 && (
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main component ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
vm: MatchCardViewModel
|
vm: MatchCardViewModel
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
@@ -23,68 +222,61 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro
|
|||||||
const theme = SCORE_THEME[tier]
|
const theme = SCORE_THEME[tier]
|
||||||
const rt = RESULT_TYPE_META[vm.resultType] ?? { label: vm.resultType, color: '#64748b' }
|
const rt = RESULT_TYPE_META[vm.resultType] ?? { label: vm.resultType, color: '#64748b' }
|
||||||
const topReason = vm.reasons[0]
|
const topReason = vm.reasons[0]
|
||||||
|
|
||||||
const isFuture = vm.resultType === 'FUTURE_AVAILABILITY'
|
const isFuture = vm.resultType === 'FUTURE_AVAILABILITY'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box sx={{
|
||||||
sx={{
|
borderRadius: 2,
|
||||||
borderRadius: 2,
|
overflow: 'hidden',
|
||||||
overflow: 'hidden',
|
boxShadow: isFuture
|
||||||
boxShadow: `0 2px 16px rgba(0,0,0,0.07), 0 0 0 1px ${theme.cardBorder}`,
|
? '0 2px 16px rgba(109,40,217,0.15), 0 0 0 2px #7c3aed'
|
||||||
border: isFuture ? '2px solid #7c3aed' : `1px solid ${theme.cardBorder}`,
|
: `0 2px 16px rgba(0,0,0,0.07), 0 0 0 1px ${theme.cardBorder}`,
|
||||||
background: theme.cardBg,
|
background: theme.cardBg,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
transition: 'box-shadow 0.15s, transform 0.1s',
|
transition: 'box-shadow 0.15s, transform 0.1s',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
boxShadow: `0 6px 28px rgba(0,0,0,0.12), 0 0 0 1px ${theme.cardBorder}`,
|
boxShadow: isFuture
|
||||||
transform: 'translateY(-1px)',
|
? '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}`,
|
||||||
}}
|
transform: 'translateY(-1px)',
|
||||||
>
|
},
|
||||||
{/* Image zone */}
|
}}>
|
||||||
<Box sx={{ position: 'relative' }}>
|
|
||||||
<LocationPreview imageUrl={imageUrl} lat={lat} lng={lng} cityLabel={cityLabel} height={175} />
|
|
||||||
|
|
||||||
{/* Score badge — overlaid top-left */}
|
{/* ── Hero zone ── */}
|
||||||
<Box
|
{isFuture ? (
|
||||||
sx={{
|
<SignalHero vm={vm} />
|
||||||
position: 'absolute',
|
) : (
|
||||||
top: 10,
|
<Box sx={{ position: 'relative' }}>
|
||||||
left: 10,
|
<LocationPreview imageUrl={imageUrl} lat={lat} lng={lng} cityLabel={cityLabel} height={175} />
|
||||||
background: theme.gradient,
|
<Box sx={{
|
||||||
borderRadius: '10px',
|
position: 'absolute', top: 10, left: 10,
|
||||||
px: 1.5,
|
background: theme.gradient, borderRadius: '10px',
|
||||||
py: 0.5,
|
px: 1.5, py: 0.5,
|
||||||
boxShadow: `0 4px 16px ${theme.glow}`,
|
boxShadow: `0 4px 16px ${theme.glow}`,
|
||||||
border: `1px solid ${theme.border}`,
|
border: `1px solid ${theme.border}`,
|
||||||
minWidth: 52,
|
minWidth: 52, textAlign: 'center',
|
||||||
textAlign: 'center',
|
}}>
|
||||||
}}
|
<Typography sx={{ fontWeight: 900, fontSize: '1.5rem', color: theme.text, lineHeight: 1 }}>
|
||||||
>
|
{vm.matchScore}%
|
||||||
<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>
|
</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 }}>
|
||||||
|
<Chip label={rt.label} size="small" sx={{ bgcolor: rt.color, color: 'white', fontWeight: 600, fontSize: 10, height: 20 }} />
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Result type chip — overlaid top-right */}
|
{/* ── Signal metadata (Schattenmarkt only) ── */}
|
||||||
<Box sx={{ position: 'absolute', top: 10, right: 44 }}>
|
{isFuture && <SignalMetaStrip vm={vm} />}
|
||||||
<Chip
|
|
||||||
label={rt.label}
|
|
||||||
size="small"
|
|
||||||
sx={{ bgcolor: rt.color, color: 'white', fontWeight: 600, fontSize: 10, height: 20 }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Card body */}
|
{/* ── Card body ── */}
|
||||||
<Box sx={{ p: 2, flex: 1, display: 'flex', flexDirection: 'column', gap: 0 }}>
|
<Box sx={{ p: isFuture ? 1.75 : 2, pt: isFuture ? 1 : 2, flex: 1, display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.3 }} noWrap>
|
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.3 }} noWrap>
|
||||||
{vm.title}
|
{vm.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -92,22 +284,32 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro
|
|||||||
{[vm.locationLabel, vm.availabilityLabel].filter(Boolean).join(' · ')}
|
{[vm.locationLabel, vm.availabilityLabel].filter(Boolean).join(' · ')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{vm.explainabilitySummary && (
|
{/* 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 && (
|
||||||
<>
|
<>
|
||||||
<Divider sx={{ my: 1.25 }} />
|
<Divider sx={{ my: 1.25 }} />
|
||||||
<Typography
|
<Typography variant="body2" sx={{
|
||||||
variant="body2"
|
fontWeight: 500, color: '#1e293b', lineHeight: 1.5, flex: 1,
|
||||||
sx={{
|
overflow: 'hidden', display: '-webkit-box',
|
||||||
fontWeight: 500,
|
WebkitLineClamp: 3, WebkitBoxOrient: 'vertical',
|
||||||
color: '#1e293b',
|
}}>
|
||||||
lineHeight: 1.5,
|
|
||||||
overflow: 'hidden',
|
|
||||||
display: '-webkit-box',
|
|
||||||
WebkitLineClamp: 3,
|
|
||||||
WebkitBoxOrient: 'vertical',
|
|
||||||
flex: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{vm.explainabilitySummary}
|
{vm.explainabilitySummary}
|
||||||
</Typography>
|
</Typography>
|
||||||
</>
|
</>
|
||||||
@@ -125,7 +327,6 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<Box sx={{ display: 'flex', gap: 0.75, mt: 1.5, pt: 1.25, borderTop: '1px solid rgba(0,0,0,0.06)' }}>
|
<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 => (
|
{vm.actions.map(a => (
|
||||||
@@ -136,13 +337,10 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro
|
|||||||
onClick={a.onClick}
|
onClick={a.onClick}
|
||||||
disabled={a.disabled}
|
disabled={a.disabled}
|
||||||
sx={{
|
sx={{
|
||||||
textTransform: 'none',
|
textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1,
|
||||||
fontSize: '0.7rem',
|
|
||||||
py: 0.375,
|
|
||||||
px: 1,
|
|
||||||
...(a.variant === 'primary' && {
|
...(a.variant === 'primary' && {
|
||||||
bgcolor: '#1e3a5f',
|
bgcolor: isFuture ? '#7c3aed' : '#1e3a5f',
|
||||||
'&:hover': { bgcolor: '#162d4a' },
|
'&:hover': { bgcolor: isFuture ? '#6d28d9' : '#162d4a' },
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -152,7 +350,7 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* FUTURE_AVAILABILITY disclaimer */}
|
{/* Disclaimer */}
|
||||||
{vm.disclaimer && (
|
{vm.disclaimer && (
|
||||||
<Alert severity="warning" sx={{ borderRadius: 0, py: 0.25, '& .MuiAlert-message': { fontSize: '0.7rem' } }}>
|
<Alert severity="warning" sx={{ borderRadius: 0, py: 0.25, '& .MuiAlert-message': { fontSize: '0.7rem' } }}>
|
||||||
{vm.disclaimer}
|
{vm.disclaimer}
|
||||||
|
|||||||
@@ -49,6 +49,15 @@ export interface MatchCardViewModel {
|
|||||||
explainabilitySummary?: string
|
explainabilitySummary?: string
|
||||||
taxCalculatorUrl?: string // deeplink to cantonal tax calculator
|
taxCalculatorUrl?: string // deeplink to cantonal tax calculator
|
||||||
|
|
||||||
|
// Schattenmarkt / FUTURE_AVAILABILITY signal fields
|
||||||
|
signalSourceType?: 'PRESS' | 'CONSTRUCTION_PERMIT' | 'JOB_POSTING' | 'COMPANY_REPORT' | 'MARKET_DATA' | 'MANUAL'
|
||||||
|
signalSourceCredibility?: 'LOW' | 'MEDIUM' | 'HIGH'
|
||||||
|
signalProbability?: number
|
||||||
|
signalMarketIndicator?: string
|
||||||
|
signalType?: string
|
||||||
|
signalIsVerified?: boolean
|
||||||
|
aiSignalSummary?: string
|
||||||
|
|
||||||
// States
|
// States
|
||||||
isSelected?: boolean
|
isSelected?: boolean
|
||||||
isCompareSelected?: boolean
|
isCompareSelected?: boolean
|
||||||
|
|||||||
@@ -42,4 +42,5 @@ export interface FutureSignal {
|
|||||||
evidence?: SignalEvidence // structured evidence block
|
evidence?: SignalEvidence // structured evidence block
|
||||||
matchabilityScore?: number // 0–100: how well this signal can be matched to needs
|
matchabilityScore?: number // 0–100: how well this signal can be matched to needs
|
||||||
reviewStatus?: ReviewStatus
|
reviewStatus?: ReviewStatus
|
||||||
|
aiSummary?: string // AI-generated explanation of what this signal means
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,5 +111,13 @@ export function buildMatchCardViewModel(
|
|||||||
taxCalculatorUrl,
|
taxCalculatorUrl,
|
||||||
isReviewRequired: match.status === 'PENDING_REVIEW',
|
isReviewRequired: match.status === 'PENDING_REVIEW',
|
||||||
isStaleData: false,
|
isStaleData: false,
|
||||||
|
// Schattenmarkt signal fields
|
||||||
|
signalSourceType: signal?.source?.type,
|
||||||
|
signalSourceCredibility: signal?.source?.credibility,
|
||||||
|
signalProbability: signal?.probability,
|
||||||
|
signalMarketIndicator: signal?.marketIndicator,
|
||||||
|
signalType: signal?.signalType,
|
||||||
|
signalIsVerified: signal?.isVerified,
|
||||||
|
aiSignalSummary: signal?.aiSummary,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const mockFutureSignals: FutureSignal[] = [
|
|||||||
organizationId: 'org-wincasa',
|
organizationId: 'org-wincasa',
|
||||||
createdAt: '2025-05-01T07:00:00Z',
|
createdAt: '2025-05-01T07:00:00Z',
|
||||||
updatedAt: '2025-05-10T07:00:00Z',
|
updatedAt: '2025-05-10T07:00:00Z',
|
||||||
|
aiSummary: 'KI hat in 90 Tagen 14 Stelleninserate von DataCloud Systems AG auf LinkedIn und Indeed ausgewertet — davon 11 mit explizitem Standort Zürich-West/Technopark und hybridem Arbeitsmodell. Das Wachstumsmuster (DevOps, Cloud-Architektur, Sales) deutet auf eine Teamvergrösserung von ~40 Personen hin, was einem Flächenbedarf von ca. 600 m² entspricht. Der Tech-Sektor in Zürich-West verzeichnet 2024 den stärksten Stellenzuwachs seit 2018.',
|
||||||
},
|
},
|
||||||
|
|
||||||
// --- signal-002: Helvetia Produktion possible move-out Reinach ---
|
// --- signal-002: Helvetia Produktion possible move-out Reinach ---
|
||||||
@@ -57,6 +58,7 @@ export const mockFutureSignals: FutureSignal[] = [
|
|||||||
organizationId: 'org-wincasa',
|
organizationId: 'org-wincasa',
|
||||||
createdAt: '2025-05-03T08:00:00Z',
|
createdAt: '2025-05-03T08:00:00Z',
|
||||||
updatedAt: '2025-05-10T08:00:00Z',
|
updatedAt: '2025-05-10T08:00:00Z',
|
||||||
|
aiSummary: 'Drei unabhängige Presseartikel (NZZ, Handelszeitung, Basellandschaftliche Zeitung) berichten über die angekündigte Restrukturierung der Muttergesellschaft mit geplantem Stellenabbau von 8–12% bis Ende 2025. Der Standort Reinach BL wird in internen Dokumenten als "unter Überprüfung" eingestuft. KI-Konfidenz bleibt moderat, da kein Auszugstermin bestätigt wurde.',
|
||||||
},
|
},
|
||||||
|
|
||||||
// --- signal-003: Bern Wankdorf Neubau Büro/Gewerbe ---
|
// --- signal-003: Bern Wankdorf Neubau Büro/Gewerbe ---
|
||||||
@@ -85,6 +87,7 @@ export const mockFutureSignals: FutureSignal[] = [
|
|||||||
organizationId: 'org-wincasa',
|
organizationId: 'org-wincasa',
|
||||||
createdAt: '2025-02-12T10:00:00Z',
|
createdAt: '2025-02-12T10:00:00Z',
|
||||||
updatedAt: '2025-05-05T09:00:00Z',
|
updatedAt: '2025-05-05T09:00:00Z',
|
||||||
|
aiSummary: 'Baubewilligung Nr. 2025-BW-0142 wurde am 10. Februar 2025 durch das Bauinspektorat Bern rechtskräftig erteilt. Das Projekt umfasst 4\'500 m² gemischte Büro- und Gewerbefläche (EG: Retail/Gewerbe, OG 1–3: Büro). Fertigstellung laut Baugesuch Q1 2027. Investorin ist die Wankdorf Immobilien AG; Vermietungsmandat noch nicht vergeben. Hohe Konfidenz durch amtliche Quelle.',
|
||||||
},
|
},
|
||||||
|
|
||||||
// --- signal-004: Pharma-Biotech Basel Expansion ---
|
// --- signal-004: Pharma-Biotech Basel Expansion ---
|
||||||
@@ -113,6 +116,7 @@ export const mockFutureSignals: FutureSignal[] = [
|
|||||||
organizationId: 'org-wincasa',
|
organizationId: 'org-wincasa',
|
||||||
createdAt: '2025-04-02T09:00:00Z',
|
createdAt: '2025-04-02T09:00:00Z',
|
||||||
updatedAt: '2025-05-08T10:00:00Z',
|
updatedAt: '2025-05-08T10:00:00Z',
|
||||||
|
aiSummary: 'Im Geschäftsbericht 2024 kommuniziert Novabio Pharma AG eine "strategische Kapazitätserweiterung im Forschungsbereich Basel-Allschwil bis 2026". Die KI hat ausserdem 6 Inserate für Senior Scientists und Lab-Manager mit Standortangabe Allschwil identifiziert. Der Life-Sciences-Korridor Basel-Allschwil weist 2024 die schweizweit höchste Laborflächen-Nachfrage auf.',
|
||||||
},
|
},
|
||||||
|
|
||||||
// --- signal-005: Finanz AG Zürich-Nord possible move-out → prop-023 ---
|
// --- signal-005: Finanz AG Zürich-Nord possible move-out → prop-023 ---
|
||||||
@@ -141,6 +145,7 @@ export const mockFutureSignals: FutureSignal[] = [
|
|||||||
organizationId: 'org-wincasa',
|
organizationId: 'org-wincasa',
|
||||||
createdAt: '2025-04-12T08:00:00Z',
|
createdAt: '2025-04-12T08:00:00Z',
|
||||||
updatedAt: '2025-05-10T09:00:00Z',
|
updatedAt: '2025-05-10T09:00:00Z',
|
||||||
|
aiSummary: 'Marktdatenanalyse zeigt: Die Finanz & Treuhand AG hat ihren LinkedIn-Unternehmenssitz von "Zürich-Nord, Seebach" auf "Zürich, Oerlikon" aktualisiert (Stand April 2025). Gleichzeitig sank die Mitarbeiterzahl laut LinkedIn von 38 auf 31 (-18%) innert 6 Monaten. Beide Indikatoren zusammen ergeben ein Verlagerungssignal. Kein offizieller Auszug bestätigt.',
|
||||||
},
|
},
|
||||||
|
|
||||||
// --- signal-006: Luzern Inseli Neubau Gewerbe ---
|
// --- signal-006: Luzern Inseli Neubau Gewerbe ---
|
||||||
|
|||||||
Reference in New Issue
Block a user