Files
property-match/src/features/matching/matchCardAdapter.ts
T
Benjamin Sutter 05ac0083b2 feat: Steuerlast-Kriterium, ImmoScout-Layout, Gold/Silver/Bronze-Trennung
- 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>
2026-05-18 14:14:10 +02:00

116 lines
3.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[] {
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,
}
}