diff --git a/src/components/match-card/IntelligenceMatchCard.tsx b/src/components/match-card/IntelligenceMatchCard.tsx index 06a7abb..4a9827d 100644 --- a/src/components/match-card/IntelligenceMatchCard.tsx +++ b/src/components/match-card/IntelligenceMatchCard.tsx @@ -1,15 +1,214 @@ -import { Alert, Box, Button, Chip, Divider, Typography } from '@mui/material' -import { CheckCircle2 } from 'lucide-react' +import { Alert, Box, Button, Chip, Divider, LinearProgress, Tooltip, Typography } from '@mui/material' +import { + BarChart2, + Briefcase, + Building2, + CheckCircle2, + FileCheck, + FileText, + Newspaper, + ShieldCheck, + TrendingUp, + User, + Zap, +} from 'lucide-react' import { LocationPreview } from '../shared/LocationPreview' import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme' import type { MatchCardViewModel } from './MatchCardViewModel' +// ── Constants ───────────────────────────────────────────────────────────────── + const RESULT_TYPE_META: Record = { VERIFIED_PORTFOLIO: { label: 'Verified Portfolio', color: '#1e3a5f' }, EXTERNAL_MARKET: { label: 'Marktinserat', color: '#d97706' }, - FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#7c3aed' }, + FUTURE_AVAILABILITY: { label: 'Schattenmarkt', color: '#7c3aed' }, } +const SOURCE_META: Record = { + JOB_POSTING: { label: 'Stelleninserate', icon: }, + PRESS: { label: 'Pressebericht', icon: }, + CONSTRUCTION_PERMIT:{ label: 'Baubewilligung', icon: }, + COMPANY_REPORT: { label: 'Geschäftsbericht',icon: }, + MARKET_DATA: { label: 'Marktdaten', icon: }, + MANUAL: { label: 'Analyst', icon: }, +} + +const SIGNAL_TYPE_LABELS: Record = { + EXPANSION: 'Expansion', + POSSIBLE_MOVE_OUT: 'Möglicher Auszug', + CONSTRUCTION_PROJECT:'Bauprojekt', + RESTRUCTURING: 'Restrukturierung', + PROJECT_DEVELOPMENT: 'Projektentwicklung', + SPACE_CONSOLIDATION: 'Flächenkonsolidierung', +} + +const CREDIBILITY_META: Record = { + 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 ( + + {/* Gradient hero background */} + + {/* Subtle pattern overlay */} + + + {/* Company initials */} + + + {initials} + + + + {/* Signal type label */} + + + + {signalLabel} + + + + {/* Verified badge */} + {vm.signalIsVerified && ( + + + Verifiziert + + )} + + + {/* Score badge — overlaid top-left */} + + + {vm.matchScore}% + + {tier !== 'bronze' && ( + + {theme.label} + + )} + + + {/* Schattenmarkt chip — top-right */} + + + + + ) +} + +// ── 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 ( + + {/* Source + credibility row */} + + {sourceMeta && ( + + {sourceMeta.icon} + {sourceMeta.label} + + )} + {credMeta && ( + + + + + {vm.signalSourceCredibility === 'HIGH' ? 'Hoch' : vm.signalSourceCredibility === 'MEDIUM' ? 'Mittel' : 'Niedrig'} + + + + )} + + + {/* Probability bar */} + {probPct !== null && ( + + + Eintretenswahrscheinlichkeit + = 70 ? '#1a7a4a' : probPct >= 50 ? '#d97706' : '#c0392b' }}> + {probPct}% + + + = 70 ? '#1a7a4a' : probPct >= 50 ? '#d97706' : '#c0392b', + borderRadius: 2, + }, + }} + /> + + )} + + {/* Market indicator */} + {vm.signalMarketIndicator && ( + + + + {vm.signalMarketIndicator} + + + )} + + ) +} + +// ── Main component ──────────────────────────────────────────────────────────── + interface Props { vm: MatchCardViewModel imageUrl?: string @@ -23,68 +222,61 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro const theme = SCORE_THEME[tier] const rt = RESULT_TYPE_META[vm.resultType] ?? { label: vm.resultType, color: '#64748b' } const topReason = vm.reasons[0] - const isFuture = vm.resultType === 'FUTURE_AVAILABILITY' return ( - - {/* Image zone */} - - + - {/* Score badge — overlaid top-left */} - + ) : ( + + + - - {vm.matchScore}% - - {tier !== 'bronze' && ( - - {theme.label} + minWidth: 52, textAlign: 'center', + }}> + + {vm.matchScore}% - )} + {tier !== 'bronze' && ( + + {theme.label} + + )} + + + + + )} - {/* Result type chip — overlaid top-right */} - - - - + {/* ── Signal metadata (Schattenmarkt only) ── */} + {isFuture && } - {/* Card body */} - + {/* ── Card body ── */} + {vm.title} @@ -92,22 +284,32 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro {[vm.locationLabel, vm.availabilityLabel].filter(Boolean).join(' · ')} - {vm.explainabilitySummary && ( + {/* AI Signal Summary (Schattenmarkt) */} + {isFuture && vm.aiSignalSummary && ( + <> + + + + + {vm.aiSignalSummary} + + + + )} + + {/* Regular explainability summary (non-future) */} + {!isFuture && vm.explainabilitySummary && ( <> - + {vm.explainabilitySummary} @@ -125,7 +327,6 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro )} - {/* Actions */} {vm.actions.map(a => ( @@ -136,13 +337,10 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro onClick={a.onClick} disabled={a.disabled} sx={{ - textTransform: 'none', - fontSize: '0.7rem', - py: 0.375, - px: 1, + textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1, ...(a.variant === 'primary' && { - bgcolor: '#1e3a5f', - '&:hover': { bgcolor: '#162d4a' }, + bgcolor: isFuture ? '#7c3aed' : '#1e3a5f', + '&:hover': { bgcolor: isFuture ? '#6d28d9' : '#162d4a' }, }), }} > @@ -152,7 +350,7 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro - {/* FUTURE_AVAILABILITY disclaimer */} + {/* Disclaimer */} {vm.disclaimer && ( {vm.disclaimer} diff --git a/src/components/match-card/MatchCardViewModel.ts b/src/components/match-card/MatchCardViewModel.ts index a8a667a..ede5bd4 100644 --- a/src/components/match-card/MatchCardViewModel.ts +++ b/src/components/match-card/MatchCardViewModel.ts @@ -49,6 +49,15 @@ export interface MatchCardViewModel { explainabilitySummary?: string 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 isSelected?: boolean isCompareSelected?: boolean diff --git a/src/domain/futureSignal.ts b/src/domain/futureSignal.ts index 57488e7..f99b107 100644 --- a/src/domain/futureSignal.ts +++ b/src/domain/futureSignal.ts @@ -42,4 +42,5 @@ export interface FutureSignal { evidence?: SignalEvidence // structured evidence block 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 } diff --git a/src/features/matching/matchCardAdapter.ts b/src/features/matching/matchCardAdapter.ts index 0bcbfbf..ad7cb86 100644 --- a/src/features/matching/matchCardAdapter.ts +++ b/src/features/matching/matchCardAdapter.ts @@ -111,5 +111,13 @@ export function buildMatchCardViewModel( taxCalculatorUrl, isReviewRequired: match.status === 'PENDING_REVIEW', 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, } } diff --git a/src/mock-data/futureSignals.ts b/src/mock-data/futureSignals.ts index 391e7fb..9c36d5c 100644 --- a/src/mock-data/futureSignals.ts +++ b/src/mock-data/futureSignals.ts @@ -28,6 +28,7 @@ export const mockFutureSignals: FutureSignal[] = [ organizationId: 'org-wincasa', 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.', }, // --- signal-002: Helvetia Produktion possible move-out Reinach --- @@ -57,6 +58,7 @@ export const mockFutureSignals: FutureSignal[] = [ organizationId: 'org-wincasa', 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.', }, // --- signal-003: Bern Wankdorf Neubau Büro/Gewerbe --- @@ -85,6 +87,7 @@ export const mockFutureSignals: FutureSignal[] = [ organizationId: 'org-wincasa', 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.', }, // --- signal-004: Pharma-Biotech Basel Expansion --- @@ -113,6 +116,7 @@ export const mockFutureSignals: FutureSignal[] = [ organizationId: 'org-wincasa', 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.', }, // --- signal-005: Finanz AG Zürich-Nord possible move-out → prop-023 --- @@ -141,6 +145,7 @@ export const mockFutureSignals: FutureSignal[] = [ organizationId: 'org-wincasa', 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.', }, // --- signal-006: Luzern Inseli Neubau Gewerbe ---