feat: Future Availability — Logiktrennung Supply/Demand + Markt-Leads

EXPANSION-Signale (Unternehmen sucht Fläche) sind Nachfragesignale und
gehören nicht in den Demand-Feed. Neue Architektur:

- signalDirection: 'SUPPLY' | 'DEMAND' auf FutureSignal-Domain
- SUPPLY-Signale: POSSIBLE_MOVE_OUT / CONSTRUCTION_PROJECT (Crawler)
  und LEASE_EXPIRY+isVerified (Verwaltung ERP) → Demand-Feed wie bisher
- DEMAND-Signale: EXPANSION → aus Demand-Feed gefiltert
- Neues useMarketLeads-Hook + /supply/market-leads Seite für Verwaltung:
  zeigt EXPANSION-Signale als potenzielle Mieter mit Portfolio-Matching
- Alle 15 Signale in futureSignals.ts inhaltlich bereinigt und
  mit korrekter signalDirection versehen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-20 15:56:29 +02:00
parent 5fe15bac62
commit b0ad6643ff
7 changed files with 626 additions and 245 deletions
+37
View File
@@ -0,0 +1,37 @@
import { useMemo } from 'react'
import { useFutureSignals } from './useFutureSignals'
import { useProperties } from './useProperties'
import type { FutureSignal } from '../domain/futureSignal'
import type { Property } from '../domain/property'
export interface MarketLead {
signal: FutureSignal
matchingProperties: Property[]
}
export function useMarketLeads(): { data: MarketLead[]; isLoading: boolean } {
const { data: signals = [], isLoading: signalsLoading } = useFutureSignals()
const { data: properties = [], isLoading: propertiesLoading } = useProperties()
const data = useMemo((): MarketLead[] => {
const demandSignals = signals.filter(s => s.signalDirection === 'DEMAND')
return demandSignals.map(signal => {
const signalCity = signal.locationHint.split(',')[0].trim().toLowerCase()
const matchingProperties = properties.filter(p => {
if (p.resultType !== 'VERIFIED_PORTFOLIO') return false
const propCity = (p.location?.city ?? '').toLowerCase()
const cityMatch = propCity.includes(signalCity) || signalCity.includes(propCity)
const areaOk = signal.areaSqmEstimate
? p.areaSqm >= signal.areaSqmEstimate * 0.5 && p.areaSqm <= signal.areaSqmEstimate * 2.2
: true
return cityMatch && areaOk
})
return { signal, matchingProperties }
})
}, [signals, properties])
return { data, isLoading: signalsLoading || propertiesLoading }
}
+2 -2
View File
@@ -37,7 +37,7 @@ export function useUnifiedResults(needId?: string) {
// Fast path: explicit FUTURE_AVAILABILITY on the match (no property lookup needed)
if (match.resultType === 'FUTURE_AVAILABILITY') {
const signal = allSignals.find(s => s.id === refId || s.propertyId === refId)
if (!signal) return []
if (!signal || signal.signalDirection === 'DEMAND') return []
return [{ matchId: match.id, needId: match.needId, matchScore: match.matchScore,
resultType: 'FUTURE_AVAILABILITY', signal, match }]
}
@@ -51,7 +51,7 @@ export function useUnifiedResults(needId?: string) {
if (rt === 'FUTURE_AVAILABILITY') {
const signal = allSignals.find(s => s.id === refId || s.propertyId === refId)
if (!signal) return []
if (!signal || signal.signalDirection === 'DEMAND') return []
return [{ matchId: match.id, needId: match.needId, matchScore: match.matchScore,
resultType: 'FUTURE_AVAILABILITY', signal, match }]
}