e169f8e310
Mieterausbau / fit-out economics — surface true total cost of occupancy: - Annuity calc (annuityFactor + effectiveAnnualBurdenPerSqm) replaces straight-line ÷5; FITOUT_ANNUITY_RATE=5% - New hardFacts.fitOutByLandlord: "Wer baut aus?" toggle in NewListing — landlord-borne fit-out is priced into rent (no surcharge), tenant-borne SHELL/BASIC adds annuitized cost minus MAB - scoreBudget now compares effective annual burden (rent + fit-out annuity) vs budget instead of cold rent only; FULL/PREMIUM and landlord-borne unchanged - FitOutCostPanel + CompareTableBody compute annuitized, tenant-aware burden - Central FIT_OUT_LABELS with industry/international vocabulary (Rohbau·Core&Shell, Edelrohbau·CAT A, etc.) - Activate existing generateFitOutAdvice via useFitOutAdvice hook + new FitOutAdvicePanel (MIETERAUSBAU/BKZ/MAB-Amortisation + negotiation tip), shown for tenant-borne SHELL/BASIC - MAB field only asked when tenant builds out (optional) — one new toggle, no extra data burden for property managers Fix: DQ/confidence modifiers were computed but never applied to finalScore (hardcoded 0 in output) — now folded into rawFinal and exposed. Trust-first: weak data quality lowers the score. Resolves 3 pre-existing red tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
115 lines
4.3 KiB
TypeScript
115 lines
4.3 KiB
TypeScript
import { Box, Chip, Paper, Typography } from '@mui/material'
|
||
import { HardHat } from 'lucide-react'
|
||
import { effectiveAnnualBurdenPerSqm } from '../../lib/fitOutUtils'
|
||
import { DS_TEXT, DS_SURFACE, DS_BORDER } from '../../lib/ds'
|
||
import { FITOUT_AMORTIZATION_YEARS, FITOUT_ANNUITY_RATE, FIT_OUT_LABELS } from '../../lib/constants'
|
||
|
||
const RATE_PCT = Math.round(FITOUT_ANNUITY_RATE * 100)
|
||
|
||
interface Props {
|
||
fitOut: string
|
||
areaSqm: number
|
||
mabPerSqm: number
|
||
rentPricePerSqm: number
|
||
fitOutByLandlord?: boolean
|
||
}
|
||
|
||
function chf(value: number): string {
|
||
return `CHF ${Math.round(value).toLocaleString('de-CH')}.–`
|
||
}
|
||
|
||
interface RowProps { label: string; value: string; sub?: string; isTotal?: boolean; isWarning?: boolean }
|
||
|
||
function Row({ label, value, sub, isTotal, isWarning }: RowProps) {
|
||
return (
|
||
<Box sx={{
|
||
display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start',
|
||
py: 0.875, borderBottom: isTotal ? 'none' : `1px solid ${DS_BORDER.default}`,
|
||
bgcolor: isTotal ? (isWarning ? DS_SURFACE.warning.bg : DS_SURFACE.success.bg) : 'transparent',
|
||
px: isTotal ? 1.5 : 0, mx: isTotal ? -1.5 : 0, borderRadius: isTotal ? 1 : 0,
|
||
}}>
|
||
<Box>
|
||
<Typography variant="body2" sx={{ fontWeight: isTotal ? 700 : 400 }}>{label}</Typography>
|
||
{sub && <Typography variant="caption" sx={{ color: DS_TEXT.muted }}>{sub}</Typography>}
|
||
</Box>
|
||
<Typography variant="body2" sx={{ fontWeight: isTotal ? 700 : 500, color: isTotal ? DS_TEXT.primary : DS_TEXT.secondary, flexShrink: 0, ml: 2 }}>
|
||
{value}
|
||
</Typography>
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
export function FitOutCostPanel({ fitOut, areaSqm, mabPerSqm, rentPricePerSqm, fitOutByLandlord }: Props) {
|
||
const { fitOutPerSqm } = effectiveAnnualBurdenPerSqm({ fitOut, rentPricePerSqm, mabPerSqm, fitOutByLandlord })
|
||
const rentPerYear = rentPricePerSqm * areaSqm
|
||
const fitOutPerYear = fitOutPerSqm * areaSqm
|
||
const totalPerYear = rentPerYear + fitOutPerYear
|
||
const fitOutLabel = FIT_OUT_LABELS[fitOut] ?? fitOut
|
||
|
||
// Kein Aufschlag → entweder Vermieter übernimmt oder Fläche bereits bezugsfertig
|
||
const hasSurcharge = fitOutPerYear > 0
|
||
const isWarning = totalPerYear > rentPerYear * 1.3
|
||
const disclaimer = hasSurcharge
|
||
? `Ausbaukosten nach CRB/BKP-Normen, annuitätisch über ${FITOUT_AMORTIZATION_YEARS} Jahre zu ${RATE_PCT}% p.a. Tatsächliche Kosten je nach Ausbauumfang.`
|
||
: null
|
||
|
||
return (
|
||
<Paper sx={{ p: 2.5 }}>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||
<HardHat size={15} color={DS_TEXT.secondary} />
|
||
<Typography variant="h6" sx={{ fontWeight: 700 }}>Reale Jahresbelastung</Typography>
|
||
{hasSurcharge && (
|
||
<Chip
|
||
label="CRB/BKP Richtwerte"
|
||
size="small"
|
||
sx={{ ml: 'auto', bgcolor: DS_SURFACE.blue.bg, color: DS_TEXT.signalDark, fontSize: 10, height: 20, border: `1px solid ${DS_SURFACE.blue.border}` }}
|
||
/>
|
||
)}
|
||
</Box>
|
||
|
||
<Box sx={{ px: 1.5, border: `1px solid ${DS_BORDER.default}`, borderRadius: 1.5, overflow: 'hidden' }}>
|
||
<Row
|
||
label="Jahresmiete"
|
||
value={chf(rentPerYear)}
|
||
sub={`CHF ${rentPricePerSqm}/m²/Jahr × ${areaSqm.toLocaleString('de-CH')} m²`}
|
||
/>
|
||
{fitOutByLandlord ? (
|
||
<Row
|
||
label="Ausbau"
|
||
value="CHF 0.–"
|
||
sub={`${fitOutLabel} — Ausbau im Mietzins enthalten`}
|
||
/>
|
||
) : !hasSurcharge ? (
|
||
<Row
|
||
label="Ausbau"
|
||
value="CHF 0.–"
|
||
sub={`${fitOutLabel} — bezugsfertig`}
|
||
/>
|
||
) : (
|
||
<Row
|
||
label={`Ausbau (annuit. ${FITOUT_AMORTIZATION_YEARS} J. / ${RATE_PCT}%)`}
|
||
value={chf(fitOutPerYear)}
|
||
sub={
|
||
`${fitOutLabel}` +
|
||
(mabPerSqm > 0 ? ` abzgl. CHF ${mabPerSqm} MAB` : '') +
|
||
` — CHF ${fitOutPerSqm}/m²/Jahr × ${areaSqm.toLocaleString('de-CH')} m²`
|
||
}
|
||
/>
|
||
)}
|
||
<Row
|
||
label="Total/Jahr"
|
||
value={chf(totalPerYear)}
|
||
isTotal
|
||
isWarning={isWarning}
|
||
/>
|
||
</Box>
|
||
|
||
{disclaimer && (
|
||
<Typography variant="caption" sx={{ color: DS_TEXT.muted, display: 'block', mt: 1.25 }}>
|
||
{disclaimer}
|
||
</Typography>
|
||
)}
|
||
</Paper>
|
||
)
|
||
}
|