feat: anonymous pre-market release per unit
Add anonymous flag to schattenmarktRelease on PropertyUnit and Property. When a unit is enabled in the pre-market panel, an Anonym toggle appears inline — hides address and property name in the matching feed, showing only area, type and region to prospective tenants. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,19 +9,20 @@ import { useToastStore } from '../../stores/toastStore'
|
||||
import { DS_BORDER, DS_PRE_MARKET, DS_SURFACE, DS_TEXT } from '../../lib/ds'
|
||||
import { SectionTitle } from './PropertyDetailHelpers'
|
||||
import { PreMarketDemandIntelligence } from './PreMarketDemandIntelligence'
|
||||
import { PreMarketUnitGrid } from './PreMarketUnitGrid'
|
||||
import { PreMarketUnitGrid, type UnitReleaseState } from './PreMarketUnitGrid'
|
||||
|
||||
export const MOCK_TODAY = new Date('2026-05-20')
|
||||
|
||||
export function PreMarketPanel({ p }: { p: Property }) {
|
||||
const [enabled, setEnabled] = useState(p.schattenmarktRelease?.enabled ?? false)
|
||||
const [leadTimeMonths, setLeadTimeMonths] = useState(p.schattenmarktRelease?.leadTimeMonths ?? 6)
|
||||
const [unitStates, setUnitStates] = useState<Record<string, { enabled: boolean; availableFrom: string }>>(() => {
|
||||
const init: Record<string, { enabled: boolean; availableFrom: string }> = {}
|
||||
const [unitStates, setUnitStates] = useState<Record<string, UnitReleaseState>>(() => {
|
||||
const init: Record<string, UnitReleaseState> = {}
|
||||
for (const u of p.units ?? []) {
|
||||
init[u.id] = {
|
||||
enabled: u.schattenmarktRelease?.enabled ?? false,
|
||||
availableFrom: u.schattenmarktRelease?.availableFrom ?? u.leaseEndDate ?? '',
|
||||
anonymous: u.schattenmarktRelease?.anonymous ?? false,
|
||||
}
|
||||
}
|
||||
return init
|
||||
@@ -80,11 +81,11 @@ export function PreMarketPanel({ p }: { p: Property }) {
|
||||
if (enabled) save(enabled, months)
|
||||
}
|
||||
|
||||
async function saveUnit(unitId: string, nextEnabled: boolean, nextDate: string) {
|
||||
async function saveUnit(unitId: string, nextEnabled: boolean, nextDate: string, nextAnonymous: boolean) {
|
||||
setUnitSaving(prev => ({ ...prev, [unitId]: true }))
|
||||
try {
|
||||
await MockupUnitProvider.update(unitId, {
|
||||
schattenmarktRelease: { enabled: nextEnabled, availableFrom: nextDate || undefined },
|
||||
schattenmarktRelease: { enabled: nextEnabled, availableFrom: nextDate || undefined, anonymous: nextAnonymous },
|
||||
})
|
||||
await queryClient.invalidateQueries({ queryKey: ['property', p.id] })
|
||||
await queryClient.invalidateQueries({ queryKey: ['properties'] })
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { Box, CircularProgress, Switch, TextField, Typography } from '@mui/material'
|
||||
import { Box, CircularProgress, Switch, TextField, Tooltip, Typography } from '@mui/material'
|
||||
import { EyeOff } from 'lucide-react'
|
||||
import type { Property } from '../../domain/property'
|
||||
import { DS_PRE_MARKET, DS_TEXT } from '../../lib/ds'
|
||||
import { floorLabel } from './PropertyDetailHelpers'
|
||||
|
||||
type PropertyUnit = NonNullable<Property['units']>[number]
|
||||
|
||||
export interface UnitReleaseState {
|
||||
enabled: boolean
|
||||
availableFrom: string
|
||||
anonymous: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
units: PropertyUnit[]
|
||||
unitStates: Record<string, { enabled: boolean; availableFrom: string }>
|
||||
unitStates: Record<string, UnitReleaseState>
|
||||
unitSaving: Record<string, boolean>
|
||||
setUnitStates: React.Dispatch<React.SetStateAction<Record<string, { enabled: boolean; availableFrom: string }>>>
|
||||
saveUnit: (unitId: string, enabled: boolean, availableFrom: string) => Promise<void>
|
||||
setUnitStates: React.Dispatch<React.SetStateAction<Record<string, UnitReleaseState>>>
|
||||
saveUnit: (unitId: string, enabled: boolean, availableFrom: string, anonymous: boolean) => Promise<void>
|
||||
}
|
||||
|
||||
export function PreMarketUnitGrid({ units, unitStates, unitSaving, setUnitStates, saveUnit }: Props) {
|
||||
@@ -20,40 +27,86 @@ export function PreMarketUnitGrid({ units, unitStates, unitSaving, setUnitStates
|
||||
Einheiten freigeben
|
||||
</Typography>
|
||||
{units.map(u => {
|
||||
const us = unitStates[u.id] ?? { enabled: false, availableFrom: '' }
|
||||
const us = unitStates[u.id] ?? { enabled: false, availableFrom: '', anonymous: false }
|
||||
return (
|
||||
<Box
|
||||
key={u.id}
|
||||
sx={{
|
||||
display: 'grid', gridTemplateColumns: '1fr 140px auto',
|
||||
gap: 1, alignItems: 'center', py: 0.75,
|
||||
gap: 1, alignItems: 'start', py: 0.875,
|
||||
borderBottom: '1px solid #f3e8ff',
|
||||
'&:last-child': { borderBottom: 'none' },
|
||||
}}
|
||||
>
|
||||
{/* Unit info + anonym toggle (shown when enabled) */}
|
||||
<Box>
|
||||
<Typography sx={{ fontSize: '0.72rem', fontWeight: 600, color: DS_TEXT.primary }}>
|
||||
{floorLabel(u)}{u.unitLabel ? ` · ${u.unitLabel}` : ''}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.65rem', color: DS_TEXT.muted }}>
|
||||
{u.areaSqm.toLocaleString('de-CH')} m²
|
||||
{u.currentTenant ? ` · ${u.currentTenant}` : ''}
|
||||
</Typography>
|
||||
{us.enabled && (
|
||||
<Tooltip title="Adresse und Objektname werden im Matching-Feed ausgeblendet — Interessenten sehen nur Fläche, Typ und Standortregion">
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 0.5, mt: 0.625,
|
||||
px: 0.75, py: 0.25, borderRadius: 1,
|
||||
border: '1px solid',
|
||||
borderColor: us.anonymous ? '#ddd6fe' : '#e2e8f0',
|
||||
bgcolor: us.anonymous ? '#f5f3ff' : 'transparent',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.15s, border-color 0.15s',
|
||||
}}
|
||||
onClick={() => {
|
||||
const next = { ...us, anonymous: !us.anonymous }
|
||||
setUnitStates(prev => ({ ...prev, [u.id]: next }))
|
||||
saveUnit(u.id, us.enabled, us.availableFrom, !us.anonymous)
|
||||
}}
|
||||
>
|
||||
<EyeOff size={11} color={us.anonymous ? DS_PRE_MARKET.accent : '#94a3b8'} />
|
||||
<Typography sx={{ fontSize: '0.62rem', fontWeight: us.anonymous ? 700 : 400, color: us.anonymous ? DS_PRE_MARKET.accent : '#94a3b8', lineHeight: 1 }}>
|
||||
Anonym
|
||||
</Typography>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={us.anonymous}
|
||||
onChange={(_, checked) => {
|
||||
const next = { ...us, anonymous: checked }
|
||||
setUnitStates(prev => ({ ...prev, [u.id]: next }))
|
||||
saveUnit(u.id, us.enabled, us.availableFrom, checked)
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
sx={{
|
||||
width: 28, height: 16, p: 0,
|
||||
'& .MuiSwitch-switchBase': { p: '2px', '&.Mui-checked': { transform: 'translateX(12px)', color: '#fff' } },
|
||||
'& .MuiSwitch-thumb': { width: 12, height: 12 },
|
||||
'& .MuiSwitch-track': { borderRadius: 8, bgcolor: '#cbd5e1' },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: '#8b5cf6' },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Available-from date */}
|
||||
<TextField
|
||||
type="date"
|
||||
size="small"
|
||||
value={us.availableFrom}
|
||||
disabled={!us.enabled}
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
sx={{ '& .MuiInputBase-input': { fontSize: '0.72rem', py: 0.5 } }}
|
||||
sx={{ '& .MuiInputBase-input': { fontSize: '0.72rem', py: 0.5 }, mt: 0.125 }}
|
||||
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)
|
||||
if (us.enabled) saveUnit(u.id, true, e.target.value, us.anonymous)
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
|
||||
{/* Enabled toggle */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.25 }}>
|
||||
{unitSaving[u.id] && <CircularProgress size={10} sx={{ color: DS_PRE_MARKET.accent }} />}
|
||||
<Switch
|
||||
size="small"
|
||||
@@ -61,7 +114,7 @@ export function PreMarketUnitGrid({ units, unitStates, unitSaving, setUnitStates
|
||||
onChange={(_, checked) => {
|
||||
const next = { ...us, enabled: checked }
|
||||
setUnitStates(prev => ({ ...prev, [u.id]: next }))
|
||||
saveUnit(u.id, checked, us.availableFrom)
|
||||
saveUnit(u.id, checked, us.availableFrom, us.anonymous)
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: DS_PRE_MARKET.accent },
|
||||
|
||||
@@ -163,7 +163,7 @@ export interface Property {
|
||||
importedAt?: string
|
||||
lastUpdatedAt?: string
|
||||
|
||||
schattenmarktRelease?: { enabled: boolean; leadTimeMonths?: number }
|
||||
schattenmarktRelease?: { enabled: boolean; leadTimeMonths?: number; anonymous?: boolean }
|
||||
|
||||
status?: 'ACTIVE' | 'INACTIVE' | 'DRAFT' | 'ARCHIVED'
|
||||
lastReviewedAt?: string
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface PropertyUnit {
|
||||
schattenmarktRelease?: {
|
||||
enabled: boolean
|
||||
availableFrom?: string // ISO date
|
||||
anonymous?: boolean // hide address/name in pre-market feed
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user