Files
property-match/src/components/results/UnifiedResultCard.tsx
T
Benjamin Sutter e95490eb72 feat: Grundriss-Feature, Listenansicht mit Bildern, Gewerbe-Label, Image-Pool
- Grundriss (FloorPlanSection): PropertyUnit.floorPlanUrl?, Property.floorPlanUrl?;
  FloorPlanSection in MatchDetail + PropertyDetail; FloorPlanUrlSection in NewListing-Formular
- Listenansicht (MatchCardCompact): horizontales Layout mit 120px Bildstreifen,
  Score-Badge, Asset-Label-Overlay, alle 3 grünen Punkte, Anfrage-Button
- Light Industrial → "Gewerbe" überall (NeedInput, NeedCardPreview, CriteriaReviewPanel,
  newListingConstants, MyListings, propertyHelpers)
- "Zum Originalinserat"-Button nur bei Maison-Work-Objekten
- Image-Pool: propertyImageResolver mit sequentiellem Pool-Index (keine doppelten Bilder),
  nur Innenaufnahmen, rotate()-Trick für Sub-Pools
- Overlay-Labels (Objekttyp + Stadtteil) in LocationPreview + IntelligenceMatchCard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 22:12:43 +02:00

141 lines
5.0 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')}` : 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
const listOverlay = 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 <MatchCardCompact vm={vm} imageUrl={listImageUrl} overlayTypeLabel={listOverlay?.type} />
}