fix: clear stale search criteria on text edit, fix Zürich false-positive, improve cost breakdown and fit-out labels

- AISearch: clear parsed criteria when user manually edits text input so stale locations (e.g. Zürich) no longer persist after retyping
- needParser: fix ZURICH_SIGNALS substring bug — "kreis N" now uses word-boundary regex so "umkreis 22" no longer matches "kreis 2"
- CompareTableBody: row 9 shows full cost breakdown (Miete + NK + amortised fit-out) using FITOUT_AMORTIZATION_YEARS from constants
- FitOutCostPanel: replace local AMORTIZATION_YEARS with FITOUT_AMORTIZATION_YEARS from constants.ts (single source of truth)
- constants.ts: add FITOUT_AMORTIZATION_YEARS = 5 — change here to affect all cost calculations
- PropertyIntelligenceCard: translate raw fitOut enum to German labels (Rohbau/Grundausbau/Vollausbau/Premium-Ausbau) with explanatory tooltips; add tooltip on contract duration chip

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-06-09 17:32:25 +02:00
parent c6d3071010
commit 87ea4c4dfc
6 changed files with 97 additions and 20 deletions
+60 -6
View File
@@ -20,6 +20,8 @@ import {
import { CompareCell, MissingDataCell } from './index'
import { RESULT_TYPE_META, DS_COLORS } from '../../lib/ds'
import { matchScoreHex } from '../../lib/utils'
import { calcFitOutInvestment } from '../../lib/fitOutUtils'
import { FITOUT_AMORTIZATION_YEARS } from '../../lib/constants'
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
// ── Local helper ──────────────────────────────────────────────────────────────
@@ -148,18 +150,70 @@ export function CompareTableBody({
: <MissingDataCell />
}))}
{/* 9. Rent / Budget Fit */}
{row('9. Miete / Budget', compareItems.map(item => {
{/* 9. Rent / Budget Fit — full cost breakdown incl. amortised fit-out */}
{row('9. Kosten / Budget', compareItems.map(item => {
const prop = getProp(item)
if (!prop) return <MissingDataCell reason="Mietpreis nur für bestätigte Objekte verfügbar" />
const FIT_OUT_LABELS: Record<string, string> = {
SHELL: 'Rohbau', BASIC: 'Grundausbau', FULL: 'Vollausbau', PREMIUM: 'Premium-Ausbau',
}
const READY_TO_MOVE_IN = new Set(['FULL', 'PREMIUM'])
const monthlyRent = prop.totalRentMonthly
?? Math.round(prop.rentPricePerSqm * prop.areaSqm / 12)
// ancillaryCosts stored as CHF/m²/Monat
const monthlyNebenkosten = prop.ancillaryCosts != null
? Math.round(prop.ancillaryCosts * prop.areaSqm)
: null
const fitOut = prop.hardFacts?.fitOut
const fitOutLabel = fitOut ? (FIT_OUT_LABELS[fitOut] ?? fitOut) : null
const mabPerSqm = prop.hardFacts?.mieterausbaubeitragPerSqm ?? 0
// Amortised fit-out monthly cost (mid-range estimate)
let fitOutMonthly = 0
let fitOutMonthlyLabel: string | null = null
if (fitOut && !READY_TO_MOVE_IN.has(fitOut)) {
const inv = calcFitOutInvestment(fitOut, prop.areaSqm, mabPerSqm, 0)
if (inv && !inv.isFullyCovered) {
const months = FITOUT_AMORTIZATION_YEARS * 12
const midMin = Math.round(inv.netTotal.min / months)
const midMax = Math.round(inv.netTotal.max / months)
fitOutMonthly = Math.round((midMin + midMax) / 2)
fitOutMonthlyLabel = midMin === midMax
? `${midMin.toLocaleString('de-CH')}`
: `${midMin.toLocaleString('de-CH')}${midMax.toLocaleString('de-CH')}`
}
}
const totalMonthly = monthlyRent
+ (monthlyNebenkosten ?? 0)
+ fitOutMonthly
return (
<Box>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.4 }}>
<Typography variant="body2" sx={{ fontWeight: 700 }}>
CHF {prop.rentPricePerSqm}/m²/Jahr
</Typography>
{prop.totalRentMonthly && (
<Typography variant="caption" color="text.secondary">
{monthlyRent.toLocaleString('de-CH')} CHF/Monat (Miete)
</Typography>
{monthlyNebenkosten != null && (
<Typography variant="caption" color="text.secondary">
{prop.totalRentMonthly.toLocaleString('de-CH')} CHF/Monat
+ {monthlyNebenkosten.toLocaleString('de-CH')} CHF/Monat (NK)
</Typography>
)}
{fitOutMonthlyLabel && (
<Typography variant="caption" color="text.secondary">
+ {fitOutMonthlyLabel} CHF/Monat (Ausbau ÷ {FITOUT_AMORTIZATION_YEARS} J.)
</Typography>
)}
<Typography variant="body2" sx={{ fontWeight: 700, color: '#1e3a5f', borderTop: '1px solid #e2e8f0', pt: 0.5, mt: 0.25 }}>
= {totalMonthly.toLocaleString('de-CH')} CHF/Monat
</Typography>
{fitOutLabel && (
<Typography variant="caption" sx={{ color: '#64748b', mt: 0.25 }}>
Ausbau: {fitOutLabel}{READY_TO_MOVE_IN.has(fitOut ?? '') ? ' (bezugsfertig)' : ''}
</Typography>
)}
</Box>