Files
property-match/src/components/supply/PropertyDetailView.tsx
T
Benjamin Sutter e597e81ff5 feat: hide Matchability tab on Inserate detail drawer
PropertyDetailView gets hideTabs prop — MyListings passes ['Matchability']
since the Suchabo chip already covers the match/contact use case inline.
Tab content now keys by name not index, so removal doesn't shift panels.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:13:50 +02:00

195 lines
6.5 KiB
TypeScript

import { useState } from 'react'
import {
Alert,
Box,
Button,
Chip,
CircularProgress,
IconButton,
Tab,
Tabs,
Typography,
} from '@mui/material'
import { Edit2, Save, X } from 'lucide-react'
import type { UpdatePropertyInput } from '../../domain/property'
import { usePropertyById, useUpdateProperty } from '../../hooks/useProperties'
import { PropertyDetailSkeleton } from './PropertyDetailSkeleton'
import { useToastStore } from '../../stores/toastStore'
import {
getAssetTypeColor,
getAssetTypeLabel,
getAvailabilityChipColor,
getAvailabilityLabel,
} from './propertyHelpers'
import { PropertyDetailOverview } from './PropertyDetailOverview'
import { MatchabilityTabPanel } from './MatchabilityTabPanel'
import { PropertyMarketSignalsTab } from './PropertyMarketSignalsTab'
interface PropertyDetailViewProps {
propertyId: string
onClose?: () => void
hideTabs?: Array<'Matchability' | 'Marktsignale'>
}
const ALL_TABS = ['Übersicht', 'Matchability', 'Marktsignale'] as const
export function PropertyDetailView({ propertyId, onClose, hideTabs = [] }: PropertyDetailViewProps) {
const TABS = ALL_TABS.filter(t => !hideTabs.includes(t as 'Matchability' | 'Marktsignale'))
const [tab, setTab] = useState(0)
const [editing, setEditing] = useState(false)
const [draft, setDraft] = useState<UpdatePropertyInput>({})
const { data: property, isLoading } = usePropertyById(propertyId)
const updateProperty = useUpdateProperty()
const showToast = useToastStore(s => s.showToast)
const saving = updateProperty.isPending
function startEdit() {
setDraft({})
setEditing(true)
}
function saveEdit() {
if (!property) return
updateProperty.mutate(
{ id: property.id, input: draft },
{
onSuccess: () => {
setEditing(false)
setDraft({})
showToast('Objekt gespeichert.', 'success')
},
onError: () => {
showToast('Fehler beim Speichern.', 'error')
},
},
)
}
if (isLoading) return <PropertyDetailSkeleton />
if (!property) {
return (
<Box sx={{ p: 3 }}>
<Alert severity="error">Objekt nicht gefunden.</Alert>
</Box>
)
}
const isLowQuality = property.dataQuality.score < 0.6
const hasCriticalGaps = property.dataQuality.missingCriticalFields.length > 0
return (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{/* Header */}
<Box sx={{ p: 2.5, borderBottom: '1px solid #e2e8f0', flexShrink: 0 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
<Box sx={{ display: 'flex', gap: 0.75, flexWrap: 'wrap' }}>
<Chip
label={getAssetTypeLabel(property.assetType)}
size="small"
sx={{ bgcolor: getAssetTypeColor(property.assetType), color: 'white', fontSize: '0.68rem' }}
/>
<Chip
label={getAvailabilityLabel(property.availabilityStatus)}
size="small"
color={getAvailabilityChipColor(property.availabilityStatus)}
variant="outlined"
/>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, ml: 1 }}>
{tab === 0 && !editing && (
<IconButton size="small" onClick={startEdit} title="Bearbeiten">
<Edit2 size={15} />
</IconButton>
)}
{tab === 0 && editing && (
<>
<Button
size="small"
variant="contained"
startIcon={saving ? <CircularProgress size={12} sx={{ color: 'white' }} /> : <Save size={13} />}
onClick={saveEdit}
disabled={saving}
sx={{ textTransform: 'none', fontSize: '0.75rem', bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#16304d' } }}
>
Speichern
</Button>
<Button
size="small"
onClick={() => { setEditing(false); setDraft({}) }}
sx={{ textTransform: 'none', fontSize: '0.75rem' }}
>
Abbrechen
</Button>
</>
)}
{onClose && (
<IconButton size="small" onClick={onClose}>
<X size={16} />
</IconButton>
)}
</Box>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, lineHeight: 1.3, mb: 0.25 }}>
{property.title}
</Typography>
<Typography variant="caption" color="text.secondary">
{property.address.street} {property.address.houseNumber}, {property.address.postalCode} {property.address.city}
</Typography>
{isLowQuality && (
<Alert severity="warning" sx={{ mt: 1, py: 0.25, fontSize: '0.75rem' }}>
Niedrige Datenqualität ({Math.round(property.dataQuality.score * 100)}%) Angaben können unvollständig sein.
</Alert>
)}
{hasCriticalGaps && !isLowQuality && (
<Alert severity="warning" sx={{ mt: 1, py: 0.25, fontSize: '0.75rem' }}>
Kritische Felder fehlen: {property.dataQuality.missingCriticalFields.slice(0, 3).join(', ')}
</Alert>
)}
</Box>
{/* Photo */}
{property.images?.[0] && (
<Box sx={{ flexShrink: 0, height: 160, overflow: 'hidden' }}>
<img
src={property.images[0]}
alt={property.title}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
/>
</Box>
)}
{/* Tabs */}
<Tabs
value={tab}
onChange={(_, v) => setTab(v)}
sx={{
borderBottom: '1px solid #e2e8f0',
flexShrink: 0,
'& .MuiTab-root': { textTransform: 'none', fontSize: '0.8rem', minWidth: 'auto', px: 2 },
}}
>
{TABS.map(label => (
<Tab key={label} label={label} />
))}
</Tabs>
{/* Tab content */}
<Box sx={{ flex: 1, overflowY: 'auto', p: TABS[tab] === 'Übersicht' ? 2.5 : 0 }}>
{TABS[tab] === 'Übersicht' && (
<PropertyDetailOverview
p={property}
editing={editing}
draft={draft}
onDraftChange={setDraft}
/>
)}
{TABS[tab] === 'Matchability' && <MatchabilityTabPanel propertyId={propertyId} />}
{TABS[tab] === 'Marktsignale' && <PropertyMarketSignalsTab propertyId={propertyId} />}
</Box>
</Box>
)
}