Files
property-match/src/components/match-detail/MarketPricePanel.tsx
T
Benjamin Sutter fca113e7ab feat(demand): market rent estimate (asking vs. fair) + pre-market expected price
- lib/rentEstimate: deterministic market estimate from Standort-Intelligence (median rent benchmark + vacancy/trend) → fair rent, verdict BELOW/AT/ABOVE, deltaPct, rationale; suggestFutureRent indexes today's price by city rent trend
- MarketPricePanel in MatchDetail: asking vs. fair market rent + verdict badge + rationale; shows pre-market expected price (heute → erwartet)
- unit.expectedRentPerSqm: future pre-market price; scorer prefers it over current rent for FUTURE_AVAILABILITY
- UnitStructurePanel editor: expected-price field (with indexed suggestion) shown for pre-market-released units
- tests: rentEstimate verdict thresholds + future-rent indexing

Note: rent estimate is a deterministic market-data calc (not an LLM) — honest & testable; can be routed through IAIService later if a real model is wanted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 23:53:00 +02:00

71 lines
3.3 KiB
TypeScript

import { Box, Chip, Paper, Typography } from '@mui/material'
import { TrendingUp } from 'lucide-react'
import { estimateMarketRent, type RentVerdict } from '../../lib/rentEstimate'
import { DS_TEXT, DS_SURFACE, DS_BORDER } from '../../lib/ds'
const VERDICT_META: Record<RentVerdict, { label: string; bg: string; border: string; fg: string }> = {
BELOW: { label: 'Unter Markt', bg: DS_SURFACE.success.bg, border: DS_SURFACE.success.border, fg: DS_TEXT.success },
AT: { label: 'Marktkonform', bg: DS_SURFACE.blue.bg, border: DS_SURFACE.blue.border, fg: DS_TEXT.signalDark },
ABOVE: { label: 'Über Markt', bg: DS_SURFACE.warning.bg, border: DS_SURFACE.warning.border, fg: DS_TEXT.warning },
}
interface Props {
city: string
assetType: string
askingRentPerSqm: number
futureRentPerSqm?: number // Pre-Market: erwarteter künftiger Preis
}
function chf(v: number): string {
return `CHF ${Math.round(v).toLocaleString('de-CH')}/m²`
}
export function MarketPricePanel({ city, assetType, askingRentPerSqm, futureRentPerSqm }: Props) {
const est = estimateMarketRent(city, assetType, askingRentPerSqm)
if (!est) return null
const meta = VERDICT_META[est.verdict]
return (
<Paper sx={{ p: 2.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
<TrendingUp size={15} color={DS_TEXT.secondary} />
<Typography variant="h6" sx={{ fontWeight: 700 }}>Marktpreis-Einschätzung</Typography>
<Chip
label={meta.label}
size="small"
sx={{ ml: 'auto', bgcolor: meta.bg, color: meta.fg, fontWeight: 700, fontSize: 11, height: 22, border: `1px solid ${meta.border}` }}
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 1.25 }}>
<Box sx={{ flex: 1, p: 1.25, border: `1px solid ${DS_BORDER.default}`, borderRadius: 1 }}>
<Typography variant="caption" sx={{ color: DS_TEXT.muted }}>Angebotsmiete</Typography>
<Typography variant="body1" sx={{ fontWeight: 700 }}>{chf(est.askingRentPerSqm)}</Typography>
</Box>
<Box sx={{ flex: 1, p: 1.25, border: `1px solid ${DS_BORDER.default}`, borderRadius: 1 }}>
<Typography variant="caption" sx={{ color: DS_TEXT.muted }}>Faire Marktmiete</Typography>
<Typography variant="body1" sx={{ fontWeight: 700 }}>
{chf(est.fairRentPerSqm)}
<Box component="span" sx={{ ml: 0.75, fontSize: 12, fontWeight: 600, color: meta.fg }}>
{est.deltaPct > 0 ? `+${est.deltaPct}%` : `${est.deltaPct}%`}
</Box>
</Typography>
</Box>
</Box>
{futureRentPerSqm != null && futureRentPerSqm > 0 && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', py: 1, px: 1.5, mb: 1.25, bgcolor: DS_SURFACE.purple.bg, border: `1px solid ${DS_SURFACE.purple.border}`, borderRadius: 1 }}>
<Typography variant="caption" sx={{ color: DS_TEXT.muted, fontWeight: 600 }}>Erwartet (Pre-Market)</Typography>
<Typography variant="body2" sx={{ fontWeight: 700 }}>
{chf(est.askingRentPerSqm)} <Box component="span" sx={{ color: DS_TEXT.muted }}></Box> {chf(futureRentPerSqm)}
</Typography>
</Box>
)}
<Typography variant="caption" sx={{ color: DS_TEXT.muted, display: 'block' }}>
{est.rationale}
</Typography>
</Paper>
)
}