Files
property-match/src/domain/unit.ts
T
Benjamin Sutter e95490eb72 feat: Grundriss-Feature, Listenansicht mit Bildern, Gewerbe-Label, Image-Pool
- Grundriss (FloorPlanSection): PropertyUnit.floorPlanUrl?, Property.floorPlanUrl?;
  FloorPlanSection in MatchDetail + PropertyDetail; FloorPlanUrlSection in NewListing-Formular
- Listenansicht (MatchCardCompact): horizontales Layout mit 120px Bildstreifen,
  Score-Badge, Asset-Label-Overlay, alle 3 grünen Punkte, Anfrage-Button
- Light Industrial → "Gewerbe" überall (NeedInput, NeedCardPreview, CriteriaReviewPanel,
  newListingConstants, MyListings, propertyHelpers)
- "Zum Originalinserat"-Button nur bei Maison-Work-Objekten
- Image-Pool: propertyImageResolver mit sequentiellem Pool-Index (keine doppelten Bilder),
  nur Innenaufnahmen, rotate()-Trick für Sub-Pools
- Overlay-Labels (Objekttyp + Stadtteil) in LocationPreview + IntelligenceMatchCard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 22:12:43 +02:00

76 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── 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.
export interface PropertyUnit {
id: string
propertyId?: string // FK to parent Property
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
currentTenant?: string
leaseTerm?: string
leaseEndDate?: string
floorPlanUrl?: string // optional floor plan image
// 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
}
}
// 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.OG3.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])}`
}