diff --git a/src/components/match-card/IntelligenceMatchCard.tsx b/src/components/match-card/IntelligenceMatchCard.tsx
index f415162..50ada5f 100644
--- a/src/components/match-card/IntelligenceMatchCard.tsx
+++ b/src/components/match-card/IntelligenceMatchCard.tsx
@@ -63,11 +63,6 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro
{vm.matchScore}%
- {tier !== 'bronze' && (
-
- {theme.label}
-
- )}
diff --git a/src/lib/ds.ts b/src/lib/ds.ts
index c512245..5190c72 100644
--- a/src/lib/ds.ts
+++ b/src/lib/ds.ts
@@ -198,8 +198,8 @@ export function scoreToDataQualityLevel(score: number): DataQualityLevel {
// ── Result type meta — single source for labels + colors ─────────────────────
export const RESULT_TYPE_META: Record = {
- VERIFIED_PORTFOLIO: { label: 'Plattform', color: '#1e3a5f', bg: 'rgba(30,58,95,0.10)' },
- EXTERNAL_MARKET: { label: 'Plattform', color: '#1e3a5f', bg: 'rgba(30,58,95,0.10)' },
- MAISON_WORK: { label: 'Maison Work', color: '#0369a1', bg: 'rgba(3,105,161,0.10)' },
- FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#7c3aed', bg: 'rgba(109,40,217,0.10)' },
+ VERIFIED_PORTFOLIO: { label: 'Verifiziertes Objekt', color: '#1e3a5f', bg: 'rgba(30,58,95,0.10)' },
+ EXTERNAL_MARKET: { label: 'Externes Angebot', color: '#d97706', bg: 'rgba(217,119,6,0.10)' },
+ MAISON_WORK: { label: 'Maison Work', color: '#0369a1', bg: 'rgba(3,105,161,0.10)' },
+ FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#7c3aed', bg: 'rgba(109,40,217,0.10)' },
}
diff --git a/src/pages/demand/MatchDetail.tsx b/src/pages/demand/MatchDetail.tsx
index 7dd587c..de4b94f 100644
--- a/src/pages/demand/MatchDetail.tsx
+++ b/src/pages/demand/MatchDetail.tsx
@@ -1,5 +1,6 @@
+import { useState } from 'react'
import { Box, Button, Chip, CircularProgress, Paper, Typography } from '@mui/material'
-import { ArrowLeft } from 'lucide-react'
+import { AlertTriangle, ArrowLeft, ChevronDown, ChevronUp } from 'lucide-react'
import { useNavigate, useParams } from 'react-router'
import { useCompareStore } from '../../stores/compareStore'
import { usePipelineStore } from '../../stores/pipelineStore'
@@ -23,18 +24,12 @@ import { useMatchDetailData } from '../../hooks/useMatchDetailData'
import { useMatchDetail } from '../../hooks/useMatches'
import { MatchDetailHero } from '../../components/match-detail/MatchDetailHero'
import { MatchDetailPropertySections } from '../../components/match-detail/MatchDetailPropertySections'
+import { RESULT_TYPE_META, DS_TEXT, DS_SURFACE, DS_BORDER, DS_BG } from '../../lib/ds'
-// ── Match helpers ──────────────────────────────────────────────────────────────
+// ── helpers ───────────────────────────────────────────────────────────────────
const HARD_CRITERIA = new Set(['area', 'location', 'budget', 'timing'])
-const RESULT_TYPE_META: Record = {
- VERIFIED_PORTFOLIO: { label: 'Verified Portfolio', color: '#1e3a5f' },
- EXTERNAL_MARKET: { label: 'Direktinserat', color: '#d97706' },
- MAISON_WORK: { label: 'Maison Work', color: '#0369a1' },
- FUTURE_AVAILABILITY: { label: 'Future Availability', color: '#7c3aed' },
-}
-
function buildReasons(match: NonNullable['data']>): MatchCardReason[] {
return match.positiveFactors.slice(0, 3).map(f => ({
type: (HARD_CRITERIA.has(f.criterion) ? 'HARD_FACT' : 'SOFT_FACTOR') as MatchCardReason['type'],
@@ -44,12 +39,14 @@ function buildReasons(match: NonNullable['data
}))
}
+// ── page ──────────────────────────────────────────────────────────────────────
export default function MatchDetail() {
const { matchId } = useParams<{ matchId: string }>()
const navigate = useNavigate()
const { addToCompare } = useCompareStore()
const { openSavedDialog } = usePipelineStore()
+ const [showFullAnalysis, setShowFullAnalysis] = useState(false)
const { match, property, need, signal, isLoading, isFuture } = useMatchDetailData(matchId ?? '')
@@ -79,12 +76,15 @@ export default function MatchDetail() {
const location = city
? `${city}${property?.location?.district ? `, ${property.location.district}` : ''}`
: signal?.locationHint ?? '–'
- // Tax calculator — only shown when AI evaluated taxEnvironment
+
const allFactors = [...match.positiveFactors, ...(match.negativeFactors ?? [])]
const hasTaxFactor = allFactors.some(f => f.criterion === 'taxEnvironment')
const cityIntel = city ? getCityIntelligence(city) : null
const taxCalculatorUrl = hasTaxFactor ? (cityIntel?.taxCalculatorUrl ?? undefined) : undefined
+ const topTradeoff = match.tradeoffs?.[0] ?? match.tradeOffs?.[0]
+ const summary = match.explainabilitySummary || `${match.matchStrength}-Match mit ${match.matchScore} Punkten.`
+
const handleCompare = () => {
if (match && !isFuture && property) {
addToCompare({
@@ -112,7 +112,6 @@ export default function MatchDetail() {
})
}
- // Key facts for the strip below the hero
const keyFacts = isFuture ? [
{ label: 'Flächenschätzung', value: signal?.areaSqmEstimate ? `~${signal.areaSqmEstimate.toLocaleString('de-CH')} m²` : '–' },
{ label: 'Zeithorizont', value: signal?.timeHorizonMonths ? `~${signal.timeHorizonMonths} Monate` : '–' },
@@ -125,12 +124,12 @@ export default function MatchDetail() {
]
return (
-
+
{/* Sticky back nav */}
@@ -138,12 +137,16 @@ export default function MatchDetail() {
startIcon={}
onClick={() => navigate(-1)}
size="small"
- sx={{ color: '#64748b', fontWeight: 500, textTransform: 'none' }}
+ sx={{ color: DS_TEXT.muted, fontWeight: 500, textTransform: 'none' }}
>
Zurück zu Resultaten
{isFuture && (
-
+
)}
@@ -160,76 +163,89 @@ export default function MatchDetail() {
onShortlist={handleShortlist}
/>
- {/* Main content */}
-
+ {/* ── Main content ── */}
+
- {/* Main column */}
-
-
-
-
- {/* ── Property Details ── */}
- {!isFuture && property && }
-
- {reasons.length > 0 && (
-
- Warum dieses Match
-
- {match.negativeFactors.length > 0 && (
-
-
- Schwächere Faktoren
-
-
- {match.negativeFactors.slice(0, 3).map((f, i) => (
-
- · {f.criterion}: {f.explanation}
-
- ))}
-
-
- )}
-
- )}
-
-
-
- {/* Map — always show when image was the hero above */}
- {!isFuture && property?.images?.[0] && property?.location?.coordinates && (
-
-
- Standort
-
- {property.address.street} {property.address.houseNumber}, {property.address.postalCode} {property.address.city}
+ {/* ── Section A: Warum dieser Match? ── */}
+
+ Warum dieser Match?
+ 0 ? 1.5 : 0, lineHeight: 1.65 }}>
+ {summary}
+
+ {reasons.length > 0 && }
+ {topTradeoff && (
+
+
+
+
+ Hauptabwägung
+
+
+ {topTradeoff.concern}
-
-
+
)}
+
-
-
-
+ {/* ── Section B: Nächste Aktion ── */}
+ {}}
+ onReject={() => {}}
+ />
- {isFuture && }
-
+ {/* ── Full analysis toggle ── */}
+
- {/* Sidebar */}
-
-
- {}}
- onReject={() => {}}
- />
-
+ {/* ── Full analysis (hidden by default) ── */}
+ {showFullAnalysis && (
+ <>
+
+
+ {!isFuture && property && }
+
+ {!isFuture && property?.images?.[0] && property?.location?.coordinates && (
+
+
+ Standort
+
+ {property.address.street} {property.address.houseNumber}, {property.address.postalCode} {property.address.city}
+
+
+
+
+ )}
+
+
+
+ {isFuture && }
+
+ >
+ )}
)
diff --git a/src/pages/demand/Results.tsx b/src/pages/demand/Results.tsx
index 71dd824..3a776ff 100644
--- a/src/pages/demand/Results.tsx
+++ b/src/pages/demand/Results.tsx
@@ -1,5 +1,6 @@
import { useState, useMemo, useEffect } from 'react'
import { Box, Button, Card, Typography } from '@mui/material'
+import { Zap } from 'lucide-react'
import { useNavigate, useLocation } from 'react-router'
import { useQueryClient } from '@tanstack/react-query'
import { useUnifiedResults } from '../../hooks/useUnifiedResults'
@@ -117,11 +118,10 @@ export default function Results() {
metrics={[
...(strongCount > 0 ? [{ label: 'starke Treffer (≥80)', value: strongCount, severity: 'positive' as const }] : []),
...(maisonWorkCount > 0 ? [{ label: 'Maison Work', value: maisonWorkCount, severity: 'neutral' as const }] : []),
- ...(futureCount > 0 ? [{ label: 'Zukunftssignale', value: futureCount, severity: 'warning' as const }] : []),
+ ...(futureCount > 0 ? [{ label: 'Zukunftssignale', value: futureCount, severity: 'neutral' as const }] : []),
...(missingDataCount > 0 ? [{ label: 'mit Datenlücken', value: missingDataCount, severity: 'warning' as const }] : []),
]}
risks={[
- ...(futureCount > 0 ? [`${futureCount} Zukunftssignal${futureCount > 1 ? 'e' : ''} sind probabilistisch — keine bestätigte Verfügbarkeit`] : []),
...(missingDataCount > 0 ? [`${missingDataCount} Treffer mit fehlenden Daten — Einschätzung eingeschränkt`] : []),
]}
actions={[
@@ -197,7 +197,26 @@ export default function Results() {
) : sorted.length === 0 ? (
) : (
-
+ <>
+ {futureCount > 0 && showFutureAvailability && (
+
+
+
+
+ {futureCount} Zukunftssignal{futureCount > 1 ? 'e' : ''} in diesen Ergebnissen
+
+
+ Noch nicht öffentlich verfügbare Flächen — gewonnen aus Mietvertrags-Auslaufzeiten, Bauprojekten und Marktbewegungen. Frühere Markttransparenz für Ihre Suche.
+
+
+
+ )}
+
+ >
)}
diff --git a/src/pages/supply/SupplyDashboard.tsx b/src/pages/supply/SupplyDashboard.tsx
index ae63ee1..abd1f6c 100644
--- a/src/pages/supply/SupplyDashboard.tsx
+++ b/src/pages/supply/SupplyDashboard.tsx
@@ -1,15 +1,11 @@
-import { Box, Button, Typography } from '@mui/material'
+import { Box, Button, Chip, Divider, Paper, Typography } from '@mui/material'
+import { ArrowRight, Building2, TrendingUp, Zap } from 'lucide-react'
import { useNavigate } from 'react-router'
import { useSupplyDashboard } from '../../hooks/useSupplyDashboard'
import { useSessionStore } from '../../stores/sessionStore'
import { UserRole } from '../../domain/enums'
-import {
- DashboardHeader,
- KpiGrid,
- ReviewTaskWidget,
- DashboardSkeleton,
-} from '../../components/supply'
-
+import { DashboardHeader, ReviewTaskWidget, DashboardSkeleton } from '../../components/supply'
+import { DS_BG, DS_BORDER, DS_SURFACE, DS_TEXT } from '../../lib/ds'
export default function SupplyDashboard() {
const { data, isLoading, isError, refetch } = useSupplyDashboard()
@@ -18,22 +14,15 @@ export default function SupplyDashboard() {
const role = currentUser?.role ?? UserRole.DEMAND_USER
const isReviewer = role === UserRole.REVIEWER
- const isOwnerViewer = role === UserRole.OWNER_VIEWER
if (isLoading) return
if (isError) {
return (
-
- Dashboard konnte nicht geladen werden
-
-
- Der Service ist vorübergehend nicht verfügbar.
-
-
+ Dashboard konnte nicht geladen werden
+ Der Service ist vorübergehend nicht verfügbar.
+
)
}
@@ -43,43 +32,161 @@ export default function SupplyDashboard() {
Noch keine Objekte vorhanden
- Fügen Sie Ihr erstes Objekt hinzu oder laden Sie Demo-Daten.
+ Fügen Sie Ihr erstes Objekt hinzu — wir matchen es sofort mit aktiven Suchanfragen.
)
}
+ const strongMatchCount = data.strongMatchCount ?? 0
+ const topMatches = data.strongMatches ?? []
+ const futureSignalCount = data.futureSignals?.total ?? 0
+
return (
-
+
- {isOwnerViewer ? (
-
-
-
- Aktive Objekte
+ {/* ── Hero: Nachfrage-Intelligence ── */}
+
+
+ {/* KPI strip */}
+
+ {/* Starke Matches */}
+ 0 ? DS_SURFACE.success.bg : 'white' }}>
+
+ 0 ? DS_TEXT.success : DS_TEXT.muted} />
+ 0 ? DS_TEXT.success : DS_TEXT.muted, fontWeight: 700, lineHeight: 1 }}>
+ Starke Match-Anfragen
+
+
+ 0 ? DS_TEXT.success : DS_TEXT.disabled, lineHeight: 1 }}>
+ {strongMatchCount}
-
- {data.activeProperties} / {data.totalProperties}
+
+ qualifizierte Interessenten ≥ 80%
-
-
- Ø Datenqualität
+
+ {/* Aktive Objekte */}
+ navigate('/supply/properties')}>
+
+
+
+ Aktive Objekte
+
+
+
+ {data.activeProperties}
+
+ / {data.totalProperties}
+
-
- {data.avgDataQuality}%
+ verfügbar · im Bestand
+
+
+ {/* Zukunftssignale */}
+ 0 ? '#faf5ff' : 'white' }}>
+
+ 0 ? '#7c3aed' : DS_TEXT.muted} />
+ 0 ? '#7c3aed' : DS_TEXT.muted, fontWeight: 700, lineHeight: 1 }}>
+ Zukunftssignale
+
+
+ 0 ? '#7c3aed' : DS_TEXT.disabled, lineHeight: 1 }}>
+ {futureSignalCount}
+
+
+ vorqualifizierte Nachfrage-Signale
- ) : (
-
+
+ {/* ── Top matches list ── */}
+ {topMatches.length > 0 && (
+ <>
+
+
+
+ Stärkste Interessenten
+
+
+
+ {topMatches.slice(0, 4).map((m, i) => (
+ 0 ? `1px solid ${DS_BORDER.muted}` : undefined,
+ cursor: 'pointer',
+ '&:hover': { bgcolor: DS_BG.subtle },
+ }}
+ onClick={() => navigate(`/demand/match/${m.matchId}`)}
+ >
+ = 80 ? DS_SURFACE.success.bg : DS_SURFACE.warning.bg,
+ border: `1px solid ${m.matchScore >= 80 ? DS_SURFACE.success.border : DS_SURFACE.warning.border}`,
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
+ }}>
+ = 80 ? DS_TEXT.success : DS_TEXT.warning }}>
+ {m.matchScore}%
+
+
+
+ {m.propertyTitle}
+ {m.topReason}
+
+
+
+
+ ))}
+
+
+
+
+
+ >
+ )}
+
+
+ {/* ── Datenpflege (sekundär, collapsed) ── */}
+ {(data.dataQuality?.critical ?? 0) > 0 && (
+
+
+
+
+ {data.dataQuality!.critical} Objekte mit Datenlücken
+
+
+ Vollständige Daten verbessern die Match-Qualität erheblich.
+
+
+
+
+
)}
{isReviewer && data.reviewTasks !== null && (