diff --git a/src/components/supply/PropertyDetailOverview.tsx b/src/components/supply/PropertyDetailOverview.tsx
index 483c720..c505092 100644
--- a/src/components/supply/PropertyDetailOverview.tsx
+++ b/src/components/supply/PropertyDetailOverview.tsx
@@ -63,114 +63,66 @@ export function PropertyDetailOverview({ p, editing, draft, onDraftChange }: Pro
) : null}
- {/* Lease & Tenant */}
-
- {editing ? (
-
-
- onDraftChange({ ...draft, currentTenant: e.target.value })}
- />
- onDraftChange({ ...draft, leaseTerm: e.target.value })}
- placeholder="z.B. 5 Jahre"
- />
- onDraftChange({ ...draft, leaseStartDate: e.target.value })}
- />
- onDraftChange({ ...draft, leaseEndDate: e.target.value })}
- />
-
-
-
-
- Mietvertrag
-
-
-
- onDraftChange({ ...draft, leaseContractUrl: e.target.value })}
- placeholder="https://…"
- slotProps={{ input: { startAdornment: } }}
- />
- onDraftChange({ ...draft, leaseContractName: e.target.value })}
- placeholder="z.B. Mietvertrag 2021–2026"
- />
-
-
- ) : (
- <>
-
-
-
-
-
-
-
-
-
-
+ {/* Floor / unit structure — primary tenant/lease view */}
+
- {/* Mietvertrag */}
-
-
-
-
- Mietvertrag
-
+ {/* Fallback: property-level contract doc only when no unit-level contracts exist */}
+ {(() => {
+ const hasUnitContracts = p.units?.some(u => u.leases?.some(l => l.contractDocumentUrl))
+ if (editing) {
+ return (
+
+
+
+
+ Gebäude-Mietvertrag (Fallback)
+
+
+
+ onDraftChange({ ...draft, leaseContractUrl: e.target.value })}
+ placeholder="https://…"
+ slotProps={{ input: { startAdornment: } }}
+ />
+ onDraftChange({ ...draft, leaseContractName: e.target.value })}
+ placeholder="z.B. Mietvertrag 2021–2026"
+ />
+
- {p.leaseContractUrl ? (
-
+ )
+ }
+ if (!p.leaseContractUrl || hasUnitContracts) return null
+ return (
+
+
+
+
{p.leaseContractName ?? 'Mietvertrag'}
- }
- href={p.leaseContractUrl}
- target="_blank"
- rel="noopener noreferrer"
- sx={{ textTransform: 'none', fontSize: '0.7rem', py: 0.25, px: 1, borderColor: '#cbd5e1', color: '#152642', whiteSpace: 'nowrap', flexShrink: 0 }}
- >
- Öffnen
-
- ) : (
-
- Noch kein Mietvertrag hinterlegt — im Bearbeitungsmodus hinzufügen.
-
- )}
+ }
+ href={p.leaseContractUrl}
+ target="_blank"
+ rel="noopener noreferrer"
+ sx={{ textTransform: 'none', fontSize: '0.7rem', py: 0.25, px: 1, borderColor: '#cbd5e1', color: '#152642', whiteSpace: 'nowrap', flexShrink: 0 }}
+ >
+ Öffnen
+
+
- >
- )}
-
- {/* Floor / unit structure */}
-
+ )
+ })()}
{/* Pre-Market Matching */}
@@ -230,6 +182,8 @@ export function PropertyDetailOverview({ p, editing, draft, onDraftChange }: Pro
+
+
)}
diff --git a/src/components/supply/UnitStructurePanel.tsx b/src/components/supply/UnitStructurePanel.tsx
index 2de6295..8e04e9f 100644
--- a/src/components/supply/UnitStructurePanel.tsx
+++ b/src/components/supply/UnitStructurePanel.tsx
@@ -1,8 +1,9 @@
import { useMemo, useState } from 'react'
import { Box, Button, Checkbox, Chip, Collapse, Divider, IconButton, Switch, TextField, Tooltip, Typography } from '@mui/material'
-import { ChevronDown, ChevronUp, Layers, Users } from 'lucide-react'
+import { ChevronDown, ChevronUp, ExternalLink, FileText, Layers, Users } from 'lucide-react'
import { useNavigate } from 'react-router'
import type { Property, PropertyUnit, UnitNeedMatch } from '../../domain/property'
+import type { Lease } from '../../domain/lease'
import { useUnitMatchesMap, useUnitBundle, useBundleMatches } from '../../hooks/useUnitMatches'
import { useUpdateUnit } from '../../hooks/useProperties'
import { DS_BORDER, DS_PRE_MARKET, DS_SURFACE, DS_TEXT } from '../../lib/ds'
@@ -24,6 +25,25 @@ function MatchPill({ m }: { m: UnitNeedMatch }) {
)
}
+function LeaseField({ label, value }: { label: string; value?: string }) {
+ if (!value) return null
+ return (
+
+
+ {label}
+
+
+ {value}
+
+
+ )
+}
+
+function formatLeaseDate(iso?: string): string | undefined {
+ if (!iso) return undefined
+ return new Date(iso).toLocaleDateString('de-CH', { day: 'numeric', month: 'short', year: 'numeric' })
+}
+
export function UnitStructurePanel({ p }: { p: Property }) {
const navigate = useNavigate()
const [selectedIds, setSelectedIds] = useState>(new Set())
@@ -40,6 +60,23 @@ export function UnitStructurePanel({ p }: { p: Property }) {
const bundle = useUnitBundle(selectedFreeUnits)
const bundleMatches = useBundleMatches(selectedFreeUnits, p)
+ // Group units by floor level (consecutive same-floor units form a group)
+ const floorGroups = useMemo(() => {
+ const groups: { floorLevel: number; units: PropertyUnit[] }[] = []
+ for (const u of p.units ?? []) {
+ const last = groups[groups.length - 1]
+ if (last && last.floorLevel === u.floorLevel) {
+ last.units.push(u)
+ } else {
+ groups.push({ floorLevel: u.floorLevel, units: [u] })
+ }
+ }
+ return groups
+ }, [p.units])
+
+ // Flat list of all units for index tracking
+ const allUnits = p.units ?? []
+
const toggleUnit = (id: string) => {
setSelectedIds(prev => {
const next = new Set(prev)
@@ -50,12 +87,289 @@ export function UnitStructurePanel({ p }: { p: Property }) {
if (!p.units || p.units.length === 0) return null
+ const renderUnitRow = (u: PropertyUnit, isFirstInGroup: boolean, isLastUnit: boolean) => {
+ const matches = u.available ? (unitMatches[u.id] ?? []) : []
+ const topMatch = matches[0]
+ const isExpanded = expandedUnit === u.id
+ const isSelected = selectedIds.has(u.id)
+ const unitIndex = allUnits.indexOf(u)
+ const isLastRow = unitIndex === allUnits.length - 1 && !isExpanded
+
+ const activeLease: Lease | undefined = u.leases?.find(l => l.status === 'ACTIVE')
+ const tenantName = activeLease?.tenant.companyName ?? u.currentTenant
+ const leaseEnd = activeLease?.endDate ?? u.leaseEndDate
+ const hasLeaseDetail = !u.available && !!activeLease
+
+ return (
+
+
+ {/* Checkbox — only for free units when multiple free units exist */}
+
+ {u.available && freeUnits.length >= 2 && (
+ toggleUnit(u.id)}
+ sx={{ p: 0, color: DS_TEXT.disabled, '&.Mui-checked': { color: '#2563eb' } }}
+ />
+ )}
+
+
+ {/* Floor — only on first unit of group */}
+
+ {isFirstInGroup ? floorLabel(u) : ''}
+
+
+ {/* Unit label */}
+
+ {u.unitLabel ?? '–'}
+
+
+ {/* Status */}
+
+ {u.available ? (
+ <>
+
+
+ {/* Teilbar toggle */}
+
+
+ {
+ const flexible = e.target.checked
+ if (flexible) {
+ setEditingFlexUnit(u.id)
+ setFlexDraft(d => ({ ...d, [u.id]: u.minLettableSqm }))
+ } else {
+ updateUnit.mutate({ unitId: u.id, data: { isFlexible: false, minLettableSqm: undefined } })
+ setEditingFlexUnit(null)
+ }
+ }}
+ sx={{ '& .MuiSwitch-thumb': { width: 10, height: 10 }, '& .MuiSwitch-switchBase': { p: '4px' }, '& .Mui-checked + .MuiSwitch-track': { bgcolor: '#1d4ed8' } }}
+ />
+
+ Teilbar
+
+
+
+
+ {/* Inline min-unit editor */}
+ {(editingFlexUnit === u.id || (u.isFlexible && !u.minLettableSqm)) && (
+
+ setFlexDraft(d => ({ ...d, [u.id]: parseInt(e.target.value) || undefined }))}
+ error={!flexDraft[u.id] && u.isFlexible && !u.minLettableSqm}
+ sx={{
+ width: 80,
+ '& .MuiInputBase-input': { fontSize: '0.68rem', py: 0.375, px: 0.75 },
+ '& .MuiOutlinedInput-root': {
+ '& fieldset': { borderColor: (!flexDraft[u.id] && u.isFlexible && !u.minLettableSqm) ? '#ef4444' : undefined },
+ },
+ }}
+ slotProps={{ htmlInput: { min: 10, max: u.areaSqm, step: 10 } }}
+ />
+
+ {editingFlexUnit === u.id && u.minLettableSqm && (
+
+ )}
+
+ )}
+
+
+ >
+ ) : (
+
+
+ {tenantName ?? 'Vermietet'}
+
+ {leaseEnd && (
+
+ bis {new Date(leaseEnd).toLocaleDateString('de-CH', { month: 'short', year: '2-digit' })}
+
+ )}
+
+ )}
+
+
+ {/* Matches column + expand */}
+
+ {topMatch && }
+ {/* Expand for free units with multiple matches */}
+ {u.available && matches.length > 1 && (
+ setExpandedUnit(isExpanded ? null : u.id)}
+ sx={{ p: 0.25, color: DS_TEXT.disabled }}
+ >
+ {isExpanded ? : }
+
+ )}
+ {/* Expand for occupied units with lease detail */}
+ {hasLeaseDetail && (
+ setExpandedUnit(isExpanded ? null : u.id)}
+ sx={{
+ p: 0.5,
+ color: isExpanded ? '#2563eb' : '#64748b',
+ borderRadius: 1,
+ '&:hover': { bgcolor: '#f1f5f9', color: '#2563eb' },
+ transition: 'color 0.15s, background 0.15s',
+ }}
+ >
+ {isExpanded ? : }
+
+ )}
+
+
+ {/* Area */}
+
+
+ {(u.offeredSqm ?? u.areaSqm).toLocaleString('de-CH')} m²
+
+ {u.isFlexible && u.minLettableSqm && (
+
+ ab {u.minLettableSqm} m²
+
+ )}
+
+
+
+ {/* Expanded: matches for free units */}
+
+
+
+ Passende Suchanfragen für diese Einheit
+
+
+ {matches.map(m => (
+
+
+
+ {m.requiredSqmMin}–{m.requiredSqmMax} m²
+ {m.matchType === 'partial' && m.suggestedSqm && ` · Teilfläche ~${m.suggestedSqm} m² anbieten`}
+
+
+ ))}
+ {matches.length === 0 && (
+ Keine passenden Suchanfragen
+ )}
+
+
+
+
+ {/* Expanded: lease detail for occupied units */}
+
+ {activeLease && (
+
+
+
+
+
+
+
+
+
+ {activeLease.contractDocumentUrl && (
+
+
+
+ {activeLease.contractDocumentName ?? 'Mietvertrag'}
+
+ }
+ href={activeLease.contractDocumentUrl}
+ target="_blank"
+ rel="noopener noreferrer"
+ sx={{ textTransform: 'none', fontSize: '0.68rem', py: 0.2, px: 0.875, borderColor: '#cbd5e1', color: '#152642', flexShrink: 0 }}
+ >
+ Öffnen
+
+
+ )}
+
+ )}
+
+
+ )
+ }
+
return (
<>
- Stockwerkstruktur
+ Einheiten & Mietverträge
{freeUnits.length >= 2 && (
@@ -67,215 +381,25 @@ export function UnitStructurePanel({ p }: { p: Property }) {
{/* Header */}
- {['', 'Stockwerk', 'Einheit', 'Mieter / Status', 'Matches', 'Fläche'].map(h => (
- {h}
+ {['', 'Stockwerk', 'Einheit', 'Mieter / Status', '', 'Fläche'].map((h, i) => (
+ {h}
))}
- {p.units!.map((u: PropertyUnit, i: number) => {
- const matches = u.available ? (unitMatches[u.id] ?? []) : []
- const topMatch = matches[0]
- const isExpanded = expandedUnit === u.id
- const isSelected = selectedIds.has(u.id)
- const isLastRow = i === p.units!.length - 1 && !isExpanded
-
- return (
-
-
- {/* Checkbox — only for free units */}
-
- {u.available && freeUnits.length >= 2 && (
- toggleUnit(u.id)}
- sx={{ p: 0, color: DS_TEXT.disabled, '&.Mui-checked': { color: '#2563eb' } }}
- />
- )}
-
-
- {/* Floor */}
-
- {floorLabel(u)}
-
-
- {/* Unit label */}
- {u.unitLabel ?? '–'}
-
- {/* Status */}
-
- {u.available ? (
- <>
-
-
- {/* Teilbar toggle */}
-
-
- {
- const flexible = e.target.checked
- if (flexible) {
- setEditingFlexUnit(u.id)
- setFlexDraft(d => ({ ...d, [u.id]: u.minLettableSqm }))
- } else {
- updateUnit.mutate({ unitId: u.id, data: { isFlexible: false, minLettableSqm: undefined } })
- setEditingFlexUnit(null)
- }
- }}
- sx={{ '& .MuiSwitch-thumb': { width: 10, height: 10 }, '& .MuiSwitch-switchBase': { p: '4px' }, '& .Mui-checked + .MuiSwitch-track': { bgcolor: '#1d4ed8' } }}
- />
-
- Teilbar
-
-
-
-
- {/* Inline min-unit editor — show when actively editing OR when teilbar is on but min sqm not set */}
- {(editingFlexUnit === u.id || (u.isFlexible && !u.minLettableSqm)) && (
-
- setFlexDraft(d => ({ ...d, [u.id]: parseInt(e.target.value) || undefined }))}
- error={!flexDraft[u.id] && u.isFlexible && !u.minLettableSqm}
- sx={{
- width: 80,
- '& .MuiInputBase-input': { fontSize: '0.68rem', py: 0.375, px: 0.75 },
- '& .MuiOutlinedInput-root': {
- '& fieldset': { borderColor: (!flexDraft[u.id] && u.isFlexible && !u.minLettableSqm) ? '#ef4444' : undefined },
- },
- }}
- slotProps={{ htmlInput: { min: 10, max: u.areaSqm, step: 10 } }}
- />
-
- {editingFlexUnit === u.id && u.minLettableSqm && (
-
- )}
-
- )}
-
-
- >
- ) : (
- {u.currentTenant ?? 'Vermietet'}
- )}
-
-
- {/* Top match pill + expand */}
-
- {topMatch && }
- {matches.length > 1 && (
- setExpandedUnit(isExpanded ? null : u.id)}
- sx={{ p: 0.25, color: DS_TEXT.disabled }}
- >
- {isExpanded ? : }
-
- )}
-
-
- {/* Area */}
-
-
- {(u.offeredSqm ?? u.areaSqm).toLocaleString('de-CH')} m²
-
- {u.isFlexible && u.minLettableSqm && (
-
- ab {u.minLettableSqm} m²
-
- )}
-
-
-
- {/* Expanded: show all matches for this unit */}
-
-
-
- Passende Suchanfragen für diese Einheit
-
-
- {matches.map(m => (
-
-
-
- {m.requiredSqmMin}–{m.requiredSqmMax} m²
- {m.matchType === 'partial' && m.suggestedSqm && ` · Teilfläche ~${m.suggestedSqm} m² anbieten`}
-
-
- ))}
- {matches.length === 0 && (
- Keine passenden Suchanfragen
- )}
-
-
-
-
- )
- })}
+ {/* Floor groups */}
+ {floorGroups.map((group, groupIdx) => (
+ 0 ? `2px solid ${DS_BORDER.default}` : 'none' }}
+ >
+ {group.units.map((u, unitIdxInGroup) => {
+ const isFirstInGroup = unitIdxInGroup === 0
+ const globalIdx = allUnits.indexOf(u)
+ const isLastUnit = globalIdx === allUnits.length - 1
+ return renderUnitRow(u, isFirstInGroup, isLastUnit)
+ })}
+
+ ))}
{/* Bundle panel */}
diff --git a/src/domain/index.ts b/src/domain/index.ts
index 200c277..ac49ffa 100644
--- a/src/domain/index.ts
+++ b/src/domain/index.ts
@@ -1,4 +1,6 @@
export * from './enums'
+export * from './tenant'
+export * from './lease'
export * from './property'
export * from './need'
export * from './match'
diff --git a/src/domain/lease.ts b/src/domain/lease.ts
new file mode 100644
index 0000000..6c07925
--- /dev/null
+++ b/src/domain/lease.ts
@@ -0,0 +1,20 @@
+import type { Tenant } from './tenant'
+
+export interface Lease {
+ id: string
+ unitId: string
+ tenant: Tenant
+ rentPerSqm: number // CHF/m²/Jahr
+ startDate: string // ISO date
+ endDate: string // ISO date
+ contractDurationYears?: number
+ breakoutOption?: boolean
+ breakoutOptionDate?: string
+ status: 'ACTIVE' | 'FUTURE' | 'EXPIRED'
+ sourceSystem?: 'SAP_REFX' | 'GARAIO_REM' | 'MANUAL' | string
+ sourceId?: string // ID in source ERP
+ importedAt?: string
+ lastUpdatedAt?: string
+ contractDocumentUrl?: string
+ contractDocumentName?: string
+}
diff --git a/src/domain/property.ts b/src/domain/property.ts
index 0c42979..e29db16 100644
--- a/src/domain/property.ts
+++ b/src/domain/property.ts
@@ -4,6 +4,7 @@ import type {
} from './enums'
import { AvailabilityStatus as AS } from './enums'
import type { PropertyUnit } from './unit'
+import type { Lease } from './lease'
// Re-export unit types so all existing imports from 'property' continue to work
export type { PropertyUnit, UnitBundle, UnitNeedMatch } from './unit'
@@ -137,18 +138,26 @@ export interface Property {
description?: string
images?: string[]
floorPlanUrl?: string
- leaseContractUrl?: string // link to the signed lease document
- leaseContractName?: string // display label, e.g. "Mietvertrag 2021–2026"
+ /** @deprecated Use unit.leases[].contractDocumentUrl instead */
+ leaseContractUrl?: string
+ /** @deprecated Use unit.leases[].contractDocumentName instead */
+ leaseContractName?: string
propertyNumber?: string
units?: PropertyUnit[]
mapImageUrl?: string
+ /** @deprecated Use unit.leases instead */
leaseTerm?: string
+ /** @deprecated Use unit.leases[].startDate instead */
leaseStartDate?: string
+ /** @deprecated Use unit.leases[].endDate instead */
leaseEndDate?: string
+ /** @deprecated Use unit.leases[].breakoutOption instead */
breakoutOption?: boolean
+ /** @deprecated Use unit.leases[].breakoutOptionDate instead */
breakoutOptionDate?: string
+ /** @deprecated Use unit.leases[].tenant.companyName instead */
currentTenant?: string
importedFrom?: string
importedAt?: string
@@ -169,9 +178,29 @@ export type UpdatePropertyInput = Partial
* Returns the rentable units of a property.
* For properties without explicit units, synthesises one unit from property-level data
* so that all matching logic can operate uniformly at unit level.
+ * Property-level lease fields are synthesised into a proper Lease object for backward compat.
*/
export function getEffectiveUnits(p: Property): PropertyUnit[] {
if (p.units && p.units.length > 0) return p.units
+
+ const syntheticLease: Lease | undefined = p.currentTenant
+ ? {
+ id: `${p.id}-lease`,
+ unitId: p.id,
+ tenant: { id: `${p.id}-tenant`, companyName: p.currentTenant },
+ rentPerSqm: p.rentPricePerSqm,
+ startDate: p.leaseStartDate ?? '',
+ endDate: p.leaseEndDate ?? '',
+ contractDurationYears: p.contractDurationMonths ? Math.round(p.contractDurationMonths / 12) : undefined,
+ breakoutOption: p.breakoutOption,
+ breakoutOptionDate: p.breakoutOptionDate,
+ status: p.leaseEndDate && new Date(p.leaseEndDate) > new Date() ? 'ACTIVE' : 'EXPIRED',
+ sourceSystem: p.importedFrom ?? 'MANUAL',
+ contractDocumentUrl: p.leaseContractUrl,
+ contractDocumentName: p.leaseContractName,
+ }
+ : undefined
+
return [{
id: p.id,
propertyId: p.id,
@@ -180,9 +209,7 @@ export function getEffectiveUnits(p: Property): PropertyUnit[] {
areaSqm: p.areaSqm,
available: p.availabilityStatus === AS.AVAILABLE_NOW || p.availabilityStatus === AS.AVAILABLE_SOON,
rentPricePerSqm: p.rentPricePerSqm,
- currentTenant: p.currentTenant,
- leaseTerm: p.leaseTerm,
- leaseEndDate: p.leaseEndDate,
+ leases: syntheticLease ? [syntheticLease] : [],
schattenmarktRelease: p.schattenmarktRelease?.enabled
? { enabled: true, availableFrom: p.leaseEndDate }
: undefined,
diff --git a/src/domain/tenant.ts b/src/domain/tenant.ts
new file mode 100644
index 0000000..b8dfe3f
--- /dev/null
+++ b/src/domain/tenant.ts
@@ -0,0 +1,10 @@
+export interface Tenant {
+ id: string
+ companyName: string
+ industry?: string
+ contactPerson?: {
+ name: string
+ email?: string
+ phone?: string
+ }
+}
diff --git a/src/domain/unit.ts b/src/domain/unit.ts
index 212bbd1..37e6ea7 100644
--- a/src/domain/unit.ts
+++ b/src/domain/unit.ts
@@ -1,22 +1,30 @@
// ── Rentable Unit — primary entity for demand-side matching ──────────────────
//
-// A Property is the building/address container.
-// A PropertyUnit is what is actually rented: a specific floor, wing, or section.
-// Floor level is business-critical: retail needs EG for walk-in traffic,
-// offices can occupy upper floors, logistics needs ground-level loading access.
+// A Property is the building/address container (Liegenschaft).
+// A PropertyUnit is what is actually rented: a specific floor, wing, or section (Mietobjekt).
+// Floor level is an attribute of the unit — not a separate entity (matches Garaio REM / SAP RE-FX).
+// Tenant and contract data live on Lease, not directly on the unit.
+
+import type { Lease } from './lease'
+import type { AvailabilityStatus } from './enums'
+import { AvailabilityStatus as AS } from './enums'
export interface PropertyUnit {
id: string
- propertyId?: string // FK to parent Property
+ propertyId?: string // FK to parent Property (Liegenschaft)
floorLevel: number // 0=EG, 1=1.OG, -1=UG1
unitLabel?: string // e.g. "Nord", "West", "Einheit A"
areaSqm: number
available: boolean
rentPricePerSqm?: number // annual CHF/m²; falls back to property.rentPricePerSqm
+ leases?: Lease[] // Mietverträge — current, historical, future
+ /** @deprecated Use leases[].tenant.companyName */
currentTenant?: string
- leaseTerm?: string
+ /** @deprecated Use leases[].endDate */
leaseEndDate?: string
- floorPlanUrl?: string // optional floor plan image
+ /** @deprecated Use leases[0].contractDurationYears */
+ leaseTerm?: string
+ floorPlanUrl?: string
// Flexible letting (Teilfläche)
isFlexible?: boolean
minLettableSqm?: number
@@ -28,6 +36,23 @@ export interface PropertyUnit {
}
}
+// Derives availability from the unit's lease list.
+// Use this instead of reading property.availabilityStatus directly.
+export function getUnitAvailability(unit: PropertyUnit): {
+ status: AvailabilityStatus
+ availableFrom?: string
+ activeLease?: Lease
+} {
+ const active = unit.leases?.find(l => l.status === 'ACTIVE')
+ if (!active) return { status: AS.AVAILABLE_NOW }
+ const now = new Date()
+ const monthsToEnd = (new Date(active.endDate).getTime() - now.getTime()) / (1000 * 60 * 60 * 24 * 30)
+ if (monthsToEnd <= 12) {
+ return { status: AS.AVAILABLE_SOON, availableFrom: active.endDate, activeLease: active }
+ }
+ return { status: AS.OCCUPIED, activeLease: active }
+}
+
// A bundle groups multiple free units into a combined offer
export interface UnitBundle {
unitIds: string[]
diff --git a/src/features/matching/matchCardAdapter.ts b/src/features/matching/matchCardAdapter.ts
index a40fb22..9f892bb 100644
--- a/src/features/matching/matchCardAdapter.ts
+++ b/src/features/matching/matchCardAdapter.ts
@@ -11,8 +11,9 @@ import type {
MatchCardReason,
} from '../../components/match-card/MatchCardViewModel'
import { getCityIntelligence } from '../../lib/locationIntelligence'
-import { formatUnitTitle, formatMultiUnitFloors } from '../../domain/unit'
+import { formatUnitTitle, formatMultiUnitFloors, getUnitAvailability } from '../../domain/unit'
import { calcFitOutInvestment } from '../../lib/fitOutUtils'
+import { AvailabilityStatus } from '../../domain/enums'
const HARD_CRITERIA = new Set(['area', 'location', 'budget', 'timing'])
@@ -69,9 +70,19 @@ export function buildMatchCardViewModel(
cardTitle = property?.title ?? signal?.companyName ?? signal?.locationHint ?? '–'
}
- const availabilityLabel =
- property?.availabilityDate ??
- (signal?.timeHorizonMonths ? `~${signal.timeHorizonMonths} Monate` : undefined)
+ // Prefer unit-level availability (from Lease), fall back to property-level date, then signal
+ const unitAvail = unit ? getUnitAvailability(unit) : null
+ const availabilityLabel = (() => {
+ if (unitAvail?.availableFrom) {
+ return `ab ${new Date(unitAvail.availableFrom).toLocaleDateString('de-CH', { month: 'short', year: '2-digit' })}`
+ }
+ if (unitAvail?.status === AvailabilityStatus.AVAILABLE_NOW) return 'Sofort verfügbar'
+ if (property?.availabilityDate) {
+ return `ab ${new Date(property.availabilityDate).toLocaleDateString('de-CH', { month: 'short', year: '2-digit' })}`
+ }
+ if (signal?.timeHorizonMonths) return `~${signal.timeHorizonMonths} Monate`
+ return undefined
+ })()
const sourceLabel =
property?.sourceLabel ??
@@ -134,6 +145,8 @@ export function buildMatchCardViewModel(
signalConfirmedFacts: signal?.confirmedFacts,
signalUnconfirmedFacts: signal?.unconfirmedFacts,
signalAreaSqmEstimate: signal?.areaSqmEstimate,
+ areaSqm: unit?.areaSqm ?? property?.areaSqm ?? signal?.areaSqmEstimate,
+ rentPerSqm: property?.rentPricePerSqm,
propertyId: property?.id ?? signal?.propertyId,
unitId: match.unitId ?? unit?.id ?? signal?.unitId,
preMarketUnit: unit,
diff --git a/src/mock-data/properties.ts b/src/mock-data/properties.ts
index 1d6105b..503f0d9 100644
--- a/src/mock-data/properties.ts
+++ b/src/mock-data/properties.ts
@@ -49,9 +49,9 @@ export const mockProperties: Property[] = [
description: 'Moderne Bürofläche im aufstrebenden Stadtquartier Zürich-West, direkt beim Trendviertel Freilager. Die hellen, offen gestalteten Flächen bieten optimale Bedingungen für kollaboratives Arbeiten. Grosszügige Fensterfronten sorgen für viel Tageslicht. Das Gebäude verfügt über einen repräsentativen Empfangsbereich, Sitzungsräume sowie eine Gemeinschaftsterrasse mit Blick auf die Stadt. ÖV-Anbindung in unmittelbarer Nähe (Tram 4/13, S-Bahn Hardbrücke, 4 Minuten zu Fuss).',
units: [
- { id: 'unit-001-1', propertyId: 'prop-001', floorLevel: 1, unitLabel: 'Nord', areaSqm: 280, available: false, rentPricePerSqm: 456, currentTenant: 'MediaGroup Schweiz AG', leaseTerm: '5 Jahre', leaseEndDate: '2026-10-31', schattenmarktRelease: { enabled: true, availableFrom: '2026-10-31' } },
- { id: 'unit-001-2', propertyId: 'prop-001', floorLevel: 2, unitLabel: 'Süd', areaSqm: 310, available: false, rentPricePerSqm: 456, currentTenant: 'MediaGroup Schweiz AG', leaseTerm: '5 Jahre', leaseEndDate: '2026-10-31', schattenmarktRelease: { enabled: true, availableFrom: '2026-10-31' } },
- { id: 'unit-001-3', propertyId: 'prop-001', floorLevel: 3, unitLabel: 'West', areaSqm: 260, available: true, rentPricePerSqm: 480, isFlexible: true, minLettableSqm: 120 },
+ { id: 'unit-001-1', propertyId: 'prop-001', floorLevel: 1, unitLabel: 'Nord', areaSqm: 280, available: false, rentPricePerSqm: 456, leases: [{ id: 'lease-001-1', unitId: 'unit-001-1', tenant: { id: 'ten-media-001', companyName: 'MediaGroup Schweiz AG', industry: 'Media & Kommunikation' }, rentPerSqm: 456, startDate: '2020-09-01', endDate: '2026-10-31', contractDurationYears: 5, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', sourceId: 'MV-2020-001', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z', contractDocumentUrl: '/docs/mietvertrag-media-2020.pdf', contractDocumentName: 'Mietvertrag 2020–2026' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-10-31' } },
+ { id: 'unit-001-2', propertyId: 'prop-001', floorLevel: 2, unitLabel: 'Süd', areaSqm: 310, available: false, rentPricePerSqm: 456, leases: [{ id: 'lease-001-2', unitId: 'unit-001-2', tenant: { id: 'ten-media-001', companyName: 'MediaGroup Schweiz AG', industry: 'Media & Kommunikation' }, rentPerSqm: 456, startDate: '2020-09-01', endDate: '2026-10-31', contractDurationYears: 5, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', sourceId: 'MV-2020-002', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z', contractDocumentUrl: '/docs/mietvertrag-media-2020.pdf', contractDocumentName: 'Mietvertrag 2020–2026' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-10-31' } },
+ { id: 'unit-001-3', propertyId: 'prop-001', floorLevel: 3, unitLabel: 'West', areaSqm: 260, available: true, rentPricePerSqm: 480, leases: [], isFlexible: true, minLettableSqm: 120 },
],
importedFrom: 'SAP RE-FX',
importedAt: '2025-01-15T08:00:00Z',
@@ -106,8 +106,8 @@ export const mockProperties: Property[] = [
propertyNumber: 'BS-2022-002',
description: 'Grossflächige Logistikanlage in direkter Rheinnähe, Kleinhüningen Basel. Zwei separate Lagerhallen A und B mit je eigenem Tor und Rampenanlage. Sprinkleranlage und Hallentemperierung vorhanden. Sehr gute Erschliessung via A2/A3, 30 Lastwagenstellplätze auf dem Areal. Ausbaupotenzial von 800 m² auf dem Grundstück verfügbar.',
units: [
- { id: 'unit-002-1', propertyId: 'prop-002', floorLevel: 0, unitLabel: 'Halle A', areaSqm: 1400, available: false, rentPricePerSqm: 168, currentTenant: 'Spedition Rhein GmbH', leaseTerm: '3 Jahre', leaseEndDate: '2026-09-30', schattenmarktRelease: { enabled: true, availableFrom: '2026-09-30' } },
- { id: 'unit-002-2', propertyId: 'prop-002', floorLevel: 0, unitLabel: 'Halle B', areaSqm: 1000, available: false, rentPricePerSqm: 168, currentTenant: 'Spedition Rhein GmbH', leaseTerm: '3 Jahre', leaseEndDate: '2026-09-30', schattenmarktRelease: { enabled: true, availableFrom: '2026-09-30' } },
+ { id: 'unit-002-1', propertyId: 'prop-002', floorLevel: 0, unitLabel: 'Halle A', areaSqm: 1400, available: false, rentPricePerSqm: 168, leases: [{ id: 'lease-002-1', unitId: 'unit-002-1', tenant: { id: 'ten-spedition-002', companyName: 'Spedition Rhein GmbH', industry: 'Logistik & Transport' }, rentPerSqm: 168, startDate: '2022-07-01', endDate: '2026-09-30', contractDurationYears: 3, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', sourceId: 'MV-2022-001', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-09-30' } },
+ { id: 'unit-002-2', propertyId: 'prop-002', floorLevel: 0, unitLabel: 'Halle B', areaSqm: 1000, available: false, rentPricePerSqm: 168, leases: [{ id: 'lease-002-2', unitId: 'unit-002-2', tenant: { id: 'ten-spedition-002', companyName: 'Spedition Rhein GmbH', industry: 'Logistik & Transport' }, rentPerSqm: 168, startDate: '2022-07-01', endDate: '2026-09-30', contractDurationYears: 3, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', sourceId: 'MV-2022-002', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-09-30' } },
],
importedFrom: 'SAP RE-FX',
importedAt: '2025-01-15T08:00:00Z',
@@ -164,9 +164,9 @@ export const mockProperties: Property[] = [
propertyNumber: 'ZH-2021-007',
description: 'Helle Büroflächen im 2. Obergeschoss an der Thurgauerstrasse, Zürich-Oerlikon. Drei Einheiten — zwei belegt und für Pre-Market freigegeben, eine flexible Einheit ab 150 m² direkt verfügbar. Hervorragende ÖV-Anbindung via Tram 11 und S-Bahn Oerlikon. Break-out-Option auf September 2026, danach gesamte 720 m² frei.',
units: [
- { id: 'unit-007-1', propertyId: 'prop-007', floorLevel: 0, unitLabel: 'EG Ost', areaSqm: 180, available: false, rentPricePerSqm: 420, currentTenant: 'Consulting Partners AG', leaseTerm: '4 Jahre', leaseEndDate: '2026-11-30', schattenmarktRelease: { enabled: true, availableFrom: '2026-11-30' } },
- { id: 'unit-007-2', propertyId: 'prop-007', floorLevel: 1, unitLabel: '1.OG', areaSqm: 260, available: false, rentPricePerSqm: 432, currentTenant: 'Consulting Partners AG', leaseTerm: '4 Jahre', leaseEndDate: '2026-11-30', schattenmarktRelease: { enabled: true, availableFrom: '2026-11-30' } },
- { id: 'unit-007-3', propertyId: 'prop-007', floorLevel: 2, unitLabel: '2.OG West', areaSqm: 280, available: true, rentPricePerSqm: 445, isFlexible: true, minLettableSqm: 150 },
+ { id: 'unit-007-1', propertyId: 'prop-007', floorLevel: 0, unitLabel: 'EG Ost', areaSqm: 180, available: false, rentPricePerSqm: 420, leases: [{ id: 'lease-007-1', unitId: 'unit-007-1', tenant: { id: 'ten-consulting-007', companyName: 'Consulting Partners AG', industry: 'Unternehmensberatung' }, rentPerSqm: 420, startDate: '2021-10-01', endDate: '2026-11-30', contractDurationYears: 4, breakoutOption: true, breakoutOptionDate: '2026-09-01', status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-11-30' } },
+ { id: 'unit-007-2', propertyId: 'prop-007', floorLevel: 1, unitLabel: '1.OG', areaSqm: 260, available: false, rentPricePerSqm: 432, leases: [{ id: 'lease-007-2', unitId: 'unit-007-2', tenant: { id: 'ten-consulting-007', companyName: 'Consulting Partners AG', industry: 'Unternehmensberatung' }, rentPerSqm: 432, startDate: '2021-10-01', endDate: '2026-11-30', contractDurationYears: 4, breakoutOption: true, breakoutOptionDate: '2026-09-01', status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-11-30' } },
+ { id: 'unit-007-3', propertyId: 'prop-007', floorLevel: 2, unitLabel: '2.OG West', areaSqm: 280, available: true, rentPricePerSqm: 445, leases: [], isFlexible: true, minLettableSqm: 150 },
],
importedFrom: 'SAP RE-FX',
importedAt: '2025-01-15T08:00:00Z',
@@ -223,10 +223,10 @@ export const mockProperties: Property[] = [
propertyNumber: 'BS-2021-008',
description: 'Moderne Büroflächen im renommierten Dreispitz-Areal, Hochbergerstrasse Basel. Vier unabhängige Etagen für verschiedene Teams oder Mieter. Parkhaus im Gebäude vorhanden, hervorragende Anbindung an Tram 11. Derzeit vollvermietet an Pharma Research GmbH — alle Einheiten für Pre-Market freigegeben.',
units: [
- { id: 'unit-008-1', propertyId: 'prop-008', floorLevel: 1, unitLabel: '1.OG', areaSqm: 220, available: false, rentPricePerSqm: 384, currentTenant: 'Pharma Research GmbH', leaseTerm: '6 Jahre', leaseEndDate: '2027-08-31', schattenmarktRelease: { enabled: true, availableFrom: '2027-08-31' } },
- { id: 'unit-008-2', propertyId: 'prop-008', floorLevel: 2, unitLabel: '2.OG West', areaSqm: 250, available: false, rentPricePerSqm: 384, currentTenant: 'Pharma Research GmbH', leaseTerm: '6 Jahre', leaseEndDate: '2027-08-31', schattenmarktRelease: { enabled: true, availableFrom: '2027-08-31' } },
- { id: 'unit-008-3', propertyId: 'prop-008', floorLevel: 3, unitLabel: '3.OG Ost', areaSqm: 230, available: false, rentPricePerSqm: 384, currentTenant: 'Pharma Research GmbH', leaseTerm: '6 Jahre', leaseEndDate: '2027-08-31', schattenmarktRelease: { enabled: true, availableFrom: '2027-08-31' } },
- { id: 'unit-008-4', propertyId: 'prop-008', floorLevel: 4, unitLabel: '4.OG', areaSqm: 200, available: false, rentPricePerSqm: 408, currentTenant: 'Pharma Research GmbH', leaseTerm: '6 Jahre', leaseEndDate: '2027-08-31', schattenmarktRelease: { enabled: false } },
+ { id: 'unit-008-1', propertyId: 'prop-008', floorLevel: 1, unitLabel: '1.OG', areaSqm: 220, available: false, rentPricePerSqm: 384, leases: [{ id: 'lease-008-1', unitId: 'unit-008-1', tenant: { id: 'ten-pharma-008', companyName: 'Pharma Research GmbH', industry: 'Pharma & Life Sciences' }, rentPerSqm: 384, startDate: '2021-09-01', endDate: '2027-08-31', contractDurationYears: 6, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2027-08-31' } },
+ { id: 'unit-008-2', propertyId: 'prop-008', floorLevel: 2, unitLabel: '2.OG West', areaSqm: 250, available: false, rentPricePerSqm: 384, leases: [{ id: 'lease-008-2', unitId: 'unit-008-2', tenant: { id: 'ten-pharma-008', companyName: 'Pharma Research GmbH', industry: 'Pharma & Life Sciences' }, rentPerSqm: 384, startDate: '2021-09-01', endDate: '2027-08-31', contractDurationYears: 6, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2027-08-31' } },
+ { id: 'unit-008-3', propertyId: 'prop-008', floorLevel: 3, unitLabel: '3.OG Ost', areaSqm: 230, available: false, rentPricePerSqm: 384, leases: [{ id: 'lease-008-3', unitId: 'unit-008-3', tenant: { id: 'ten-pharma-008', companyName: 'Pharma Research GmbH', industry: 'Pharma & Life Sciences' }, rentPerSqm: 384, startDate: '2021-09-01', endDate: '2027-08-31', contractDurationYears: 6, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2027-08-31' } },
+ { id: 'unit-008-4', propertyId: 'prop-008', floorLevel: 4, unitLabel: '4.OG', areaSqm: 200, available: false, rentPricePerSqm: 408, leases: [{ id: 'lease-008-4', unitId: 'unit-008-4', tenant: { id: 'ten-pharma-008', companyName: 'Pharma Research GmbH', industry: 'Pharma & Life Sciences' }, rentPerSqm: 408, startDate: '2021-09-01', endDate: '2027-08-31', contractDurationYears: 6, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: false } },
],
importedFrom: 'SAP RE-FX',
importedAt: '2025-01-15T08:00:00Z',
@@ -281,8 +281,8 @@ export const mockProperties: Property[] = [
propertyNumber: 'ZH-2022-009',
description: 'Modernes Logistikzentrum im Töss-Quartier Winterthur, mit direkter A1-Anbindung. Lager Nord und Süd können separat oder gemeinsam angemietet werden. Ebene Andienung mit 4 Toren, Hallenhöhe 8 m, Bodenbelastung 3 t/m². 25 Lastwagenstellplätze. Ausbaupotenzial von 600 m² vorhanden.',
units: [
- { id: 'unit-009-1', propertyId: 'prop-009', floorLevel: 0, unitLabel: 'Lager Nord', areaSqm: 900, available: false, rentPricePerSqm: 156, currentTenant: 'Sperrgut Logistik AG', leaseTerm: '6 Jahre', leaseEndDate: '2026-10-31', schattenmarktRelease: { enabled: true, availableFrom: '2026-10-31' } },
- { id: 'unit-009-2', propertyId: 'prop-009', floorLevel: 0, unitLabel: 'Lager Süd', areaSqm: 900, available: false, rentPricePerSqm: 156, currentTenant: 'Sperrgut Logistik AG', leaseTerm: '6 Jahre', leaseEndDate: '2026-10-31', schattenmarktRelease: { enabled: true, availableFrom: '2026-10-31' } },
+ { id: 'unit-009-1', propertyId: 'prop-009', floorLevel: 0, unitLabel: 'Lager Nord', areaSqm: 900, available: false, rentPricePerSqm: 156, leases: [{ id: 'lease-009-1', unitId: 'unit-009-1', tenant: { id: 'ten-sperrgut-009', companyName: 'Sperrgut Logistik AG', industry: 'Logistik & Transport' }, rentPerSqm: 156, startDate: '2020-10-01', endDate: '2026-10-31', contractDurationYears: 6, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-10-31' } },
+ { id: 'unit-009-2', propertyId: 'prop-009', floorLevel: 0, unitLabel: 'Lager Süd', areaSqm: 900, available: false, rentPricePerSqm: 156, leases: [{ id: 'lease-009-2', unitId: 'unit-009-2', tenant: { id: 'ten-sperrgut-009', companyName: 'Sperrgut Logistik AG', industry: 'Logistik & Transport' }, rentPerSqm: 156, startDate: '2020-10-01', endDate: '2026-10-31', contractDurationYears: 6, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-10-31' } },
],
importedFrom: 'SAP RE-FX',
importedAt: '2025-01-15T08:00:00Z',
@@ -337,8 +337,8 @@ export const mockProperties: Property[] = [
propertyNumber: 'ZH-2024-010',
description: 'Exklusive Retailfläche am Löwenplatz, Zürich-Innenstadt. Direkter Zugang zu Bahnhof und Tramknotenpunkt, maximale Laufkundschaft rund um die Uhr. EG-Verkaufsfläche mit repräsentativer Schaufensterfront. Derzeit von Fashion Concept GmbH belegt — Pre-Market-Freigabe bereits aktiv.',
units: [
- { id: 'unit-010-1', propertyId: 'prop-010', floorLevel: 0, unitLabel: 'EG Verkauf', areaSqm: 205, available: false, rentPricePerSqm: 1056, currentTenant: 'Fashion Concept GmbH', leaseTerm: '6 Jahre', leaseEndDate: '2026-09-30', schattenmarktRelease: { enabled: true, availableFrom: '2026-09-30' } },
- { id: 'unit-010-2', propertyId: 'prop-010', floorLevel: -1, unitLabel: 'UG Lager', areaSqm: 80, available: false, rentPricePerSqm: 432, currentTenant: 'Fashion Concept GmbH', leaseTerm: '6 Jahre', leaseEndDate: '2026-09-30', schattenmarktRelease: { enabled: false } },
+ { id: 'unit-010-1', propertyId: 'prop-010', floorLevel: 0, unitLabel: 'EG Verkauf', areaSqm: 205, available: false, rentPricePerSqm: 1056, leases: [{ id: 'lease-010-1', unitId: 'unit-010-1', tenant: { id: 'ten-fashion-010', companyName: 'Fashion Concept GmbH', industry: 'Retail & Mode' }, rentPerSqm: 1056, startDate: '2020-09-01', endDate: '2026-09-30', contractDurationYears: 6, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-09-30' } },
+ { id: 'unit-010-2', propertyId: 'prop-010', floorLevel: -1, unitLabel: 'UG Lager', areaSqm: 80, available: false, rentPricePerSqm: 432, leases: [{ id: 'lease-010-2', unitId: 'unit-010-2', tenant: { id: 'ten-fashion-010', companyName: 'Fashion Concept GmbH', industry: 'Retail & Mode' }, rentPerSqm: 432, startDate: '2020-09-01', endDate: '2026-09-30', contractDurationYears: 6, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: false } },
],
importedFrom: 'SAP RE-FX',
importedAt: '2025-01-15T08:00:00Z',
@@ -393,8 +393,8 @@ export const mockProperties: Property[] = [
propertyNumber: 'BE-2023-011',
description: 'Weiträumige Produktions- und Logistikhalle im Gewerbequartier Brünnen Bern. Kranbahn (5 t) in Halle West, Dreiphasenstrom 400V vorhanden. Gut angebunden an A12-Anschluss Bern-Bümpliz. Zwei Hallenabschnitte separat oder gemeinsam anmietbar. Ausbaupotenzial von 1200 m² auf dem Grundstück möglich.',
units: [
- { id: 'unit-011-1', propertyId: 'prop-011', floorLevel: 0, unitLabel: 'Halle West', areaSqm: 1600, available: false, rentPricePerSqm: 144, currentTenant: 'Metallbau Bern AG', leaseTerm: '11 Jahre', leaseEndDate: '2026-11-30', schattenmarktRelease: { enabled: true, availableFrom: '2026-11-30' } },
- { id: 'unit-011-2', propertyId: 'prop-011', floorLevel: 0, unitLabel: 'Halle Ost', areaSqm: 1200, available: false, rentPricePerSqm: 144, currentTenant: 'Metallbau Bern AG', leaseTerm: '11 Jahre', leaseEndDate: '2026-11-30', schattenmarktRelease: { enabled: true, availableFrom: '2026-11-30' } },
+ { id: 'unit-011-1', propertyId: 'prop-011', floorLevel: 0, unitLabel: 'Halle West', areaSqm: 1600, available: false, rentPricePerSqm: 144, leases: [{ id: 'lease-011-1', unitId: 'unit-011-1', tenant: { id: 'ten-metallbau-011', companyName: 'Metallbau Bern AG', industry: 'Produktion & Metallverarbeitung' }, rentPerSqm: 144, startDate: '2015-11-01', endDate: '2026-11-30', contractDurationYears: 11, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-11-30' } },
+ { id: 'unit-011-2', propertyId: 'prop-011', floorLevel: 0, unitLabel: 'Halle Ost', areaSqm: 1200, available: false, rentPricePerSqm: 144, leases: [{ id: 'lease-011-2', unitId: 'unit-011-2', tenant: { id: 'ten-metallbau-011', companyName: 'Metallbau Bern AG', industry: 'Produktion & Metallverarbeitung' }, rentPerSqm: 144, startDate: '2015-11-01', endDate: '2026-11-30', contractDurationYears: 11, breakoutOption: false, status: 'ACTIVE', sourceSystem: 'SAP_REFX', importedAt: '2025-01-15T08:00:00Z', lastUpdatedAt: '2025-05-10T10:00:00Z' }], schattenmarktRelease: { enabled: true, availableFrom: '2026-11-30' } },
],
importedFrom: 'SAP RE-FX',
importedAt: '2025-01-15T08:00:00Z',