d902feb6c0
- 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>
129 lines
4.2 KiB
TypeScript
129 lines
4.2 KiB
TypeScript
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,
|
||
}
|
||
}
|