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:
@@ -1,4 +1,6 @@
|
||||
export * from './enums'
|
||||
export * from './tenant'
|
||||
export * from './lease'
|
||||
export * from './property'
|
||||
export * from './need'
|
||||
export * from './match'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+32
-5
@@ -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<CreatePropertyInput>
|
||||
* 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,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface Tenant {
|
||||
id: string
|
||||
companyName: string
|
||||
industry?: string
|
||||
contactPerson?: {
|
||||
name: string
|
||||
email?: string
|
||||
phone?: string
|
||||
}
|
||||
}
|
||||
+32
-7
@@ -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[]
|
||||
|
||||
Reference in New Issue
Block a user