Files
property-match/src/components/match-detail/NeedAlignmentPanel.tsx
T
Benjamin Sutter e0fe238c26 fix: correct typo Gesuch → Gesucht in NeedAlignmentPanel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 01:01:38 +02:00

154 lines
5.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, Chip, Paper, Typography } from '@mui/material'
import { CheckCircle2, XCircle, Minus } from 'lucide-react'
import type { Match } from '../../domain/match'
import type { Property } from '../../domain/property'
import type { Need } from '../../domain/need'
type FitStatus = 'MATCH' | 'NO_MATCH' | 'PARTIAL' | 'UNKNOWN'
interface AlignmentRow {
label: string
needValue: string
resultValue: string
fit: FitStatus
}
function fitIcon(fit: FitStatus) {
if (fit === 'MATCH') return <CheckCircle2 size={16} color="#1a7a4a" />
if (fit === 'NO_MATCH') return <XCircle size={16} color="#c0392b" />
if (fit === 'PARTIAL') return <Minus size={16} color="#d97706" />
return <Minus size={16} color="#94a3b8" />
}
function fitColor(fit: FitStatus): string {
if (fit === 'MATCH') return '#f0fdf4'
if (fit === 'NO_MATCH') return '#fef2f2'
if (fit === 'PARTIAL') return '#fffbeb'
return '#f8fafc'
}
function buildRows(need: Need, property: Property): AlignmentRow[] {
const rows: AlignmentRow[] = []
// Area
const areaSqm = property.areaSqm
const areaFit: FitStatus = areaSqm >= need.requiredArea.min && areaSqm <= need.requiredArea.max
? 'MATCH'
: areaSqm >= need.requiredArea.min * 0.85
? 'PARTIAL'
: 'NO_MATCH'
rows.push({
label: 'Fläche',
needValue: `${need.requiredArea.min}${need.requiredArea.max}`,
resultValue: `${areaSqm}`,
fit: areaFit,
})
// Location
const locationFit: FitStatus = need.preferredLocations.some(
l => l.toLowerCase() === property.location.city.toLowerCase() ||
l.toLowerCase() === property.location.district?.toLowerCase()
) ? 'MATCH' : 'PARTIAL'
rows.push({
label: 'Standort',
needValue: need.preferredLocations.join(', ') || '',
resultValue: property.location.city,
fit: locationFit,
})
// Budget
const budgetFit: FitStatus = property.rentPricePerSqm <= need.budgetRange.maxPerSqm
? 'MATCH'
: property.rentPricePerSqm <= need.budgetRange.maxPerSqm * 1.1
? 'PARTIAL'
: 'NO_MATCH'
rows.push({
label: 'Budget',
needValue: `max. CHF ${need.budgetRange.maxPerSqm}/m²`,
resultValue: `CHF ${property.rentPricePerSqm}/m²`,
fit: budgetFit,
})
// Timing
const availDate = property.availabilityDate
const latestMoveIn = need.timing.latestMoveIn
const timingFit: FitStatus = !availDate ? 'UNKNOWN'
: availDate <= latestMoveIn ? 'MATCH' : 'PARTIAL'
rows.push({
label: 'Verfügbarkeit',
needValue: `bis ${need.timing.latestMoveIn}`,
resultValue: availDate || 'unbekannt',
fit: timingFit,
})
return rows
}
interface Props {
match: Match
need: Need | null
property: Property | null
}
export function NeedAlignmentPanel({ match: _match, need, property }: Props) {
if (!need || !property) return null
const rows = buildRows(need, property)
const mustHaves = need.mustHaveCriteria ?? []
return (
<Paper sx={{ p: 2.5, mb: 2 }}>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1.5 }}>Need-Alignment</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1.5 }}>
Gesucht: {need.companyName} · {need.assetType}
</Typography>
{/* Comparison table */}
<Box sx={{ mb: 2 }}>
<Box sx={{ display: 'flex', gap: 0, bgcolor: '#f8fafc', px: 1.5, py: 0.75, borderRadius: 1, mb: 0.5 }}>
<Typography variant="caption" sx={{ fontWeight: 600, flex: '0 0 130px' }}>Kriterium</Typography>
<Typography variant="caption" sx={{ fontWeight: 600, flex: 1 }}>Gesucht</Typography>
<Typography variant="caption" sx={{ fontWeight: 600, flex: 1 }}>Objekt</Typography>
<Typography variant="caption" sx={{ fontWeight: 600, width: 28, textAlign: 'center' }}>Fit</Typography>
</Box>
{rows.map((row, i) => (
<Box
key={i}
sx={{
display: 'flex',
alignItems: 'center',
gap: 0,
px: 1.5,
py: 1,
bgcolor: fitColor(row.fit),
borderRadius: 0.5,
mb: 0.5,
}}
>
<Typography variant="body2" sx={{ fontWeight: 500, flex: '0 0 130px' }}>{row.label}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ flex: 1 }}>{row.needValue}</Typography>
<Typography variant="body2" sx={{ flex: 1 }}>{row.resultValue}</Typography>
<Box sx={{ width: 28, display: 'flex', justifyContent: 'center' }}>
{fitIcon(row.fit)}
</Box>
</Box>
))}
</Box>
{/* Must-haves */}
{mustHaves.length > 0 && (
<Box>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#64748b', display: 'block', mb: 0.75 }}>
Must-have Kriterien
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{mustHaves.map((m, i) => (
<Chip key={i} label={m.criterion} size="small" variant="outlined" />
))}
</Box>
</Box>
)}
</Paper>
)
}