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 SutterandClaude Opus 4.8 committed 2026-06-20 23:44:56 +02:00
1 parent 647493bf3c
commit fd6ed105bb
7 files changed
+144 -71

No files matched your search

+13 -12
View File
@@ -13,6 +13,7 @@ import type {
import { getCityIntelligence } from '../../lib/locationIntelligence'
import { formatUnitTitle, formatMultiUnitFloors, getUnitAvailability } from '../../domain/unit'
import { calcFitOutInvestment } from '../../lib/fitOutUtils'
import { resolveUnitFacts } from '../../lib/unitFacts'
import { AvailabilityStatus } from '../../domain/enums'
const HARD_CRITERIA = new Set(['area', 'location', 'budget', 'timing'])
@@ -152,30 +153,30 @@ export function buildMatchCardViewModel(
preMarketUnit: unit,
preMarketAllUnits: property?.units,
fitOutLabel: (() => {
// Einheit-Ausbau hat Vorrang vor dem Objekt-Ausbau (Pre-Market)
const fitOut = unit?.fitOut ?? property?.hardFacts?.fitOut
// Einheit-Werte haben Vorrang vor Objekt-Werten (Pre-Market / Mehr-Einheiten)
if (!property) return undefined
const { fitOut, mabPerSqm } = resolveUnitFacts(property, unit)
if (!fitOut) return undefined
const LABELS: Record<string, string> = { SHELL: 'Rohbau', BASIC: 'Basisausbau', FULL: 'Vollausbau', PREMIUM: 'Premiumausbau' }
const mab = property?.hardFacts?.mieterausbaubeitragPerSqm
if (fitOut === 'FULL' || fitOut === 'PREMIUM') return `${LABELS[fitOut]} — bezugsfertig`
if (mab) return `${LABELS[fitOut]} + CHF ${mab} MAB`
if (mabPerSqm) return `${LABELS[fitOut]} + CHF ${mabPerSqm} MAB`
return LABELS[fitOut] ?? fitOut
})(),
fitOutViable: (() => {
const fitOut = unit?.fitOut ?? property?.hardFacts?.fitOut
if (!property) return undefined
const { fitOut, mabPerSqm } = resolveUnitFacts(property, unit)
if (!fitOut) return undefined
if (fitOut === 'FULL' || fitOut === 'PREMIUM') return true
const mab = property?.hardFacts?.mieterausbaubeitragPerSqm ?? 0
if (fitOut === 'BASIC' && mab >= 150) return true
if (fitOut === 'SHELL' && mab >= 350) return true
if (fitOut === 'BASIC' && mabPerSqm >= 150) return true
if (fitOut === 'SHELL' && mabPerSqm >= 350) return true
return undefined
})(),
fitOutInvestment: (() => {
const fitOut = unit?.fitOut ?? property?.hardFacts?.fitOut
if (!property) return undefined
const { fitOut, mabPerSqm } = resolveUnitFacts(property, unit)
if (!fitOut || fitOut === 'FULL' || fitOut === 'PREMIUM') return undefined
const mab = property?.hardFacts?.mieterausbaubeitragPerSqm ?? 0
const area = unit?.areaSqm ?? property?.areaSqm ?? 0
return calcFitOutInvestment(fitOut, area, mab, 0) ?? undefined
const area = unit?.areaSqm ?? property.areaSqm ?? 0
return calcFitOutInvestment(fitOut, area, mabPerSqm, 0) ?? undefined
})(),
isDivisible: property?.units ? property.units.length > 1 : false,
minDivisibleUnitSqm: (() => {