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>
This commit is contained in:
Benjamin Sutter
2026-06-20 23:53:00 +02:00
parent fd6ed105bb
commit fca113e7ab
8 changed files with 181 additions and 2 deletions
+12 -1
View File
@@ -8,7 +8,7 @@ import { useInquiryStore } from '../../stores/inquiryStore'
import { AddToPipelineDialog } from '../../components/shortlist'
import { MatchReasonList } from '../../components/match-card/MatchReasonList'
import { getCityIntelligence, lookupCityCoords } from '../../lib/locationIntelligence'
import { NextActionsPanel, FitOutCostPanel, FitOutAdvicePanel } from '../../components/match-detail'
import { NextActionsPanel, FitOutCostPanel, FitOutAdvicePanel, MarketPricePanel } from '../../components/match-detail'
import type { MatchCardReason } from '../../components/match-card/MatchCardViewModel'
import { useMatchDetailData } from '../../hooks/useMatchDetailData'
import { useMatchDetail } from '../../hooks/useMatches'
@@ -39,6 +39,7 @@ export default function MatchDetail() {
const { openInquiryDialog } = useInquiryStore()
const { match, property, need, signal, isLoading, isFuture } = useMatchDetailData(matchId ?? '')
const detailUnit = property?.units?.find(u => u.id === match?.unitId)
if (isLoading) {
return (
@@ -238,6 +239,16 @@ export default function MatchDetail() {
/>
)}
{/* ── Marktpreis-Einschätzung (Angebot vs. Markt; bei Pre-Market inkl. Zukunftspreis) ── */}
{property?.location?.city && property.rentPricePerSqm > 0 && (
<MarketPricePanel
city={property.location.city}
assetType={property.assetType}
askingRentPerSqm={detailUnit?.rentPricePerSqm ?? property.rentPricePerSqm}
futureRentPerSqm={isFuture ? detailUnit?.expectedRentPerSqm : undefined}
/>
)}
{/* ── Full analysis toggle + expanded panels ── */}
<MatchDetailScoreBreakdown
match={match}