From 29be4f6e5427cc9b91c002285557220c6641b59c Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Sun, 21 Jun 2026 00:42:51 +0200 Subject: [PATCH] =?UTF-8?q?refactor(supply):=20pre-market=20unit=20config?= =?UTF-8?q?=20inline=20=E2=80=94=20attributes=20first,=20then=20price=20(n?= =?UTF-8?q?o=20double=20price)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Released pre-market unit now shows a single always-visible config: fit-out/parking/cost-bearer (auto-save) THEN the KI price recommendation with explicit deviation vs. current price - Removes the duplicate price (base price no longer shown here; only the pre-market expected price is set), and removes the edit-only pencil for released units - Not-yet-released units keep the compact read strip + pencil editor Co-Authored-By: Claude Opus 4.8 --- .../supply/PreMarketPriceAdvisor.tsx | 129 ++++++++++++++---- src/components/supply/PreMarketUnitGrid.tsx | 43 +++--- 2 files changed, 127 insertions(+), 45 deletions(-) diff --git a/src/components/supply/PreMarketPriceAdvisor.tsx b/src/components/supply/PreMarketPriceAdvisor.tsx index e91612b..52cdf6d 100644 --- a/src/components/supply/PreMarketPriceAdvisor.tsx +++ b/src/components/supply/PreMarketPriceAdvisor.tsx @@ -1,53 +1,126 @@ import { useState } from 'react' -import { Box, Button, Chip, CircularProgress, TextField, Typography } from '@mui/material' +import { Box, Button, Chip, CircularProgress, Divider, MenuItem, TextField, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material' import { Sparkles } from 'lucide-react' import type { Property, PropertyUnit } from '../../domain/property' import { usePreMarketRentRecommendation } from '../../hooks/useAI' import { useUpdateUnit } from '../../hooks/useProperties' import { resolveUnitFacts } from '../../lib/unitFacts' -import { DS_PRE_MARKET, DS_SURFACE, DS_TEXT } from '../../lib/ds' +import { FIT_OUT_LABELS } from '../../lib/constants' +import { DS_BORDER, DS_PRE_MARKET, DS_SURFACE, DS_TEXT } from '../../lib/ds' +const NEEDS_BUILD = new Set(['SHELL', 'BASIC']) const VERDICT_META: Record = { UNDERPRICED: { label: 'zu günstig', bg: '#fef3c7', fg: '#92400e' }, FAIR: { label: 'marktgerecht', bg: '#dcfce7', fg: '#166534' }, AMBITIOUS: { label: 'ambitioniert', bg: '#fee2e2', fg: '#991b1b' }, } +interface Draft { + fitOut?: string + fitOutByLandlord?: boolean + mieterausbaubeitragPerSqm?: number + parkingSpots?: number + expectedRentPerSqm?: number +} + interface Props { property: Property unit: PropertyUnit } -/** KI-Preisempfehlung für eine Pre-Market-Einheit (regionale Vergleichsmieten, Angebot, Nachfrage). */ +/** Pre-Market-Konfiguration einer Einheit: erst Ausbau/Parkplätze, dann KI-Preisempfehlung. */ export function PreMarketPriceAdvisor({ property, unit }: Props) { + const updateUnit = useUpdateUnit(property.id) + const [d, setD] = useState(() => ({ + fitOut: unit.fitOut ?? '', + fitOutByLandlord: unit.fitOutByLandlord, + mieterausbaubeitragPerSqm: unit.mieterausbaubeitragPerSqm, + parkingSpots: unit.parkingSpots, + expectedRentPerSqm: unit.expectedRentPerSqm, + })) + const facts = resolveUnitFacts(property, unit) + const current = facts.rentPricePerSqm + const effFit = d.fitOut || property.hardFacts?.fitOut + const needsBuild = NEEDS_BUILD.has(effFit ?? '') + const { data, isLoading, isError } = usePreMarketRentRecommendation({ city: property.location?.city ?? '', assetType: property.assetType, areaSqm: unit.areaSqm, - currentRentPerSqm: facts.rentPricePerSqm, + currentRentPerSqm: current, availableFrom: unit.schattenmarktRelease?.availableFrom, }) - const updateUnit = useUpdateUnit(property.id) - const [draft, setDraft] = useState(unit.expectedRentPerSqm != null ? String(unit.expectedRentPerSqm) : '') - - function saveExpected(value: string) { - updateUnit.mutate({ unitId: unit.id, data: { expectedRentPerSqm: parseInt(value) || undefined } }) - } - const rec = data?.data const verdict = rec ? VERDICT_META[rec.verdict] : null + const save = (patch: Partial) => updateUnit.mutate({ unitId: unit.id, data: patch }) + return ( - + + {/* 1) Ausbau & Ausstattung */} + + { + const v = e.target.value + const nb = v === 'SHELL' || v === 'BASIC' + setD(p => ({ ...p, fitOut: v })) + save({ fitOut: (v || undefined) as PropertyUnit['fitOut'], ...(nb ? {} : { fitOutByLandlord: undefined, mieterausbaubeitragPerSqm: undefined }) }) + }} + > + Wie Objekt{property.hardFacts?.fitOut ? ` (${FIT_OUT_LABELS[property.hardFacts.fitOut] ?? property.hardFacts.fitOut})` : ''} + {FIT_OUT_LABELS.SHELL} + {FIT_OUT_LABELS.BASIC} + {FIT_OUT_LABELS.FULL} + {FIT_OUT_LABELS.PREMIUM} + + setD(p => ({ ...p, parkingSpots: parseInt(e.target.value) || undefined }))} + onBlur={e => save({ parkingSpots: parseInt(e.target.value) || undefined })} + slotProps={{ htmlInput: { min: 0 } }} + helperText={property.hardFacts?.parking != null ? `Pool: ${property.hardFacts.parking}` : undefined} + /> + + + {needsBuild && ( + + Wer baut aus? + { + if (!v) return + const landlord = v === 'landlord' + setD(p => ({ ...p, fitOutByLandlord: landlord, ...(landlord ? { mieterausbaubeitragPerSqm: undefined } : {}) })) + save({ fitOutByLandlord: landlord, ...(landlord ? { mieterausbaubeitragPerSqm: undefined } : {}) }) + }} + > + Vermieter + Mieter + + {d.fitOutByLandlord === false && ( + setD(p => ({ ...p, mieterausbaubeitragPerSqm: parseInt(e.target.value) || undefined }))} + onBlur={e => save({ mieterausbaubeitragPerSqm: parseInt(e.target.value) || undefined })} + slotProps={{ htmlInput: { min: 0, max: 1000, step: 25 } }} + /> + )} + + )} + + + + {/* 2) KI-Preisempfehlung */} - - KI-Preisempfehlung Pre-Market - - {verdict && ( - - )} + KI-Preisempfehlung Pre-Market + {verdict && } {isLoading ? ( @@ -59,26 +132,32 @@ export function PreMarketPriceAdvisor({ property, unit }: Props) { Keine Empfehlung verfügbar. ) : ( <> - + Empfehlung: CHF {rec.recommendedPerSqm}/m² (CHF {rec.rangeMinPerSqm}–{rec.rangeMaxPerSqm}) - + + Heute CHF {current}/m² + + {rec.deltaVsCurrentPct > 0 ? `+${rec.deltaVsCurrentPct}%` : `${rec.deltaVsCurrentPct}%`} vs. heute + + + {rec.drivers.join(' · ')} setDraft(e.target.value)} - onBlur={e => saveExpected(e.target.value)} + value={d.expectedRentPerSqm ?? ''} + onChange={e => setD(p => ({ ...p, expectedRentPerSqm: parseInt(e.target.value) || undefined }))} + onBlur={e => save({ expectedRentPerSqm: parseInt(e.target.value) || undefined })} slotProps={{ inputLabel: { shrink: true }, htmlInput: { min: 0, step: 10 } }} sx={{ width: 150, '& .MuiInputBase-input': { fontSize: '0.72rem', py: 0.5 } }} />