feat: Objekt→Einheit architecture — unit-level pre-market, property detail page, click-through from cards
- Domain: PropertyUnit extended with propertyId + schattenmarktRelease per unit
- Domain: FutureAvailabilityResult carries resolved property + unit
- useSchattenmarktSignals: generates unit-level signals (schattenmarkt-{propId}-{unitId})
- useUnifiedResults: resolves backing property + unit on FUTURE_AVAILABILITY fast path
- IUnitProvider + MockupUnitProvider: first-class unit access and mutation
- matchCardAdapter: maps preMarketUnit, preMarketAllUnits, propertyId, unitId to ViewModel
- IntelligenceMatchCard: PRE-MARKET VERIFIED shows unit info strip + "Zur Einheit →" button
- PropertyDetailView: unit-level toggles + date pickers inside PreMarketPanel
- New page: /demand/property/:propertyId with unit table, status chips, inquiry form
- App.tsx: demand route /demand/property/:propertyId registered
- Mock data: prop-001/007/037 units updated with correct lease dates + unit-level schattenmarktRelease
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { Property } from '../domain/property'
|
||||
import type { Property, PropertyUnit } from '../domain/property'
|
||||
import type { FutureSignal } from '../domain/futureSignal'
|
||||
import { SignalType, RiskLevel, ResultType } from '../domain/enums'
|
||||
|
||||
@@ -11,16 +11,38 @@ export function useSchattenmarktSignals(properties: Property[]): FutureSignal[]
|
||||
const signals: FutureSignal[] = []
|
||||
for (const p of properties) {
|
||||
if (p.resultType !== ResultType.VERIFIED_PORTFOLIO) continue
|
||||
|
||||
const rel = p.schattenmarktRelease
|
||||
if (!rel?.enabled) continue
|
||||
const triggerDate = getEarliestTriggerDate(p, rel.leadTimeMonths)
|
||||
if (!triggerDate || MOCK_TODAY < triggerDate) continue
|
||||
signals.push(buildSignal(p))
|
||||
|
||||
// Unit-level: generate one signal per released unit that has schattenmarktRelease.enabled
|
||||
const releasedUnits = (p.units ?? []).filter(u => u.schattenmarktRelease?.enabled)
|
||||
|
||||
if (releasedUnits.length > 0) {
|
||||
for (const unit of releasedUnits) {
|
||||
const availableFrom = unit.schattenmarktRelease?.availableFrom ?? unit.leaseEndDate ?? p.leaseEndDate
|
||||
if (!availableFrom) continue
|
||||
const triggerDate = getTriggerDate(availableFrom, rel.leadTimeMonths)
|
||||
if (MOCK_TODAY < triggerDate) continue
|
||||
signals.push(buildUnitSignal(p, unit, availableFrom))
|
||||
}
|
||||
} else {
|
||||
// Backward compat: property-level signal (no explicit unit releases defined)
|
||||
const triggerDate = getEarliestTriggerDate(p, rel.leadTimeMonths)
|
||||
if (!triggerDate || MOCK_TODAY < triggerDate) continue
|
||||
signals.push(buildPropertySignal(p))
|
||||
}
|
||||
}
|
||||
return signals
|
||||
}, [properties])
|
||||
}
|
||||
|
||||
function getTriggerDate(availableFrom: string, leadTimeMonths: number): Date {
|
||||
const d = new Date(availableFrom)
|
||||
d.setMonth(d.getMonth() - leadTimeMonths)
|
||||
return d
|
||||
}
|
||||
|
||||
function getEarliestTriggerDate(p: Property, leadTimeMonths: number): Date | null {
|
||||
const candidates: Date[] = []
|
||||
if (p.leaseEndDate) {
|
||||
@@ -36,13 +58,61 @@ function getEarliestTriggerDate(p: Property, leadTimeMonths: number): Date | nul
|
||||
return candidates.length ? candidates.reduce((a, b) => (a < b ? a : b)) : null
|
||||
}
|
||||
|
||||
function buildSignal(p: Property): FutureSignal {
|
||||
function buildUnitSignal(p: Property, unit: PropertyUnit, availableFrom: string): FutureSignal {
|
||||
const targetDate = new Date(availableFrom)
|
||||
const monthsUntil = Math.max(1, Math.round(
|
||||
(targetDate.getTime() - MOCK_TODAY.getTime()) / (1000 * 60 * 60 * 24 * 30),
|
||||
))
|
||||
const locationLabel = `${p.location.city}${p.location.district ? `, ${p.location.district}` : ''}`
|
||||
const monthName = targetDate.toLocaleDateString('de-CH', { month: 'long', year: 'numeric' })
|
||||
const floorLabel = unit.floorLevel === 0 ? 'EG' : unit.floorLevel < 0 ? `UG ${Math.abs(unit.floorLevel)}` : `${unit.floorLevel}.OG`
|
||||
const unitDesc = unit.unitLabel ? `${floorLabel} · ${unit.unitLabel}` : floorLabel
|
||||
|
||||
return {
|
||||
id: `schattenmarkt-${p.id}-${unit.id}`,
|
||||
signalType: SignalType.LEASE_EXPIRY,
|
||||
propertyId: p.id,
|
||||
unitId: unit.id,
|
||||
title: `${p.title} · ${unitDesc} — frei ab ${monthName}`,
|
||||
locationHint: locationLabel,
|
||||
areaSqmEstimate: unit.areaSqm,
|
||||
probability: 0.92,
|
||||
confidenceScore: 0.92,
|
||||
timeHorizonMonths: monthsUntil,
|
||||
source: { type: 'LEASE_CONTRACT', credibility: 'HIGH' },
|
||||
sensitivityLevel: 'INTERNAL',
|
||||
disclaimer: 'Verwaltung hat diese Einheit für Pre-Market-Sichtbarkeit freigegeben. Vertragsende aus internem ERP bestätigt — höchste Signalqualität.',
|
||||
riskLevel: RiskLevel.LOW,
|
||||
relevanceScore: 0.92,
|
||||
isVerified: true,
|
||||
organizationId: p.organizationId,
|
||||
createdAt: MOCK_TODAY.toISOString(),
|
||||
updatedAt: MOCK_TODAY.toISOString(),
|
||||
aiSummary: `Vertrag der ${unit.currentTenant ?? p.currentTenant ?? 'aktuellen Mietpartei'} läuft in ${monthsUntil} Monaten aus (${monthName}). Einheit: ${unit.areaSqm.toLocaleString('de-CH')} m² · ${unitDesc} · ${locationLabel}. Die Verwaltung hat diese Einheit explizit für den Markt freigegeben — vertraglich bestätigt.`,
|
||||
marketIndicators: [
|
||||
`Vertragsende ${monthName} aus Verwaltungssystem bestätigt`,
|
||||
`Einheit ${unitDesc} · ${unit.areaSqm.toLocaleString('de-CH')} m² · ${locationLabel}`,
|
||||
`Pre-Market-Freigabe durch Verwaltung erteilt`,
|
||||
],
|
||||
confirmedFacts: [
|
||||
`Vertragsende ${monthName} aus ERP bestätigt`,
|
||||
`${unit.areaSqm.toLocaleString('de-CH')} m² · ${unitDesc} — bestätigt`,
|
||||
`Standort ${locationLabel} — bestätigt`,
|
||||
],
|
||||
unconfirmedFacts: [
|
||||
'Ob Nachmieter bereits bekannt',
|
||||
'Ob Umbaumassnahmen geplant',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function buildPropertySignal(p: Property): FutureSignal {
|
||||
const targetDate = p.breakoutOption && p.breakoutOptionDate
|
||||
? new Date(p.breakoutOptionDate)
|
||||
: p.leaseEndDate ? new Date(p.leaseEndDate) : new Date()
|
||||
|
||||
const monthsUntil = Math.max(1, Math.round(
|
||||
(targetDate.getTime() - MOCK_TODAY.getTime()) / (1000 * 60 * 60 * 24 * 30)
|
||||
(targetDate.getTime() - MOCK_TODAY.getTime()) / (1000 * 60 * 60 * 24 * 30),
|
||||
))
|
||||
|
||||
const locationLabel = `${p.location.city}${p.location.district ? `, ${p.location.district}` : ''}`
|
||||
|
||||
Reference in New Issue
Block a user