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:
@@ -25,6 +25,7 @@ import { PropertyMap } from '../shared'
|
||||
import { NeedMatchCard } from './NeedMatchCard'
|
||||
import { PropertyMarketSignalsTab } from './PropertyMarketSignalsTab'
|
||||
import type { Property, PropertyUnit, UnitNeedMatch, UpdatePropertyInput } from '../../domain/property'
|
||||
import { MockupUnitProvider } from '../../provider/MockupUnitProvider'
|
||||
import { unitMatchService } from '../../services/unitMatchService'
|
||||
import type { PropertyNeedMatch } from '../../domain/match'
|
||||
import { usePropertyById } from '../../hooks/useProperties'
|
||||
@@ -317,6 +318,17 @@ function PreMarketPanel({ p }: { p: Property }) {
|
||||
const [enabled, setEnabled] = useState(p.schattenmarktRelease?.enabled ?? false)
|
||||
const [leadTimeMonths, setLeadTimeMonths] = useState(p.schattenmarktRelease?.leadTimeMonths ?? 6)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [unitStates, setUnitStates] = useState<Record<string, { enabled: boolean; availableFrom: string }>>(() => {
|
||||
const init: Record<string, { enabled: boolean; availableFrom: string }> = {}
|
||||
for (const u of p.units ?? []) {
|
||||
init[u.id] = {
|
||||
enabled: u.schattenmarktRelease?.enabled ?? false,
|
||||
availableFrom: u.schattenmarktRelease?.availableFrom ?? u.leaseEndDate ?? '',
|
||||
}
|
||||
}
|
||||
return init
|
||||
})
|
||||
const [unitSaving, setUnitSaving] = useState<Record<string, boolean>>({})
|
||||
const queryClient = useQueryClient()
|
||||
const showToast = useToastStore(s => s.showToast)
|
||||
|
||||
@@ -369,6 +381,21 @@ function PreMarketPanel({ p }: { p: Property }) {
|
||||
if (enabled) save(enabled, months)
|
||||
}
|
||||
|
||||
async function saveUnit(unitId: string, nextEnabled: boolean, nextDate: string) {
|
||||
setUnitSaving(prev => ({ ...prev, [unitId]: true }))
|
||||
try {
|
||||
await MockupUnitProvider.update(unitId, {
|
||||
schattenmarktRelease: { enabled: nextEnabled, availableFrom: nextDate || undefined },
|
||||
})
|
||||
await queryClient.invalidateQueries({ queryKey: ['property', p.id] })
|
||||
await queryClient.invalidateQueries({ queryKey: ['properties'] })
|
||||
} catch {
|
||||
showToast('Fehler beim Speichern der Einheit.', 'error')
|
||||
} finally {
|
||||
setUnitSaving(prev => ({ ...prev, [unitId]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
@@ -459,6 +486,68 @@ function PreMarketPanel({ p }: { p: Property }) {
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Unit-level release controls */}
|
||||
{(p.units?.length ?? 0) > 0 && (
|
||||
<Box sx={{ mt: 1.25, pt: 1.25, borderTop: '1px solid #e9d5ff' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: '#4c1d95', textTransform: 'uppercase', letterSpacing: 0.5, fontSize: '0.63rem', display: 'block', mb: 0.875 }}>
|
||||
Einheiten freigeben
|
||||
</Typography>
|
||||
{p.units!.map(u => {
|
||||
const us = unitStates[u.id] ?? { enabled: false, availableFrom: '' }
|
||||
return (
|
||||
<Box
|
||||
key={u.id}
|
||||
sx={{
|
||||
display: 'grid', gridTemplateColumns: '1fr 140px auto',
|
||||
gap: 1, alignItems: 'center', py: 0.75,
|
||||
borderBottom: '1px solid #f3e8ff',
|
||||
'&:last-child': { borderBottom: 'none' },
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography sx={{ fontSize: '0.72rem', fontWeight: 600, color: '#1e293b' }}>
|
||||
{floorLabel(u)}{u.unitLabel ? ` · ${u.unitLabel}` : ''}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.65rem', color: '#64748b' }}>
|
||||
{u.areaSqm.toLocaleString('de-CH')} m²
|
||||
{u.currentTenant ? ` · ${u.currentTenant}` : ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
type="date"
|
||||
size="small"
|
||||
value={us.availableFrom}
|
||||
disabled={!us.enabled}
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
sx={{ '& .MuiInputBase-input': { fontSize: '0.72rem', py: 0.5 } }}
|
||||
onChange={e => {
|
||||
const next = { ...us, availableFrom: e.target.value }
|
||||
setUnitStates(prev => ({ ...prev, [u.id]: next }))
|
||||
if (us.enabled) saveUnit(u.id, true, e.target.value)
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
{unitSaving[u.id] && <CircularProgress size={10} sx={{ color: '#7c3aed' }} />}
|
||||
<Switch
|
||||
size="small"
|
||||
checked={us.enabled}
|
||||
onChange={(_, checked) => {
|
||||
const next = { ...us, enabled: checked }
|
||||
setUnitStates(prev => ({ ...prev, [u.id]: next }))
|
||||
saveUnit(u.id, checked, us.availableFrom)
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: '#7c3aed' },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: '#8b5cf6' },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Demand Intelligence */}
|
||||
<Box sx={{ mt: 1.25, pt: 1.25, borderTop: '1px solid #e9d5ff' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: '#4c1d95', textTransform: 'uppercase', letterSpacing: 0.5, fontSize: '0.63rem', display: 'block', mb: 0.875 }}>
|
||||
|
||||
Reference in New Issue
Block a user