Files
property-match/src/features/matching/matchCardAdapter.ts
T
Benjamin Sutter d902feb6c0 feat: Objekt→Einheit architecture — unit-level pre-market, property detail page, click-through from cards
- Domain: PropertyUnit extended with propertyId + schattenmarktRelease per unit
- Domain: FutureAvailabilityResult carries resolved property + unit
- useSchattenmarktSignals: generates unit-level signals (schattenmarkt-{propId}-{unitId})
- useUnifiedResults: resolves backing property + unit on FUTURE_AVAILABILITY fast path
- IUnitProvider + MockupUnitProvider: first-class unit access and mutation
- matchCardAdapter: maps preMarketUnit, preMarketAllUnits, propertyId, unitId to ViewModel
- IntelligenceMatchCard: PRE-MARKET VERIFIED shows unit info strip + "Zur Einheit →" button
- PropertyDetailView: unit-level toggles + date pickers inside PreMarketPanel
- New page: /demand/property/:propertyId with unit table, status chips, inquiry form
- App.tsx: demand route /demand/property/:propertyId registered
- Mock data: prop-001/007/037 units updated with correct lease dates + unit-level schattenmarktRelease

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 19:47:59 +02:00

129 lines
4.2 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 type {
UnifiedMatchResult,
VerifiedPortfolioResult,
ExternalMarketResult,
FutureAvailabilityResult,
} from '../../domain/unifiedResult'
import type { ScoreFactor } from '../../domain/match'
import type {
MatchCardViewModel,
MatchCardAction,
MatchCardReason,
} from '../../components/match-card/MatchCardViewModel'
import { getCityIntelligence } from '../../lib/locationIntelligence'
const HARD_CRITERIA = new Set(['area', 'location', 'budget', 'timing'])
function buildReasons(positiveFactors: ScoreFactor[]): MatchCardReason[] {
return positiveFactors.slice(0, 3).map(f => ({
type: (HARD_CRITERIA.has(f.criterion) ? 'HARD_FACT' : 'SOFT_FACTOR') as MatchCardReason['type'],
label: capitalize(f.criterion),
explanation: f.explanation,
score: f.score,
}))
}
function capitalize(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1)
}
export function buildMatchCardViewModel(
result: UnifiedMatchResult,
actions: MatchCardAction[],
): MatchCardViewModel {
const { match, matchScore, resultType } = result
const isFuture = resultType === 'FUTURE_AVAILABILITY'
const property = !isFuture
? (result as VerifiedPortfolioResult | ExternalMarketResult).property
: (result as FutureAvailabilityResult).property
const signal = isFuture
? (result as FutureAvailabilityResult).signal
: undefined
const unit = isFuture ? (result as FutureAvailabilityResult).unit : undefined
const city = property?.location?.city
const district = property?.location?.district
const locationLabel = city
? `${city}${district ? `, ${district}` : ''}`
: signal?.locationHint ?? ''
// For PRE-MARKET: title comes from unit if available, then signal, then property
const availabilityLabel =
property?.availabilityDate ??
(signal?.timeHorizonMonths ? `~${signal.timeHorizonMonths} Monate` : undefined)
const sourceLabel =
property?.sourceLabel ??
property?.sourceMeta?.sourceLabel ??
property?.sourceMeta?.sourceType
const externalUrl = property?.sourceUrl ?? property?.sourceMeta?.sourceUrl
const reasons = buildReasons(match.positiveFactors)
const cityIntel = city ? getCityIntelligence(city) : null
const taxCalculatorUrl = cityIntel?.taxCalculatorUrl
const dataQualityScore =
property?.dataQuality?.score ?? signal?.confidenceScore ?? 0.5
return {
id: result.matchId,
title:
signal?.title ??
property?.title ??
signal?.companyName ??
signal?.locationHint ??
'',
resultType,
assetType: property?.assetType,
matchScore,
confidenceScore: match.confidenceLevel,
dataQualityScore,
locationLabel,
availabilityLabel,
sourceLabel,
externalUrl,
reasons,
tradeoffs: match.tradeoffs ?? [],
risks: match.risks ?? [],
missingData: match.missingData ?? [],
actions,
disclaimer: isFuture
? (signal?.disclaimer ?? 'Probabilistisches Signal kein bestätigtes Objekt')
: undefined,
explainabilitySummary: match.explainabilitySummary,
taxCalculatorUrl,
isReviewRequired: match.status === 'PENDING_REVIEW',
isStaleData: false,
// Schattenmarkt signal fields
signalSourceType: signal?.source?.type,
signalSourceUrl: signal?.source?.url,
signalSourceCredibility: signal?.source?.credibility,
signalProbability: signal?.probability,
signalMarketIndicator: signal?.marketIndicator,
signalType: signal?.signalType,
signalIsVerified: signal?.isVerified,
aiSignalSummary: signal?.aiSummary,
signalQuality: (() => {
if (!signal) return undefined
if (signal.isVerified || signal.probability >= 0.70) return 'HIGH'
if (signal.probability >= 0.50) return 'MEDIUM'
return 'LOW'
})(),
signalIsControlled: signal?.signalType === 'LEASE_EXPIRY' && signal?.isVerified === true,
signalMarketIndicators: signal?.marketIndicators,
signalStrategicInterpretation: signal?.strategicInterpretation,
signalConfirmedFacts: signal?.confirmedFacts,
signalUnconfirmedFacts: signal?.unconfirmedFacts,
signalAreaSqmEstimate: signal?.areaSqmEstimate,
propertyId: property?.id ?? signal?.propertyId,
unitId: unit?.id ?? signal?.unitId,
preMarketUnit: unit,
preMarketAllUnits: property?.units,
}
}