import { useMemo, useState } from 'react' import { Alert, Box, Button, CircularProgress, IconButton, MenuItem, TextField, Typography, } from '@mui/material' import { X } from 'lucide-react' import type { Property, UpdatePropertyInput } from '../../domain/property' import { AiDisclaimer } from '../ui' import { usePropertyById, useUpdateProperty } from '../../hooks/useProperties' import { PropertyDetailSkeleton } from './PropertyDetailSkeleton' import { useToastStore } from '../../stores/toastStore' import { ObjectDeepLink } from '../team' import { FIT_OUT_OPTIONS } from '../../pages/supply/newListingConstants' import { DS_BRAND, DS_TEXT } from '../../lib/ds' /** * Pflichtfelder für einen brauchbaren Objektdatensatz. * * Bewusst nur das, was für Vermarktung und Matching zwingend gebraucht wird — * eine längere Liste würde jede zweite Zeile rot einrahmen und damit gar nichts * mehr hervorheben. `label` erscheint im Formular, `isEmpty` entscheidet über * die rote Umrandung (Runde 4, §6.3). */ interface EditableField { key: string label: string required: boolean type: 'text' | 'number' | 'multiline' | 'select' options?: { value: string; label: string }[] get: (p: Property, draft: UpdatePropertyInput) => string set: (value: string, draft: UpdatePropertyInput, p: Property) => UpdatePropertyInput } function hf(p: Property, draft: UpdatePropertyInput) { return draft.hardFacts ?? p.hardFacts ?? {} } const FIELDS: EditableField[] = [ { key: 'title', label: 'Objektname', required: true, type: 'text', get: (p, d) => d.title ?? p.title ?? '', set: (v, d) => ({ ...d, title: v }), }, { key: 'areaSqm', label: 'Fläche (m²)', required: true, type: 'number', get: (p, d) => String(d.areaSqm ?? p.areaSqm ?? ''), set: (v, d) => ({ ...d, areaSqm: v === '' ? undefined : Number(v) }), }, { key: 'rentPricePerSqm', label: 'Mietpreis (CHF/m²/Jahr)', required: true, type: 'number', get: (p, d) => String(d.rentPricePerSqm ?? p.rentPricePerSqm ?? ''), set: (v, d) => ({ ...d, rentPricePerSqm: v === '' ? undefined : Number(v) }), }, { key: 'ancillaryCosts', label: 'Nebenkosten (CHF/m²)', required: false, type: 'number', get: (p, d) => String(d.ancillaryCosts ?? p.ancillaryCosts ?? ''), set: (v, d) => ({ ...d, ancillaryCosts: v === '' ? undefined : Number(v) }), }, { key: 'availabilityDate', label: 'Verfügbar ab', required: true, type: 'text', get: (p, d) => (d.availabilityDate ?? p.availabilityDate ?? '').slice(0, 10), set: (v, d) => ({ ...d, availabilityDate: v || undefined }), }, { key: 'fitOut', label: 'Ausbaustandard', required: true, type: 'select', options: FIT_OUT_OPTIONS.map(o => ({ value: o.value, label: o.label })), get: (p, d) => hf(p, d).fitOut ?? '', set: (v, d, p) => ({ ...d, hardFacts: { ...hf(p, d), fitOut: (v || undefined) as Property['hardFacts'] extends undefined ? never : 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM' | undefined }, }), }, { key: 'parking', label: 'Parkplätze', required: false, type: 'number', get: (p, d) => String(hf(p, d).parking ?? ''), set: (v, d, p) => ({ ...d, hardFacts: { ...hf(p, d), parking: v === '' ? undefined : Number(v) } }), }, { key: 'ceilingHeightM', label: 'Deckenhöhe (m)', required: false, type: 'number', get: (p, d) => String(hf(p, d).ceilingHeightM ?? ''), set: (v, d, p) => ({ ...d, hardFacts: { ...hf(p, d), ceilingHeightM: v === '' ? undefined : Number(v) } }), }, { key: 'description', label: 'Beschreibung', required: true, type: 'multiline', get: (p, d) => d.description ?? p.description ?? '', set: (v, d) => ({ ...d, description: v }), }, ] interface Props { propertyId: string onClose?: () => void } /** * Sinas Objekt-Detailansicht (Runde 4, §6.3). * * Bewusst nicht `PropertyDetailView` mit weiteren Schaltern: dort geht es ums * Ansehen, hier ums Ausfüllen. Es gibt deshalb keinen Bearbeitungsstift und * keinen Bearbeitungsmodus — jedes Feld ist immer editierbar. «Speichern» und * «Abbrechen» erscheinen erst, sobald tatsächlich etwas geändert wurde; ein * dauerhaft sichtbarer Speichern-Knopf ohne Änderung wäre nur Dekoration. * * Leere Pflichtfelder sind rot umrandet — sie sind der Grund, weshalb der * Datenqualitätsscore nicht steigt. Reine Anzeigeblöcke wie Matchability, * Marktsignale, Datenqualität und Standort sind entfallen. */ export function EditableObjectOverview({ propertyId, onClose }: Props) { const { data: property, isLoading } = usePropertyById(propertyId) const updateProperty = useUpdateProperty() const showToast = useToastStore(s => s.showToast) const [draft, setDraft] = useState({}) const isDirty = Object.keys(draft).length > 0 const missingRequired = useMemo(() => { if (!property) return new Set() return new Set( FIELDS.filter(f => f.required && f.get(property, draft).trim() === '').map(f => f.key), ) }, [property, draft]) if (isLoading) return if (!property) { return ( Objekt nicht gefunden. ) } function handleSave() { if (!property) return updateProperty.mutate( { id: property.id, input: draft }, { onSuccess: () => { setDraft({}) showToast('Objekt gespeichert.', 'success') }, onError: () => showToast('Fehler beim Speichern.', 'error'), }, ) } return ( {/* Kopf — Objektname führt nach «Meine Objekte»; keine Typ- oder Verfügbarkeitsetiketten und kein «Kritische Felder fehlen»-Hinweis. */} {property.address.street} {property.address.houseNumber}, {property.address.postalCode} {property.address.city} {onClose && ( )} {/* Startbild */} {property.images?.[0] && ( {property.title} )} {/* Übersicht — direkt editierbar */} Übersicht {FIELDS.map(field => { const value = field.get(property, draft) const isMissing = missingRequired.has(field.key) const fullWidth = field.type === 'multiline' || field.key === 'title' return ( setDraft(field.set(e.target.value, draft, property))} sx={{ '& .MuiOutlinedInput-notchedOutline': isMissing ? { borderColor: DS_TEXT.error, borderWidth: 2 } : {}, }} > {field.type === 'select' && (field.options ?? []).map(o => ( {o.label} ))} ) })} {/* Der KI-Hinweis steht innerhalb des scrollenden Bereichs. Ausserhalb läge er hinter dem `overflow: hidden` des äusseren Rahmens und wäre nie zu sehen — der Aktionsbalken darunter ist bewusst der einzige Teil, der fix am unteren Rand klebt. */} {/* Speichern erscheint erst bei einer Änderung, «Abbrechen» links davon. */} {isDirty && ( )} ) }