Files
property-match/src/components/supply/PropertyTable.tsx
T
Benjamin Sutter 920ce8eb09 feat: Verwaltungszugang refactor — lease data, 3-tab detail, market signals
- Dashboard: remove QuickActionPanel, StrongMatchOverview, FutureSignalWidget,
  DataQualityWidget, header buttons, Marktchancen nav; KpiGrid → 4 cards
- Property domain: 9 new fields (leaseTerm, leaseStartDate/End, breakoutOption,
  currentTenant, importedFrom, importedAt, lastUpdatedAt)
- Mock data: annual CHF/m²/Jahr prices (×12), Swiss images, tenant/lease data
  for all 10 VERIFIED_PORTFOLIO properties; need budgets updated to annual
- PropertyTable: swap columns — add Aktueller Mieter, Mietlaufzeit,
  Breakoutoption, Breakoutoption Zeitpunkt; remove Verfügbarkeit, Konfidenz, Quelle
- PropertyDetailView: 3 tabs (Übersicht, Matchability, Marktsignale) with
  inline edit mode, NeedMatchCard list, PropertyMarketSignalsTab
- New: marketReport domain + mock data + service, NeedMatchCard,
  PropertyMarketSignalsTab with 4 sections + PDF button
- matchService: getNeedMatchesForProperty(propertyId, {minScore})

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 15:43:52 +02:00

