f487435a94
- 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>
143 lines
5.2 KiB
TypeScript
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} m²`} 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>
|
|
)
|
|
})
|