Files
property-match/src/components/supply/PropertyIntelligenceCard.tsx
T
Benjamin Sutter f487435a94 fix: correct rent unit labels and monthly/annual calculation consistency
- NeedAlignmentPanel: display budget row in monthly (CHF/m²/Mt.) to match
  the Preis section, use exact division (no Math.round) to avoid 13×12≠152
- MatchDetailPropertySections + PropertyDetailPublicSections: replace
  Math.round(rentPricePerSqm/12) with exact division; show 2 decimal places
  when monthly is not a whole number (e.g. CHF 12.67 instead of CHF 13)
- Add /Jahr suffix to all 15 displays showing rentPricePerSqm or maxPerSqm
  without a time unit across results, compare, match-detail, supply, and
  anfragencenter components

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 14:35:20 +02:00

143 lines
5.2 KiB
TypeScript

import { memo } from 'react'
import { Box, Button, Chip, LinearProgress, Typography } from '@mui/material'
import { LocationPreview } from '../shared/LocationPreview'
import { getAssetTypeColor, getAssetTypeLabel, getAvailabilityLabel } from './propertyHelpers'
import type { Property } from '../../domain/property'
interface Props {
property: Property
onSelect: (id: string) => void
}
function availabilityBadgeColor(status: string): string {
if (status === 'AVAILABLE_NOW') return '#1a7a4a'
if (status === 'AVAILABLE_SOON') return '#d97706'
return '#64748b'
}
export const PropertyIntelligenceCard = memo(function PropertyIntelligenceCard({ property: p, onSelect }: Props) {
const confPct = Math.round(p.confidenceScore * 100)
const confColor = p.confidenceScore >= 0.75 ? '#1a7a4a' : p.confidenceScore >= 0.55 ? '#d97706' : '#c0392b'
return (
<Box
sx={{
borderRadius: 2,
overflow: 'hidden',
border: '1px solid #e8d5c4',
background: 'linear-gradient(160deg,#fdf8f3,#faf0e6)',
boxShadow: '0 2px 12px rgba(0,0,0,0.06)',
display: 'flex',
flexDirection: 'column',
transition: 'box-shadow 0.15s, transform 0.1s',
cursor: 'pointer',
'&:hover': {
boxShadow: '0 6px 24px rgba(0,0,0,0.10)',
transform: 'translateY(-1px)',
},
}}
onClick={() => onSelect(p.id)}
>
{/* Image zone */}
<Box sx={{ position: 'relative' }}>
<LocationPreview
imageUrl={p.images?.[0]}
lat={p.location.coordinates?.lat}
lng={p.location.coordinates?.lng}
address={`${p.address.street} ${p.address.houseNumber}, ${p.address.city}`}
cityLabel={p.location.city}
height={175}
/>
{/* Availability badge */}
<Box sx={{ position: 'absolute', top: 10, left: 10 }}>
<Chip
label={getAvailabilityLabel(p.availabilityStatus)}
size="small"
sx={{
bgcolor: availabilityBadgeColor(p.availabilityStatus),
color: 'white',
fontWeight: 700,
fontSize: 10,
height: 20,
}}
/>
</Box>
{/* Asset type chip */}
<Box sx={{ position: 'absolute', top: 10, right: 44 }}>
<Chip
label={getAssetTypeLabel(p.assetType)}
size="small"
sx={{
bgcolor: getAssetTypeColor(p.assetType),
color: 'white',
fontWeight: 600,
fontSize: 10,
height: 20,
}}
/>
</Box>
</Box>
{/* Card body */}
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 0, flex: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.3 }} noWrap>
{p.title}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.25, mb: 1.25 }}>
{p.location.city}{p.location.district ? ` · ${p.location.district}` : ''}
</Typography>
{/* Key specs */}
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mb: 1.25 }}>
<Chip label={`${p.areaSqm}`} size="small" sx={{ fontSize: 11, bgcolor: '#f1f5f9', height: 22 }} />
<Chip label={`CHF ${p.rentPricePerSqm}/m²/Jahr`} size="small" sx={{ fontSize: 11, bgcolor: '#f1f5f9', height: 22 }} />
{p.contractDurationMonths && (
<Chip label={`${p.contractDurationMonths}M Vertrag`} size="small" sx={{ fontSize: 11, bgcolor: '#f1f5f9', height: 22 }} />
)}
</Box>
{/* Soft factors */}
{p.softFactors && (
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mb: 1.25 }}>
{p.softFactors.prestige != null && (
<Chip label={`Prestige ${p.softFactors.prestige}`} size="small"
sx={{ fontSize: 10, bgcolor: 'rgba(30,58,95,0.08)', color: '#1e3a5f', height: 20 }} />
)}
{p.softFactors.accessibility != null && (
<Chip label={`ÖV ${p.softFactors.publicTransportMinutes ?? p.softFactors.accessibility}min`} size="small"
sx={{ fontSize: 10, bgcolor: 'rgba(30,58,95,0.08)', color: '#1e3a5f', height: 20 }} />
)}
</Box>
)}
{/* Confidence */}
<Box sx={{ mt: 'auto', pt: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" color="text.secondary">Datenkonfidenz</Typography>
<Typography variant="caption" sx={{ fontWeight: 700, color: confColor }}>{confPct}%</Typography>
</Box>
<LinearProgress
variant="determinate"
value={confPct}
sx={{
height: 5, borderRadius: 3, bgcolor: '#e2e8f0',
'& .MuiLinearProgress-bar': { bgcolor: confColor },
}}
/>
</Box>
<Button
size="small"
variant="text"
onClick={e => { e.stopPropagation(); onSelect(p.id) }}
sx={{ mt: 1.25, alignSelf: 'flex-end', textTransform: 'none', fontSize: '0.75rem', color: '#1e3a5f', px: 0 }}
>
Details anzeigen
</Button>
</Box>
</Box>
)
})