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 && (