refactor: unit-centric lease view — Einheiten & Mietverträge

Replace building-level Miet- & Mieterinformationen section with
unit-level lease display. Each unit row now shows tenant name and
lease end date directly; expand reveals full contract details
(Mietbeginn, Mietende, Laufzeit, Breakout, source system, doc link).

Floor grouping groups consecutive same-floor units under one label.
Domain model extended with Lease and Tenant types; deprecated
property-level tenant fields preserved via @deprecated for compat.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-06-10 22:52:18 +02:00
parent d771029633
commit ee229f8f4f
9 changed files with 518 additions and 343 deletions
+32 -7
View File
@@ -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[]