Files
property-match/src/hooks/useNewListingForm.ts
T
Benjamin Sutter 647493bf3c feat(supply): per-unit price/availability editor + mandatory fit-out cost-bearer for shell/edelrohbau
- unit.availableFrom: explicit per-unit availability date
- UnitStructurePanel: inline expandable editor (pencil) for per-unit price + availability via useUpdateUnit
- fitOutByLandlord now boolean|undefined ("not chosen"); toggle has no default for SHELL/BASIC
- NewListing: validate() blocks save until cost-bearer chosen for Rohbau/Edelrohbau
- PropertyDetailView.saveEdit: same guard for existing listings; required asterisk + hint in both forms
- newListingMapper persists explicit tenant(false) choice (was collapsed to undefined)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 23:26:45 +02:00

223 lines
9.0 KiB
TypeScript

import { useState } from 'react'
import { useCreateProperty } from './useProperties'
import { useParseListingText } from './useAI'
import { SOFT_FACTORS } from '../pages/supply/newListingConstants'
import { buildCreatePropertyInput } from '../pages/supply/newListingMapper'
import type { Prefill } from '../pages/supply/newListingConstants'
function emptySoftLevels(): Record<string, string> {
return Object.fromEntries(SOFT_FACTORS.map(f => [f.key, '']))
}
function validate(fields: {
street: string
postalCode: string
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'
if (!fields.city.trim()) return 'Ort erforderlich'
if (!fields.areaSqm || isNaN(Number(fields.areaSqm))
|| 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
}
export interface NewListingFormState {
// Core
assetType: string
areaSqm: string
rentPerSqm: string
availableFrom: string
description: string
// Address
street: string
houseNumber: string
postalCode: string
city: string
// Soft factors
softLevels: Record<string, string>
// Technical
floor: string
fitOut: string
fitOutByLandlord: boolean | undefined
parking: string
ceilingHeight: string
mieterausbaubeitrag: string
isFlexible: boolean
minLettableSqm: string
// Contact
contactName: string
contactEmail: string
contactPhone: string
// Images
images: string[]
imageInput: string
floorPlanUrl: string
// AI
aiText: string
aiApplied: boolean
// Status
error: string | null
created: boolean
aiParsing: boolean
submitting: boolean
isPrefilled: boolean
}
export interface NewListingFormHandlers {
setAssetType: (v: string) => void
setAreaSqm: (v: string) => void
setRentPerSqm: (v: string) => void
setAvailableFrom: (v: string) => void
setDescription: (v: string) => void
setStreet: (v: string) => void
setHouseNumber: (v: string) => void
setPostalCode: (v: string) => void
setCity: (v: string) => void
setSoftLevel: (key: string, value: string) => void
setFloor: (v: string) => void
setFitOut: (v: string) => void
setFitOutByLandlord: (v: boolean | undefined) => void
setParking: (v: string) => void
setCeilingHeight: (v: string) => void
setMieterausbaubeitrag: (v: string) => void
setIsFlexible: (v: boolean) => void
setMinLettableSqm: (v: string) => void
setContactName: (v: string) => void
setContactEmail: (v: string) => void
setContactPhone: (v: string) => void
setImageInput: (v: string) => void
setFloorPlanUrl: (v: string) => void
setAiText: (v: string) => void
addImage: () => void
removeImage: (index: number) => void
handleAiParse: () => void
handleSubmit: () => void
resetForm: () => void
}
export function useNewListingForm(pre: Prefill): NewListingFormState & NewListingFormHandlers {
const createProperty = useCreateProperty()
const parseListingMutation = useParseListingText()
const [assetType, setAssetType] = useState(pre.assetType ?? 'OFFICE')
const [street, setStreet] = useState(pre.street ?? '')
const [houseNumber, setHouseNumber] = useState(pre.houseNumber ?? '')
const [postalCode, setPostalCode] = useState(pre.postalCode ?? '')
const [city, setCity] = useState(pre.city ?? '')
const [areaSqm, setAreaSqm] = useState(pre.areaSqm ? String(pre.areaSqm) : '')
const [rentPerSqm, setRentPerSqm] = useState(pre.rentPricePerSqm ? String(pre.rentPricePerSqm) : '')
const [availableFrom, setAvailableFrom] = useState(pre.availableFrom ?? '')
const [description, setDescription] = useState('')
const [contactName, setContactName] = useState('')
const [contactEmail, setContactEmail] = useState('')
const [contactPhone, setContactPhone] = useState('')
const [softLevels, setSoftLevels] = useState<Record<string, string>>(() => ({ ...emptySoftLevels(), ...pre.softLevels }))
const [floor, setFloor] = useState(pre.floor ?? '')
const [fitOut, setFitOut] = useState(pre.fitOut ?? '')
const [fitOutByLandlord, setFitOutByLandlord] = useState<boolean | undefined>(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) : '')
const [isFlexible, setIsFlexible] = useState(pre.isFlexible ?? false)
const [minLettableSqm, setMinLettableSqm] = useState(pre.minLettableSqm != null ? String(pre.minLettableSqm) : '')
const [images, setImages] = useState<string[]>([])
const [imageInput, setImageInput] = useState('')
const [floorPlanUrl, setFloorPlanUrl] = useState('')
const [aiText, setAiText] = useState('')
const [aiApplied, setAiApplied] = useState(false)
const [error, setError] = useState<string | null>(null)
const [created, setCreated] = useState(false)
function setSoftLevel(key: string, value: string) {
setSoftLevels(prev => ({ ...prev, [key]: value }))
}
function addImage() {
const url = imageInput.trim()
if (url && !images.includes(url)) setImages(prev => [...prev, url])
setImageInput('')
}
function removeImage(index: number) {
setImages(prev => prev.filter((_, i) => i !== index))
}
function handleAiParse() {
if (!aiText.trim()) return
parseListingMutation.mutate(aiText, {
onSuccess: (parsed) => {
if (parsed.assetType) setAssetType(parsed.assetType)
if (parsed.areaSqm) setAreaSqm(String(parsed.areaSqm))
if (parsed.rentPerSqm) setRentPerSqm(String(parsed.rentPerSqm))
if (parsed.city && !city) setCity(parsed.city)
if (parsed.fitOut) setFitOut(parsed.fitOut)
if (parsed.parking) setParking(String(parsed.parking))
if (parsed.softLevels) setSoftLevels(prev => ({ ...prev, ...parsed.softLevels }))
setAiApplied(true)
},
})
}
function handleSubmit() {
const err = validate({ street, postalCode, city, areaSqm, rentPerSqm, fitOut, fitOutByLandlord })
if (err) { setError(err); return }
setError(null)
createProperty.mutate(
buildCreatePropertyInput({
assetType, street, houseNumber, postalCode, city,
areaSqm: Number(areaSqm), rentPerSqm: Number(rentPerSqm),
availableFrom, description, softLevels,
floor, fitOut, fitOutByLandlord, parking, ceilingHeight, images, floorPlanUrl,
mieterausbaubeitrag, isFlexible, minLettableSqm,
}),
{
onSuccess: () => setCreated(true),
onError: () => setError('Fehler beim Erstellen des Inserats. Bitte erneut versuchen.'),
},
)
}
function resetForm() {
setAssetType('OFFICE')
setStreet(''); setHouseNumber(''); setPostalCode(''); setCity('')
setAreaSqm(''); setRentPerSqm(''); setAvailableFrom(''); setDescription('')
setContactName(''); setContactEmail(''); setContactPhone('')
setSoftLevels(emptySoftLevels())
setFloor(''); setFitOut(''); setFitOutByLandlord(undefined); setParking(''); setCeilingHeight('')
setMieterausbaubeitrag(''); setIsFlexible(false); setMinLettableSqm('')
setImages([]); setImageInput(''); setFloorPlanUrl('')
setAiText(''); setAiApplied(false)
setCreated(false); setError(null)
}
return {
assetType, areaSqm, rentPerSqm, availableFrom, description,
street, houseNumber, postalCode, city,
softLevels, floor, fitOut, fitOutByLandlord, parking, ceilingHeight, mieterausbaubeitrag, isFlexible, minLettableSqm,
contactName, contactEmail, contactPhone,
images, imageInput, floorPlanUrl, aiText, aiApplied,
error, created,
aiParsing: parseListingMutation.isPending,
submitting: createProperty.isPending,
isPrefilled: !!pre.propertyId,
setAssetType, setAreaSqm, setRentPerSqm, setAvailableFrom, setDescription,
setStreet, setHouseNumber, setPostalCode, setCity,
setSoftLevel,
setFloor, setFitOut, setFitOutByLandlord, setParking, setCeilingHeight, setMieterausbaubeitrag, setIsFlexible, setMinLettableSqm,
setContactName, setContactEmail, setContactPhone,
setImageInput, setFloorPlanUrl, setAiText,
addImage, removeImage,
handleAiParse, handleSubmit, resetForm,
}
}