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:
@@ -7,8 +7,8 @@ import { usePipelineStore } from '../../stores/pipelineStore'
|
||||
import { useInquiryStore } from '../../stores/inquiryStore'
|
||||
import { AddToPipelineDialog } from '../../components/shortlist'
|
||||
import { MatchReasonList } from '../../components/match-card/MatchReasonList'
|
||||
import { getCityIntelligence } from '../../lib/locationIntelligence'
|
||||
import { NextActionsPanel } from '../../components/match-detail'
|
||||
import { getCityIntelligence, lookupCityCoords } from '../../lib/locationIntelligence'
|
||||
import { NextActionsPanel, FitOutCostPanel } from '../../components/match-detail'
|
||||
import type { MatchCardReason } from '../../components/match-card/MatchCardViewModel'
|
||||
import { useMatchDetailData } from '../../hooks/useMatchDetailData'
|
||||
import { useMatchDetail } from '../../hooks/useMatches'
|
||||
@@ -72,6 +72,9 @@ export default function MatchDetail() {
|
||||
const cityIntel = city ? getCityIntelligence(city) : null
|
||||
const taxCalculatorUrl = hasTaxFactor ? (cityIntel?.taxCalculatorUrl ?? undefined) : undefined
|
||||
|
||||
const searchCenter = need?.preferredLocations?.[0] ? lookupCityCoords(need.preferredLocations[0]) : null
|
||||
const searchLabel = need?.preferredLocations?.[0]
|
||||
|
||||
const topTradeoff = match.tradeoffs?.[0] ?? match.tradeOffs?.[0]
|
||||
const summary = match.explainabilitySummary || `${match.matchStrength}-Match mit ${match.matchScore} Punkten.`
|
||||
|
||||
@@ -111,6 +114,15 @@ export default function MatchDetail() {
|
||||
{ label: 'Miete/m²/Jahr', value: property?.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}` : '–' },
|
||||
{ label: 'Verfügbar ab', value: property?.availabilityDate ?? '–' },
|
||||
{ label: 'Nutzungsart', value: property?.assetType ?? '–' },
|
||||
...(property?.hardFacts?.fitOut ? [{
|
||||
label: 'Ausbaustandard',
|
||||
value: (() => {
|
||||
const LABELS: Record<string, string> = { SHELL: 'Rohbau', BASIC: 'Basisausbau', FULL: 'Vollausbau', PREMIUM: 'Premiumausbau' }
|
||||
const base = LABELS[property.hardFacts!.fitOut!] ?? property.hardFacts!.fitOut!
|
||||
const mab = property.hardFacts!.mieterausbaubeitragPerSqm
|
||||
return mab ? `${base} + CHF ${mab} MAB` : base
|
||||
})(),
|
||||
}] : []),
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -152,6 +164,10 @@ export default function MatchDetail() {
|
||||
keyFacts={keyFacts}
|
||||
onCompare={handleCompare}
|
||||
onShortlist={handleShortlist}
|
||||
searchCenterLat={searchCenter?.lat}
|
||||
searchCenterLng={searchCenter?.lng}
|
||||
searchRadiusKm={need?.searchRadius}
|
||||
searchLabel={searchLabel}
|
||||
/>
|
||||
|
||||
{/* ── Main content ── */}
|
||||
@@ -197,6 +213,17 @@ export default function MatchDetail() {
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* ── Section C: Reale Jahresbelastung (alle fitOut-Werte) ── */}
|
||||
{!isFuture && property?.hardFacts?.fitOut && (
|
||||
<FitOutCostPanel
|
||||
fitOut={property.hardFacts.fitOut}
|
||||
areaSqm={property.areaSqm}
|
||||
mabPerSqm={property.hardFacts.mieterausbaubeitragPerSqm ?? 0}
|
||||
rentPricePerSqm={property.rentPricePerSqm}
|
||||
tenantBudgetPerSqm={need?.fitOutBudgetMaxPerSqm ?? 0}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ── Full analysis toggle + expanded panels ── */}
|
||||
<MatchDetailScoreBreakdown
|
||||
match={match}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import { Box, Button, Card, Typography } from '@mui/material'
|
||||
import { Box, Button, Card, Chip, Typography } from '@mui/material'
|
||||
import { Zap } from 'lucide-react'
|
||||
import { useNavigate, useLocation } from 'react-router'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
@@ -161,6 +161,29 @@ export default function Results() {
|
||||
<strong>Bezug ab:</strong> {new Date(activeNeed.timing.earliestMoveIn).toLocaleDateString('de-CH', { month: 'long', year: 'numeric' })}
|
||||
</Typography>
|
||||
)}
|
||||
{activeNeed.searchRadius && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
<strong>Radius:</strong> {activeNeed.searchRadius} km
|
||||
</Typography>
|
||||
)}
|
||||
{activeNeed.requiresDivisibility && activeNeed.minDivisibleUnit && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
<strong>Teilbar ab:</strong> {activeNeed.minDivisibleUnit} m²
|
||||
</Typography>
|
||||
)}
|
||||
{activeNeed.requiredFitOut && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
<strong>Ausbaustandard:</strong> min. {
|
||||
activeNeed.requiredFitOut === 'BASIC' ? 'Basisausbau' :
|
||||
activeNeed.requiredFitOut === 'FULL' ? 'Vollausbau' : 'Premiumausbau'
|
||||
}
|
||||
</Typography>
|
||||
)}
|
||||
{activeNeed.fitOutBudgetMaxPerSqm && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
<strong>Ausbaubudget:</strong> max. CHF {activeNeed.fitOutBudgetMaxPerSqm}/m²
|
||||
</Typography>
|
||||
)}
|
||||
{activeNeed.mustCriteriaText && activeNeed.mustCriteriaText.length > 0 && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
<strong>Must-haves:</strong> {activeNeed.mustCriteriaText.slice(0, 3).join(' · ')}
|
||||
@@ -168,6 +191,14 @@ export default function Results() {
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexShrink: 0 }}>
|
||||
{activeNeed.isAnonymous && (
|
||||
<Chip
|
||||
label="Anonyme Suche"
|
||||
size="small"
|
||||
sx={{ bgcolor: '#7c3aed', color: 'white', fontWeight: 700, fontSize: 10, height: 20 }}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
@@ -176,6 +207,7 @@ export default function Results() {
|
||||
>
|
||||
Suche ändern
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -83,6 +83,9 @@ export default function NewListing() {
|
||||
fitOut={form.fitOut} onFitOutChange={form.setFitOut}
|
||||
parking={form.parking} onParkingChange={form.setParking}
|
||||
ceilingHeight={form.ceilingHeight} onCeilingHeightChange={form.setCeilingHeight}
|
||||
mieterausbaubeitrag={form.mieterausbaubeitrag} onMieterausbaubeitragChange={form.setMieterausbaubeitrag}
|
||||
isFlexible={form.isFlexible} onIsFlexibleChange={form.setIsFlexible}
|
||||
minLettableSqm={form.minLettableSqm} onMinLettableSqmChange={form.setMinLettableSqm}
|
||||
/>
|
||||
|
||||
<ImageUrlSection
|
||||
|
||||
@@ -19,11 +19,15 @@ export function buildCreatePropertyInput(fields: {
|
||||
ceilingHeight: string
|
||||
images: string[]
|
||||
floorPlanUrl: string
|
||||
mieterausbaubeitrag?: string
|
||||
isFlexible?: boolean
|
||||
minLettableSqm?: string
|
||||
}): CreatePropertyInput {
|
||||
const {
|
||||
assetType, street, houseNumber, postalCode, city,
|
||||
areaSqm, rentPerSqm, availableFrom, description,
|
||||
softLevels, floor, fitOut, parking, ceilingHeight, images, floorPlanUrl,
|
||||
mieterausbaubeitrag, isFlexible, minLettableSqm,
|
||||
} = fields
|
||||
|
||||
const sf = {
|
||||
@@ -41,6 +45,7 @@ export function buildCreatePropertyInput(fields: {
|
||||
const hf = {
|
||||
floor: floor ? parseInt(floor) : undefined,
|
||||
fitOut: (fitOut || undefined) as 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM' | undefined,
|
||||
mieterausbaubeitragPerSqm: mieterausbaubeitrag ? parseInt(mieterausbaubeitrag) : undefined,
|
||||
parking: parking ? parseInt(parking) : undefined,
|
||||
ceilingHeightM: ceilingHeight ? parseFloat(ceilingHeight) : undefined,
|
||||
}
|
||||
@@ -76,5 +81,15 @@ export function buildCreatePropertyInput(fields: {
|
||||
warnings: [],
|
||||
},
|
||||
status: 'ACTIVE',
|
||||
units: isFlexible ? [{
|
||||
id: `unit-listing-${Date.now()}`,
|
||||
propertyId: '',
|
||||
floorLevel: hf.floor ?? 0,
|
||||
areaSqm,
|
||||
available: true,
|
||||
rentPricePerSqm: rentPerSqm,
|
||||
isFlexible: true,
|
||||
minLettableSqm: minLettableSqm ? parseInt(minLettableSqm) : undefined,
|
||||
}] : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user