feat: Reminder Manager + Schattenmarkt-Freigabe + mock data overhaul
- Add Reminder Manager page (/supply/reminder-manager) with KPI bar, filter bar, list/card feed, and detail drawer (7 sections incl. activity log, snooze, complete, dismiss actions) - Add Schattenmarkt-Freigabe toggle on PropertyDetailView: Verwaltung can opt-in properties for early market exposure before contract expiry - Auto-generate FutureSignal cards via useSchattenmarktSignals hook when leaseEndDate - leadTimeMonths <= MOCK_TODAY - Fix useUnifiedResults: use property.resultType as fallback (was defaulting everything to VERIFIED_PORTFOLIO) - Fix demand Results: hide VERIFIED_PORTFOLIO from non-manager users even when showOwnProperties toggle was previously enabled - Fix AppShell: redirect to allowed workspace on user role switch - Fix 3 wrong match scores (match-002: 93→62, match-009: 86→55, match-017: 87→52) - Add 7 new match records (match-050–056) for need-001, need-002, need-011 - Add need-011 (Retail Bern Innenstadt) - Add prop-031–036 (EXTERNAL_MARKET / MAISON_WORK / FUTURE_AVAILABILITY) - Fix duplicate image URLs across all properties - Add signal-011 (Bern Altstadt, Mode Boutique) + propertyId to signal-001 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { Property } from '../domain/property'
|
||||
import type { FutureSignal } from '../domain/futureSignal'
|
||||
import { SignalType, RiskLevel, ResultType } from '../domain/enums'
|
||||
|
||||
// Matches the mock date used throughout the prototype (currentDate context: 2026-05-20)
|
||||
const MOCK_TODAY = new Date('2026-05-20')
|
||||
|
||||
export function useSchattenmarktSignals(properties: Property[]): FutureSignal[] {
|
||||
return useMemo(() => {
|
||||
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))
|
||||
}
|
||||
return signals
|
||||
}, [properties])
|
||||
}
|
||||
|
||||
function getEarliestTriggerDate(p: Property, leadTimeMonths: number): Date | null {
|
||||
const candidates: Date[] = []
|
||||
if (p.leaseEndDate) {
|
||||
const d = new Date(p.leaseEndDate)
|
||||
d.setMonth(d.getMonth() - leadTimeMonths)
|
||||
candidates.push(d)
|
||||
}
|
||||
if (p.breakoutOption && p.breakoutOptionDate) {
|
||||
const d = new Date(p.breakoutOptionDate)
|
||||
d.setMonth(d.getMonth() - leadTimeMonths)
|
||||
candidates.push(d)
|
||||
}
|
||||
return candidates.length ? candidates.reduce((a, b) => (a < b ? a : b)) : null
|
||||
}
|
||||
|
||||
function buildSignal(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)
|
||||
))
|
||||
|
||||
const locationLabel = `${p.location.city}${p.location.district ? `, ${p.location.district}` : ''}`
|
||||
const monthName = targetDate.toLocaleDateString('de-CH', { month: 'long', year: 'numeric' })
|
||||
|
||||
return {
|
||||
id: `schattenmarkt-${p.id}`,
|
||||
signalType: SignalType.LEASE_EXPIRY,
|
||||
propertyId: p.id,
|
||||
title: `${p.title} — frei ab ${monthName}`,
|
||||
locationHint: locationLabel,
|
||||
areaSqmEstimate: p.areaSqm,
|
||||
probability: 0.92,
|
||||
confidenceScore: 0.92,
|
||||
timeHorizonMonths: monthsUntil,
|
||||
source: { type: 'LEASE_CONTRACT', credibility: 'HIGH' },
|
||||
sensitivityLevel: 'INTERNAL',
|
||||
disclaimer: 'Verwaltung hat dieses Objekt für den Schattenmarkt 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 ${p.currentTenant ?? 'aktuellen Mietpartei'} läuft in ${monthsUntil} Monaten aus (${monthName}). Fläche: ${p.areaSqm.toLocaleString('de-CH')} m² · ${locationLabel}. Die Verwaltung hat dieses Objekt explizit für den Markt freigegeben — vertraglich bestätigt, keine Schätzung.`,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user