36ce548169
`npx tsc -b` brach mit 16 Fehlern ab. Neue Fehler wären darin untergegangen.
Echte Defekte:
- LatentInquiriesTab: OwnPropertyMatchList wurde verwendet, aber nie importiert
— der Zweig "Eigene Objekte" auf Mobil hätte zur Laufzeit geworfen
- unitService: throwServiceError nimmt ein Argument, nicht zwei
- SavedNeedDetail: MUI v9 kennt kein `inputProps` mehr, ersetzt durch slotProps
- Review{Status,Priority}Badge: `showBorder` existiert an GenericBadge nicht;
die Default-Variante "outlined" zeichnet den Rand ohnehin, Darstellung unverändert
- UnifiedResultFeed: die Ternärkette verglich theme.text gegen zwei Werte, die
SCORE_THEME seit einer Palettenumstellung nicht mehr führt — sie lief immer in
denselben Zweig. Auf diesen einen Wert zusammengezogen, Darstellung unverändert,
nebenbei zwei rohe Hex-Werte weniger
Der Rest waren ungenutzte Importe und Bindings (noUnusedLocals).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
160 lines
5.7 KiB
TypeScript
160 lines
5.7 KiB
TypeScript
import { memo } from 'react'
|
|
import { Box, Button, Typography } from '@mui/material'
|
|
import { Ruler } from 'lucide-react'
|
|
import { useNavigate } from 'react-router'
|
|
import type { Property } from '../../domain/property'
|
|
import type { VerifiedPortfolioResult } from '../../domain/unifiedResult'
|
|
import type { Match } from '../../domain/match'
|
|
import { MatchStrength, ResultType } from '../../domain/enums'
|
|
import { useInquiryStore } from '../../stores/inquiryStore'
|
|
import { usePipelineStore } from '../../stores/pipelineStore'
|
|
import { useCompareStore } from '../../stores/compareStore'
|
|
|
|
interface Props {
|
|
property: Property
|
|
score: number
|
|
needId: string
|
|
}
|
|
|
|
function buildSyntheticResult(property: Property, needId: string, score: number, matchId: string): VerifiedPortfolioResult {
|
|
const strength = score >= 85 ? MatchStrength.STRONG : score >= 70 ? MatchStrength.MODERATE : MatchStrength.WEAK
|
|
const syntheticMatch = {
|
|
id: matchId,
|
|
needId,
|
|
propertyId: property.id,
|
|
matchScore: score,
|
|
matchStrength: strength,
|
|
scoreBreakdown: { hardMatchScore: score, softFactorScore: score, confidenceModifier: 0, dataQualityModifier: 0, totalScore: score },
|
|
confidenceLevel: score / 100,
|
|
} as unknown as Match
|
|
|
|
return {
|
|
matchId,
|
|
needId,
|
|
matchScore: score,
|
|
resultType: ResultType.VERIFIED_PORTFOLIO,
|
|
property,
|
|
match: syntheticMatch,
|
|
}
|
|
}
|
|
|
|
export const PropertyMatchRow = memo(function PropertyMatchRow({ property, score, needId }: Props) {
|
|
const navigate = useNavigate()
|
|
const openInquiryDialog = useInquiryStore(s => s.openInquiryDialog)
|
|
const openSavedDialog = usePipelineStore(s => s.openSavedDialog)
|
|
const { addToCompare, removeFromCompare, isInCompare, isFull } = useCompareStore()
|
|
|
|
const scoreColor = score >= 85 ? '#16a34a' : score >= 70 ? '#d97706' : '#dc2626'
|
|
const scoreBg = score >= 85 ? '#dcfce7' : score >= 70 ? '#fef3c7' : '#fee2e2'
|
|
// Match IDs in the store use double-underscore format: m__propId__prop__needId
|
|
const matchId = `m__${property.id}__prop__${needId}`
|
|
const inCompare = isInCompare(matchId)
|
|
|
|
function handleInquire() {
|
|
openInquiryDialog({
|
|
propertyTitle: property.title,
|
|
location: property.location.city,
|
|
matchScore: score,
|
|
matchId,
|
|
propertyId: property.id,
|
|
areaLabel: property.areaSqm ? `${property.areaSqm} m²` : undefined,
|
|
rentLabel: property.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}/m²/Jahr` : undefined,
|
|
})
|
|
}
|
|
|
|
function handleShortlist() {
|
|
openSavedDialog({
|
|
resultId: matchId,
|
|
resultType: 'VERIFIED_PORTFOLIO',
|
|
title: property.title,
|
|
matchScore: score,
|
|
location: property.location.city,
|
|
propertyId: property.id,
|
|
propertyAddress: `${property.title}, ${property.location.city}`,
|
|
areaLabel: property.areaSqm ? `${property.areaSqm} m²` : undefined,
|
|
rentLabel: property.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}/m²/Jahr` : undefined,
|
|
})
|
|
}
|
|
|
|
function handleCompare() {
|
|
if (inCompare) {
|
|
removeFromCompare(matchId)
|
|
} else if (!isFull()) {
|
|
addToCompare(buildSyntheticResult(property, needId, score, matchId))
|
|
}
|
|
}
|
|
|
|
function handleDetails() {
|
|
navigate(`/demand/results/${matchId}`)
|
|
}
|
|
|
|
function handleUnit() {
|
|
navigate(`/demand/property/${property.id}`)
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ bgcolor: 'white', border: '1px solid #e2e8f0', borderRadius: 1.5, overflow: 'hidden' }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, p: 1.25 }}>
|
|
{property.images?.[0] ? (
|
|
<Box component="img" src={property.images[0]} alt=""
|
|
sx={{ width: 52, height: 52, borderRadius: 1, objectFit: 'cover', flexShrink: 0 }} />
|
|
) : (
|
|
<Box sx={{ width: 52, height: 52, borderRadius: 1, bgcolor: '#e0e7ff', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<Ruler size={18} color="#3730a3" />
|
|
</Box>
|
|
)}
|
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
|
<Typography variant="body2" sx={{ fontWeight: 600, fontSize: '0.825rem', color: '#0f172a' }} noWrap>
|
|
{property.title}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ color: '#64748b', fontSize: '0.75rem' }}>
|
|
{property.location.city} · {property.areaSqm} m²
|
|
</Typography>
|
|
</Box>
|
|
<Box sx={{ bgcolor: scoreBg, color: scoreColor, px: 1, py: 0.375, borderRadius: 1, fontWeight: 700, fontSize: '0.8rem', flexShrink: 0 }}>
|
|
{score}%
|
|
</Box>
|
|
</Box>
|
|
|
|
<Box sx={{ display: 'flex', gap: 0.75, px: 1.25, pb: 1.25, flexWrap: 'wrap' }}>
|
|
<ActionButton onClick={handleInquire} primary>Anfrage</ActionButton>
|
|
<ActionButton onClick={handleShortlist}>Merken</ActionButton>
|
|
<ActionButton onClick={handleCompare} disabled={!inCompare && isFull()}>
|
|
{inCompare ? 'Im Vergleich' : 'Vergleichen'}
|
|
</ActionButton>
|
|
<ActionButton onClick={handleDetails}>Details →</ActionButton>
|
|
<ActionButton onClick={handleUnit}>Zur Einheit →</ActionButton>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
})
|
|
|
|
function ActionButton({ children, onClick, primary, disabled }: {
|
|
children: React.ReactNode
|
|
onClick: () => void
|
|
primary?: boolean
|
|
disabled?: boolean
|
|
}) {
|
|
return (
|
|
<Button
|
|
size="small"
|
|
variant={primary ? 'contained' : 'outlined'}
|
|
onClick={onClick}
|
|
disabled={disabled}
|
|
sx={{
|
|
textTransform: 'none',
|
|
fontWeight: 600,
|
|
fontSize: '0.75rem',
|
|
py: 0.25,
|
|
px: 1,
|
|
minWidth: 0,
|
|
...(primary
|
|
? { bgcolor: '#152642', '&:hover': { bgcolor: '#16304d' } }
|
|
: { borderColor: '#cbd5e1', color: '#475569', '&:hover': { borderColor: '#152642', color: '#152642' } }),
|
|
}}
|
|
>
|
|
{children}
|
|
</Button>
|
|
)
|
|
}
|