e169f8e310
Mieterausbau / fit-out economics — surface true total cost of occupancy: - Annuity calc (annuityFactor + effectiveAnnualBurdenPerSqm) replaces straight-line ÷5; FITOUT_ANNUITY_RATE=5% - New hardFacts.fitOutByLandlord: "Wer baut aus?" toggle in NewListing — landlord-borne fit-out is priced into rent (no surcharge), tenant-borne SHELL/BASIC adds annuitized cost minus MAB - scoreBudget now compares effective annual burden (rent + fit-out annuity) vs budget instead of cold rent only; FULL/PREMIUM and landlord-borne unchanged - FitOutCostPanel + CompareTableBody compute annuitized, tenant-aware burden - Central FIT_OUT_LABELS with industry/international vocabulary (Rohbau·Core&Shell, Edelrohbau·CAT A, etc.) - Activate existing generateFitOutAdvice via useFitOutAdvice hook + new FitOutAdvicePanel (MIETERAUSBAU/BKZ/MAB-Amortisation + negotiation tip), shown for tenant-borne SHELL/BASIC - MAB field only asked when tenant builds out (optional) — one new toggle, no extra data burden for property managers Fix: DQ/confidence modifiers were computed but never applied to finalScore (hardcoded 0 in output) — now folded into rawFinal and exposed. Trust-first: weak data quality lowers the score. Resolves 3 pre-existing red tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
220 lines
7.2 KiB
TypeScript
220 lines
7.2 KiB
TypeScript
import type {
|
|
AssetType, ResultType, AvailabilityStatus, AvailabilityType,
|
|
FreshnessStatus, RiskLevel, SourceType, DataQualityLevel,
|
|
} 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'
|
|
|
|
// ── Location / Address ────────────────────────────────────────────────────────
|
|
|
|
export interface Location {
|
|
city: string
|
|
district?: string
|
|
canton?: string
|
|
region?: string
|
|
country: string
|
|
coordinates?: { lat: number; lng: number }
|
|
}
|
|
|
|
export interface Address {
|
|
street: string
|
|
houseNumber: string
|
|
postalCode: string
|
|
city: string
|
|
country: string
|
|
}
|
|
|
|
// ── Source Metadata ───────────────────────────────────────────────────────────
|
|
|
|
export interface SourceMeta {
|
|
sourceType: SourceType | string
|
|
sourceLabel?: string
|
|
sourceUrl?: string
|
|
sourceUpdatedAt?: string
|
|
externalId?: string
|
|
}
|
|
|
|
// ── Data Quality ──────────────────────────────────────────────────────────────
|
|
|
|
export interface DataQuality {
|
|
score: number
|
|
qualityLevel?: DataQualityLevel
|
|
missingCriticalFields: string[]
|
|
missingOptionalFields: string[]
|
|
lastVerifiedAt?: string
|
|
freshness: FreshnessStatus
|
|
warnings: string[]
|
|
}
|
|
|
|
// ── Hard Facts ────────────────────────────────────────────────────────────────
|
|
|
|
export interface PropertyHardFacts {
|
|
floor?: number
|
|
fitOut?: 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM'
|
|
mieterausbaubeitragPerSqm?: number
|
|
/** true = Vermieter übernimmt den Ausbau & preist ihn in die Miete ein (kein Aufschlag, kein MAB). */
|
|
fitOutByLandlord?: boolean
|
|
parking?: number
|
|
publicTransportScore?: number
|
|
usageType?: string
|
|
ceilingHeightM?: number
|
|
loadingDocksCount?: number
|
|
powerSupplyKva?: number
|
|
hasServerRoom?: boolean
|
|
isBarrierFree?: boolean
|
|
hasAirConditioning?: boolean
|
|
hasStorefront?: boolean
|
|
}
|
|
|
|
// ── Soft Factors ──────────────────────────────────────────────────────────────
|
|
|
|
export interface SoftFactors {
|
|
prestigeScore?: number
|
|
visibilityScore?: number
|
|
footfallScore?: number
|
|
commuterAccessScore?: number
|
|
talentAccessScore?: number
|
|
esgScore?: number
|
|
flexibilityScore?: number
|
|
expansionPotentialScore?: number
|
|
taxEnvironmentScore?: number
|
|
// Legacy aliases kept for mock-data backward compatibility
|
|
prestige?: number
|
|
accessibility?: number
|
|
talentAccess?: number
|
|
esgRating?: string
|
|
passerbyFrequency?: 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH'
|
|
parkingSpots?: number
|
|
publicTransportMinutes?: number
|
|
infrastructureNotes?: string
|
|
}
|
|
|
|
// ── Property ──────────────────────────────────────────────────────────────────
|
|
|
|
export interface Property {
|
|
id: string
|
|
organizationId?: string
|
|
title: string
|
|
|
|
assetType: AssetType
|
|
resultType: ResultType
|
|
|
|
location: Location
|
|
address: Address
|
|
|
|
areaSqm: number
|
|
areaSqmMin?: number
|
|
areaSqmMax?: number
|
|
|
|
rentPricePerSqm: number
|
|
rentChfSqmYear?: number
|
|
totalRentMonthly?: number
|
|
ancillaryCosts?: number
|
|
|
|
availabilityDate: string
|
|
availabilityStatus: AvailabilityStatus
|
|
availabilityType?: AvailabilityType
|
|
|
|
sourceType: string
|
|
sourceLabel?: string
|
|
sourceUrl?: string
|
|
sourceUpdatedAt?: string
|
|
sourceMeta?: SourceMeta
|
|
|
|
confidenceScore: number
|
|
dataQuality: DataQuality
|
|
|
|
softFactors?: SoftFactors
|
|
hardFacts?: PropertyHardFacts
|
|
|
|
// Legacy fields — kept for backward compat
|
|
floorLevel?: number
|
|
expansionPotentialSqm?: number
|
|
|
|
contractDurationMonths?: number
|
|
riskLevel?: RiskLevel
|
|
description?: string
|
|
images?: string[]
|
|
floorPlanUrl?: string
|
|
/** @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
|
|
lastUpdatedAt?: string
|
|
|
|
schattenmarktRelease?: { enabled: boolean; leadTimeMonths?: number; anonymous?: boolean }
|
|
|
|
status?: 'ACTIVE' | 'INACTIVE' | 'DRAFT' | 'ARCHIVED'
|
|
lastReviewedAt?: string
|
|
createdAt: string
|
|
updatedAt: string
|
|
}
|
|
|
|
export type CreatePropertyInput = Omit<Property, 'id' | 'createdAt' | 'updatedAt'>
|
|
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,
|
|
floorLevel: p.hardFacts?.floor ?? p.floorLevel ?? 0,
|
|
unitLabel: undefined,
|
|
areaSqm: p.areaSqm,
|
|
available: p.availabilityStatus === AS.AVAILABLE_NOW || p.availabilityStatus === AS.AVAILABLE_SOON,
|
|
rentPricePerSqm: p.rentPricePerSqm,
|
|
leases: syntheticLease ? [syntheticLease] : [],
|
|
schattenmarktRelease: p.schattenmarktRelease?.enabled
|
|
? { enabled: true, availableFrom: p.leaseEndDate }
|
|
: undefined,
|
|
}]
|
|
}
|