feat: collapsible filter bar, compact property cards, offer wizard field selection
- Properties page: collapsible decision/filter strip (localStorage state), slim single-row Datenpflege status bar (chips + button), 4-column card grid - PropertyIntelligenceCard: reduced image height (160→120px), tighter body padding - OfferWizard (latente Anfragen): new "Felder wählen" step between property selection and PDF preview; uses ReportObjectFieldSelector per property - MockPdfPreview: adds second PDF page showing selected properties side by side with photo, title, location and all chosen hard/soft fact fields - offerWizardStore: adds select_fields step type and fieldSelections state Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { Box, Drawer, useMediaQuery, useTheme } from '@mui/material'
|
||||
import { Box, Button, Chip, Collapse, Drawer, Typography, useMediaQuery, useTheme } from '@mui/material'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { PageHeader } from '../../components/layout'
|
||||
import { DecisionContextPanel } from '../../components/ui'
|
||||
import { ViewToggle } from '../../components/shared'
|
||||
import { useProperties } from '../../hooks/useProperties'
|
||||
import { PropertyFilterBar, PropertyTable, PropertyDetailView, PropertyIntelligenceCard } from '../../components/supply'
|
||||
@@ -56,6 +56,9 @@ export default function Properties() {
|
||||
const [view, setView] = useState<'list' | 'grid'>(() =>
|
||||
(localStorage.getItem('view-properties') as 'list' | 'grid') ?? 'list'
|
||||
)
|
||||
const [headerOpen, setHeaderOpen] = useState(() =>
|
||||
localStorage.getItem('props-header-open') !== 'false'
|
||||
)
|
||||
|
||||
const { data: properties = [], isLoading, isError } = useProperties()
|
||||
|
||||
@@ -103,54 +106,92 @@ export default function Properties() {
|
||||
}
|
||||
/>
|
||||
|
||||
{!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'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{/* Collapse toggle strip */}
|
||||
<Box
|
||||
onClick={() => {
|
||||
const next = !headerOpen
|
||||
setHeaderOpen(next)
|
||||
localStorage.setItem('props-header-open', String(next))
|
||||
}}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
px: 3,
|
||||
py: 0.5,
|
||||
bgcolor: '#f8fafc',
|
||||
borderBottom: '1px solid #e2e8f0',
|
||||
cursor: 'pointer',
|
||||
flexShrink: 0,
|
||||
userSelect: 'none',
|
||||
'&:hover': { bgcolor: '#f1f5f9' },
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontSize: '0.68rem', color: '#94a3b8', letterSpacing: 0.5, textTransform: 'uppercase' }}>
|
||||
Filter & Übersicht
|
||||
</Typography>
|
||||
{headerOpen ? <ChevronUp size={12} color="#94a3b8" /> : <ChevronDown size={12} color="#94a3b8" />}
|
||||
</Box>
|
||||
|
||||
<PropertyFilterBar filters={filters} onFiltersChange={setFilters} />
|
||||
<Collapse in={headerOpen}>
|
||||
{!isLoading && properties.length > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
px: 2.5,
|
||||
py: 0.75,
|
||||
bgcolor: '#f8fafc',
|
||||
borderBottom: '1px solid #e2e8f0',
|
||||
borderLeft: '3px solid #1e3a5f',
|
||||
flexShrink: 0,
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<Chip
|
||||
label={`${matchReady.length} matchbereit`}
|
||||
size="small"
|
||||
sx={{ height: 20, fontSize: '0.68rem', fontWeight: 600,
|
||||
bgcolor: matchReady.length > 0 ? '#f0fdf4' : '#fef9c3',
|
||||
color: matchReady.length > 0 ? '#1a7a4a' : '#92400e' }}
|
||||
/>
|
||||
{criticalGaps.length > 0 && (
|
||||
<Chip
|
||||
label={`${criticalGaps.length} kritische Datenlücken`}
|
||||
size="small"
|
||||
sx={{ height: 20, fontSize: '0.68rem', fontWeight: 600, bgcolor: '#fef2f2', color: '#991b1b' }}
|
||||
/>
|
||||
)}
|
||||
{staleOrOutdated.length > 0 && (
|
||||
<Chip
|
||||
label={`${staleOrOutdated.length} veraltete Daten`}
|
||||
size="small"
|
||||
sx={{ height: 20, fontSize: '0.68rem', fontWeight: 600, bgcolor: '#fef9c3', color: '#92400e' }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Button
|
||||
size="small"
|
||||
variant={criticalGaps.length > 0 || staleOrOutdated.length > 0 ? 'contained' : 'outlined'}
|
||||
onClick={() => navigate('/supply/data-quality')}
|
||||
sx={criticalGaps.length > 0 || staleOrOutdated.length > 0
|
||||
? { bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' }, textTransform: 'none', fontSize: '0.75rem', py: 0.375, px: 1.5, flexShrink: 0 }
|
||||
: { textTransform: 'none', fontSize: '0.75rem', py: 0.375, px: 1.5, flexShrink: 0 }
|
||||
}
|
||||
>
|
||||
Datenpflege
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<PropertyFilterBar filters={filters} onFiltersChange={setFilters} />
|
||||
</Collapse>
|
||||
|
||||
<Box sx={{ flex: 1, overflowY: 'auto' }}>
|
||||
{view === 'grid' ? (
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: 2, p: 2 }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 1.5, p: 1.5 }}>
|
||||
{filtered.map(p => (
|
||||
<PropertyIntelligenceCard key={p.id} property={p} onSelect={setSelectedId} />
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user