feat(supply): per-unit fit-out/cost-bearer/MAB/parking with object fallback (resolveUnitFacts)

- unit: add fitOutByLandlord, mieterausbaubeitragPerSqm, parkingSpots (parking = allocation from object pool)
- lib/unitFacts.resolveUnitFacts: single resolver (unit value ?? object value) used by scorer + card adapter
- matchSyncService: pre-market unit scored via resolved facts (fit-out, MAB, cost-bearer, parking allocation)
- matchCardAdapter: fit-out label/viability/investment via resolveUnitFacts
- UnitStructurePanel: consolidate ALL per-unit overrides into the one expandable row editor (price, availability, Ausbaustandard, Wer-baut-aus, MAB, parking allocation); show parking on row
- PreMarketUnitGrid/Panel: drop the duplicate per-unit fit-out select (now in the unit editor)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-06-20 23:44:56 +02:00
parent 647493bf3c
commit fd6ed105bb
7 changed files with 144 additions and 71 deletions
+12 -5
View File
@@ -12,6 +12,7 @@ import type { Need } from '../domain/need'
import type { Property } from '../domain/property'
import { getEffectiveUnits } from '../domain/property'
import { calculateScore } from '../features/matching/scoreCalculator'
import { resolveUnitFacts } from '../lib/unitFacts'
function strengthFromScore(s: number): MatchStrength {
if (s >= 75) return MatchStrength.STRONG
@@ -71,16 +72,16 @@ function scoreProperty(
overrideArea?: number,
overridePrice?: number,
overrideResultType?: string,
overrideFitOut?: 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM',
overrideHardFacts?: Partial<NonNullable<Property['hardFacts']>>,
): MatchEngineOutput {
if (overrideArea !== undefined || overridePrice !== undefined || overrideResultType !== undefined || overrideFitOut !== undefined) {
if (overrideArea !== undefined || overridePrice !== undefined || overrideResultType !== undefined || overrideHardFacts !== undefined) {
return calculateScore(need, {
...prop,
areaSqm: overrideArea ?? prop.areaSqm,
rentPricePerSqm: overridePrice ?? prop.rentPricePerSqm,
resultType: (overrideResultType ?? prop.resultType) as ResultType,
// Einheit-Ausbau überschreibt den Objekt-Ausbau (Fallback bleibt prop.hardFacts.fitOut)
hardFacts: overrideFitOut ? { ...prop.hardFacts, fitOut: overrideFitOut } : prop.hardFacts,
// Einheit-Werte überschreiben Objekt-Werte (Fallback bleibt prop.hardFacts)
hardFacts: overrideHardFacts ? { ...prop.hardFacts, ...overrideHardFacts } : prop.hardFacts,
})
}
return calculateScore(need, prop)
@@ -104,7 +105,13 @@ export function generateMatchesForNeed(need: Need): void {
}
for (const unit of prop.units!) {
if (!unit.schattenmarktRelease?.enabled) continue
const unitOutput = scoreProperty(need, prop, unit.areaSqm, unit.rentPricePerSqm ?? prop.rentPricePerSqm, ResultType.FUTURE_AVAILABILITY, unit.fitOut)
const facts = resolveUnitFacts(prop, unit)
const unitOutput = scoreProperty(need, prop, unit.areaSqm, unit.rentPricePerSqm ?? prop.rentPricePerSqm, ResultType.FUTURE_AVAILABILITY, {
fitOut: facts.fitOut,
mieterausbaubeitragPerSqm: facts.mabPerSqm,
fitOutByLandlord: facts.fitOutByLandlord,
parking: facts.parkingSpots,
})
if (unitOutput.excluded || unitOutput.finalScore < MIN_SCORE) continue
const resultId = `schattenmarkt-${prop.id}-${unit.id}`
matchStore.push(buildMatch(prop, unit.id, need, unitOutput, ResultType.FUTURE_AVAILABILITY, resultId, now))