feat: F027 screen-level decision design — DecisionContextPanel + Properties + Results
- Add DecisionContextPanel (src/components/ui): compact decision-framing strip with question, metric chips, expandable risks/missing data, actions. Answers: Welche Entscheidung? Welche Daten helfen/fehlen? Welche Risiken? Welche nächste Aktion? - Properties.tsx: replace generic listing header with decision frame: "Welche Objekte sind matchbereit und wo blockieren Datenlücken Matches?" Surfaces: matchbereit count, critical gaps, low-confidence count, stale data, missing field names, risk statements. Primary CTA → Datenpflege. - Results.tsx: add decision frame above result feed: "Welche Treffer lohnen sich für die Shortlist — welche tragen Risiken?" Surfaces: strong-match count, result-type breakdown, data-gap count, future-signal probabilistic risk warning. CTAs → Compare, refine search. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { useNavigate, useLocation } from 'react-router'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useUnifiedResults } from '../../hooks/useUnifiedResults'
|
||||
import { needService } from '../../services/needService'
|
||||
import { DecisionContextPanel } from '../../components/ui'
|
||||
import {
|
||||
FeedEmptyState,
|
||||
FeedSkeleton,
|
||||
@@ -71,9 +72,14 @@ export default function Results() {
|
||||
const verifiedCount = results.filter(r => r.resultType === 'VERIFIED_PORTFOLIO').length
|
||||
const externalCount = results.filter(r => r.resultType === 'EXTERNAL_MARKET').length
|
||||
const futureCount = results.filter(r => r.resultType === 'FUTURE_AVAILABILITY').length
|
||||
const strongCount = results.filter(r => r.matchScore >= 80).length
|
||||
const missingDataCount = results.filter(r =>
|
||||
'match' in r && Array.isArray((r as { match?: { missingData?: unknown[] } }).match?.missingData) &&
|
||||
((r as { match?: { missingData?: unknown[] } }).match?.missingData?.length ?? 0) > 0
|
||||
).length
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
<AddToShortlistDialog />
|
||||
<ResultFeedHeader
|
||||
total={sorted.length}
|
||||
@@ -82,7 +88,29 @@ export default function Results() {
|
||||
futureCount={futureCount}
|
||||
/>
|
||||
|
||||
<Box sx={{ px: 3, py: 3 }}>
|
||||
{!isLoading && results.length > 0 && (
|
||||
<DecisionContextPanel
|
||||
decision="Welche Treffer lohnen sich für die Shortlist — und welche tragen Risiken, die zuerst geprüft werden müssen?"
|
||||
context={activeNeed ? `Suche: ${activeNeed.assetType} · ${activeNeed.requiredArea.min}–${activeNeed.requiredArea.max} m² · ${activeNeed.preferredLocations.join(', ')}` : undefined}
|
||||
metrics={[
|
||||
...(strongCount > 0 ? [{ label: 'starke Treffer (≥80)', value: strongCount, severity: 'positive' as const }] : []),
|
||||
...(verifiedCount > 0 ? [{ label: 'verifiziertes Portfolio', value: verifiedCount, severity: 'neutral' as const }] : []),
|
||||
...(externalCount > 0 ? [{ label: 'externer Markt', value: externalCount, severity: 'neutral' as const }] : []),
|
||||
...(futureCount > 0 ? [{ label: 'Zukunftssignale', value: futureCount, severity: 'warning' 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={[
|
||||
{ label: 'Vergleich öffnen', onClick: () => navigate('/demand/compare') },
|
||||
{ label: 'Suche anpassen', onClick: () => navigate('/demand/ai-search') },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box sx={{ flex: 1, overflowY: 'auto', px: 3, py: 3 }}>
|
||||
{activeNeed && (
|
||||
<Card sx={{ bgcolor: '#eff6ff', p: 2, mb: 2, border: '1px solid #bfdbfe' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 2 }}>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { Box, Drawer } from '@mui/material'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { PageHeader } from '../../components/layout'
|
||||
import { DecisionContextPanel } from '../../components/ui'
|
||||
import { useProperties } from '../../hooks/useProperties'
|
||||
import { PropertyFilterBar, PropertyTable, PropertyDetailView } from '../../components/supply'
|
||||
import type { PropertyTableFilters } from '../../components/supply'
|
||||
@@ -45,18 +47,79 @@ function applyFilters(properties: Property[], filters: PropertyTableFilters): Pr
|
||||
}
|
||||
|
||||
export default function Properties() {
|
||||
const navigate = useNavigate()
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const [filters, setFilters] = useState<PropertyTableFilters>({})
|
||||
|
||||
const { data: properties = [], isLoading, isError } = useProperties()
|
||||
const filtered = applyFilters(properties, filters)
|
||||
|
||||
// Decision-relevant aggregates
|
||||
const matchReady = properties.filter(
|
||||
p => (p.availabilityStatus === 'AVAILABLE_NOW' || p.availabilityStatus === 'AVAILABLE_SOON') &&
|
||||
p.confidenceScore >= 0.7 && p.dataQuality.missingCriticalFields.length === 0
|
||||
)
|
||||
const criticalGaps = properties.filter(p => p.dataQuality.missingCriticalFields.length > 0)
|
||||
const lowConfidence = properties.filter(p => p.confidenceScore < 0.55)
|
||||
const staleOrOutdated = properties.filter(
|
||||
p => p.dataQuality.freshness === 'STALE' || p.dataQuality.freshness === 'OUTDATED'
|
||||
)
|
||||
|
||||
// Unique missing fields across all objects
|
||||
const allMissingFields = [...new Set(
|
||||
properties.flatMap(p => p.dataQuality.missingCriticalFields)
|
||||
)].slice(0, 4)
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
<PageHeader
|
||||
title="Objektverwaltung"
|
||||
subtitle={`${filtered.length} von ${properties.length} Objekten`}
|
||||
/>
|
||||
|
||||
{!isLoading && properties.length > 0 && (
|
||||
<DecisionContextPanel
|
||||
decision="Welche Objekte sind matchbereit — und wo blockieren Datenlücken Matches?"
|
||||
context="Objekte mit fehlenden Pflichtfeldern oder Konfidenz < 55% erscheinen im Match-Center nicht oder mit niedrigem Rang."
|
||||
metrics={[
|
||||
{ label: 'matchbereit', value: matchReady.length, severity: matchReady.length > 0 ? 'positive' : 'warning' },
|
||||
...(criticalGaps.length > 0
|
||||
? [{ label: 'kritische Datenlücken', value: criticalGaps.length, severity: 'critical' as const }]
|
||||
: []
|
||||
),
|
||||
...(lowConfidence.length > 0
|
||||
? [{ label: 'Konfidenz < 55%', value: lowConfidence.length, severity: 'warning' as const }]
|
||||
: []
|
||||
),
|
||||
...(staleOrOutdated.length > 0
|
||||
? [{ label: 'veraltete Daten', value: staleOrOutdated.length, severity: 'warning' as const }]
|
||||
: []
|
||||
),
|
||||
]}
|
||||
missing={allMissingFields.length > 0
|
||||
? [`Fehlende Pflichtfelder bei ${criticalGaps.length} Objekten: ${allMissingFields.join(', ')}`]
|
||||
: []
|
||||
}
|
||||
risks={[
|
||||
...(criticalGaps.length > 0
|
||||
? [`${criticalGaps.length} Objekte werden potenziellen Mietern nicht angezeigt`]
|
||||
: []
|
||||
),
|
||||
...(staleOrOutdated.length > 0
|
||||
? [`${staleOrOutdated.length} Objekte mit veralteten Preisen oder Verfügbarkeiten`]
|
||||
: []
|
||||
),
|
||||
]}
|
||||
actions={[
|
||||
{
|
||||
label: 'Datenpflege starten',
|
||||
primary: criticalGaps.length > 0 || staleOrOutdated.length > 0,
|
||||
onClick: () => navigate('/supply/data-quality'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PropertyFilterBar filters={filters} onFiltersChange={setFilters} />
|
||||
|
||||
<Box sx={{ flex: 1, overflowY: 'auto' }}>
|
||||
|
||||
Reference in New Issue
Block a user