From 3e1e94566152642489c075390ffb587810e1c33b Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Sun, 21 Jun 2026 00:14:27 +0200 Subject: [PATCH] fix(supply): unit edits now persist + regroup editor + expected price as pre-market KI view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MockupUnitProvider.update: immutable update (new unit/units/property refs) so React Query detects the change and re-renders — fixes "parking/price edit doesn't take effect" - UnitFieldsEditor: group Ausbaustandard + Wer-baut-aus + MAB into one "Ausbau" section; conditions (price/availability/parking) on top; removed the plain expected-price field - PreMarketUnitGrid: expected price is now a KI recommendation view (indexed suggestion + rationale) shown only for released units, with an adjustable override + "Vorschlag übernehmen" Co-Authored-By: Claude Opus 4.8 --- src/components/supply/PreMarketUnitGrid.tsx | 56 ++++++++++++- src/components/supply/UnitFieldsEditor.tsx | 93 ++++++++++----------- src/provider/MockupUnitProvider.ts | 11 ++- 3 files changed, 104 insertions(+), 56 deletions(-) diff --git a/src/components/supply/PreMarketUnitGrid.tsx b/src/components/supply/PreMarketUnitGrid.tsx index b7a1f96..385dd5a 100644 --- a/src/components/supply/PreMarketUnitGrid.tsx +++ b/src/components/supply/PreMarketUnitGrid.tsx @@ -1,10 +1,12 @@ import { useState } from 'react' -import { Box, CircularProgress, Collapse, IconButton, Switch, TextField, Tooltip, Typography } from '@mui/material' -import { EyeOff, Pencil } from 'lucide-react' +import { Box, Button, CircularProgress, Collapse, IconButton, Switch, TextField, Tooltip, Typography } from '@mui/material' +import { EyeOff, Pencil, Sparkles } from 'lucide-react' import type { Property } from '../../domain/property' -import { DS_BORDER, DS_PRE_MARKET, DS_TEXT } from '../../lib/ds' +import { useUpdateUnit } from '../../hooks/useProperties' +import { DS_BORDER, DS_PRE_MARKET, DS_SURFACE, DS_TEXT } from '../../lib/ds' import { FIT_OUT_LABELS } from '../../lib/constants' import { resolveUnitFacts } from '../../lib/unitFacts' +import { suggestFutureRent } from '../../lib/rentEstimate' import { floorLabel } from './PropertyDetailHelpers' import { UnitFieldsEditor } from './UnitFieldsEditor' @@ -27,6 +29,14 @@ interface Props { export function PreMarketUnitGrid({ property, units, unitStates, unitSaving, setUnitStates, saveUnit }: Props) { const [editing, setEditing] = useState(null) + const [expectedDraft, setExpectedDraft] = useState>(() => + Object.fromEntries(units.map(u => [u.id, u.expectedRentPerSqm != null ? String(u.expectedRentPerSqm) : ''])), + ) + const updateUnit = useUpdateUnit(property.id) + + function saveExpected(unitId: string, value: string) { + updateUnit.mutate({ unitId, data: { expectedRentPerSqm: parseInt(value) || undefined } }) + } return ( @@ -143,6 +153,46 @@ export function PreMarketUnitGrid({ property, units, unitStates, unitSaving, set {detailParts.join(' · ')} + {/* KI-Preis-Ansicht: nur wenn freigegeben — Vorschlag, anpassbar wenn zu günstig */} + {us.enabled && (() => { + const current = f.rentPricePerSqm + const suggestion = suggestFutureRent(property.location?.city ?? '', current) + return ( + + + + + KI-Preisempfehlung Pre-Market + + + + Heute CHF {current}/m² + {suggestion ? ` → indexiert CHF ${suggestion}/m²` : ''} — anpassen, falls zu günstig. + + + setExpectedDraft(prev => ({ ...prev, [u.id]: e.target.value }))} + onBlur={e => saveExpected(u.id, e.target.value)} + slotProps={{ inputLabel: { shrink: true }, htmlInput: { min: 0, step: 10 } }} + sx={{ width: 150, '& .MuiInputBase-input': { fontSize: '0.72rem', py: 0.5 } }} + /> + {suggestion != null && ( + + )} + + + ) + })()} + {/* Inline editor (shared) */} diff --git a/src/components/supply/UnitFieldsEditor.tsx b/src/components/supply/UnitFieldsEditor.tsx index f752871..c7d87af 100644 --- a/src/components/supply/UnitFieldsEditor.tsx +++ b/src/components/supply/UnitFieldsEditor.tsx @@ -3,7 +3,6 @@ import { Box, Button, MenuItem, TextField, ToggleButton, ToggleButtonGroup, Typo import type { Property, PropertyUnit } from '../../domain/property' import { useUpdateUnit } from '../../hooks/useProperties' import { FIT_OUT_LABELS } from '../../lib/constants' -import { suggestFutureRent } from '../../lib/rentEstimate' import { DS_BORDER, DS_TEXT } from '../../lib/ds' const NEEDS_BUILD = new Set(['SHELL', 'BASIC']) @@ -15,7 +14,6 @@ interface UnitDraft { fitOutByLandlord?: boolean mieterausbaubeitragPerSqm?: number parkingSpots?: number - expectedRentPerSqm?: number } interface Props { @@ -24,7 +22,7 @@ interface Props { onClose: () => void } -/** Shared per-unit editor: price, availability, fit-out, cost-bearer, MAB, parking allocation, expected price. */ +/** Shared per-unit editor: conditions (price, availability), fit-out group, parking allocation. */ export function UnitFieldsEditor({ property, unit, onClose }: Props) { const updateUnit = useUpdateUnit(property.id) const [d, setD] = useState(() => ({ @@ -34,12 +32,10 @@ export function UnitFieldsEditor({ property, unit, onClose }: Props) { fitOutByLandlord: unit.fitOutByLandlord, mieterausbaubeitragPerSqm: unit.mieterausbaubeitragPerSqm, parkingSpots: unit.parkingSpots, - expectedRentPerSqm: unit.expectedRentPerSqm, })) const effFit = d.fitOut || property.hardFacts?.fitOut const needsBuild = NEEDS_BUILD.has(effFit ?? '') - const suggestion = suggestFutureRent(property.location?.city ?? '', d.rentPricePerSqm ?? unit.rentPricePerSqm ?? property.rentPricePerSqm) function save() { const fit = (d.fitOut || undefined) as 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM' | undefined @@ -53,15 +49,15 @@ export function UnitFieldsEditor({ property, unit, onClose }: Props) { fitOutByLandlord: nb ? d.fitOutByLandlord : undefined, mieterausbaubeitragPerSqm: nb && d.fitOutByLandlord === false ? d.mieterausbaubeitragPerSqm : undefined, parkingSpots: d.parkingSpots, - expectedRentPerSqm: d.expectedRentPerSqm, }, }) onClose() } return ( - - + + {/* Konditionen */} + )} setD(p => ({ ...p, parkingSpots: parseInt(e.target.value) || undefined }))} + slotProps={{ htmlInput: { min: 0 } }} + helperText={property.hardFacts?.parking != null ? `Objekt-Pool: ${property.hardFacts.parking}` : 'Leer = Objekt-Wert'} + /> + + + {/* Ausbau — Standard + Träger + MAB als Gruppe */} + + + Ausbau + + setD(p => ({ ...p, fitOut: e.target.value }))} > @@ -89,49 +99,32 @@ export function UnitFieldsEditor({ property, unit, onClose }: Props) { {FIT_OUT_LABELS.FULL} {FIT_OUT_LABELS.PREMIUM} - setD(p => ({ ...p, parkingSpots: parseInt(e.target.value) || undefined }))} - slotProps={{ htmlInput: { min: 0 } }} - helperText={property.hardFacts?.parking != null ? `aus Objekt-Pool: ${property.hardFacts.parking}` : 'Leer = Objekt-Wert'} - /> + + {needsBuild && ( + + Wer baut aus? + { if (v) setD(p => ({ ...p, fitOutByLandlord: v === 'landlord', ...(v === 'landlord' ? { mieterausbaubeitragPerSqm: undefined } : {}) })) }} + > + Vermieter + Mieter + + {d.fitOutByLandlord === false && ( + setD(p => ({ ...p, mieterausbaubeitragPerSqm: parseInt(e.target.value) || undefined }))} + slotProps={{ htmlInput: { min: 0, max: 1000, step: 25 } }} + /> + )} + leer = wie Objekt + + )} - - setD(p => ({ ...p, expectedRentPerSqm: parseInt(e.target.value) || undefined }))} - slotProps={{ htmlInput: { min: 0, step: 10 } }} - helperText={suggestion ? `Vorschlag (indexiert): CHF ${suggestion}/m² — leer = heutiger Preis` : 'Leer = heutiger Preis'} - /> - - - {needsBuild && ( - - Wer baut aus? - { if (v) setD(p => ({ ...p, fitOutByLandlord: v === 'landlord', ...(v === 'landlord' ? { mieterausbaubeitragPerSqm: undefined } : {}) })) }} - > - Vermieter - Mieter - - {d.fitOutByLandlord === false && ( - setD(p => ({ ...p, mieterausbaubeitragPerSqm: parseInt(e.target.value) || undefined }))} - slotProps={{ htmlInput: { min: 0, max: 1000, step: 25 } }} - /> - )} - leer = wie Objekt - - )} - - + diff --git a/src/provider/MockupUnitProvider.ts b/src/provider/MockupUnitProvider.ts index 994e8cd..45d6ad7 100644 --- a/src/provider/MockupUnitProvider.ts +++ b/src/provider/MockupUnitProvider.ts @@ -17,11 +17,16 @@ export const MockupUnitProvider: IUnitProvider = { return getAllUnits().find(u => u.id === unitId) ?? null }, async update(unitId: string, data: Partial) { - for (const prop of propertyStore) { + for (let i = 0; i < propertyStore.length; i++) { + const prop = propertyStore[i] const idx = (prop.units ?? []).findIndex(u => u.id === unitId) if (idx === -1) continue - prop.units![idx] = { ...prop.units![idx], ...data } - return prop.units![idx] + // Immutable update: neue Referenzen (Unit, units-Array, Property), damit React Query + // die Änderung via Structural-Sharing erkennt und neu rendert. + const nextUnits = [...prop.units!] + nextUnits[idx] = { ...nextUnits[idx], ...data } + propertyStore[i] = { ...prop, units: nextUnits } + return nextUnits[idx] } throw new Error(`Unit ${unitId} not found`) },