feat: Reale Jahresbelastung, must-have fixes, fitOut data coverage

Reale Jahresbelastung (Change 6):
- FitOutCostPanel zeigt Jahresmiete + amortisierte Ausbaukosten für alle fitOut-Werte
- FULL/PREMIUM: Ausbau CHF 0 (bezugsfertig), SHELL/BASIC: CRB/BKP-Richtwerte amortisiert über 5 J.
- Match-Card-Chip zeigt geschätzte Investition (orange wenn >100k)
- FitOutInvestment-Typ + calcFitOutInvestment() in fitOutUtils.ts
- BackendAIService + MockAIService mit generateFitOutAdvice (IAIService-Interface)

Must-have Kriterien (Nicht prüfbar Fix):
- ÖV-Anbindung: Minutengrenze aus Freitext extrahiert, gegen publicTransportMinutes geprüft
- Mindestfläche: m²-Wert aus Freitext extrahiert, gegen areaSqm geprüft
- Ausbaugrad: neues Keyword-Rule für FULL/PREMIUM

fitOut-Datenpflege:
- fitOut-Werte zu 32 fehlenden Properties ergänzt (Logistik=SHELL, Standard=BASIC, Modern=FULL)
- MAB-Werte (200/150/250 CHF/m²) zu 3 BASIC-Objekten hinzugefügt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-06-06 20:09:55 +02:00
parent a825672197
commit b2530f9e20
40 changed files with 1755 additions and 733 deletions
+35 -9
View File
@@ -463,24 +463,50 @@ function scoreMinContractDuration(need: Need, property: Property): ScoreFactor |
}
}
const FIT_OUT_UPGRADE_COST: Record<string, number> = {
'SHELL_BASIC': 200,
'SHELL_FULL': 350,
'SHELL_PREMIUM': 450,
'BASIC_FULL': 150,
'BASIC_PREMIUM': 250,
'FULL_PREMIUM': 100,
}
function scoreFitOut(need: Need, property: Property): ScoreFactor | null {
if (!need.requiredFitOut) return null
const propFitOut = property.hardFacts?.fitOut
const LABELS: Record<string, string> = { SHELL: 'Rohbau', BASIC: 'Basisausbau', FULL: 'Vollausbau', PREMIUM: 'Premiumausbau' }
if (!propFitOut) {
return { criterion: 'fitOut', weight: 0.04, score: 50, contribution: 2, explanation: `Ausbaustandard ${need.requiredFitOut} erforderlich — keine Daten`, estimated: true }
}
const reqLevel = FIT_OUT_LEVELS[need.requiredFitOut] ?? 1
const propLevel = FIT_OUT_LEVELS[propFitOut] ?? 0
const score = propLevel >= reqLevel ? 100 : Math.max(10, Math.round(50 - (reqLevel - propLevel) * 25))
const LABELS: Record<string, string> = { SHELL: 'Rohbau', BASIC: 'Basisausbau', FULL: 'Vollausbau', PREMIUM: 'Premiumausbau' }
if (propLevel >= reqLevel) {
return {
criterion: 'fitOut', weight: 0.04, score: 100, contribution: 4,
explanation: `${LABELS[propFitOut] ?? propFitOut} erfüllt Anforderung (${LABELS[need.requiredFitOut] ?? need.requiredFitOut})`,
}
}
// Check if combined budget (tenant + MAB) covers the upgrade gap
const upgradeKey = `${propFitOut}_${need.requiredFitOut}`
const upgradeCost = FIT_OUT_UPGRADE_COST[upgradeKey]
const mab = property.hardFacts?.mieterausbaubeitragPerSqm ?? 0
const tenantBudget = need.fitOutBudgetMaxPerSqm ?? 0
const totalBudget = mab + tenantBudget
if (upgradeCost && totalBudget >= upgradeCost) {
const mabNote = mab > 0 ? ` (inkl. CHF ${mab} MAB)` : ''
return {
criterion: 'fitOut', weight: 0.04, score: 80, contribution: 3.2,
explanation: `${LABELS[propFitOut]} — Ausbau auf ${LABELS[need.requiredFitOut]} mit CHF ${totalBudget}/m² Budget überbrückbar${mabNote}`,
}
}
const score = Math.max(10, Math.round(50 - (reqLevel - propLevel) * 25))
const budgetNote = tenantBudget > 0 || mab > 0
? ` (Budget CHF ${totalBudget}/m² — Ausbau ~CHF ${upgradeCost ?? '?'}/m²)`
: ''
return {
criterion: 'fitOut',
weight: 0.04,
score,
contribution: score * 0.04,
explanation: propLevel >= reqLevel
? `Ausbaustandard ${LABELS[propFitOut] ?? propFitOut} erfüllt Anforderung ${LABELS[need.requiredFitOut] ?? need.requiredFitOut}`
: `${LABELS[propFitOut] ?? propFitOut}${LABELS[need.requiredFitOut] ?? need.requiredFitOut} erforderlich`,
criterion: 'fitOut', weight: 0.04, score, contribution: score * 0.04,
explanation: `${LABELS[propFitOut] ?? propFitOut}${LABELS[need.requiredFitOut] ?? need.requiredFitOut} erforderlich${budgetNote}`,
}
}