From 7800d127fe481a12efc34e7f92a81ccc75e93d94 Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Tue, 19 May 2026 16:14:22 +0200 Subject: [PATCH] fix: BerichtDialog, images, and add property number + floor/unit structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BerichtDialog: conditionally mount instead of always-mounted with open prop; starts in 'generating' state immediately on open - Images: fix all 6 wrong VERIFIED_PORTFOLIO and EXTERNAL_MARKET photos to match spec per assetType (OFFICE/RETAIL/LOGISTICS/PRODUCTION/MIXED) - Domain: add PropertyUnit interface + propertyNumber field to Property - Mock data: propertyNumber + units[] for prop-001/007/012/013 - UI: Stockwerkstruktur section in Übersicht tab (floor/unit table with availability, tenant, area); Objekt-Nr. shown below key metrics Co-Authored-By: Claude Sonnet 4.6 --- src/components/supply/PropertyDetailView.tsx | 42 ++++++++++++++++- .../supply/PropertyMarketSignalsTab.tsx | 27 +++++------ src/domain/property.ts | 17 +++++++ src/mock-data/properties.ts | 45 ++++++++++++++----- 4 files changed, 104 insertions(+), 27 deletions(-) diff --git a/src/components/supply/PropertyDetailView.tsx b/src/components/supply/PropertyDetailView.tsx index b2dde83..79b9d59 100644 --- a/src/components/supply/PropertyDetailView.tsx +++ b/src/components/supply/PropertyDetailView.tsx @@ -20,7 +20,7 @@ import { useOfferWizardStore } from '../../stores/offerWizardStore' import { PropertyMap } from '../shared' import { NeedMatchCard } from './NeedMatchCard' import { PropertyMarketSignalsTab } from './PropertyMarketSignalsTab' -import type { Property, UpdatePropertyInput } from '../../domain/property' +import type { Property, PropertyUnit, UpdatePropertyInput } from '../../domain/property' import type { PropertyNeedMatch } from '../../domain/match' import { usePropertyById } from '../../hooks/useProperties' import { PropertyDetailSkeleton } from './PropertyDetailSkeleton' @@ -90,7 +90,7 @@ function OverviewPanel({ p, editing, draft, onDraftChange }: OverviewPanelProps) return ( {/* Key metrics */} - + {[ { label: 'Fläche', value: `${p.areaSqm.toLocaleString('de-CH')} m²` }, { label: 'CHF/m²/Jahr', value: rentLabel }, @@ -104,6 +104,11 @@ function OverviewPanel({ p, editing, draft, onDraftChange }: OverviewPanelProps) ))} + {p.propertyNumber && ( + + Objekt-Nr. {p.propertyNumber} + + )} {/* Description */} {editing ? ( @@ -177,11 +182,44 @@ function OverviewPanel({ p, editing, draft, onDraftChange }: OverviewPanelProps) )} + {/* Floor / unit structure */} + {p.units && p.units.length > 0 && ( + <> + + + + {/* header */} + + {['Stockwerk', 'Einheit', 'Mieter / Status', 'Fläche'].map(h => ( + {h} + ))} + + {p.units.map((u: PropertyUnit, i: number) => ( + + + {u.floorLevel === 0 ? 'EG' : u.floorLevel < 0 ? `UG${Math.abs(u.floorLevel)}` : `${u.floorLevel}.OG`} + + {u.unitLabel ?? '–'} + + {u.available ? ( + + ) : ( + {u.currentTenant ?? 'Vermietet'} + )} + + {u.areaSqm.toLocaleString('de-CH')} m² + + ))} + + + )} + {/* Object details */} + {p.propertyNumber && } diff --git a/src/components/supply/PropertyMarketSignalsTab.tsx b/src/components/supply/PropertyMarketSignalsTab.tsx index 372bfcf..dbc2970 100644 --- a/src/components/supply/PropertyMarketSignalsTab.tsx +++ b/src/components/supply/PropertyMarketSignalsTab.tsx @@ -98,33 +98,29 @@ function SignalCard({ signal }: { signal: PropertyMarketSignal }) { type PdfState = 'idle' | 'generating' | 'ready' interface BerichtDialogProps { - open: boolean onClose: () => void report: MarketReport | null propertyId: string } -function BerichtDialog({ open, onClose, report, propertyId }: BerichtDialogProps) { - const [state, setState] = useState('idle') +function BerichtDialog({ onClose, report, propertyId }: Omit) { + const [state, setState] = useState('generating') const [progress, setProgress] = useState(0) const timerRef = useRef | null>(null) useEffect(() => { - if (!open) { setState('idle'); setProgress(0); return } - setState('generating') - setProgress(0) let p = 0 timerRef.current = setInterval(() => { p += Math.random() * 18 + 8 if (p >= 100) { p = 100 clearInterval(timerRef.current!) - setTimeout(() => setState('ready'), 300) + setState('ready') } setProgress(Math.min(100, p)) }, 200) return () => { if (timerRef.current) clearInterval(timerRef.current) } - }, [open]) + }, []) function handleDownload() { const signals = report?.signals ?? [] @@ -152,7 +148,7 @@ function BerichtDialog({ open, onClose, report, propertyId }: BerichtDialogProps } return ( - + Bericht erstellen @@ -289,12 +285,13 @@ export function PropertyMarketSignalsTab({ propertyId }: Props) { }) )} - setDialogOpen(false)} - report={report} - propertyId={propertyId} - /> + {dialogOpen && ( + setDialogOpen(false)} + report={report} + propertyId={propertyId} + /> + )} ) } diff --git a/src/domain/property.ts b/src/domain/property.ts index 208549a..aef3656 100644 --- a/src/domain/property.ts +++ b/src/domain/property.ts @@ -82,6 +82,20 @@ export interface SoftFactors { infrastructureNotes?: string } +// ── Floor / Unit structure ──────────────────────────────────────────────────── + +export interface PropertyUnit { + id: string + floorLevel: number // 0=EG, 1=1.OG, -1=UG + unitLabel?: string // e.g. "Ost", "West", "Einheit A" + areaSqm: number + available: boolean + rentPricePerSqm?: number + currentTenant?: string + leaseTerm?: string + leaseEndDate?: string +} + // ── Property ────────────────────────────────────────────────────────────────── export interface Property { @@ -129,6 +143,9 @@ export interface Property { description?: string images?: string[] + propertyNumber?: string + units?: PropertyUnit[] + leaseTerm?: string leaseStartDate?: string leaseEndDate?: string diff --git a/src/mock-data/properties.ts b/src/mock-data/properties.ts index eb69960..781723c 100644 --- a/src/mock-data/properties.ts +++ b/src/mock-data/properties.ts @@ -42,6 +42,12 @@ export const mockProperties: Property[] = [ ancillaryCosts: 5.5, riskLevel: RiskLevel.LOW, images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], + propertyNumber: 'ZH-2024-001', + units: [ + { id: 'unit-001-1', floorLevel: 1, unitLabel: 'Nord', areaSqm: 280, available: false, rentPricePerSqm: 456, currentTenant: 'MediaGroup Schweiz AG', leaseTerm: '5 Jahre', leaseEndDate: '2025-08-31' }, + { id: 'unit-001-2', floorLevel: 2, unitLabel: 'Süd', areaSqm: 310, available: false, rentPricePerSqm: 456, currentTenant: 'MediaGroup Schweiz AG', leaseTerm: '5 Jahre', leaseEndDate: '2025-08-31' }, + { id: 'unit-001-3', floorLevel: 3, unitLabel: 'West', areaSqm: 260, available: true, rentPricePerSqm: 480 }, + ], importedFrom: 'SAP RE-FX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z', @@ -138,6 +144,12 @@ export const mockProperties: Property[] = [ ancillaryCosts: 5.0, riskLevel: RiskLevel.LOW, images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], + propertyNumber: 'ZH-2021-007', + units: [ + { id: 'unit-007-1', floorLevel: 0, unitLabel: 'EG Ost', areaSqm: 180, available: false, rentPricePerSqm: 420, currentTenant: 'Consulting Partners AG', leaseTerm: '4 Jahre', leaseEndDate: '2025-09-30' }, + { id: 'unit-007-2', floorLevel: 1, unitLabel: '1.OG', areaSqm: 260, available: false, rentPricePerSqm: 432, currentTenant: 'Consulting Partners AG', leaseTerm: '4 Jahre', leaseEndDate: '2025-09-30' }, + { id: 'unit-007-3', floorLevel: 2, unitLabel: '2.OG West', areaSqm: 280, available: true, rentPricePerSqm: 445 }, + ], importedFrom: 'SAP RE-FX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z', @@ -186,7 +198,7 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 4.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1481277542470-605612bd2d61?w=800&h=400&fit=crop'], + images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], importedFrom: 'SAP RE-FX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z', @@ -233,7 +245,7 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 2.8, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1566438480900-0b5b967f4a25?w=800&h=400&fit=crop'], + images: ['https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=800&h=400&fit=crop'], importedFrom: 'SAP RE-FX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z', @@ -327,7 +339,7 @@ export const mockProperties: Property[] = [ contractDurationMonths: 120, ancillaryCosts: 2.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?w=800&h=400&fit=crop'], + images: ['https://images.unsplash.com/photo-1565043589221-1a6fd9ae45c7?w=800&h=400&fit=crop'], importedFrom: 'SAP RE-FX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z', @@ -375,7 +387,13 @@ export const mockProperties: Property[] = [ contractDurationMonths: 36, ancillaryCosts: 6.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1564013799919-ab600027ffc6?w=800&h=400&fit=crop'], + images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], + propertyNumber: 'ZG-2022-012', + units: [ + { id: 'unit-012-1', floorLevel: 3, unitLabel: '3.OG A', areaSqm: 180, available: false, rentPricePerSqm: 504, currentTenant: 'FinTech Zug AG', leaseTerm: '3 Jahre', leaseEndDate: '2025-09-30' }, + { id: 'unit-012-2', floorLevel: 4, unitLabel: '4.OG', areaSqm: 190, available: false, rentPricePerSqm: 504, currentTenant: 'FinTech Zug AG', leaseTerm: '3 Jahre', leaseEndDate: '2025-09-30' }, + { id: 'unit-012-3', floorLevel: 5, unitLabel: '5.OG Süd', areaSqm: 180, available: true, rentPricePerSqm: 528 }, + ], importedFrom: 'SAP RE-FX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z', @@ -424,7 +442,14 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 5.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1524758631624-e2822e304c36?w=800&h=400&fit=crop'], + images: ['https://images.unsplash.com/photo-1486325212027-8081e485255e?w=800&h=400&fit=crop'], + propertyNumber: 'ZH-2021-013', + units: [ + { id: 'unit-013-1', floorLevel: 0, unitLabel: 'EG Laden', areaSqm: 320, available: false, rentPricePerSqm: 600, currentTenant: 'Design Studio Zürich GmbH', leaseTerm: '4 Jahre', leaseEndDate: '2025-10-31' }, + { id: 'unit-013-2', floorLevel: 1, unitLabel: '1.OG Büro A', areaSqm: 480, available: false, rentPricePerSqm: 540, currentTenant: 'Design Studio Zürich GmbH', leaseTerm: '4 Jahre', leaseEndDate: '2025-10-31' }, + { id: 'unit-013-3', floorLevel: 1, unitLabel: '1.OG Büro B', areaSqm: 280, available: true, rentPricePerSqm: 540 }, + { id: 'unit-013-4', floorLevel: 2, unitLabel: '2.OG', areaSqm: 220, available: true, rentPricePerSqm: 520 }, + ], importedFrom: 'SAP RE-FX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z', @@ -471,7 +496,7 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 2.8, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1553413077-190dd305871c?w=800&h=400&fit=crop'], + images: ['https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=800&h=400&fit=crop'], importedFrom: 'SAP RE-FX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z', @@ -548,7 +573,7 @@ export const mockProperties: Property[] = [ warnings: ['Daten aus Drittquelle – nicht verifiziert'], }, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1524758631624-e2822e304c36?w=800&h=400&fit=crop'], + images: ['https://images.unsplash.com/photo-1486325212027-8081e485255e?w=800&h=400&fit=crop'], createdAt: '2025-03-01T10:00:00Z', updatedAt: '2025-03-20T15:00:00Z', }, @@ -685,7 +710,7 @@ export const mockProperties: Property[] = [ publicTransportMinutes: 8, }, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1481277542470-605612bd2d61?w=800&h=400&fit=crop'], + images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], createdAt: '2025-02-28T10:00:00Z', updatedAt: '2025-04-02T09:00:00Z', }, @@ -714,7 +739,7 @@ export const mockProperties: Property[] = [ warnings: ['Hallenhöhe nicht verifiziert', 'Kranbahn Status unklar'], }, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?w=800&h=400&fit=crop'], + images: ['https://images.unsplash.com/photo-1565043589221-1a6fd9ae45c7?w=800&h=400&fit=crop'], createdAt: '2025-03-10T08:00:00Z', updatedAt: '2025-03-25T14:00:00Z', }, @@ -817,7 +842,7 @@ export const mockProperties: Property[] = [ publicTransportMinutes: 6, }, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1524758631624-e2822e304c36?w=800&h=400&fit=crop'], + images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], createdAt: '2025-03-08T08:00:00Z', updatedAt: '2025-04-14T10:00:00Z', },