diff --git a/src/components/match-card/IntelligenceMatchCard.tsx b/src/components/match-card/IntelligenceMatchCard.tsx
index 74ee9c5..4146fbc 100644
--- a/src/components/match-card/IntelligenceMatchCard.tsx
+++ b/src/components/match-card/IntelligenceMatchCard.tsx
@@ -258,7 +258,6 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro
const tier = getScoreTier(vm.matchScore)
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 (
@@ -360,14 +359,23 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro
>
)}
- {topReason && (
+ {vm.reasons.length > 0 && (
<>
-
-
-
- {topReason.label}
-
+
+ {vm.reasons.slice(0, 3).map((r, i) => (
+
+
+
+
+ {r.label}
+
+
+ {r.explanation}
+
+
+
+ ))}
>
)}
diff --git a/src/components/match-detail/FutureAvailabilityContextPanel.tsx b/src/components/match-detail/FutureAvailabilityContextPanel.tsx
index 09c251d..ae1818b 100644
--- a/src/components/match-detail/FutureAvailabilityContextPanel.tsx
+++ b/src/components/match-detail/FutureAvailabilityContextPanel.tsx
@@ -1,4 +1,4 @@
-import { Alert, Box, Button, Chip, Divider, Paper, Typography } from '@mui/material'
+import { Box, Button, Chip, Divider, Paper, Typography } from '@mui/material'
import {
BarChart2,
Briefcase,
@@ -20,6 +20,7 @@ const SIGNAL_TYPE_LABELS: Record = {
RESTRUCTURING: 'Restrukturierung',
PROJECT_DEVELOPMENT: 'Projektentwicklung',
SPACE_CONSOLIDATION: 'Flächenkonsolidierung',
+ LEASE_EXPIRY: 'Vertragsende (Schattenmarkt)',
}
const SIGNAL_RELEVANCE_BRIDGE: Record = {
@@ -29,6 +30,7 @@ const SIGNAL_RELEVANCE_BRIDGE: Record = {
RESTRUCTURING: 'Restrukturierung deutet auf Flächenänderungen hin.',
PROJECT_DEVELOPMENT: 'Projektentwicklung könnte neue Gewerbeflächen schaffen.',
SPACE_CONSOLIDATION: 'Konsolidierung — Teilflächen könnten frei werden.',
+ LEASE_EXPIRY: 'Die Verwaltung hat dieses Objekt für den Markt freigegeben — Vertragsende bekannt und bestätigt.',
}
const SOURCE_META: Record = {
@@ -38,6 +40,7 @@ const SOURCE_META: Record = {
COMPANY_REPORT: { label: 'Geschäftsbericht', icon: },
MARKET_DATA: { label: 'Marktdaten', icon: },
MANUAL: { label: 'Analyst', icon: },
+ LEASE_CONTRACT: { label: 'Vertrag verifiziert (ERP)', icon: },
}
const CREDIBILITY_META: Record = {
@@ -65,11 +68,11 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
const sensitiveMeta = SENSITIVITY_META[signal.sensitivityLevel] ?? SENSITIVITY_META.PUBLIC
const relevanceBridge = signal.signalType ? (SIGNAL_RELEVANCE_BRIDGE[signal.signalType] ?? null) : null
const probPct = Math.round(signal.probability * 100)
- const confPct = Math.round(signal.confidenceScore * 100)
+ const isVerifiedContract = signal.isVerified && signal.source.type === 'LEASE_CONTRACT'
return (
-
+
Schattenmarkt-Signal
{signal.isVerified && (
@@ -80,23 +83,87 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
)}
- {/* Mandatory disclaimer */}
-
- {signal.disclaimer}
-
+ {/* 1. Quelle & Verlässlichkeit — FIRST */}
+
+ Quelle & Verlässlichkeit
- {/* Relevance bridge */}
- {relevanceBridge && (
-
-
-
- Warum erscheint dieses Signal in Ihrer Suche?
- {relevanceBridge}
-
+
+ {sourceMeta && (
+
+ {sourceMeta.icon}
+ {sourceMeta.label}
+
+ )}
+ {credMeta && (
+
+
+ {credMeta.label}
+
+ )}
+ {signal.source.publishedAt && (
+
+ Publiziert: {new Date(signal.source.publishedAt).toLocaleDateString('de-CH', { day: '2-digit', month: 'long', year: 'numeric' })}
+
+ )}
- )}
- {/* Key metrics row */}
+ {isVerifiedContract && (
+
+
+
+ Vertragsende aus internem ERP bestätigt
+
+
+
+ Verwaltung hat Marktfreigabe erteilt
+
+
+ )}
+
+ {signal.source.url && (
+
+ }
+ sx={{ textTransform: 'none', color: '#7c3aed', borderColor: '#c4b5fd', '&:hover': { borderColor: '#7c3aed', bgcolor: '#faf5ff' } }}
+ >
+ Originalquelle öffnen
+
+
+ )}
+
+ {signal.evidence?.sourceUrls && signal.evidence.sourceUrls.length > 0 && (
+
+
+ Weitere Belege ({signal.evidence.sourceUrls.length})
+
+
+ {signal.evidence.sourceUrls.map((url, i) => (
+
+
+ {url}
+
+ ))}
+
+
+ )}
+
+
+
+
+ {/* 2. Signal-Kenndaten */}
{signal.signalType && (
@@ -108,10 +175,6 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
Wahrscheinlichkeit
= 70 ? '#1a7a4a' : probPct >= 50 ? '#d97706' : '#c0392b' }}>{probPct}%
-
- Konfidenz
- {confPct}%
-
Zeithorizont
~{signal.timeHorizonMonths} Monate
@@ -122,15 +185,11 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
~{signal.areaSqmEstimate.toLocaleString('de-CH')} m²
)}
-
- Vertraulichkeit
-
-
- {/* AI Summary */}
+ {/* 3. KI-Analyse */}
{signal.aiSummary && (
@@ -145,7 +204,18 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
)}
- {/* Evidence */}
+ {/* 4. Relevance bridge */}
+ {relevanceBridge && (
+
+
+
+ Warum erscheint dieses Signal in Ihrer Suche?
+ {relevanceBridge}
+
+
+ )}
+
+ {/* 5. Evidence */}
{signal.evidence?.summary && (
Evidenz
@@ -155,66 +225,16 @@ export function FutureAvailabilityContextPanel({ match: _match, signal }: Props)
)}
-
-
- {/* Source section */}
- Quelle
-
- {sourceMeta && (
-
- {sourceMeta.icon}
- {sourceMeta.label}
-
- )}
- {credMeta && (
-
-
- {credMeta.label}
-
- )}
- {signal.source.publishedAt && (
-
- Publiziert: {new Date(signal.source.publishedAt).toLocaleDateString('de-CH', { day: '2-digit', month: 'long', year: 'numeric' })}
-
- )}
- {signal.source.url && (
- }
- sx={{ textTransform: 'none', color: '#7c3aed', borderColor: '#c4b5fd', '&:hover': { borderColor: '#7c3aed', bgcolor: '#faf5ff' } }}
- >
- Originalquelle öffnen
-
- )}
+ {/* 6. Vertraulichkeit */}
+
+ Vertraulichkeit:
+
- {signal.evidence?.sourceUrls && signal.evidence.sourceUrls.length > 0 && (
-
-
- Weitere Belege ({signal.evidence.sourceUrls.length})
-
-
- {signal.evidence.sourceUrls.map((url, i) => (
-
-
- {url}
-
- ))}
-
-
- )}
+ {/* 7. Disclaimer — grey footnote */}
+
+ {signal.disclaimer}
+
)
}
diff --git a/src/components/match-detail/ScoreBreakdownPanel.tsx b/src/components/match-detail/ScoreBreakdownPanel.tsx
index c4429de..38e45a8 100644
--- a/src/components/match-detail/ScoreBreakdownPanel.tsx
+++ b/src/components/match-detail/ScoreBreakdownPanel.tsx
@@ -1,6 +1,21 @@
import { Box, Divider, LinearProgress, Link, Paper, Typography } from '@mui/material'
-import { ExternalLink } from 'lucide-react'
+import { CheckCircle2, ExternalLink, ShieldCheck, X } from 'lucide-react'
import type { Match } from '../../domain/match'
+import type { FutureSignal } from '../../domain/futureSignal'
+
+// ── Shared helpers ─────────────────────────────────────────────────────────────
+
+function scoreColor(v: number): 'success' | 'warning' | 'error' {
+ return v >= 70 ? 'success' : v >= 50 ? 'warning' : 'error'
+}
+
+const CREDIBILITY_LABELS: Record = {
+ HIGH: 'Hohe Quellenqualität',
+ MEDIUM: 'Mittlere Quellenqualität',
+ LOW: 'Niedrige Quellenqualität',
+}
+
+// ── Standard breakdown (VERIFIED_PORTFOLIO / EXTERNAL / MAISON) ──────────────
interface BreakdownRowProps {
label: string
@@ -40,18 +55,174 @@ function BreakdownRow({ label, value, max, description, color = 'primary', modif
)
}
+// ── Signal score derivation (FUTURE_AVAILABILITY) ─────────────────────────────
+
+interface SignalScoreDerivationProps {
+ match: Match
+ signal: FutureSignal | null
+}
+
+function SignalScoreDerivation({ match, signal }: SignalScoreDerivationProps) {
+ const sb = match.scoreBreakdown
+ const isVerifiedContract = signal?.isVerified && signal?.source?.type === 'LEASE_CONTRACT'
+ const probPct = signal ? Math.round(signal.probability * 100) : null
+ const credLabel = signal ? (CREDIBILITY_LABELS[signal.source.credibility] ?? signal.source.credibility) : null
+
+ // Approximate signal deduction for display purposes
+ const signalDeduction = signal && !isVerifiedContract
+ ? Math.round((1 - signal.probability) * sb.hardMatchScore * 0.18)
+ : 0
+
+ return (
+ <>
+ {/* Schritt 1 — Kriterien-Match */}
+
+
+ Schritt 1 — Kriterien-Match
+
+
+ Wie gut passt die Fläche, wenn das Signal eintrifft?
+
+
+ {match.positiveFactors.map((f, i) => (
+
+
+
+
+ {f.criterion}
+ {f.score}/100
+
+
+
+
+ {f.explanation}
+
+
+ ))}
+
+ {(match.negativeFactors ?? []).map((f, i) => (
+
+
+
+
+ {f.criterion}
+ {f.score}/100
+
+
+
+
+ {f.explanation}
+
+
+ ))}
+
+
+ Basis-Score
+ {sb.hardMatchScore}/100
+
+
+
+
+
+ {/* Schritt 2 — Verlässlichkeit */}
+
+
+ Schritt 2 — Verlässlichkeit des Signals
+
+
+ {isVerifiedContract ? (
+ <>
+
+
+
+ Vertragsende aus ERP verifiziert — keine Schätzung
+
+
+
+
+
+ Verwaltung hat Freigabe erteilt
+
+
+
+ Signal-Abschlag
+ keiner
+
+ >
+ ) : (
+ <>
+ {probPct !== null && (
+
+ Eintretenswahrscheinlichkeit: {probPct}%
+ –{Math.round(signalDeduction * 0.7)} Pkt.
+
+ )}
+ {credLabel && (
+
+ Quellenqualität: {credLabel}
+ –{Math.round(signalDeduction * 0.3)} Pkt.
+
+ )}
+
+ Signal-Abschlag
+ –{signalDeduction} Punkte
+
+ >
+ )}
+
+
+
+
+ {/* Gesamt */}
+
+ Gesamt-Score
+ = 78 ? '#1a7a4a' : sb.totalScore >= 52 ? '#d97706' : '#c0392b',
+ }}
+ >
+ {sb.totalScore}/100
+
+
+ >
+ )
+}
+
+// ── Public component ───────────────────────────────────────────────────────────
+
interface Props {
match: Match
taxCalculatorUrl?: string
+ isFuture?: boolean
+ signal?: FutureSignal | null
}
-export function ScoreBreakdownPanel({ match, taxCalculatorUrl }: Props) {
+export function ScoreBreakdownPanel({ match, taxCalculatorUrl, isFuture, signal }: Props) {
const sb = match.scoreBreakdown
- const hardColor: 'success' | 'warning' | 'error' =
- sb.hardMatchScore >= 70 ? 'success' : sb.hardMatchScore >= 50 ? 'warning' : 'error'
- const softColor: 'success' | 'warning' | 'error' =
- sb.softFactorScore >= 70 ? 'success' : sb.softFactorScore >= 50 ? 'warning' : 'error'
+ if (isFuture) {
+ return (
+
+ Bewertungsherleitung
+
+
+ )
+ }
+
+ const hardColor = scoreColor(sb.hardMatchScore)
+ const softColor = scoreColor(sb.softFactorScore)
return (
diff --git a/src/features/matching/matchCardAdapter.ts b/src/features/matching/matchCardAdapter.ts
index 5dabf18..0cd60fe 100644
--- a/src/features/matching/matchCardAdapter.ts
+++ b/src/features/matching/matchCardAdapter.ts
@@ -15,29 +15,12 @@ import { getCityIntelligence } from '../../lib/locationIntelligence'
const HARD_CRITERIA = new Set(['area', 'location', 'budget', 'timing'])
function buildReasons(positiveFactors: ScoreFactor[]): MatchCardReason[] {
- const reasons: MatchCardReason[] = []
-
- const hardFact = positiveFactors.find(f => HARD_CRITERIA.has(f.criterion))
- const softFact = positiveFactors.find(f => !HARD_CRITERIA.has(f.criterion))
-
- if (hardFact) {
- reasons.push({
- type: 'HARD_FACT',
- label: capitalize(hardFact.criterion),
- explanation: hardFact.explanation,
- score: hardFact.score,
- })
- }
- if (softFact) {
- reasons.push({
- type: 'SOFT_FACTOR',
- label: capitalize(softFact.criterion),
- explanation: softFact.explanation,
- score: softFact.score,
- })
- }
-
- return reasons
+ return positiveFactors.slice(0, 3).map(f => ({
+ type: (HARD_CRITERIA.has(f.criterion) ? 'HARD_FACT' : 'SOFT_FACTOR') as MatchCardReason['type'],
+ label: capitalize(f.criterion),
+ explanation: f.explanation,
+ score: f.score,
+ }))
}
function capitalize(s: string): string {
diff --git a/src/pages/demand/MatchDetail.tsx b/src/pages/demand/MatchDetail.tsx
index 30fa618..c0c0b50 100644
--- a/src/pages/demand/MatchDetail.tsx
+++ b/src/pages/demand/MatchDetail.tsx
@@ -45,12 +45,6 @@ function buildReasons(match: NonNullable['data
}))
}
-function confColor(c: number) {
- return c >= 0.75 ? '#1a7a4a' : c >= 0.55 ? '#d97706' : '#c0392b'
-}
-function dqColor(q: number) {
- return q >= 0.80 ? '#1a7a4a' : q >= 0.60 ? '#d97706' : '#c0392b'
-}
export default function MatchDetail() {
const { matchId } = useParams<{ matchId: string }>()
@@ -108,10 +102,6 @@ export default function MatchDetail() {
const location = city
? `${city}${property?.location?.district ? `, ${property.location.district}` : ''}`
: signal?.locationHint ?? '–'
- const dqScore = property?.dataQuality?.score ?? signal?.confidenceScore ?? 0.5
- const confPct = Math.round(match.confidenceLevel * 100)
- const dqPct = Math.round(dqScore * 100)
-
// Tax calculator — only shown when AI evaluated taxEnvironment
const allFactors = [...match.positiveFactors, ...(match.negativeFactors ?? [])]
const hasTaxFactor = allFactors.some(f => f.criterion === 'taxEnvironment')
@@ -223,16 +213,6 @@ export default function MatchDetail() {
{property?.assetType && (
)}
-
-
@@ -343,7 +323,7 @@ export default function MatchDetail() {
{/* Sidebar */}
-
+