// ── Rentable Unit — primary entity for demand-side matching ────────────────── // // 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 (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 // Unit-level fit-out override — falls back to property.hardFacts.fitOut when unset. // Relevant for pre-market, where units of one property are matched individually. fitOut?: 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM' leases?: Lease[] // Mietverträge — current, historical, future /** @deprecated Use leases[].tenant.companyName */ currentTenant?: string /** @deprecated Use leases[].endDate */ leaseEndDate?: string /** @deprecated Use leases[0].contractDurationYears */ leaseTerm?: string floorPlanUrl?: string // Flexible letting (Teilfläche) isFlexible?: boolean minLettableSqm?: number offeredSqm?: number // currently offered area (≤ areaSqm) // Unit-level pre-market release schattenmarktRelease?: { enabled: boolean availableFrom?: string // ISO date anonymous?: boolean // hide address/name in pre-market feed } } // 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[] combinedSqm: number label: string // e.g. "1.OG + 2.OG" } // Result of unit-level need matching (used in supply-side detail views) export interface UnitNeedMatch { needId: string tenantName: string tenantCompany?: string requiredSqmMin: number requiredSqmMax: number matchScore: number matchType: 'exact' | 'partial' | 'bundle' suggestedSqm?: number } // ── Display helpers ─────────────────────────────────────────────────────────── export function formatFloorLabel(level: number): string { if (level === 0) return 'EG' if (level < 0) return `UG${Math.abs(level)}` return `${level}.OG` } /** Returns the primary display title for a specific unit: "1.OG Nord · Zollstrasse 12" */ export function formatUnitTitle( unit: PropertyUnit, address: { street: string; houseNumber: string }, ): string { const floor = formatFloorLabel(unit.floorLevel) const label = unit.unitLabel ? ` ${unit.unitLabel}` : '' return `${floor}${label} · ${address.street} ${address.houseNumber}` } /** Returns a compact floor-range string for multi-unit properties: "1.OG–3.OG" or "EG · 1.OG" */ export function formatMultiUnitFloors(units: PropertyUnit[]): string { const floors = [...new Set(units.map(u => u.floorLevel))].sort((a, b) => a - b) if (floors.length === 0) return '' if (floors.length === 1) return formatFloorLabel(floors[0]) if (floors.length === 2) return `${formatFloorLabel(floors[0])} · ${formatFloorLabel(floors[1])}` return `${formatFloorLabel(floors[0])}–${formatFloorLabel(floors[floors.length - 1])}` }