feat(supply): unit-centric fit-out & overview — per-unit fit-out, editable demand budget, multi-unit price/availability

- domain/unit: optional per-unit fitOut override (falls back to property.hardFacts.fitOut)
- matchSyncService: pre-market unit scored with its own fitOut; matchCardAdapter prefers unit fitOut
- PreMarketUnitGrid/PreMarketPanel: per-unit Ausbaustandard override in the release list (default "Wie Objekt")
- PropertyDetailOverview: edit/read "Wer baut aus?" for existing listings (SHELL/BASIC), central FIT_OUT_LABELS
- Phase 1 unit-centric overview: header shows area+count, price as range, "Verfügbar ab → pro Einheit"; unit table shows per-unit price + availability; "Objekt & Lage" labels marked as object-defaults for multi-unit
- NeedExtendedRequirements: "Eigenes Ausbaubudget" only shown when min fit-out set + clearer helper text
- UnitStructurePanel: spin-off prefill carries fitOutByLandlord

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-06-20 23:06:31 +02:00
parent e169f8e310
commit 5d26dd4a6c
8 changed files with 176 additions and 68 deletions
@@ -73,16 +73,18 @@ export function NeedExtendedRequirements({ criteria: c, onChange: set }: Props)
slotProps={{ htmlInput: { min: 0, max: 360, step: 12 } }}
/>
</Box>
<Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>Eigenes Ausbaubudget (max. CHF/m²)</Typography>
<TextField
size="small" type="number" fullWidth placeholder="z.B. 200"
value={c.fitOutBudgetMaxPerSqm ?? ''}
onChange={e => set({ ...c, fitOutBudgetMaxPerSqm: parseInt(e.target.value) || undefined })}
slotProps={{ htmlInput: { min: 0, max: 5000, step: 50 } }}
helperText="Ihr Beitrag — exkl. MAB des Vermieters"
/>
</Box>
{c.requiredFitOut && (
<Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>Eigenes Ausbaubudget (max. CHF/m²)</Typography>
<TextField
size="small" type="number" fullWidth placeholder="z.B. 200"
value={c.fitOutBudgetMaxPerSqm ?? ''}
onChange={e => set({ ...c, fitOutBudgetMaxPerSqm: parseInt(e.target.value) || undefined })}
slotProps={{ htmlInput: { min: 0, max: 5000, step: 50 } }}
helperText="Überbrückt Flächen unter Ihrem Mindest-Ausbaustandard — Sie tragen die Differenz selbst (exkl. MAB des Vermieters)."
/>
</Box>
)}
</Box>
{/* Divisibility */}
+23
View File
@@ -27,6 +27,9 @@ export function PreMarketPanel({ p }: { p: Property }) {
}
return init
})
const [unitFitOut, setUnitFitOut] = useState<Record<string, string>>(() =>
Object.fromEntries((p.units ?? []).map(u => [u.id, u.fitOut ?? ''])),
)
const [unitSaving, setUnitSaving] = useState<Record<string, boolean>>({})
const queryClient = useQueryClient()
const updateProperty = useUpdateProperty()
@@ -96,6 +99,23 @@ export function PreMarketPanel({ p }: { p: Property }) {
}
}
async function saveUnitFitOut(unitId: string, fitOut: string) {
setUnitFitOut(prev => ({ ...prev, [unitId]: fitOut }))
setUnitSaving(prev => ({ ...prev, [unitId]: true }))
try {
await MockupUnitProvider.update(unitId, {
fitOut: (fitOut || undefined) as 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM' | undefined,
})
await queryClient.invalidateQueries({ queryKey: ['property', p.id] })
await queryClient.invalidateQueries({ queryKey: ['properties'] })
await queryClient.invalidateQueries({ queryKey: ['matches'] })
} catch {
showToast('Fehler beim Speichern des Ausbaustandards.', 'error')
} finally {
setUnitSaving(prev => ({ ...prev, [unitId]: false }))
}
}
return (
<>
<Divider sx={{ my: 2 }} />
@@ -193,6 +213,9 @@ export function PreMarketPanel({ p }: { p: Property }) {
unitSaving={unitSaving}
setUnitStates={setUnitStates}
saveUnit={saveUnit}
propertyFitOut={p.hardFacts?.fitOut}
unitFitOut={unitFitOut}
saveUnitFitOut={saveUnitFitOut}
/>
)}
+24 -3
View File
@@ -1,7 +1,8 @@
import { Box, CircularProgress, Switch, TextField, Tooltip, Typography } from '@mui/material'
import { Box, CircularProgress, MenuItem, Switch, TextField, Tooltip, Typography } from '@mui/material'
import { EyeOff } from 'lucide-react'
import type { Property } from '../../domain/property'
import { DS_PRE_MARKET, DS_TEXT } from '../../lib/ds'
import { FIT_OUT_LABELS } from '../../lib/constants'
import { floorLabel } from './PropertyDetailHelpers'
type PropertyUnit = NonNullable<Property['units']>[number]
@@ -18,9 +19,13 @@ interface Props {
unitSaving: Record<string, boolean>
setUnitStates: React.Dispatch<React.SetStateAction<Record<string, UnitReleaseState>>>
saveUnit: (unitId: string, enabled: boolean, availableFrom: string, anonymous: boolean) => Promise<void>
propertyFitOut?: string
unitFitOut: Record<string, string>
saveUnitFitOut: (unitId: string, fitOut: string) => void
}
export function PreMarketUnitGrid({ units, unitStates, unitSaving, setUnitStates, saveUnit }: Props) {
export function PreMarketUnitGrid({ units, unitStates, unitSaving, setUnitStates, saveUnit, propertyFitOut, unitFitOut, saveUnitFitOut }: Props) {
const inheritLabel = propertyFitOut ? `Wie Objekt (${FIT_OUT_LABELS[propertyFitOut] ?? propertyFitOut})` : 'Wie Objekt'
return (
<Box sx={{ mt: 1.25, pt: 1.25, borderTop: `1px solid ${DS_PRE_MARKET.border}` }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: '#4c1d95', textTransform: 'uppercase', letterSpacing: 0.5, fontSize: '0.63rem', display: 'block', mb: 0.875 }}>
@@ -32,7 +37,7 @@ export function PreMarketUnitGrid({ units, unitStates, unitSaving, setUnitStates
<Box
key={u.id}
sx={{
display: 'grid', gridTemplateColumns: '1fr 140px auto',
display: 'grid', gridTemplateColumns: '1fr 150px 140px auto',
gap: 1, alignItems: 'start', py: 0.875,
borderBottom: '1px solid #f3e8ff',
'&:last-child': { borderBottom: 'none' },
@@ -90,6 +95,22 @@ export function PreMarketUnitGrid({ units, unitStates, unitSaving, setUnitStates
)}
</Box>
{/* Ausbaustandard-Override (Fallback: Objekt-Wert) */}
<TextField
select
size="small"
value={unitFitOut[u.id] ?? ''}
slotProps={{ inputLabel: { shrink: true } }}
sx={{ '& .MuiInputBase-input': { fontSize: '0.72rem', py: 0.5 }, mt: 0.125 }}
onChange={e => saveUnitFitOut(u.id, e.target.value)}
>
<MenuItem value="" sx={{ fontSize: '0.72rem' }}>{inheritLabel}</MenuItem>
<MenuItem value="SHELL" sx={{ fontSize: '0.72rem' }}>{FIT_OUT_LABELS.SHELL}</MenuItem>
<MenuItem value="BASIC" sx={{ fontSize: '0.72rem' }}>{FIT_OUT_LABELS.BASIC}</MenuItem>
<MenuItem value="FULL" sx={{ fontSize: '0.72rem' }}>{FIT_OUT_LABELS.FULL}</MenuItem>
<MenuItem value="PREMIUM" sx={{ fontSize: '0.72rem' }}>{FIT_OUT_LABELS.PREMIUM}</MenuItem>
</TextField>
{/* Available-from date */}
<TextField
type="date"
@@ -1,4 +1,4 @@
import { Box, Button, Divider, LinearProgress, MenuItem, TextField, Typography } from '@mui/material'
import { Box, Button, Divider, LinearProgress, MenuItem, TextField, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material'
import { ExternalLink, FileText } from 'lucide-react'
import type { Property, UpdatePropertyInput } from '../../domain/property'
import { PropertyMap } from '../shared'
@@ -6,6 +6,11 @@ import { getAssetTypeLabel, qualityColor } from './propertyHelpers'
import { Field, FieldGrid, SectionTitle } from './PropertyDetailHelpers'
import { UnitStructurePanel } from './UnitStructurePanel'
import { PreMarketPanel } from './PreMarketPanel'
import { FIT_OUT_LABELS } from '../../lib/constants'
import { FIT_OUT_OPTIONS } from '../../pages/supply/newListingConstants'
// Stufen, die noch Mieterausbau benötigen — nur dann ist die Träger-Frage relevant
const FIT_OUT_NEEDS_BUILD = new Set(['SHELL', 'BASIC'])
interface PropertyDetailOverviewProps {
p: Property
@@ -15,16 +20,38 @@ interface PropertyDetailOverviewProps {
}
export function PropertyDetailOverview({ p, editing, draft, onDraftChange }: PropertyDetailOverviewProps) {
const rentLabel = p.rentPricePerSqm > 0 ? `CHF ${p.rentPricePerSqm.toLocaleString('de-CH')}` : undefined
// Mehr-Einheiten-Objekt: Preis als Spanne, Fläche mit Anzahl, Verfügbarkeit pro Einheit
const units = p.units ?? []
const isMultiUnit = units.length > 1
const unitPrices = units.map(u => u.rentPricePerSqm ?? p.rentPricePerSqm).filter(v => v > 0)
const priceMin = unitPrices.length ? Math.min(...unitPrices) : p.rentPricePerSqm
const priceMax = unitPrices.length ? Math.max(...unitPrices) : p.rentPricePerSqm
const rentLabel = isMultiUnit && priceMin !== priceMax
? `CHF ${priceMin.toLocaleString('de-CH')}${priceMax.toLocaleString('de-CH')}`
: p.rentPricePerSqm > 0 ? `CHF ${p.rentPricePerSqm.toLocaleString('de-CH')}` : undefined
const areaLabel = isMultiUnit
? `${p.areaSqm.toLocaleString('de-CH')} m² · ${units.length} Einheiten`
: `${p.areaSqm.toLocaleString('de-CH')}`
const availLabel = isMultiUnit
? 'pro Einheit ↓'
: p.availabilityDate ? new Date(p.availabilityDate).toLocaleDateString('de-CH') : undefined
// Effektive (Draft-überlagerte) hardFacts für den Bearbeiten-Modus
const hf = draft.hardFacts ?? p.hardFacts ?? {}
const editFitOut = hf.fitOut ?? ''
const editByLandlord = hf.fitOutByLandlord ?? false
const showBuildResponsibility = FIT_OUT_NEEDS_BUILD.has(editFitOut)
const patchHF = (patch: Partial<NonNullable<Property['hardFacts']>>) =>
onDraftChange({ ...draft, hardFacts: { ...hf, ...patch } })
return (
<Box>
{/* Key metrics */}
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 1.5, mb: 0.75 }}>
{[
{ label: 'Fläche', value: `${p.areaSqm.toLocaleString('de-CH')}` },
{ label: 'CHF/m²/Jahr', value: rentLabel },
{ label: 'Verfügbar ab', value: p.availabilityDate ? new Date(p.availabilityDate).toLocaleDateString('de-CH') : undefined },
{ label: 'Fläche', value: areaLabel },
{ label: isMultiUnit ? 'CHF/m²/Jahr (Spanne)' : 'CHF/m²/Jahr', value: rentLabel },
{ label: 'Verfügbar ab', value: availLabel },
].map(({ label, value }) => (
<Box key={label} sx={{ p: 1.5, border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
<Typography variant="caption" color="text.secondary">{label}</Typography>
@@ -132,54 +159,71 @@ export function PropertyDetailOverview({ p, editing, draft, onDraftChange }: Pro
{/* Object details */}
<SectionTitle title="Objekt & Lage" />
{editing ? (
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1.5, mb: 2 }}>
<TextField
select label="Ausbaustandard"
size="small" fullWidth
value={draft.hardFacts?.fitOut ?? p.hardFacts?.fitOut ?? ''}
onChange={e => onDraftChange({ ...draft, hardFacts: { ...(draft.hardFacts ?? p.hardFacts ?? {}), fitOut: (e.target.value || undefined) as 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM' | undefined } })}
>
{[
{ value: '', label: 'Keine Angabe' },
{ value: 'SHELL', label: 'Rohbau' },
{ value: 'BASIC', label: 'Basisausbau' },
{ value: 'FULL', label: 'Vollausbau' },
{ value: 'PREMIUM', label: 'Premiumausbau' },
].map(o => <MenuItem key={o.value} value={o.value}>{o.label}</MenuItem>)}
</TextField>
<TextField
label="Mieterausbaubeitrag (CHF/m²)"
size="small" fullWidth type="number"
slotProps={{ htmlInput: { min: 0, max: 1000, step: 25 } }}
helperText="Beitrag des Vermieters zum Ausbau"
value={draft.hardFacts?.mieterausbaubeitragPerSqm ?? p.hardFacts?.mieterausbaubeitragPerSqm ?? ''}
onChange={e => onDraftChange({ ...draft, hardFacts: { ...(draft.hardFacts ?? p.hardFacts ?? {}), mieterausbaubeitragPerSqm: parseInt(e.target.value) || undefined } })}
/>
<TextField
label="Parkplätze"
size="small" fullWidth type="number"
slotProps={{ htmlInput: { min: 0 } }}
value={draft.hardFacts?.parking ?? p.hardFacts?.parking ?? ''}
onChange={e => onDraftChange({ ...draft, hardFacts: { ...(draft.hardFacts ?? p.hardFacts ?? {}), parking: parseInt(e.target.value) || undefined } })}
/>
<TextField
label="Deckenhöhe (m)"
size="small" fullWidth type="number"
slotProps={{ htmlInput: { step: 0.1, min: 2 } }}
value={draft.hardFacts?.ceilingHeightM ?? p.hardFacts?.ceilingHeightM ?? ''}
onChange={e => onDraftChange({ ...draft, hardFacts: { ...(draft.hardFacts ?? p.hardFacts ?? {}), ceilingHeightM: parseFloat(e.target.value) || undefined } })}
/>
<Box sx={{ mb: 2 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1.5 }}>
<TextField
select label="Ausbaustandard"
size="small" fullWidth
value={editFitOut}
onChange={e => patchHF({ fitOut: (e.target.value || undefined) as 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM' | undefined })}
>
{FIT_OUT_OPTIONS.map(o => <MenuItem key={o.value} value={o.value}>{o.label}</MenuItem>)}
</TextField>
<TextField
label="Parkplätze"
size="small" fullWidth type="number"
slotProps={{ htmlInput: { min: 0 } }}
value={hf.parking ?? ''}
onChange={e => patchHF({ parking: parseInt(e.target.value) || undefined })}
/>
<TextField
label="Deckenhöhe (m)"
size="small" fullWidth type="number"
slotProps={{ htmlInput: { step: 0.1, min: 2 } }}
value={hf.ceilingHeightM ?? ''}
onChange={e => patchHF({ ceilingHeightM: parseFloat(e.target.value) || undefined })}
/>
</Box>
{showBuildResponsibility && (
<Box sx={{ mt: 1.5 }}>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>Wer baut aus?</Typography>
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center', flexWrap: 'wrap' }}>
<ToggleButtonGroup
exclusive size="small"
value={editByLandlord ? 'landlord' : 'tenant'}
onChange={(_, v) => { if (v) patchHF({ fitOutByLandlord: v === 'landlord', ...(v === 'landlord' ? { mieterausbaubeitragPerSqm: undefined } : {}) }) }}
>
<ToggleButton value="landlord" sx={{ textTransform: 'none' }}>Vermieter übernimmt</ToggleButton>
<ToggleButton value="tenant" sx={{ textTransform: 'none' }}>Mieter baut aus</ToggleButton>
</ToggleButtonGroup>
{!editByLandlord && (
<TextField
label="Mieterausbaubeitrag (CHF/m²)"
size="small" type="number" sx={{ width: 240 }}
slotProps={{ htmlInput: { min: 0, max: 1000, step: 25 } }}
helperText="Beitrag des Vermieters — optional"
value={hf.mieterausbaubeitragPerSqm ?? ''}
onChange={e => patchHF({ mieterausbaubeitragPerSqm: parseInt(e.target.value) || undefined })}
/>
)}
</Box>
</Box>
)}
</Box>
) : (
<FieldGrid>
{p.propertyNumber && <Field label="Objekt-Nr." value={p.propertyNumber} />}
<Field label="Objekttyp" value={getAssetTypeLabel(p.assetType)} />
<Field label="Etage" value={p.hardFacts?.floor ?? p.floorLevel} />
<Field label="Ausbaustandard" value={p.hardFacts?.fitOut} />
{p.hardFacts?.mieterausbaubeitragPerSqm && (
<Field label={isMultiUnit ? 'Ausbaustandard (Objekt-Standard)' : 'Ausbaustandard'} value={p.hardFacts?.fitOut ? (FIT_OUT_LABELS[p.hardFacts.fitOut] ?? p.hardFacts.fitOut) : undefined} />
{p.hardFacts?.fitOut && FIT_OUT_NEEDS_BUILD.has(p.hardFacts.fitOut) && (
<Field label="Ausbau-Träger" value={p.hardFacts.fitOutByLandlord ? 'Vermieter (im Mietzins)' : 'Mieter'} />
)}
{p.hardFacts?.mieterausbaubeitragPerSqm && !p.hardFacts.fitOutByLandlord && (
<Field label="Mieterausbaubeitrag" value={`CHF ${p.hardFacts.mieterausbaubeitragPerSqm}/m²`} />
)}
<Field label="Parkplätze" value={p.hardFacts?.parking ?? p.softFactors?.parkingSpots} />
<Field label={isMultiUnit ? 'Parkplätze (Objekt-Pool)' : 'Parkplätze'} value={p.hardFacts?.parking ?? p.softFactors?.parkingSpots} />
<Field label="ÖV (Min.)" value={p.softFactors?.publicTransportMinutes} />
<Field label="Nebenkosten/m²" value={p.ancillaryCosts ? `CHF ${p.ancillaryCosts}` : undefined} />
<Field label="Importiert aus" value={p.importedFrom} />
+12 -1
View File
@@ -229,6 +229,7 @@ export function UnitStructurePanel({ p }: { p: Property }) {
propertyId: p.id,
floor: floorLabel(u),
fitOut: p.hardFacts?.fitOut,
fitOutByLandlord: p.hardFacts?.fitOutByLandlord,
parking: p.hardFacts?.parking,
ceilingHeight: p.hardFacts?.ceilingHeightM,
mieterausbaubeitragPerSqm: p.hardFacts?.mieterausbaubeitragPerSqm,
@@ -287,7 +288,7 @@ export function UnitStructurePanel({ p }: { p: Property }) {
)}
</Box>
{/* Area */}
{/* Area + Preis + Verfügbarkeit pro Einheit */}
<Box sx={{ textAlign: 'right' }}>
<Typography variant="caption" sx={{ color: '#374151', fontWeight: u.available ? 600 : 400 }}>
{(u.offeredSqm ?? u.areaSqm).toLocaleString('de-CH')} m²
@@ -297,6 +298,16 @@ export function UnitStructurePanel({ p }: { p: Property }) {
ab {u.minLettableSqm} m²
</Typography>
)}
<Typography variant="caption" sx={{ color: DS_TEXT.muted, fontSize: '0.6rem', display: 'block' }}>
CHF {(u.rentPricePerSqm ?? p.rentPricePerSqm).toLocaleString('de-CH')}/m²
</Typography>
{u.available && (
<Typography variant="caption" sx={{ color: DS_TEXT.disabled, fontSize: '0.6rem', display: 'block' }}>
{u.schattenmarktRelease?.availableFrom
? `ab ${new Date(u.schattenmarktRelease.availableFrom).toLocaleDateString('de-CH', { month: 'short', year: '2-digit' })}`
: 'sofort'}
</Typography>
)}
</Box>
</Box>
+3
View File
@@ -17,6 +17,9 @@ export interface PropertyUnit {
areaSqm: number
available: boolean
rentPricePerSqm?: number // annual CHF/m²; falls back to property.rentPricePerSqm
// Unit-level fit-out override — falls back to property.hardFacts.fitOut when unset.
// Relevant for pre-market, where units of one property are matched individually.
fitOut?: 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM'
leases?: Lease[] // Mietverträge — current, historical, future
/** @deprecated Use leases[].tenant.companyName */
currentTenant?: string
+8 -7
View File
@@ -152,28 +152,29 @@ export function buildMatchCardViewModel(
preMarketUnit: unit,
preMarketAllUnits: property?.units,
fitOutLabel: (() => {
const fitOut = property?.hardFacts?.fitOut
// Einheit-Ausbau hat Vorrang vor dem Objekt-Ausbau (Pre-Market)
const fitOut = unit?.fitOut ?? property?.hardFacts?.fitOut
if (!fitOut) return undefined
const LABELS: Record<string, string> = { SHELL: 'Rohbau', BASIC: 'Basisausbau', FULL: 'Vollausbau', PREMIUM: 'Premiumausbau' }
const mab = property.hardFacts?.mieterausbaubeitragPerSqm
const mab = property?.hardFacts?.mieterausbaubeitragPerSqm
if (fitOut === 'FULL' || fitOut === 'PREMIUM') return `${LABELS[fitOut]} — bezugsfertig`
if (mab) return `${LABELS[fitOut]} + CHF ${mab} MAB`
return LABELS[fitOut] ?? fitOut
})(),
fitOutViable: (() => {
const fitOut = property?.hardFacts?.fitOut
const fitOut = unit?.fitOut ?? property?.hardFacts?.fitOut
if (!fitOut) return undefined
if (fitOut === 'FULL' || fitOut === 'PREMIUM') return true
const mab = property.hardFacts?.mieterausbaubeitragPerSqm ?? 0
const mab = property?.hardFacts?.mieterausbaubeitragPerSqm ?? 0
if (fitOut === 'BASIC' && mab >= 150) return true
if (fitOut === 'SHELL' && mab >= 350) return true
return undefined
})(),
fitOutInvestment: (() => {
const fitOut = property?.hardFacts?.fitOut
const fitOut = unit?.fitOut ?? property?.hardFacts?.fitOut
if (!fitOut || fitOut === 'FULL' || fitOut === 'PREMIUM') return undefined
const mab = property.hardFacts?.mieterausbaubeitragPerSqm ?? 0
const area = property.areaSqm ?? 0
const mab = property?.hardFacts?.mieterausbaubeitragPerSqm ?? 0
const area = unit?.areaSqm ?? property?.areaSqm ?? 0
return calcFitOutInvestment(fitOut, area, mab, 0) ?? undefined
})(),
isDivisible: property?.units ? property.units.length > 1 : false,
+5 -2
View File
@@ -71,13 +71,16 @@ function scoreProperty(
overrideArea?: number,
overridePrice?: number,
overrideResultType?: string,
overrideFitOut?: 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM',
): MatchEngineOutput {
if (overrideArea !== undefined || overridePrice !== undefined || overrideResultType !== undefined) {
if (overrideArea !== undefined || overridePrice !== undefined || overrideResultType !== undefined || overrideFitOut !== 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,
})
}
return calculateScore(need, prop)
@@ -101,7 +104,7 @@ 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)
const unitOutput = scoreProperty(need, prop, unit.areaSqm, unit.rentPricePerSqm ?? prop.rentPricePerSqm, ResultType.FUTURE_AVAILABILITY, unit.fitOut)
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))