e597e81ff5
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>
195 lines
6.5 KiB
TypeScript
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>
|
|
)
|
|
}
|