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:
@@ -12,10 +12,17 @@ import {
|
||||
ShieldCheck,
|
||||
User,
|
||||
} from 'lucide-react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { LocationPreview } from '../shared/LocationPreview'
|
||||
import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme'
|
||||
import type { MatchCardViewModel } from './MatchCardViewModel'
|
||||
|
||||
function floorLabel(level: number): string {
|
||||
if (level === 0) return 'EG'
|
||||
if (level < 0) return `UG ${Math.abs(level)}`
|
||||
return `${level}.OG`
|
||||
}
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const RESULT_TYPE_META: Record<string, { label: string; color: string }> = {
|
||||
@@ -81,6 +88,7 @@ function SignalQualityDots({ quality }: { quality: 'HIGH' | 'MEDIUM' | 'LOW' | u
|
||||
|
||||
function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
|
||||
const isControlled = vm.signalIsControlled ?? false
|
||||
const navigate = useNavigate()
|
||||
|
||||
// PRE-MARKET VERIFIED: soft purple / institutional premium
|
||||
// MARKET SIGNAL: slate blue / analytical
|
||||
@@ -207,6 +215,28 @@ function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* PRE-MARKET VERIFIED: specific unit info */}
|
||||
{isControlled && vm.preMarketUnit && (
|
||||
<Box sx={{ bgcolor: '#f0fdf4', border: '1px solid #bbf7d0', borderRadius: 1, px: 1.25, py: 0.875, mb: 1.25 }}>
|
||||
<Typography sx={{ fontSize: '0.63rem', fontWeight: 700, color: '#1a7a4a', textTransform: 'uppercase', letterSpacing: 0.5, mb: 0.4 }}>
|
||||
Freigegebene Einheit
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}>
|
||||
<Typography sx={{ fontSize: '0.72rem', color: '#1e293b', fontWeight: 600 }}>
|
||||
{floorLabel(vm.preMarketUnit.floorLevel)}{vm.preMarketUnit.unitLabel ? ` · ${vm.preMarketUnit.unitLabel}` : ''}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.72rem', color: '#475569' }}>
|
||||
{vm.preMarketUnit.areaSqm.toLocaleString('de-CH')} m²
|
||||
</Typography>
|
||||
{vm.preMarketUnit.schattenmarktRelease?.availableFrom && (
|
||||
<Typography sx={{ fontSize: '0.72rem', color: '#475569' }}>
|
||||
ab {new Date(vm.preMarketUnit.schattenmarktRelease.availableFrom).toLocaleDateString('de-CH', { month: 'short', year: 'numeric' })}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* MARKET SIGNAL: market indicators */}
|
||||
{!isControlled && (vm.signalMarketIndicators?.length ?? 0) > 0 && (
|
||||
<Box sx={{ mb: 1.25 }}>
|
||||
@@ -246,7 +276,7 @@ function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<Box sx={{ display: 'flex', gap: 0.75, mt: 'auto', pt: 1.25, borderTop: '1px solid rgba(0,0,0,0.06)' }}>
|
||||
<Box sx={{ display: 'flex', gap: 0.75, mt: 'auto', pt: 1.25, borderTop: '1px solid rgba(0,0,0,0.06)', flexWrap: 'wrap' }}>
|
||||
{vm.actions.map(a => (
|
||||
<Button
|
||||
key={a.id}
|
||||
@@ -265,6 +295,19 @@ function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
|
||||
{a.label}
|
||||
</Button>
|
||||
))}
|
||||
{isControlled && vm.propertyId && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={() => navigate(`/demand/property/${vm.propertyId}${vm.unitId ? `?unit=${vm.unitId}` : ''}`)}
|
||||
sx={{
|
||||
textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1,
|
||||
bgcolor: '#7c3aed', '&:hover': { bgcolor: '#6d28d9' }, ml: 'auto',
|
||||
}}
|
||||
>
|
||||
Zur Einheit →
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Disclaimer footnote */}
|
||||
|
||||
Reference in New Issue
Block a user