diff --git a/src/components/new-listing/TechnicalDetailsSection.tsx b/src/components/new-listing/TechnicalDetailsSection.tsx index ff3b843..b034a8e 100644 --- a/src/components/new-listing/TechnicalDetailsSection.tsx +++ b/src/components/new-listing/TechnicalDetailsSection.tsx @@ -9,7 +9,7 @@ interface Props { onFloorChange: (v: string) => void fitOut: string onFitOutChange: (v: string) => void - fitOutByLandlord: boolean + fitOutByLandlord: boolean | undefined onFitOutByLandlordChange: (v: boolean) => void parking: string onParkingChange: (v: string) => void @@ -66,19 +66,25 @@ export function TechnicalDetailsSection({ {/* Ausbau-Träger — nur relevant, wenn die Fläche noch Ausbau benötigt (Rohbau/Edelrohbau) */} {showFitOutResponsibility && ( - Wer baut aus? + + Wer baut aus? * + { if (v) onFitOutByLandlordChange(v === 'landlord') }} > Vermieter übernimmt Mieter baut aus - {fitOutByLandlord ? ( + {fitOutByLandlord === undefined ? ( + + Pflichtangabe bei Rohbau/Edelrohbau — bitte wählen, wer den Ausbau trägt. + + ) : fitOutByLandlord ? ( Ausbau im Mietzins enthalten — bitte die bezugsfertige Miete eintragen. Es wird kein Aufschlag berechnet. diff --git a/src/components/supply/PropertyDetailOverview.tsx b/src/components/supply/PropertyDetailOverview.tsx index e066917..d5c3bfb 100644 --- a/src/components/supply/PropertyDetailOverview.tsx +++ b/src/components/supply/PropertyDetailOverview.tsx @@ -39,7 +39,7 @@ export function PropertyDetailOverview({ p, editing, draft, onDraftChange }: Pro // Effektive (Draft-überlagerte) hardFacts für den Bearbeiten-Modus const hf = draft.hardFacts ?? p.hardFacts ?? {} const editFitOut = hf.fitOut ?? '' - const editByLandlord = hf.fitOutByLandlord ?? false + const editByLandlord = hf.fitOutByLandlord const showBuildResponsibility = FIT_OUT_NEEDS_BUILD.has(editFitOut) const patchHF = (patch: Partial>) => onDraftChange({ ...draft, hardFacts: { ...hf, ...patch } }) @@ -187,17 +187,24 @@ export function PropertyDetailOverview({ p, editing, draft, onDraftChange }: Pro {showBuildResponsibility && ( - Wer baut aus? + + Wer baut aus? * + { if (v) patchHF({ fitOutByLandlord: v === 'landlord', ...(v === 'landlord' ? { mieterausbaubeitragPerSqm: undefined } : {}) }) }} > Vermieter übernimmt Mieter baut aus - {!editByLandlord && ( + {editByLandlord === undefined && ( + + Pflichtangabe bei Rohbau/Edelrohbau + + )} + {editByLandlord === false && ( (null) const [editingFlexUnit, setEditingFlexUnit] = useState(null) const [flexDraft, setFlexDraft] = useState>({}) + const [editingUnit, setEditingUnit] = useState(null) + const [unitDraft, setUnitDraft] = useState<{ rentPricePerSqm?: number; availableFrom?: string }>({}) const updateUnit = useUpdateUnit(p.id) + function openUnitEditor(u: PropertyUnit) { + setUnitDraft({ rentPricePerSqm: u.rentPricePerSqm ?? p.rentPricePerSqm, availableFrom: u.availableFrom }) + setEditingUnit(u.id) + } + function saveUnitEditor(unitId: string) { + updateUnit.mutate({ unitId, data: { rentPricePerSqm: unitDraft.rentPricePerSqm, availableFrom: unitDraft.availableFrom || undefined } }) + setEditingUnit(null) + } + const freeUnits = useMemo(() => (p.units ?? []).filter(u => u.available), [p.units]) const unitMatches = useUnitMatchesMap(freeUnits, p) @@ -286,6 +297,16 @@ export function UnitStructurePanel({ p }: { p: Property }) { {isExpanded ? : } )} + {/* Edit price / availability per unit */} + + (editingUnit === u.id ? setEditingUnit(null) : openUnitEditor(u))} + sx={{ p: 0.25, color: editingUnit === u.id ? '#2563eb' : DS_TEXT.disabled }} + > + + + {/* Area + Preis + Verfügbarkeit pro Einheit */} @@ -301,16 +322,49 @@ export function UnitStructurePanel({ p }: { p: Property }) { CHF {(u.rentPricePerSqm ?? p.rentPricePerSqm).toLocaleString('de-CH')}/m² - {u.available && ( - - {u.schattenmarktRelease?.availableFrom - ? `ab ${new Date(u.schattenmarktRelease.availableFrom).toLocaleDateString('de-CH', { month: 'short', year: '2-digit' })}` - : 'sofort'} - - )} + {u.available && (() => { + const av = u.availableFrom ?? u.schattenmarktRelease?.availableFrom + return ( + + {av ? `ab ${new Date(av).toLocaleDateString('de-CH', { month: 'short', year: '2-digit' })}` : 'sofort'} + + ) + })()} + {/* Inline editor: price + availability per unit */} + + + + setUnitDraft(d => ({ ...d, rentPricePerSqm: parseInt(e.target.value) || undefined }))} + slotProps={{ htmlInput: { min: 0, step: 10 } }} + helperText="Leer = Objekt-Preis" + /> + {u.available && ( + setUnitDraft(d => ({ ...d, availableFrom: e.target.value }))} + helperText="Leer = sofort" + /> + )} + + + + + + + + {/* Expanded: matches for free units */} diff --git a/src/domain/unit.ts b/src/domain/unit.ts index 94ec711..430d4a1 100644 --- a/src/domain/unit.ts +++ b/src/domain/unit.ts @@ -17,6 +17,7 @@ export interface PropertyUnit { areaSqm: number available: boolean rentPricePerSqm?: number // annual CHF/m²; falls back to property.rentPricePerSqm + availableFrom?: string // explicit availability date for a free unit (ISO); else derived from leases // Unit-level fit-out override — falls back to property.hardFacts.fitOut when unset. // Relevant for pre-market, where units of one property are matched individually. fitOut?: 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM' diff --git a/src/hooks/useNewListingForm.ts b/src/hooks/useNewListingForm.ts index a5cde66..8d01d50 100644 --- a/src/hooks/useNewListingForm.ts +++ b/src/hooks/useNewListingForm.ts @@ -15,6 +15,8 @@ function validate(fields: { city: string areaSqm: string rentPerSqm: string + fitOut: string + fitOutByLandlord: boolean | undefined }): string | null { if (!fields.street.trim()) return 'Strasse erforderlich' if (!fields.postalCode.trim()) return 'PLZ erforderlich' @@ -23,6 +25,9 @@ function validate(fields: { || Number(fields.areaSqm) <= 0) return 'Gültige Fläche eingeben' if (!fields.rentPerSqm || isNaN(Number(fields.rentPerSqm)) || Number(fields.rentPerSqm) <= 0) return 'Gültigen Mietpreis eingeben' + // Bei Rohbau/Edelrohbau muss der Ausbau-Träger aktiv gewählt werden + if ((fields.fitOut === 'SHELL' || fields.fitOut === 'BASIC') + && fields.fitOutByLandlord === undefined) return 'Bitte wählen, wer den Ausbau trägt (Vermieter oder Mieter)' return null } @@ -43,7 +48,7 @@ export interface NewListingFormState { // Technical floor: string fitOut: string - fitOutByLandlord: boolean + fitOutByLandlord: boolean | undefined parking: string ceilingHeight: string mieterausbaubeitrag: string @@ -81,7 +86,7 @@ export interface NewListingFormHandlers { setSoftLevel: (key: string, value: string) => void setFloor: (v: string) => void setFitOut: (v: string) => void - setFitOutByLandlord: (v: boolean) => void + setFitOutByLandlord: (v: boolean | undefined) => void setParking: (v: string) => void setCeilingHeight: (v: string) => void setMieterausbaubeitrag: (v: string) => void @@ -119,7 +124,7 @@ export function useNewListingForm(pre: Prefill): NewListingFormState & NewListin const [softLevels, setSoftLevels] = useState>(() => ({ ...emptySoftLevels(), ...pre.softLevels })) const [floor, setFloor] = useState(pre.floor ?? '') const [fitOut, setFitOut] = useState(pre.fitOut ?? '') - const [fitOutByLandlord, setFitOutByLandlord] = useState(pre.fitOutByLandlord ?? false) + const [fitOutByLandlord, setFitOutByLandlord] = useState(pre.fitOutByLandlord) const [parking, setParking] = useState(pre.parking != null ? String(pre.parking) : '') const [ceilingHeight, setCeilingHeight]= useState(pre.ceilingHeight != null ? String(pre.ceilingHeight) : '') const [mieterausbaubeitrag, setMieterausbaubeitrag] = useState(pre.mieterausbaubeitragPerSqm != null ? String(pre.mieterausbaubeitragPerSqm) : '') @@ -164,7 +169,7 @@ export function useNewListingForm(pre: Prefill): NewListingFormState & NewListin } function handleSubmit() { - const err = validate({ street, postalCode, city, areaSqm, rentPerSqm }) + const err = validate({ street, postalCode, city, areaSqm, rentPerSqm, fitOut, fitOutByLandlord }) if (err) { setError(err); return } setError(null) createProperty.mutate( @@ -188,7 +193,7 @@ export function useNewListingForm(pre: Prefill): NewListingFormState & NewListin setAreaSqm(''); setRentPerSqm(''); setAvailableFrom(''); setDescription('') setContactName(''); setContactEmail(''); setContactPhone('') setSoftLevels(emptySoftLevels()) - setFloor(''); setFitOut(''); setFitOutByLandlord(false); setParking(''); setCeilingHeight('') + setFloor(''); setFitOut(''); setFitOutByLandlord(undefined); setParking(''); setCeilingHeight('') setMieterausbaubeitrag(''); setIsFlexible(false); setMinLettableSqm('') setImages([]); setImageInput(''); setFloorPlanUrl('') setAiText(''); setAiApplied(false) diff --git a/src/pages/supply/newListingMapper.ts b/src/pages/supply/newListingMapper.ts index 4db654c..25c7a1a 100644 --- a/src/pages/supply/newListingMapper.ts +++ b/src/pages/supply/newListingMapper.ts @@ -46,7 +46,7 @@ export function buildCreatePropertyInput(fields: { const hf = { floor: floor ? parseInt(floor) : undefined, fitOut: (fitOut || undefined) as 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM' | undefined, - fitOutByLandlord: fitOutByLandlord || undefined, + fitOutByLandlord, // MAB nur relevant, wenn der Mieter ausbaut — sonst nicht speichern mieterausbaubeitragPerSqm: !fitOutByLandlord && mieterausbaubeitrag ? parseInt(mieterausbaubeitrag) : undefined, parking: parking ? parseInt(parking) : undefined,