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:
@@ -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
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Box, Button, Chip, Divider, Paper, Typography } from '@mui/material'
|
||||
import {
|
||||
AlertTriangle,
|
||||
BarChart2,
|
||||
Briefcase,
|
||||
CheckCircle2,
|
||||
ExternalLink,
|
||||
FileCheck,
|
||||
FileText,
|
||||
@@ -74,7 +76,7 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
|
||||
<Paper sx={{ p: 2.5, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<Zap size={16} color="#7c3aed" />
|
||||
<Typography variant="h6" sx={{ fontWeight: 700 }}>Schattenmarkt-Signal</Typography>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700 }}>Future Availability Signal</Typography>
|
||||
{signal.isVerified && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.4, bgcolor: '#f0fdf4', border: '1px solid #86efac', borderRadius: 1, px: 0.75, py: 0.2 }}>
|
||||
<ShieldCheck size={11} color="#1a7a4a" />
|
||||
@@ -204,7 +206,64 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 4. Relevance bridge */}
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{/* 4. Erkannte Marktindikatoren */}
|
||||
{signal.marketIndicators && signal.marketIndicators.length > 0 && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1, fontSize: '0.8125rem' }}>
|
||||
Erkannte Marktindikatoren
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.625 }}>
|
||||
{signal.marketIndicators.map((indicator, i) => (
|
||||
<Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.75 }}>
|
||||
<Box sx={{ width: 6, height: 6, borderRadius: '50%', bgcolor: '#1d4ed8', mt: '5px', flexShrink: 0 }} />
|
||||
<Typography variant="body2" sx={{ color: '#374151', lineHeight: 1.45 }}>{indicator}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 5. Strategische Interpretation */}
|
||||
{signal.strategicInterpretation && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 0.75, fontSize: '0.8125rem' }}>
|
||||
Strategische Interpretation
|
||||
</Typography>
|
||||
<Box sx={{ bgcolor: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: 1, p: 1.5 }}>
|
||||
<Typography variant="body2" sx={{ color: '#374151', lineHeight: 1.65, fontStyle: 'italic' }}>
|
||||
{signal.strategicInterpretation}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 6. Transparenz (Bestätigt / Nicht bestätigt) */}
|
||||
{((signal.confirmedFacts && signal.confirmedFacts.length > 0) ||
|
||||
(signal.unconfirmedFacts && signal.unconfirmedFacts.length > 0)) && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1, fontSize: '0.8125rem' }}>
|
||||
Transparenz
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
{(signal.confirmedFacts ?? []).map((fact, i) => (
|
||||
<Box key={`c-${i}`} sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<CheckCircle2 size={13} color="#1a7a4a" style={{ flexShrink: 0 }} />
|
||||
<Typography variant="caption" sx={{ color: '#1a7a4a', fontWeight: 500 }}>{fact}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
{(signal.unconfirmedFacts ?? []).map((fact, i) => (
|
||||
<Box key={`u-${i}`} sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<AlertTriangle size={13} color="#d97706" style={{ flexShrink: 0 }} />
|
||||
<Typography variant="caption" sx={{ color: '#d97706', fontWeight: 500 }}>{fact}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 7. Relevance bridge */}
|
||||
{relevanceBridge && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, bgcolor: '#faf5ff', border: '1px solid #e9d5ff', borderRadius: 1, px: 1.5, py: 1.25, mb: 2 }}>
|
||||
<Zap size={14} color="#7c3aed" style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
|
||||
@@ -63,7 +63,7 @@ function Slide0() {
|
||||
<FeatureRow
|
||||
icon={<Zap size={18} />}
|
||||
color="#7c3aed"
|
||||
text="Schattenmarkt-Signale der KI"
|
||||
text="Future Availability Intelligence"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -81,7 +81,7 @@ function Slide1() {
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Schreiben Sie Ihren Flächenbedarf in natürlicher Sprache. Die KI extrahiert alle Kriterien
|
||||
und findet die besten Matches aus Portfolio, Markt und Schattenmarkt.
|
||||
und findet die besten Matches aus Portfolio, Markt und Future Availability.
|
||||
</Typography>
|
||||
<Box className="w-full text-left">
|
||||
<FeatureRow
|
||||
@@ -106,11 +106,11 @@ function Slide2() {
|
||||
<Zap size={48} color="#7c3aed" />
|
||||
</Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, mb: 1 }}>
|
||||
Schattenmarkt — Vor dem Markt informiert
|
||||
Future Availability Intelligence
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Die KI überwacht Stelleninserate, Pressemeldungen und Baubewilligungen — frühzeitige
|
||||
Hinweise auf mögliche Verfügbarkeiten, bevor sie ausgeschrieben werden.
|
||||
Frühzeitige Marktintelligenz zu potenziell verfügbaren Flächen — verifizierte
|
||||
Vertragsenden und erkannte Marktsignale, bevor sie öffentlich werden.
|
||||
</Typography>
|
||||
<Box className="w-full text-left">
|
||||
<FeatureRow
|
||||
|
||||
@@ -10,8 +10,8 @@ interface Props {
|
||||
onFilterChange: (v: FilterSource) => void
|
||||
sortBy: SortBy
|
||||
onSortChange: (v: SortBy) => void
|
||||
showSchattenmarkt: boolean
|
||||
onShowSchattenmarktChange: (v: boolean) => void
|
||||
showFutureAvailability: boolean
|
||||
onShowFutureAvailabilityChange: (v: boolean) => void
|
||||
showOwnProperties?: boolean
|
||||
onShowOwnPropertiesChange?: (v: boolean) => void
|
||||
isPropertyManager?: boolean
|
||||
@@ -34,8 +34,8 @@ export function ResultFilterBar({
|
||||
onFilterChange,
|
||||
sortBy,
|
||||
onSortChange,
|
||||
showSchattenmarkt,
|
||||
onShowSchattenmarktChange,
|
||||
showFutureAvailability,
|
||||
onShowFutureAvailabilityChange,
|
||||
showOwnProperties,
|
||||
onShowOwnPropertiesChange,
|
||||
isPropertyManager,
|
||||
@@ -68,19 +68,19 @@ export function ResultFilterBar({
|
||||
|
||||
<Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} />
|
||||
|
||||
{/* Schattenmarkt toggle */}
|
||||
{/* Future Availability toggle */}
|
||||
<Chip
|
||||
size="small"
|
||||
clickable
|
||||
icon={<Zap size={11} color={showSchattenmarkt ? '#7c3aed' : '#94a3b8'} />}
|
||||
label="Schattenmarkt"
|
||||
onClick={() => onShowSchattenmarktChange(!showSchattenmarkt)}
|
||||
icon={<Zap size={11} color={showFutureAvailability ? '#7c3aed' : '#94a3b8'} />}
|
||||
label="Future Availability"
|
||||
onClick={() => onShowFutureAvailabilityChange(!showFutureAvailability)}
|
||||
sx={{
|
||||
bgcolor: showSchattenmarkt ? '#f3e8ff' : 'transparent',
|
||||
color: showSchattenmarkt ? '#6d28d9' : 'text.disabled',
|
||||
border: `1px solid ${showSchattenmarkt ? '#c4b5fd' : '#e2e8f0'}`,
|
||||
fontWeight: showSchattenmarkt ? 600 : 400,
|
||||
textDecoration: showSchattenmarkt ? 'none' : 'line-through',
|
||||
bgcolor: showFutureAvailability ? '#f3e8ff' : 'transparent',
|
||||
color: showFutureAvailability ? '#6d28d9' : 'text.disabled',
|
||||
border: `1px solid ${showFutureAvailability ? '#c4b5fd' : '#e2e8f0'}`,
|
||||
fontWeight: showFutureAvailability ? 600 : 400,
|
||||
textDecoration: showFutureAvailability ? 'none' : 'line-through',
|
||||
'& .MuiChip-icon': { ml: 0.75 },
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -309,7 +309,7 @@ function UnitStructurePanel({ p }: { p: Property }) {
|
||||
)
|
||||
}
|
||||
|
||||
// ── Schattenmarkt release panel ───────────────────────────────────────────────
|
||||
// ── Pre-Market release panel ──────────────────────────────────────────────────
|
||||
|
||||
const MOCK_TODAY = new Date('2026-05-20')
|
||||
|
||||
@@ -346,7 +346,7 @@ function SchattenmarktReleasePanel({ p }: { p: Property }) {
|
||||
await propertyService.update(p.id, { schattenmarktRelease: { enabled: nextEnabled, leadTimeMonths: nextLeadTime } })
|
||||
await queryClient.invalidateQueries({ queryKey: ['property', p.id] })
|
||||
await queryClient.invalidateQueries({ queryKey: ['properties'] })
|
||||
showToast(nextEnabled ? 'Schattenmarkt-Freigabe aktiviert.' : 'Schattenmarkt-Freigabe deaktiviert.', 'success')
|
||||
showToast(nextEnabled ? 'Pre-Market Freigabe aktiviert.' : 'Pre-Market Freigabe deaktiviert.', 'success')
|
||||
} catch {
|
||||
showToast('Fehler beim Speichern.', 'error')
|
||||
} finally {
|
||||
@@ -367,7 +367,7 @@ function SchattenmarktReleasePanel({ p }: { p: Property }) {
|
||||
return (
|
||||
<>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<SectionTitle title="Schattenmarkt" />
|
||||
<SectionTitle title="Pre-Market Freigabe" />
|
||||
<Box
|
||||
sx={{
|
||||
border: '1px solid',
|
||||
@@ -383,10 +383,10 @@ function SchattenmarktReleasePanel({ p }: { p: Property }) {
|
||||
<Zap size={15} color={enabled ? '#7c3aed' : '#94a3b8'} style={{ marginTop: 2 }} />
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, lineHeight: 1.3 }}>
|
||||
Für Schattenmarkt freigeben
|
||||
Pre-Market freigeben
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Nachfragesuchende sehen dieses Objekt vor Vertragsende im Feed
|
||||
Fläche vor offizieller Vermarktung im Markt sichtbar machen
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -455,7 +455,7 @@ function SchattenmarktReleasePanel({ p }: { p: Property }) {
|
||||
{!enabled && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
|
||||
Wenn aktiviert, erscheint dieses Objekt {leadTimeMonths} Monate vor Vertragsende als
|
||||
verifiziertes Schattenmarkt-Signal im Nachfrage-Feed.
|
||||
verifiziertes Pre-Market Signal im Nachfrage-Feed.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -43,4 +43,10 @@ export interface FutureSignal {
|
||||
matchabilityScore?: number // 0–100: how well this signal can be matched to needs
|
||||
reviewStatus?: ReviewStatus
|
||||
aiSummary?: string // AI-generated explanation of what this signal means
|
||||
|
||||
// Strategic intelligence fields
|
||||
marketIndicators?: string[] // ["Mitarbeiterzahl rückläufig -15%", ...]
|
||||
strategicInterpretation?: string // "Die Signale deuten auf ..."
|
||||
confirmedFacts?: string[] // ["Region bestätigt", ...]
|
||||
unconfirmedFacts?: string[] // ["Exakte Fläche unbekannt", ...]
|
||||
}
|
||||
|
||||
@@ -104,5 +104,17 @@ export function buildMatchCardViewModel(
|
||||
signalType: signal?.signalType,
|
||||
signalIsVerified: signal?.isVerified,
|
||||
aiSignalSummary: signal?.aiSummary,
|
||||
signalQuality: (() => {
|
||||
if (!signal) return undefined
|
||||
if (signal.isVerified || signal.probability >= 0.70) return 'HIGH'
|
||||
if (signal.probability >= 0.50) return 'MEDIUM'
|
||||
return 'LOW'
|
||||
})(),
|
||||
signalIsControlled: signal?.signalType === 'LEASE_EXPIRY' && signal?.isVerified === true,
|
||||
signalMarketIndicators: signal?.marketIndicators,
|
||||
signalStrategicInterpretation: signal?.strategicInterpretation,
|
||||
signalConfirmedFacts: signal?.confirmedFacts,
|
||||
signalUnconfirmedFacts: signal?.unconfirmedFacts,
|
||||
signalAreaSqmEstimate: signal?.areaSqmEstimate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ function buildSignal(p: Property): FutureSignal {
|
||||
timeHorizonMonths: monthsUntil,
|
||||
source: { type: 'LEASE_CONTRACT', credibility: 'HIGH' },
|
||||
sensitivityLevel: 'INTERNAL',
|
||||
disclaimer: 'Verwaltung hat dieses Objekt für den Schattenmarkt freigegeben. Vertragsende aus internem ERP bestätigt — höchste Signalqualität.',
|
||||
disclaimer: 'Verwaltung hat diese Fläche für Pre-Market-Sichtbarkeit freigegeben. Vertragsende aus internem ERP bestätigt — höchste Signalqualität.',
|
||||
riskLevel: RiskLevel.LOW,
|
||||
relevanceScore: 0.92,
|
||||
isVerified: true,
|
||||
@@ -68,5 +68,19 @@ function buildSignal(p: Property): FutureSignal {
|
||||
createdAt: MOCK_TODAY.toISOString(),
|
||||
updatedAt: MOCK_TODAY.toISOString(),
|
||||
aiSummary: `Vertrag der ${p.currentTenant ?? 'aktuellen Mietpartei'} läuft in ${monthsUntil} Monaten aus (${monthName}). Fläche: ${p.areaSqm.toLocaleString('de-CH')} m² · ${locationLabel}. Die Verwaltung hat dieses Objekt explizit für den Markt freigegeben — vertraglich bestätigt, keine Schätzung.`,
|
||||
marketIndicators: [
|
||||
`Vertragsende ${monthName} aus Verwaltungssystem bestätigt`,
|
||||
`Fläche ${p.areaSqm.toLocaleString('de-CH')} m² — ${locationLabel}`,
|
||||
`Pre-Market-Freigabe durch Verwaltung erteilt`,
|
||||
],
|
||||
confirmedFacts: [
|
||||
`Vertragsende ${monthName} aus ERP bestätigt`,
|
||||
`Fläche ${p.areaSqm.toLocaleString('de-CH')} m² — bestätigt`,
|
||||
`Standort ${locationLabel} — bestätigt`,
|
||||
],
|
||||
unconfirmedFacts: [
|
||||
'Ob Nachmieter bereits bekannt',
|
||||
'Ob Umbaumassnahmen geplant',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
+169
-63
@@ -2,13 +2,14 @@ import { SignalType, RiskLevel } from '../domain/enums'
|
||||
import type { FutureSignal } from '../domain/futureSignal'
|
||||
|
||||
export const mockFutureSignals: FutureSignal[] = [
|
||||
// --- signal-001: DataCloud Expansion Zürich-West ---
|
||||
// --- signal-001: DataCloud Systems AG — Büroexpansion Zürich-West ---
|
||||
{
|
||||
id: 'signal-001',
|
||||
signalType: SignalType.EXPANSION,
|
||||
propertyId: 'prop-005',
|
||||
companyName: 'DataCloud Systems AG',
|
||||
locationHint: 'Zürich-West / Technopark',
|
||||
title: 'DataCloud Systems AG sucht ~600 m² Bürofläche in Zürich-West',
|
||||
areaSqmEstimate: 600,
|
||||
probability: 0.72,
|
||||
confidenceScore: 0.68,
|
||||
@@ -30,15 +31,24 @@ export const mockFutureSignals: FutureSignal[] = [
|
||||
createdAt: '2025-05-01T07: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.',
|
||||
marketIndicators: [
|
||||
'14 Stelleninserate mit Standort Zürich-West in 90 Tagen',
|
||||
'Teamwachstum ~40 Personen (DevOps, Cloud-Architektur, Sales)',
|
||||
'Tech-Sektor Zürich-West: +38% Stellenwachstum 2024',
|
||||
],
|
||||
strategicInterpretation: 'DataCloud Systems AG befindet sich in einer klaren Wachstumsphase mit Fokus auf Zürich-West. Die Muster sprechen für organische Expansion — kein Standortwechsel erkennbar.',
|
||||
confirmedFacts: ['Region Zürich-West aus Inseraten bestätigt', 'Flächentyp Büro plausibel', 'Wachstumstrend seit 6 Monaten vorhanden'],
|
||||
unconfirmedFacts: ['Exakter Umzugszeitpunkt', 'Ob interner Ausbau am aktuellen Standort geplant', 'Finale Flächengrösse'],
|
||||
},
|
||||
|
||||
// --- signal-002: Helvetia Produktion possible move-out Reinach ---
|
||||
// --- signal-002: Helvetia Produktion GmbH — möglicher Auszug Reinach BL ---
|
||||
{
|
||||
id: 'signal-002',
|
||||
signalType: SignalType.POSSIBLE_MOVE_OUT,
|
||||
companyName: 'Helvetia Produktion GmbH',
|
||||
propertyId: 'prop-006',
|
||||
locationHint: 'Reinach BL, Industriezone Nord',
|
||||
title: 'Helvetia Produktion GmbH — Standort Reinach unter Restrukturierungsdruck',
|
||||
areaSqmEstimate: 3200,
|
||||
probability: 0.48,
|
||||
confidenceScore: 0.44,
|
||||
@@ -60,13 +70,22 @@ export const mockFutureSignals: FutureSignal[] = [
|
||||
createdAt: '2025-05-03T08: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.',
|
||||
marketIndicators: [
|
||||
'3 unabhängige Presseartikel zu Restrukturierung der Muttergesellschaft',
|
||||
'Geplanter Stellenabbau 8–12% bis Ende 2025 bestätigt',
|
||||
'Standort Reinach BL in internen Dokumenten als «unter Überprüfung» eingestuft',
|
||||
],
|
||||
strategicInterpretation: 'Die Restrukturierung der Muttergesellschaft erhöht die Wahrscheinlichkeit einer Standortschliessung oder -reduktion in Reinach. Kein Auszugstermin bestätigt — frühes Beobachtungsstadium.',
|
||||
confirmedFacts: ['Restrukturierung der Muttergesellschaft offiziell bestätigt', 'Stellenabbau angekündigt', 'Logistik-Standort Reinach betroffen'],
|
||||
unconfirmedFacts: ['Ob Standort Reinach geschlossen wird', 'Zeitpunkt des Auszugs', 'Ob Fläche vermietet oder verkauft wird'],
|
||||
},
|
||||
|
||||
// --- signal-003: Bern Wankdorf Neubau Büro/Gewerbe ---
|
||||
// --- signal-003: Bern Wankdorf — Neubau 4'500 m² Büro/Gewerbe ---
|
||||
{
|
||||
id: 'signal-003',
|
||||
signalType: SignalType.CONSTRUCTION_PROJECT,
|
||||
locationHint: 'Bern, Wankdorf',
|
||||
title: 'Neubau Wankdorf West — 4\'500 m² Büro/Gewerbe, Fertigstellung Q1 2027',
|
||||
areaSqmEstimate: 4500,
|
||||
probability: 0.85,
|
||||
confidenceScore: 0.80,
|
||||
@@ -89,14 +108,23 @@ export const mockFutureSignals: FutureSignal[] = [
|
||||
createdAt: '2025-02-12T10: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.',
|
||||
marketIndicators: [
|
||||
'Baubewilligung für 4\'500 m² Büro-/Gewerbefläche öffentlich eingereicht',
|
||||
'Fertigstellung gemäss Baugesuch Q1 2027',
|
||||
'Bauherr: Wankdorf Center Entwicklungs AG',
|
||||
],
|
||||
strategicInterpretation: 'Baubewilligung öffentlich — hohe Realisierungswahrscheinlichkeit. Vermietungsstart voraussichtlich 6–9 Monate vor Fertigstellung. Standort Wankdorf mit guter ÖV-Anbindung (Tram, Bahn).',
|
||||
confirmedFacts: ['Baubewilligung erteilt', 'Fläche und Standort bestätigt', 'Fertigstellungsdatum Q1 2027 aus Gesuch'],
|
||||
unconfirmedFacts: ['Endmietzins', 'Ob Fläche bereits vorvermietet', 'Ausbaustandard'],
|
||||
},
|
||||
|
||||
// --- signal-004: Pharma-Biotech Basel Expansion ---
|
||||
// --- signal-004: Novabio Pharma AG — Laborexpansion Basel Allschwil ---
|
||||
{
|
||||
id: 'signal-004',
|
||||
signalType: SignalType.EXPANSION,
|
||||
companyName: 'Novabio Pharma AG',
|
||||
locationHint: 'Basel, Allschwil',
|
||||
title: 'Novabio Pharma AG sucht Lab-/Bürofläche im Life-Sciences-Korridor Allschwil',
|
||||
areaSqmEstimate: 700,
|
||||
probability: 0.63,
|
||||
confidenceScore: 0.60,
|
||||
@@ -118,15 +146,24 @@ export const mockFutureSignals: FutureSignal[] = [
|
||||
createdAt: '2025-04-02T09: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.',
|
||||
marketIndicators: [
|
||||
'Geschäftsbericht 2024: «strategische Kapazitätserweiterung Basel-Allschwil bis 2026» explizit erwähnt',
|
||||
'6 Inserate für Senior Scientists und Lab-Manager mit Standort Allschwil in 60 Tagen',
|
||||
'Life-Sciences-Korridor Basel-Allschwil: höchste CH-weite Laborflächen-Nachfrage 2024',
|
||||
],
|
||||
strategicInterpretation: 'Novabio Pharma AG kommuniziert Expansion öffentlich — Flächensuche sehr wahrscheinlich. Der Fokus auf Laborpersonal deutet auf kombinierten Lab-/Bürobedarf von 600–800 m² hin.',
|
||||
confirmedFacts: ['Expansionspläne im Geschäftsbericht bestätigt', 'Region Basel-Allschwil als Zielstandort genannt', 'Einstellungstrend seit Q1 2025 sichtbar'],
|
||||
unconfirmedFacts: ['Ob Neubau oder Bestandsfläche gesucht', 'Exakter Flächenbedarf Labor vs. Büro', 'Zeitpunkt des Einzugs'],
|
||||
},
|
||||
|
||||
// --- signal-005: Finanz AG Zürich-Nord possible move-out → prop-023 ---
|
||||
// --- signal-005: Finanz & Treuhand AG — möglicher Auszug Zürich-Nord ---
|
||||
{
|
||||
id: 'signal-005',
|
||||
signalType: SignalType.POSSIBLE_MOVE_OUT,
|
||||
companyName: 'Finanz & Treuhand AG',
|
||||
propertyId: 'prop-023',
|
||||
locationHint: 'Zürich-Nord, Seebach',
|
||||
title: 'Finanz & Treuhand AG — Kostensenkungssignale deuten auf Standortaufgabe Seebach',
|
||||
areaSqmEstimate: 850,
|
||||
probability: 0.58,
|
||||
confidenceScore: 0.54,
|
||||
@@ -147,13 +184,22 @@ export const mockFutureSignals: FutureSignal[] = [
|
||||
createdAt: '2025-04-12T08: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.',
|
||||
marketIndicators: [
|
||||
'LinkedIn-Firmensitz von Seebach auf Oerlikon aktualisiert (April 2025)',
|
||||
'Mitarbeiterzahl rückläufig: von 38 auf 31 (-18%) in 6 Monaten',
|
||||
'Leerstand Zürich-Nord Q1 2025: +12% gegenüber Vorquartal',
|
||||
],
|
||||
strategicInterpretation: 'Adressänderung auf LinkedIn kombiniert mit Stellenabbau ist ein starkes Frühsignal für Standortaufgabe. Frühzeitiger Kontakt mit der Verwaltung des Objekts empfohlen.',
|
||||
confirmedFacts: ['Mitarbeiterzahl-Rückgang auf LinkedIn dokumentiert', 'Adressänderung auf LinkedIn publiziert', 'Aktueller Standort Seebach bekannt'],
|
||||
unconfirmedFacts: ['Ob Mietvertrag gekündigt wurde', 'Zeitpunkt des Auszugs', 'Ob Zusammenschluss mit anderem Standort geplant'],
|
||||
},
|
||||
|
||||
// --- signal-006: Luzern Inseli Neubau Gewerbe ---
|
||||
// --- signal-006: Luzern Inseli — Neubau Büro/Gewerbe am Wasser ---
|
||||
{
|
||||
id: 'signal-006',
|
||||
signalType: SignalType.CONSTRUCTION_PROJECT,
|
||||
locationHint: 'Luzern, Inseli-Quartier',
|
||||
title: 'Neubau Inseli Luzern — 2\'000 m² Gewerbefläche am Wasser, Q1 2027',
|
||||
areaSqmEstimate: 2000,
|
||||
probability: 0.78,
|
||||
confidenceScore: 0.74,
|
||||
@@ -173,15 +219,24 @@ export const mockFutureSignals: FutureSignal[] = [
|
||||
organizationId: 'org-wincasa',
|
||||
createdAt: '2025-01-25T11:00:00Z',
|
||||
updatedAt: '2025-05-06T14:00:00Z',
|
||||
marketIndicators: [
|
||||
'Baubewilligung für 2\'000 m² Gewerbe-/Bürofläche am Inseli eingereicht',
|
||||
'Fertigstellung gemäss Baugesuch ca. Q1 2027',
|
||||
'Luzern Innenstadt: Büroflächennachfrage 2024 +18% gegenüber Vorjahr',
|
||||
],
|
||||
strategicInterpretation: 'Attraktive Wasserlage am Luzerner Inseli mit ÖV-Direktanbindung (Bahnhof 8 Minuten zu Fuss). Nutzungskonzept noch offen — Büro und Gastronomie/Retail im EG wahrscheinlich. Frühzeitiges Interesse anmelden sinnvoll.',
|
||||
confirmedFacts: ['Baubewilligung öffentlich eingereicht', 'Standort Inseli-Quartier bestätigt', 'Fertigstellung Q1 2027 aus Gesuch'],
|
||||
unconfirmedFacts: ['Endgültige Nutzungsaufteilung (Büro vs. Retail)', 'Mietzinsniveau', 'Ob Vermietungsmandat vergeben'],
|
||||
},
|
||||
|
||||
// --- signal-007: E-Commerce Zug Expansion → prop-026 ---
|
||||
// --- signal-007: SwissCart E-Commerce GmbH — Logistikexpansion Zug ---
|
||||
{
|
||||
id: 'signal-007',
|
||||
signalType: SignalType.EXPANSION,
|
||||
companyName: 'SwissCart E-Commerce GmbH',
|
||||
propertyId: 'prop-026',
|
||||
locationHint: 'Zug, Industriestrasse',
|
||||
title: 'SwissCart E-Commerce GmbH sucht Logistikfläche in Zug',
|
||||
areaSqmEstimate: 580,
|
||||
probability: 0.66,
|
||||
confidenceScore: 0.62,
|
||||
@@ -202,15 +257,24 @@ export const mockFutureSignals: FutureSignal[] = [
|
||||
organizationId: 'org-wincasa',
|
||||
createdAt: '2025-04-20T07:00:00Z',
|
||||
updatedAt: '2025-05-09T08:00:00Z',
|
||||
marketIndicators: [
|
||||
'9 Stelleninserate für Lager- und Logistikpersonal in Zug in 45 Tagen',
|
||||
'E-Commerce-Umsatz CH 2024: +22% — Nachfrage nach Lagerflächen steigt',
|
||||
'Zug Industriestrasse: aktuelle Leerstandsquote Lager unter 3%',
|
||||
],
|
||||
strategicInterpretation: 'Massiver Personalaufbau im Logistikbereich deutet auf konkreten Flächenbedarf hin. Zug als Steuerkanton bleibt attraktiver Firmensitz — kein Standortwechsel erwartet, nur Erweiterung.',
|
||||
confirmedFacts: ['Region Zug aus Inseraten bestätigt', 'Flächentyp Logistik/Lager aus Stellenprofilen ableitbar', 'Wachstumstrend seit Q3 2024 sichtbar'],
|
||||
unconfirmedFacts: ['Ob zusätzliche Bürofläche benötigt', 'Exakter Flächenbedarf', 'Bevorzugte Strassenlage (Anlieferung)'],
|
||||
},
|
||||
|
||||
// --- signal-008: Retail Zürich Niederdorf possible move-out → prop-025 ---
|
||||
// --- signal-008: Textilhaus Zürich AG — Retail-Verkleinerung Niederdorf ---
|
||||
{
|
||||
id: 'signal-008',
|
||||
signalType: SignalType.POSSIBLE_MOVE_OUT,
|
||||
companyName: 'Textilhaus Zürich AG',
|
||||
propertyId: 'prop-025',
|
||||
locationHint: 'Zürich Niederdorf, Münstergasse',
|
||||
title: 'Textilhaus Zürich AG — Retailfläche Münstergasse könnte frei werden',
|
||||
areaSqmEstimate: 280,
|
||||
probability: 0.55,
|
||||
confidenceScore: 0.50,
|
||||
@@ -230,13 +294,22 @@ export const mockFutureSignals: FutureSignal[] = [
|
||||
organizationId: 'org-wincasa',
|
||||
createdAt: '2025-04-01T10:00:00Z',
|
||||
updatedAt: '2025-05-07T11:00:00Z',
|
||||
marketIndicators: [
|
||||
'Onlineanteil CH-Textilhandel 2024: 38% — stationäre Verkaufsflächen unter Druck',
|
||||
'Mitarbeiterzahl Textilhaus Zürich AG laut LinkedIn: -25% in 12 Monaten',
|
||||
'Stationärer Handel Zürich Altstadt: Leerstandsquote +8% 2024',
|
||||
],
|
||||
strategicInterpretation: 'Struktureller Rückgang im stationären Textilhandel trifft kleine Altstadt-Läden besonders. Kombination aus Personalabbau und Branchentrend ergibt ein Verlagerungssignal, das im Auge behalten werden sollte.',
|
||||
confirmedFacts: ['Branchentrend stationärer Handel dokumentiert', 'Aktueller Standort Münstergasse bekannt', 'Personalabbau auf LinkedIn sichtbar'],
|
||||
unconfirmedFacts: ['Ob Mietvertrag noch läuft oder ausläuft', 'Ob Filialnetz insgesamt verkleinert wird', 'Zeitpunkt eines möglichen Auszugs'],
|
||||
},
|
||||
|
||||
// --- signal-009: Winterthur Zentrum Neubau Büro ---
|
||||
// --- signal-009: Winterthur Zentrum — Neubau Bürofläche ---
|
||||
{
|
||||
id: 'signal-009',
|
||||
signalType: SignalType.CONSTRUCTION_PROJECT,
|
||||
locationHint: 'Winterthur, Zentrum Technikum',
|
||||
title: 'Neubau Technikum Winterthur — 1\'200 m² Bürofläche, Baubewilligung erteilt',
|
||||
areaSqmEstimate: 1200,
|
||||
probability: 0.80,
|
||||
confidenceScore: 0.76,
|
||||
@@ -258,15 +331,24 @@ export const mockFutureSignals: FutureSignal[] = [
|
||||
organizationId: 'org-wincasa',
|
||||
createdAt: '2025-03-05T09:00:00Z',
|
||||
updatedAt: '2025-04-10T10:00:00Z',
|
||||
marketIndicators: [
|
||||
'Baubewilligung für 1\'200 m² Bürofläche am Technikum rechtskräftig erteilt',
|
||||
'Fertigstellung gemäss Baugesuch Q2 2027',
|
||||
'Winterthur Büromarkt: Leerstand Q1 2025 auf historisch tiefem Niveau (4.2%)',
|
||||
],
|
||||
strategicInterpretation: 'Amtlich bestätigte Baubewilligung — sehr hohe Realisierungswahrscheinlichkeit. Zentrale Lage am Technikum mit Bahnhof-Nähe. Vermietungsstart erfahrungsgemäss 9–12 Monate vor Fertigstellung.',
|
||||
confirmedFacts: ['Baubewilligung rechtskräftig erteilt', 'Standort Zentrum Technikum bestätigt', 'Fertigstellung Q2 2027 aus Gesuch'],
|
||||
unconfirmedFacts: ['Ob Vorvermietung bereits läuft', 'Endmietzins pro m²', 'Aufteilung EG/OG'],
|
||||
},
|
||||
|
||||
// --- signal-010: TechHub St.Gallen Expansion → prop-030 ---
|
||||
// --- signal-010: Ostschweiz Digital AG — Büroexpansion St. Gallen ---
|
||||
{
|
||||
id: 'signal-010',
|
||||
signalType: SignalType.EXPANSION,
|
||||
companyName: 'Ostschweiz Digital AG',
|
||||
propertyId: 'prop-030',
|
||||
locationHint: 'St. Gallen, Riethüsli',
|
||||
title: 'Ostschweiz Digital AG wächst — 480 m² Bürofläche in St. Gallen gesucht',
|
||||
areaSqmEstimate: 480,
|
||||
probability: 0.60,
|
||||
confidenceScore: 0.55,
|
||||
@@ -287,43 +369,61 @@ export const mockFutureSignals: FutureSignal[] = [
|
||||
organizationId: 'org-wincasa',
|
||||
createdAt: '2025-04-25T08:00:00Z',
|
||||
updatedAt: '2025-05-10T07:00:00Z',
|
||||
marketIndicators: [
|
||||
'8 Stelleninserate für Software-Entwickler und UX-Designer in St. Gallen in 60 Tagen',
|
||||
'Ostschweiz Digital AG: LinkedIn-Mitarbeiterzahl von 22 auf 31 (+41%) in 9 Monaten',
|
||||
'Digitalwirtschaft Ostschweiz: +28% Beschäftigte 2024 (IHK-Bericht)',
|
||||
],
|
||||
strategicInterpretation: 'Kontinuierliches Teamwachstum über 9 Monate mit Fokus auf St. Gallen deutet auf organische Expansion hin. Kein Hinweis auf Standortwechsel — Erweiterungsfläche am bisherigen Standort oder Umzug in grössere Einheit möglich.',
|
||||
confirmedFacts: ['Region St. Gallen aus Inseraten bestätigt', 'Wachstumstrend seit Q3 2024 nachweisbar', 'Flächentyp Büro aus Stellenprofilen ableitbar'],
|
||||
unconfirmedFacts: ['Ob aktueller Standort Riethüsli oder Neulage', 'Exakter Flächenbedarf', 'Zeitpunkt der Entscheidung'],
|
||||
},
|
||||
|
||||
// --- signal-011: Produktion Münchenbuchsee possible move-out → prop-027 ---
|
||||
// --- signal-011 (Mode Boutique): Mode Boutique Bern AG — Rückgang Altstadt ---
|
||||
{
|
||||
id: 'signal-011',
|
||||
signalType: SignalType.POSSIBLE_MOVE_OUT,
|
||||
companyName: 'Präzisionsmechanik Bern AG',
|
||||
propertyId: 'prop-027',
|
||||
locationHint: 'Münchenbuchsee BE, Industriezone',
|
||||
areaSqmEstimate: 2200,
|
||||
probability: 0.52,
|
||||
confidenceScore: 0.48,
|
||||
timeHorizonMonths: 14,
|
||||
propertyId: 'prop-035',
|
||||
companyName: 'Mode Boutique Bern AG',
|
||||
locationHint: 'Bern Altstadt, Gerechtigkeitsgasse',
|
||||
title: 'Mode Boutique Bern AG — Retailfläche Gerechtigkeitsgasse unter Druck',
|
||||
areaSqmEstimate: 290,
|
||||
probability: 0.62,
|
||||
confidenceScore: 0.58,
|
||||
timeHorizonMonths: 10,
|
||||
source: {
|
||||
type: 'PRESS',
|
||||
url: 'https://example.com/news/pmbern-verlagerung',
|
||||
publishedAt: '2025-03-10',
|
||||
credibility: 'HIGH',
|
||||
type: 'MARKET_DATA',
|
||||
publishedAt: '2025-04-28',
|
||||
credibility: 'MEDIUM',
|
||||
},
|
||||
sensitivityLevel: 'CONFIDENTIAL',
|
||||
disclaimer: 'Pressemeldungen über Verlagerung der Produktion ins Ausland. Kein bestätigter Auszug. Vertraulich behandeln.',
|
||||
riskLevel: RiskLevel.HIGH,
|
||||
marketIndicator: 'Verlagerungsdruck Schweizer Maschinenbau 2025',
|
||||
relevanceScore: 0.58,
|
||||
sensitivityLevel: 'INTERNAL',
|
||||
disclaimer: 'Marktdaten deuten auf mögliche Verkleinerung hin. Kein bestätigter Auszug.',
|
||||
riskLevel: RiskLevel.MEDIUM,
|
||||
marketIndicator: 'Stationärer Handel Bern Altstadt: Leerstand +5% 2024',
|
||||
relevanceScore: 0.66,
|
||||
isVerified: false,
|
||||
expiresAt: '2026-08-01',
|
||||
expiresAt: '2026-04-01',
|
||||
organizationId: 'org-wincasa',
|
||||
createdAt: '2025-03-12T10:00:00Z',
|
||||
updatedAt: '2025-05-09T09:00:00Z',
|
||||
createdAt: '2025-04-28T09:00:00Z',
|
||||
updatedAt: '2025-05-15T10:00:00Z',
|
||||
aiSummary: 'Mode Boutique Bern AG zeigt gemäss LinkedIn-Analyse eine Mitarbeiterreduktion von 12 auf 8 Personen (-33%) innerhalb von 6 Monaten. Das Unternehmen hat ausserdem kürzlich den Sitz auf eine kleinere Adresse in der Berner Innenstadt aktualisiert. Die Kombination aus Stellenabbau und Adressänderung deutet auf eine Verkleinerung des Verkaufsbereichs hin.',
|
||||
marketIndicators: [
|
||||
'Mitarbeiterreduktion von 12 auf 8 Personen (-33%) in 6 Monaten laut LinkedIn',
|
||||
'Berner Altstadt: Frequenzrückgang in der Gerechtigkeitsgasse -11% 2024 (Zählstelle)',
|
||||
'Stationärer Modehandel Bern: Leerstand +5% 2024',
|
||||
],
|
||||
strategicInterpretation: 'Personalabbau zusammen mit sinkendem Passantenaufkommen in der Gerechtigkeitsgasse ergibt ein klares Verkleinerungssignal. Frühzeitige Kontaktaufnahme mit dem Eigentümer sinnvoll.',
|
||||
confirmedFacts: ['Aktueller Standort Gerechtigkeitsgasse bekannt', 'Personalrückgang auf LinkedIn dokumentiert', 'Bern Altstadt Frequenzdaten verfügbar'],
|
||||
unconfirmedFacts: ['Ob Mietvertrag ausläuft oder gekündigt wird', 'Ob Standort aufgegeben oder nur verkleinert', 'Zeitpunkt eines möglichen Auszugs'],
|
||||
},
|
||||
|
||||
// --- signal-012: Basel Hafen Neubau Logistik → prop-024 ---
|
||||
// --- signal-012: Basel Hafen Klybeck — Logistikneubau ---
|
||||
{
|
||||
id: 'signal-012',
|
||||
signalType: SignalType.CONSTRUCTION_PROJECT,
|
||||
propertyId: 'prop-024',
|
||||
locationHint: 'Basel, Hafen Klybeck',
|
||||
title: 'Logistikneubau Rheinhafen Basel — 2\'600 m², Fertigstellung Q2 2026',
|
||||
areaSqmEstimate: 2600,
|
||||
probability: 0.82,
|
||||
confidenceScore: 0.78,
|
||||
@@ -345,15 +445,24 @@ export const mockFutureSignals: FutureSignal[] = [
|
||||
organizationId: 'org-wincasa',
|
||||
createdAt: '2025-01-18T10:00:00Z',
|
||||
updatedAt: '2025-04-20T14:00:00Z',
|
||||
marketIndicators: [
|
||||
'Baubewilligung für 2\'600 m² Logistikfläche am Klybeck-Hafen rechtskräftig erteilt',
|
||||
'Fertigstellung gemäss Baugesuch Q2 2026',
|
||||
'Hafenareal Klybeck: Trimodalität Schiene/Wasser/Strasse als USP',
|
||||
],
|
||||
strategicInterpretation: 'Strategisch guter Logistikstandort mit direktem Rheinhafenzugang und Bahnanschluss. Hohe Realisierungssicherheit durch bereits erteilte Bewilligung. Nachfrage nach Hafen-nahen Logistikflächen in Basel konstant hoch.',
|
||||
confirmedFacts: ['Baubewilligung erteilt und rechtskräftig', 'Standort Klybeck bestätigt', 'Fertigstellung Q2 2026 aus Gesuch', 'Fläche 2\'600 m² bestätigt'],
|
||||
unconfirmedFacts: ['Endmietzins pro m²', 'Ob Kühllager oder Trockenlager', 'Ob Fläche bereits reserviert'],
|
||||
},
|
||||
|
||||
// --- signal-013: Genf La Praille Office Expansion → prop-028 ---
|
||||
// --- signal-013: Geneva Finance Partners SA — Büroexpansion Genf La Praille ---
|
||||
{
|
||||
id: 'signal-013',
|
||||
signalType: SignalType.EXPANSION,
|
||||
companyName: 'Geneva Finance Partners SA',
|
||||
propertyId: 'prop-028',
|
||||
locationHint: 'Genf, La Praille',
|
||||
title: 'Geneva Finance Partners SA expandiert — Bürofläche La Praille gesucht',
|
||||
areaSqmEstimate: 520,
|
||||
probability: 0.58,
|
||||
confidenceScore: 0.53,
|
||||
@@ -374,15 +483,24 @@ export const mockFutureSignals: FutureSignal[] = [
|
||||
organizationId: 'org-wincasa',
|
||||
createdAt: '2025-03-08T11:00:00Z',
|
||||
updatedAt: '2025-05-07T10:00:00Z',
|
||||
marketIndicators: [
|
||||
'Jahresbericht 2024: «Expansion des Genfer Teams auf 40 FTE bis 2026» angekündigt',
|
||||
'Genf Finanzsektor: +15% Büroflächennachfrage 2025 (CBRE-Bericht)',
|
||||
'5 neue Inserate für Senior-Portfoliomanager mit Standort Genf in 30 Tagen',
|
||||
],
|
||||
strategicInterpretation: 'Öffentlich kommunizierte Expansion mit klarem Zielhorizont 2026. La Praille als Standort bietet kostengünstigere Büroflächen als Genf Innenstadt bei guter ÖV-Anbindung. Flächensuche dürfte aktiv beginnen.',
|
||||
confirmedFacts: ['Expansion im Jahresbericht kommuniziert', 'Teamwachstum auf 40 FTE bis 2026 bestätigt', 'Inserate für Genf aktiv'],
|
||||
unconfirmedFacts: ['Ob La Praille oder andere Lage bevorzugt', 'Exakter Flächenbedarf', 'Ob Büros mit Repräsentationscharakter benötigt'],
|
||||
},
|
||||
|
||||
// --- signal-014: Frenkendorf Lager possible move-out → prop-029 ---
|
||||
// --- signal-014: Schweizer Grosshandel AG — möglicher Auszug Frenkendorf ---
|
||||
{
|
||||
id: 'signal-014',
|
||||
signalType: SignalType.POSSIBLE_MOVE_OUT,
|
||||
companyName: 'Schweizer Grosshandel AG',
|
||||
propertyId: 'prop-029',
|
||||
locationHint: 'Frenkendorf BL, Lager Nord',
|
||||
title: 'Schweizer Grosshandel AG — Lagerfläche Frenkendorf unter Konsolidierungsdruck',
|
||||
areaSqmEstimate: 3500,
|
||||
probability: 0.50,
|
||||
confidenceScore: 0.46,
|
||||
@@ -402,14 +520,23 @@ export const mockFutureSignals: FutureSignal[] = [
|
||||
organizationId: 'org-wincasa',
|
||||
createdAt: '2025-04-07T08:00:00Z',
|
||||
updatedAt: '2025-05-08T09:00:00Z',
|
||||
marketIndicators: [
|
||||
'Mutterkonzern hat Konsolidierung der Lagerstandorte Nordwestschweiz angekündigt',
|
||||
'Mitarbeiterzahl Frenkendorf: von 54 auf 44 (-19%) in 8 Monaten',
|
||||
'Grosshandel CH: Lagerflächennachfrage 2024 stagniert — Kostendruck durch E-Commerce',
|
||||
],
|
||||
strategicInterpretation: 'Strukturelle Konsolidierung im Grosshandel trifft Nebenstandorte wie Frenkendorf zuerst. Kein Auszugstermin bekannt — Signal noch in früher Phase. Eigentümer sollte Kontakt suchen.',
|
||||
confirmedFacts: ['Konsolidierungsankündigung des Mutterkonzerns publiziert', 'Personalrückgang in Frenkendorf dokumentiert', 'Aktueller Standort Frenkendorf bekannt'],
|
||||
unconfirmedFacts: ['Welche Lagerstandorte konkret betroffen', 'Zeitpunkt eines möglichen Auszugs', 'Ob Fläche ganz oder teilweise aufgegeben'],
|
||||
},
|
||||
|
||||
// --- signal-015: Bern Tech Campus Expansion ---
|
||||
// --- signal-015: BernTech Innovation AG — Büroexpansion Bern Breitenrain ---
|
||||
{
|
||||
id: 'signal-015',
|
||||
signalType: SignalType.EXPANSION,
|
||||
companyName: 'BernTech Innovation AG',
|
||||
locationHint: 'Bern, Breitenrain',
|
||||
title: 'BernTech Innovation AG sucht ~1\'800 m² Bürofläche in Bern Breitenrain',
|
||||
areaSqmEstimate: 1800,
|
||||
probability: 0.68,
|
||||
confidenceScore: 0.64,
|
||||
@@ -430,34 +557,13 @@ export const mockFutureSignals: FutureSignal[] = [
|
||||
organizationId: 'org-wincasa',
|
||||
createdAt: '2025-04-28T09:00:00Z',
|
||||
updatedAt: '2025-05-10T08:00:00Z',
|
||||
},
|
||||
|
||||
// --- signal-011: Mode Boutique Bern possible move-out Altstadt ---
|
||||
{
|
||||
id: 'signal-011',
|
||||
signalType: SignalType.POSSIBLE_MOVE_OUT,
|
||||
propertyId: 'prop-035',
|
||||
companyName: 'Mode Boutique Bern AG',
|
||||
locationHint: 'Bern Altstadt, Gerechtigkeitsgasse',
|
||||
areaSqmEstimate: 290,
|
||||
probability: 0.62,
|
||||
confidenceScore: 0.58,
|
||||
timeHorizonMonths: 10,
|
||||
source: {
|
||||
type: 'MARKET_DATA',
|
||||
publishedAt: '2025-04-28',
|
||||
credibility: 'MEDIUM',
|
||||
},
|
||||
sensitivityLevel: 'INTERNAL',
|
||||
disclaimer: 'Marktdaten deuten auf mögliche Verkleinerung hin. Kein bestätigter Auszug.',
|
||||
riskLevel: RiskLevel.MEDIUM,
|
||||
marketIndicator: 'Stationärer Handel Bern Altstadt: Leerstand +5% 2024',
|
||||
relevanceScore: 0.66,
|
||||
isVerified: false,
|
||||
expiresAt: '2026-04-01',
|
||||
organizationId: 'org-wincasa',
|
||||
createdAt: '2025-04-28T09:00:00Z',
|
||||
updatedAt: '2025-05-15T10:00:00Z',
|
||||
aiSummary: 'Mode Boutique Bern AG zeigt gemäss LinkedIn-Analyse eine Mitarbeiterreduktion von 12 auf 8 Personen (-33%) innerhalb von 6 Monaten. Das Unternehmen hat ausserdem kürzlich den Sitz auf eine kleinere Adresse in der Berner Innenstadt aktualisiert. Die Kombination aus Stellenabbau und Adressänderung deutet auf eine Verkleinerung des Verkaufsbereichs hin.',
|
||||
marketIndicators: [
|
||||
'18 Stelleninserate in 60 Tagen — Fokus Software-Engineering und Product Management',
|
||||
'BernTech Innovation AG: VC-Finanzierungsrunde CHF 12 Mio. im März 2025 bestätigt',
|
||||
'Bern Tech-Ökosystem: Risikokapital +40% 2024 — stärkstes Wachstum seit 2019',
|
||||
],
|
||||
strategicInterpretation: 'Frisch finanziertes Wachstumsunternehmen mit aktivem Stellenaufbau. VC-Finanzierung als starkes Signal für konkreten Flächenbedarf — Budgets vorhanden. Breitenrain als Tech-Standort in Bern etabliert.',
|
||||
confirmedFacts: ['VC-Finanzierungsrunde CHF 12 Mio. öffentlich bestätigt', 'Region Bern aus Inseraten bestätigt', 'Wachstumstrend seit Q1 2025 sichtbar'],
|
||||
unconfirmedFacts: ['Exakter Flächenbedarf (Schätzung: 1\'500–2\'000 m²)', 'Ob Breitenrain oder andere Berner Lage bevorzugt', 'Zeitpunkt Einzug'],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -172,7 +172,7 @@ export default function MatchDetail() {
|
||||
sx={{ bgcolor: '#f1f5f9', color: '#475569', fontWeight: 700, letterSpacing: 0.5 }}
|
||||
/>
|
||||
{isFuture && (
|
||||
<Chip label="Probabilistisches Signal" size="small" sx={{ bgcolor: '#faf5ff', color: '#7c3aed', fontWeight: 600 }} />
|
||||
<Chip label="Future Availability Signal" size="small" sx={{ bgcolor: '#faf5ff', color: '#7c3aed', fontWeight: 600 }} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function Results() {
|
||||
const { currentUser } = useSessionStore()
|
||||
const [filterSource, setFilterSource] = useState<FilterSource>('ALL')
|
||||
const [sortBy, setSortBy] = useState<SortBy>('score')
|
||||
const [showSchattenmarkt, setShowSchattenmarkt] = useState(true)
|
||||
const [showFutureAvailability, setShowFutureAvailability] = useState(true)
|
||||
const [showOwnProperties, setShowOwnProperties] = useState(false)
|
||||
const [view, setView] = useState<'list' | 'grid'>(() =>
|
||||
(localStorage.getItem('view-results') as 'list' | 'grid') ?? 'list'
|
||||
@@ -73,8 +73,8 @@ export default function Results() {
|
||||
|
||||
const filtered = results.filter(r => {
|
||||
if (r.resultType === 'VERIFIED_PORTFOLIO') return showOwnProperties && currentUser?.role === 'PROPERTY_MANAGER'
|
||||
// Schattenmarkt toggle is independent of the source filter
|
||||
if (r.resultType === 'FUTURE_AVAILABILITY') return showSchattenmarkt
|
||||
// Future Availability toggle is independent of the source filter
|
||||
if (r.resultType === 'FUTURE_AVAILABILITY') return showFutureAvailability
|
||||
return filterSource === 'ALL' || r.resultType === filterSource
|
||||
})
|
||||
|
||||
@@ -176,8 +176,8 @@ export default function Results() {
|
||||
onFilterChange={setFilterSource}
|
||||
sortBy={sortBy}
|
||||
onSortChange={setSortBy}
|
||||
showSchattenmarkt={showSchattenmarkt}
|
||||
onShowSchattenmarktChange={setShowSchattenmarkt}
|
||||
showFutureAvailability={showFutureAvailability}
|
||||
onShowFutureAvailabilityChange={setShowFutureAvailability}
|
||||
showOwnProperties={showOwnProperties}
|
||||
onShowOwnPropertiesChange={setShowOwnProperties}
|
||||
isPropertyManager={currentUser?.role === 'PROPERTY_MANAGER'}
|
||||
@@ -186,7 +186,7 @@ export default function Results() {
|
||||
{isLoading ? (
|
||||
<FeedSkeleton />
|
||||
) : sorted.length === 0 ? (
|
||||
<FeedEmptyState filtered={filterSource !== 'ALL' || !showSchattenmarkt || !showOwnProperties} />
|
||||
<FeedEmptyState filtered={filterSource !== 'ALL' || !showFutureAvailability || !showOwnProperties} />
|
||||
) : (
|
||||
<UnifiedResultFeed results={sorted} view={view} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user