647493bf3c
- 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>
223 lines
9.0 KiB
TypeScript
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,
|
|
}
|
|
}
|