beecf754d5
- PropertyIntelligenceCard: aspect-ratio image, hover animation, compact data-completeness row, tighter body padding - Results: replace verbose Card with compact active-search strip, remove DecisionContextPanel - Properties: add 3/5/10 column toggle in grid view, persist to localStorage - NeedInput, UnifiedResultCard/Feed, MatchCardViewModel: cleanup from earlier session Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
131 lines
4.6 KiB
TypeScript
131 lines
4.6 KiB
TypeScript
import { useNavigate } from 'react-router'
|
|
import { MatchCardCompact } from '../match-card/MatchCardCompact'
|
|
import { IntelligenceMatchCard } from '../match-card/IntelligenceMatchCard'
|
|
import { buildMatchCardViewModel } from '../../features/matching/matchCardAdapter'
|
|
import { resolveImageLabel } from '../../lib/propertyImageResolver'
|
|
import { useCompareStore } from '../../stores/compareStore'
|
|
import { usePipelineStore } from '../../stores/pipelineStore'
|
|
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
|
|
import type { MatchCardAction } from '../match-card/MatchCardViewModel'
|
|
|
|
interface Props {
|
|
result: UnifiedMatchResult
|
|
view?: 'list' | 'grid'
|
|
}
|
|
|
|
function getResultTitle(result: UnifiedMatchResult): string {
|
|
if (result.resultType !== 'FUTURE_AVAILABILITY') {
|
|
return result.property?.title ?? result.matchId
|
|
}
|
|
return result.signal?.companyName ?? result.matchId
|
|
}
|
|
|
|
export function UnifiedResultCard({ result, view = 'list' }: Props) {
|
|
const navigate = useNavigate()
|
|
const { addToCompare, removeFromCompare, isInCompare, isFull } = useCompareStore()
|
|
const { openSavedDialog } = usePipelineStore()
|
|
const inCompare = isInCompare(result.matchId)
|
|
|
|
const isPortfolio = result.resultType === 'VERIFIED_PORTFOLIO'
|
|
|
|
const actions: MatchCardAction[] = [
|
|
{
|
|
id: 'shortlist',
|
|
label: 'Merken',
|
|
actionType: 'SAVE_SHORTLIST',
|
|
variant: 'secondary',
|
|
onClick: () => {
|
|
const prop = result.resultType !== 'FUTURE_AVAILABILITY' ? (result as any).property : null
|
|
openSavedDialog({
|
|
resultId: result.matchId,
|
|
resultType: result.resultType,
|
|
title: getResultTitle(result),
|
|
matchScore: result.matchScore,
|
|
location: prop?.location?.city,
|
|
propertyId: prop?.id,
|
|
propertyAddress: prop ? `${prop.title}, ${prop.location?.city ?? ''}` : undefined,
|
|
areaLabel: prop?.areaSqm ? `${prop.areaSqm.toLocaleString('de-CH')} m²` : undefined,
|
|
rentLabel: prop?.rentPricePerSqm ? `CHF ${prop.rentPricePerSqm}/m²/Jahr` : undefined,
|
|
})
|
|
},
|
|
},
|
|
{
|
|
id: 'compare',
|
|
label: inCompare ? 'Im Vergleich' : 'Vergleichen',
|
|
actionType: 'ADD_COMPARE',
|
|
variant: inCompare ? 'primary' : 'secondary',
|
|
disabled: !inCompare && isFull(),
|
|
onClick: () => {
|
|
if (inCompare) removeFromCompare(result.matchId)
|
|
else addToCompare(result)
|
|
},
|
|
},
|
|
{
|
|
id: 'details',
|
|
label: 'Details →',
|
|
actionType: 'OPEN_DETAIL',
|
|
// Downgrade to secondary when the unit CTA is the primary action
|
|
variant: isPortfolio ? 'secondary' : 'primary',
|
|
onClick: () => navigate(`/demand/results/${result.matchId}`),
|
|
},
|
|
// Portfolio properties: direct link to unit page + management inquiry form
|
|
...(isPortfolio ? [{
|
|
id: 'contact',
|
|
label: 'Zur Einheit →',
|
|
actionType: 'OPEN_DETAIL' as const,
|
|
variant: 'primary' as const,
|
|
onClick: () => {
|
|
const propId = result.property.id
|
|
const unitId = result.match.unitId
|
|
navigate(`/demand/property/${propId}${unitId ? `?unit=${unitId}` : ''}`)
|
|
},
|
|
}] : []),
|
|
]
|
|
|
|
const vm = buildMatchCardViewModel(result, actions)
|
|
|
|
if (view === 'grid') {
|
|
const imageUrl = result.resultType !== 'FUTURE_AVAILABILITY'
|
|
? result.property.images?.[0]
|
|
: undefined
|
|
const lat = result.resultType !== 'FUTURE_AVAILABILITY'
|
|
? result.property.location.coordinates?.lat
|
|
: undefined
|
|
const lng = result.resultType !== 'FUTURE_AVAILABILITY'
|
|
? result.property.location.coordinates?.lng
|
|
: undefined
|
|
const cityLabel = result.resultType !== 'FUTURE_AVAILABILITY'
|
|
? result.property.location.city
|
|
: result.signal.locationHint ?? undefined
|
|
|
|
const overlayLabels = result.resultType !== 'FUTURE_AVAILABILITY'
|
|
? resolveImageLabel({
|
|
assetType: result.property.assetType,
|
|
city: result.property.location.city,
|
|
district: result.property.location.district,
|
|
prestige: result.property.softFactors?.prestige,
|
|
floorLevel: result.property.floorLevel,
|
|
propertyId: result.property.id,
|
|
})
|
|
: null
|
|
|
|
return (
|
|
<IntelligenceMatchCard
|
|
vm={vm}
|
|
imageUrl={imageUrl}
|
|
lat={lat}
|
|
lng={lng}
|
|
cityLabel={cityLabel}
|
|
overlayTypeLabel={overlayLabels?.type}
|
|
overlayLocationLabel={overlayLabels?.location}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const listImageUrl = result.resultType !== 'FUTURE_AVAILABILITY'
|
|
? result.property.images?.[0]
|
|
: undefined
|
|
|
|
return <MatchCardCompact vm={vm} imageUrl={listImageUrl} />
|
|
}
|