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:
Benjamin Sutter
2026-05-17 13:56:12 +02:00
parent 4429a7890a
commit 9e7a09f8a2
4 changed files with 253 additions and 2 deletions
+63
View File
@@ -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' }}>