feat: Pre-Market Intelligence UX rework — institutional positioning

Redesign the Future Availability card system to clearly distinguish two classes:

PRE-MARKET VERIFIED (Verwaltung-controlled):
- Soft purple (#7c3aed) accent, institutional premium feel
- Badge: "PRE-MARKET VERIFIED" with shield icon
- "Direkte Verwaltungsquelle" attribution chip in header
- Shows confirmed ERP facts with green checkmarks

MARKET SIGNAL (crawler/probabilistic):
- Slate blue (#1d4ed8) accent, analytical feel
- Badge: "MARKET SIGNAL"
- Amber notice box: "Probabilistischer Marktindikator — kein bestätigtes Objekt"
- Shows market indicators and explicit probability %
- Source attribution (press, job posting, etc.)

Both cards answer: What? How reliable? Where from? Why relevant? Risks? Action?

PropertyDetailView: rename SchattenmarktReleasePanel → PreMarketPanel, update
toggle label to "Pre-Market Matching aktivieren", add Matching Demand Intelligence
block showing active search profile counts and early matching opportunity.

FutureAvailabilityContextPanel: update LEASE_EXPIRY signal type label and
relevance bridge text to remove "Schattenmarkt" references.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-20 17:50:37 +02:00
parent 67c0701bfe
commit 8f7372157c
3 changed files with 127 additions and 54 deletions
+52 -14
View File
@@ -19,7 +19,7 @@ import {
Typography,
} from '@mui/material'
import { useQueryClient } from '@tanstack/react-query'
import { ChevronDown, ChevronUp, Clock, Edit2, Layers, Save, ShieldCheck, Users, X, Zap } from 'lucide-react'
import { ChevronDown, ChevronUp, Clock, Edit2, Layers, Save, ShieldCheck, Target, TrendingUp, Users, X, Zap } from 'lucide-react'
import { useOfferWizardStore } from '../../stores/offerWizardStore'
import { PropertyMap } from '../shared'
import { NeedMatchCard } from './NeedMatchCard'
@@ -309,11 +309,11 @@ function UnitStructurePanel({ p }: { p: Property }) {
)
}
// ── Pre-Market release panel ─────────────────────────────────────────────────
// ── Pre-Market Matching panel ─────────────────────────────────────────────────
const MOCK_TODAY = new Date('2026-05-20')
function SchattenmarktReleasePanel({ p }: { p: Property }) {
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)
@@ -340,13 +340,18 @@ function SchattenmarktReleasePanel({ p }: { p: Property }) {
? Math.max(0, Math.round((targetDate.getTime() - MOCK_TODAY.getTime()) / (1000 * 60 * 60 * 24 * 30)))
: null
// Mock demand intelligence (derived deterministically from property characteristics)
const demandProfiles = Math.min(14, (p.areaSqm >= 1000 ? 5 : p.areaSqm >= 500 ? 8 : 4) +
(['Zürich', 'Basel', 'Bern', 'Zug'].some(c => (p.location?.city ?? '').includes(c)) ? 4 : 1))
const highQualityLeads = Math.max(1, Math.floor(demandProfiles * 0.38))
async function save(nextEnabled: boolean, nextLeadTime: number) {
setSaving(true)
try {
await propertyService.update(p.id, { schattenmarktRelease: { enabled: nextEnabled, leadTimeMonths: nextLeadTime } })
await queryClient.invalidateQueries({ queryKey: ['property', p.id] })
await queryClient.invalidateQueries({ queryKey: ['properties'] })
showToast(nextEnabled ? 'Pre-Market Freigabe aktiviert.' : 'Pre-Market Freigabe deaktiviert.', 'success')
showToast(nextEnabled ? 'Pre-Market Matching aktiviert.' : 'Pre-Market Matching deaktiviert.', 'success')
} catch {
showToast('Fehler beim Speichern.', 'error')
} finally {
@@ -367,7 +372,7 @@ function SchattenmarktReleasePanel({ p }: { p: Property }) {
return (
<>
<Divider sx={{ my: 2 }} />
<SectionTitle title="Pre-Market Freigabe" />
<SectionTitle title="Pre-Market Matching" />
<Box
sx={{
border: '1px solid',
@@ -378,15 +383,16 @@ function SchattenmarktReleasePanel({ p }: { p: Property }) {
transition: 'background 0.2s, border-color 0.2s',
}}
>
{/* Toggle row */}
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
<Zap size={15} color={enabled ? '#7c3aed' : '#94a3b8'} style={{ marginTop: 2 }} />
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, lineHeight: 1.3 }}>
Pre-Market freigeben
Pre-Market Matching aktivieren
</Typography>
<Typography variant="caption" color="text.secondary">
Fläche vor offizieller Vermarktung im Markt sichtbar machen
Kontrollierte Freigabe für qualifizierte Suchanfragen vor offizieller Insertion
</Typography>
</Box>
</Box>
@@ -404,8 +410,10 @@ function SchattenmarktReleasePanel({ p }: { p: Property }) {
</Box>
</Box>
{/* Active: lead time + status + demand intelligence */}
{enabled && (
<Box sx={{ mt: 1.5 }}>
{/* Lead time selector */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.25 }}>
<Typography variant="caption" sx={{ color: '#374151', fontWeight: 500, minWidth: 72 }}>
Lead Time
@@ -428,6 +436,7 @@ function SchattenmarktReleasePanel({ p }: { p: Property }) {
</Box>
</Box>
{/* Activation status */}
<Box
sx={{
display: 'flex', alignItems: 'flex-start', gap: 0.75, p: 1,
@@ -442,20 +451,49 @@ function SchattenmarktReleasePanel({ p }: { p: Property }) {
}
<Typography variant="caption" sx={{ color: isActive ? '#166534' : '#92400e', fontWeight: 500, lineHeight: 1.4 }}>
{isActive
? `Signal aktiv seit ${triggerDate?.toLocaleDateString('de-CH')}Objekt im Nachfrage-Feed sichtbar`
? `PRE-MARKET VERIFIED aktiv seit ${triggerDate?.toLocaleDateString('de-CH')}Fläche im Matching-Feed sichtbar`
: triggerDate
? `Signal aktiv ab ${triggerDate.toLocaleDateString('de-CH')} — noch ${monthsUntil} Monate bis Vertragsende`
? `Freigabe startet ${triggerDate.toLocaleDateString('de-CH')} — noch ${monthsUntil} Monate bis Vertragsende`
: 'Kein Vertragsende hinterlegt'
}
</Typography>
</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 }}>
Matching Demand Intelligence
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
<Users size={11} color="#7c3aed" style={{ flexShrink: 0 }} />
<Typography variant="caption" sx={{ color: '#374151', lineHeight: 1.3 }}>
<strong>{demandProfiles} aktive Suchprofile</strong> im System erkannt
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
<Target size={11} color="#7c3aed" style={{ flexShrink: 0 }} />
<Typography variant="caption" sx={{ color: '#374151', lineHeight: 1.3 }}>
<strong>{highQualityLeads} hochwertige Suchanfragen</strong> mit passendem Flächenbedarf
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
<TrendingUp size={11} color="#7c3aed" style={{ flexShrink: 0 }} />
<Typography variant="caption" sx={{ color: '#374151', lineHeight: 1.3 }}>
Frühzeitige Matchgelegenheit vor offizieller Vermarktung exklusiv verfügbar
</Typography>
</Box>
</Box>
</Box>
</Box>
)}
{/* Inactive: explain value proposition */}
{!enabled && (
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
Wenn aktiviert, erscheint dieses Objekt {leadTimeMonths} Monate vor Vertragsende als
verifiziertes Pre-Market Signal im Nachfrage-Feed.
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1, lineHeight: 1.5 }}>
Wenn aktiviert, erscheint diese Fläche {leadTimeMonths} Monate vor Vertragsende als
verifiziertes <strong>PRE-MARKET VERIFIED</strong> Signal für qualifizierte Suchanfragen
kein öffentliches Inserat, kontrolliertes Early Matching.
</Typography>
)}
</Box>
@@ -573,8 +611,8 @@ function OverviewPanel({ p, editing, draft, onDraftChange }: OverviewPanelProps)
{/* Floor / unit structure */}
<UnitStructurePanel p={p} />
{/* Schattenmarkt release */}
<SchattenmarktReleasePanel p={p} />
{/* Pre-Market Matching */}
<PreMarketPanel p={p} />
<Divider sx={{ my: 2 }} />