273 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
Alert,
Box,
Chip,
IconButton,
LinearProgress,
Skeleton,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Tooltip,
Typography,
} from '@mui/material'
import { Bookmark, Eye } from 'lucide-react'
import type { Property } from '../../domain/property'
import type { PropertyTableFilters } from './PropertyFilterBar'
import {
getAssetTypeColor,
getAssetTypeLabel,
qualityColor,
} from './propertyHelpers'
interface PropertyTableProps {
properties: Property[]
isLoading: boolean
isError: boolean
selectedId: string | null
onSelect: (id: string) => void
onViewDetail?: (id: string) => void
filters: PropertyTableFilters
onFiltersChange: (f: PropertyTableFilters) => void
}
const COL_HEADERS = [
'Objekt', 'Typ', 'Standort', 'Fläche', 'Miete CHF/m²/Jahr',
'Aktueller Mieter', 'Mietlaufzeit', 'Breakoutoption', 'Breakoutoption Zeitpunkt',
'Datenqualität', 'Aktionen',
]
function LoadingRows() {
return (
<>
{Array.from({ length: 5 }).map((_, i) => (
<TableRow key={i}>
{COL_HEADERS.map((h) => (
<TableCell key={h}>
<Skeleton variant="text" width={h === 'Objekt' ? 140 : 60} />
</TableCell>
))}
</TableRow>
))}
</>
)
}
export function PropertyTable({
properties,
isLoading,
isError,
selectedId,
onSelect,
onViewDetail,
filters,
onFiltersChange,
}: PropertyTableProps) {
if (isError) {
return <Alert severity="error" sx={{ m: 3 }}>Objekte konnten nicht geladen werden.</Alert>
}
function handleSort(field: PropertyTableFilters['sortBy']) {
if (filters.sortBy === field) {
onFiltersChange({ ...filters, sortDir: filters.sortDir === 'asc' ? 'desc' : 'asc' })
} else {
onFiltersChange({ ...filters, sortBy: field, sortDir: 'desc' })
}
}
function SortableHeader({ field, label }: { field: PropertyTableFilters['sortBy']; label: string }) {
const isActive = filters.sortBy === field
return (
<Typography
variant="caption"
sx={{
fontWeight: 600,
cursor: 'pointer',
color: isActive ? 'primary.main' : 'text.primary',
userSelect: 'none',
'&:hover': { color: 'primary.main' },
}}
onClick={() => handleSort(field)}
>
{label}{isActive ? (filters.sortDir === 'asc' ? ' ↑' : ' ↓') : ''}
</Typography>
)
}
return (
<Box sx={{ overflowX: 'auto' }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow sx={{ bgcolor: 'grey.50' }}>
<TableCell><Typography variant="caption" sx={{ fontWeight: 600 }}>Objekt</Typography></TableCell>
<TableCell><Typography variant="caption" sx={{ fontWeight: 600 }}>Typ</Typography></TableCell>
<TableCell><Typography variant="caption" sx={{ fontWeight: 600 }}>Standort</Typography></TableCell>
<TableCell><SortableHeader field="area" label="Fläche (m²)" /></TableCell>
<TableCell><SortableHeader field="rent" label="Miete CHF/m²/Jahr" /></TableCell>
<TableCell><Typography variant="caption" sx={{ fontWeight: 600 }}>Aktueller Mieter</Typography></TableCell>
<TableCell><Typography variant="caption" sx={{ fontWeight: 600 }}>Mietlaufzeit</Typography></TableCell>
<TableCell><Typography variant="caption" sx={{ fontWeight: 600 }}>Breakoutoption</Typography></TableCell>
<TableCell><Typography variant="caption" sx={{ fontWeight: 600 }}>Breakoutoption Zeitpunkt</Typography></TableCell>
<TableCell><SortableHeader field="dataQuality" label="Datenqualität" /></TableCell>
<TableCell><Typography variant="caption" sx={{ fontWeight: 600 }}>Aktionen</Typography></TableCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<LoadingRows />
) : properties.length === 0 ? (
<TableRow>
<TableCell colSpan={11}>
<Typography variant="body2" sx={{ color: 'text.secondary', textAlign: 'center', py: 4 }}>
Keine Objekte gefunden.
</Typography>
</TableCell>
</TableRow>
) : (
properties.map(p => {
const isSelected = p.id === selectedId
const qScore = p.dataQuality.score
const hasCritical = p.dataQuality.missingCriticalFields.length > 0
return (
<TableRow
key={p.id}
hover
onClick={() => onSelect(p.id)}
sx={{
cursor: 'pointer',
bgcolor: isSelected
? 'rgba(30,58,95,0.06)'
: hasCritical
? 'rgba(192,57,43,0.03)'
: 'inherit',
borderLeft: isSelected ? '3px solid #1e3a5f' : '3px solid transparent',
'&:hover': { bgcolor: isSelected ? 'rgba(30,58,95,0.08)' : 'rgba(0,0,0,0.02)' },
}}
>
{/* Objekt */}
<TableCell>
<Typography variant="body2" sx={{ fontWeight: 500, lineHeight: 1.3 }}>
{p.title}
</Typography>
<Typography variant="caption" color="text.secondary">
{p.address.street} {p.address.houseNumber}, {p.address.city}
</Typography>
</TableCell>
{/* Typ */}
<TableCell>
<Chip
label={getAssetTypeLabel(p.assetType)}
size="small"
sx={{ bgcolor: getAssetTypeColor(p.assetType), color: 'white', fontSize: '0.68rem' }}
/>
</TableCell>
{/* Standort */}
<TableCell>
<Typography variant="body2">{p.location.city}</Typography>
{p.location.canton && (
<Typography variant="caption" color="text.secondary">{p.location.canton}</Typography>
)}
</TableCell>
{/* Fläche */}
<TableCell>
<Typography variant="body2">{p.areaSqm.toLocaleString('de-CH')}</Typography>
</TableCell>
{/* Miete */}
<TableCell>
<Typography variant="body2">
{p.rentPricePerSqm > 0 ? `CHF ${p.rentPricePerSqm.toLocaleString('de-CH')}` : <span style={{ color: '#94a3b8' }}>k.A.</span>}
</Typography>
</TableCell>
{/* Aktueller Mieter */}
<TableCell>
<Typography variant="body2" sx={{ whiteSpace: 'nowrap' }}>
{p.currentTenant ?? <span style={{ color: '#94a3b8' }}></span>}
</Typography>
</TableCell>
{/* Mietlaufzeit */}
<TableCell>
<Typography variant="body2">
{p.leaseTerm ?? <span style={{ color: '#94a3b8' }}></span>}
</Typography>
</TableCell>
{/* Breakoutoption */}
<TableCell>
{p.breakoutOption == null
? <Typography variant="body2" sx={{ color: '#94a3b8' }}></Typography>
: <Chip
label={p.breakoutOption ? 'Ja' : 'Nein'}
size="small"
sx={{
bgcolor: p.breakoutOption ? '#dcfce7' : '#f1f5f9',
color: p.breakoutOption ? '#166534' : '#64748b',
fontSize: '0.68rem',
}}
/>
}
</TableCell>
{/* Breakoutoption Zeitpunkt */}
<TableCell>
<Typography variant="body2">
{p.breakoutOptionDate
? new Date(p.breakoutOptionDate).toLocaleDateString('de-CH')
: <span style={{ color: '#94a3b8' }}></span>
}
</Typography>
</TableCell>
{/* Datenqualität */}
<TableCell>
<Tooltip
title={hasCritical ? `Kritische Felder: ${p.dataQuality.missingCriticalFields.join(', ')}` : 'Keine kritischen Lücken'}
placement="top"
>
<Box sx={{ width: 80 }}>
<LinearProgress
variant="determinate"
value={qScore * 100}
color={qualityColor(qScore)}
sx={{ height: 6, borderRadius: 3, mb: 0.25 }}
/>
<Typography variant="caption" color="text.secondary">
{Math.round(qScore * 100)}%
</Typography>
</Box>
</Tooltip>
</TableCell>
{/* Aktionen */}
<TableCell onClick={e => e.stopPropagation()}>
<Box sx={{ display: 'flex', gap: 0.25 }}>
<Tooltip title="Details anzeigen">
<IconButton size="small" onClick={() => onViewDetail ? onViewDetail(p.id) : onSelect(p.id)}>
<Eye size={15} />
</IconButton>
</Tooltip>
<Tooltip title="Zur Shortlist">
<IconButton size="small">
<Bookmark size={15} />
</IconButton>
</Tooltip>
</Box>
</TableCell>
</TableRow>
)
})
)}
</TableBody>
</Table>
</Box>
)
}