05ac0083b2
- Flächensuche: Flexibilität durch Steuerlast (taxEnvironment) ersetzt - MatchDetail: Bild als Hero-Banner oben, Karte eingebettet im Inhalt darunter - Ergebnisliste: Gold/Silber/Bronze-Abschnitte mit farbigen Trennlinien - ScoreBreakdown: KI-Steuerlast-Link zur kantonalen Steuerrechner-Seite - Beispieldaten: alle Objekte mit passenden Bildern versehen - locationIntelligence: taxCalculatorUrl pro Kanton ergänzt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
116 lines
3.2 KiB
TypeScript
116 lines
3.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[] {
|
||
const reasons: MatchCardReason[] = []
|
||
|
||
const hardFact = positiveFactors.find(f => HARD_CRITERIA.has(f.criterion))
|
||
const softFact = positiveFactors.find(f => !HARD_CRITERIA.has(f.criterion))
|
||
|
||
if (hardFact) {
|
||
reasons.push({
|
||
type: 'HARD_FACT',
|
||
label: capitalize(hardFact.criterion),
|
||
explanation: hardFact.explanation,
|
||
score: hardFact.score,
|
||
})
|
||
}
|
||
if (softFact) {
|
||
reasons.push({
|
||
type: 'SOFT_FACTOR',
|
||
label: capitalize(softFact.criterion),
|
||
explanation: softFact.explanation,
|
||
score: softFact.score,
|
||
})
|
||
}
|
||
|
||
return reasons
|
||
}
|
||
|
||
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
|
||
: undefined
|
||
const signal = isFuture
|
||
? (result as FutureAvailabilityResult).signal
|
||
: undefined
|
||
|
||
const city = property?.location?.city
|
||
const district = property?.location?.district
|
||
const locationLabel = city
|
||
? `${city}${district ? `, ${district}` : ''}`
|
||
: signal?.locationHint ?? '–'
|
||
|
||
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:
|
||
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,
|
||
}
|
||
}
|