Files
property-match/src/components/compare/CompareColumnHeader.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

158 lines
6.0 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 { Box, Button, Chip, IconButton, Tooltip, Typography } from '@mui/material'
import { X, AlertTriangle, BookmarkCheck, ExternalLink, Kanban } from 'lucide-react'
import { useNavigate } from 'react-router'
import { usePipelineItems } from '../../hooks/usePipeline'
import { usePipelineStore } from '../../stores/pipelineStore'
import { RESULT_TYPE_META } from '../../lib/ds'
import { matchScoreHex } from '../../lib/utils'
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
interface Props {
item: UnifiedMatchResult
onRemove: () => void
}
export function CompareColumnHeader({ item, onRemove }: Props) {
const navigate = useNavigate()
const { data: pipelineItems = [] } = usePipelineItems()
const { openSavedDialog } = usePipelineStore()
const meta = RESULT_TYPE_META[item.resultType] ?? { label: item.resultType, color: '#64748b' }
const prop = item.resultType !== 'FUTURE_AVAILABILITY' ? (item as any).property : null
const sig = item.resultType === 'FUTURE_AVAILABILITY' ? (item as any).signal : null
const title = prop?.title ?? sig?.companyName ?? sig?.locationHint ?? ''
const subtitle = prop?.location?.city ?? sig?.locationHint ?? ''
const district = prop?.location?.district
const availability = prop?.availabilityDate ?? (sig ? `~${sig.timeHorizonMonths} Monate` : null)
const confidence = Math.round(item.match.confidenceLevel * 100)
const source = prop?.sourceLabel ?? sig?.source?.type ?? ''
const isInPipeline = pipelineItems.some(
pi => pi.id === item.matchId || (prop?.id && pi.propertyId === prop.id)
)
function handleSave() {
if (isInPipeline) return
openSavedDialog({
resultId: item.matchId,
resultType: item.resultType,
title,
location: prop?.location?.city ?? sig?.locationHint,
matchScore: item.matchScore,
propertyId: prop?.id,
propertyAddress: prop ? `${title}, ${subtitle}${district ? `, ${district}` : ''}` : undefined,
areaLabel: prop?.areaSqm ? `${prop.areaSqm.toLocaleString('de-CH')}` : undefined,
rentLabel: prop?.rentPricePerSqm ? `CHF ${prop.rentPricePerSqm}/m²/Jahr` : undefined,
availabilityLabel: prop?.availabilityDate ?? undefined,
})
}
return (
<Box sx={{ p: 0.5 }}>
{/* Row 1: type chip + remove */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Chip label={meta.label} size="small" sx={{ bgcolor: meta.color, color: 'white', fontWeight: 600, fontSize: 10 }} />
<Tooltip title="Aus Vergleich entfernen">
<IconButton size="small" onClick={onRemove} sx={{ p: 0.375, color: '#94a3b8', '&:hover': { color: '#c0392b' } }}>
<X size={15} />
</IconButton>
</Tooltip>
</Box>
{/* Row 2: title (clickable) */}
<Typography
variant="subtitle1"
sx={{
fontWeight: 700, lineHeight: 1.3, mb: 0.25,
cursor: 'pointer',
'&:hover': { color: '#1e3a5f' },
}}
onClick={() => navigate(`/demand/results/${item.matchId}`)}
>
{title}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1.25 }}>
{subtitle}{district ? `, ${district}` : ''}
</Typography>
{/* Row 3: score */}
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.5, mb: 1 }}>
<Typography variant="h4" sx={{ fontWeight: 800, color: matchScoreHex(item.matchScore), lineHeight: 1 }}>
{item.matchScore}
</Typography>
<Typography variant="caption" color="text.secondary">/100</Typography>
</Box>
{/* Row 4: meta chips */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mb: 1.25 }}>
<Tooltip title="Konfidenz">
<Chip
label={`${confidence}% Konfidenz`}
size="small"
icon={confidence < 60 ? <AlertTriangle size={10} /> : undefined}
sx={{
fontSize: 10,
bgcolor: confidence < 60 ? '#fef3c7' : '#f0fdf4',
color: confidence < 60 ? '#92400e' : '#166534',
}}
/>
</Tooltip>
{source && source !== '' && (
<Chip label={source} size="small" sx={{ fontSize: 10 }} />
)}
{availability && (
<Chip label={availability} size="small" sx={{ fontSize: 10 }} />
)}
</Box>
{item.resultType === 'FUTURE_AVAILABILITY' && sig && (
<Box sx={{ mb: 1.25, p: 0.75, bgcolor: '#faf5ff', borderRadius: 1, border: '1px solid #e9d5ff' }}>
<Typography variant="caption" sx={{ color: '#7c3aed', fontWeight: 600, display: 'block' }}>
Probabilistisches Signal
</Typography>
<Typography variant="caption" color="text.secondary">
{Math.round(sig.probability * 100)}% Wahrscheinlichkeit
</Typography>
</Box>
)}
{/* Row 5: action buttons */}
<Box sx={{ display: 'flex', gap: 0.75, pt: 1, borderTop: '1px solid #f1f5f9' }}>
<Button
size="small"
variant="outlined"
startIcon={<ExternalLink size={12} />}
onClick={() => navigate(`/demand/results/${item.matchId}`)}
sx={{ flex: 1, textTransform: 'none', fontSize: '0.72rem', py: 0.5 }}
>
Details
</Button>
{isInPipeline ? (
<Button
size="small"
variant="outlined"
startIcon={<BookmarkCheck size={12} />}
disabled
sx={{ flex: 1, textTransform: 'none', fontSize: '0.72rem', py: 0.5, color: '#1a7a4a', borderColor: '#86efac' }}
>
In Pipeline
</Button>
) : (
<Button
size="small"
variant="contained"
startIcon={<Kanban size={12} />}
onClick={handleSave}
sx={{ flex: 1, textTransform: 'none', fontSize: '0.72rem', py: 0.5 }}
>
Merken
</Button>
)}
</Box>
</Box>
)
}