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:
Benjamin Sutter
2026-05-20 11:52:24 +02:00
parent 0663757cde
commit 9f391d17cb
36 changed files with 3011 additions and 71 deletions
+142
View File
@@ -0,0 +1,142 @@
import { Box, Typography, Chip, IconButton, Tooltip } from '@mui/material'
import { Check, Bell, X } from 'lucide-react'
import type { Reminder } from '../../domain/reminder'
import { ReminderPriorityBadge } from './ReminderPriorityBadge'
import { ReminderTypeBadge } from './ReminderTypeBadge'
import { ReminderDaysIndicator } from './ReminderDaysIndicator'
import { useCompleteReminder, useDismissReminder, useSnoozeReminder } from '../../hooks/useReminders'
import { useReminderStore } from '../../stores/reminderStore'
import { ReminderStatus } from '../../domain/reminder'
const STATUS_CHIP_COLOR: Record<string, 'default' | 'success' | 'warning' | 'error' | 'info'> = {
ACTIVE: 'info',
SNOOZED: 'warning',
COMPLETED: 'success',
DISMISSED: 'default',
}
const STATUS_LABEL: Record<string, string> = {
ACTIVE: 'Aktiv',
SNOOZED: 'Schlummernd',
COMPLETED: 'Erledigt',
DISMISSED: 'Verworfen',
}
interface Props {
reminder: Reminder
}
export function ReminderListRow({ reminder }: Props) {
const { setSelectedId, setDrawerOpen } = useReminderStore()
const complete = useCompleteReminder()
const dismiss = useDismissReminder()
const snooze = useSnoozeReminder()
const isActionable = reminder.status === ReminderStatus.ACTIVE || reminder.status === ReminderStatus.SNOOZED
function handleRowClick() {
setSelectedId(reminder.id)
setDrawerOpen(true)
}
function handleComplete(e: React.MouseEvent) {
e.stopPropagation()
complete.mutate({ id: reminder.id })
}
function handleDismiss(e: React.MouseEvent) {
e.stopPropagation()
dismiss.mutate({ id: reminder.id })
}
function handleSnooze(e: React.MouseEvent) {
e.stopPropagation()
// Snooze 7 days from mock today
snooze.mutate({ id: reminder.id, until: '2026-05-27' })
}
return (
<Box
onClick={handleRowClick}
sx={{
display: 'grid',
gridTemplateColumns: '100px 130px 1fr 140px 90px 100px 90px',
alignItems: 'center',
gap: 1,
px: 2,
py: 1.25,
borderBottom: '1px solid #f1f5f9',
bgcolor: 'white',
cursor: 'pointer',
'&:hover': { bgcolor: '#f8fafc' },
transition: 'background-color 0.1s',
}}
>
{/* Priority badge */}
<Box>
<ReminderPriorityBadge priority={reminder.priority} />
</Box>
{/* Type badge */}
<Box>
<ReminderTypeBadge type={reminder.type} />
</Box>
{/* Property + tenant */}
<Box sx={{ minWidth: 0 }}>
<Typography variant="body2" sx={{ fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{reminder.propertyTitle}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{reminder.propertyCity} · {reminder.tenantName}
</Typography>
</Box>
{/* Due date */}
<Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', fontSize: '0.7rem' }}>
{new Date(reminder.dueDate).toLocaleDateString('de-CH')}
</Typography>
<ReminderDaysIndicator dueDate={reminder.dueDate} />
</Box>
{/* Area */}
<Typography variant="caption" color="text.secondary" sx={{ whiteSpace: 'nowrap' }}>
{reminder.areaSqm.toLocaleString('de-CH')} m²
</Typography>
{/* Status chip */}
<Box>
<Chip
label={STATUS_LABEL[reminder.status]}
color={STATUS_CHIP_COLOR[reminder.status]}
size="small"
sx={{ height: 20, fontSize: '0.7rem' }}
/>
</Box>
{/* Actions */}
<Box sx={{ display: 'flex', gap: 0.25 }} onClick={e => e.stopPropagation()}>
{isActionable && (
<>
<Tooltip title="Erledigen">
<IconButton size="small" onClick={handleComplete} sx={{ color: '#16a34a' }}>
<Check size={14} />
</IconButton>
</Tooltip>
<Tooltip title="7 Tage schlummern">
<IconButton size="small" onClick={handleSnooze} sx={{ color: '#ca8a04' }}>
<Bell size={14} />
</IconButton>
</Tooltip>
<Tooltip title="Verwerfen">
<IconButton size="small" onClick={handleDismiss} sx={{ color: '#dc2626' }}>
<X size={14} />
</IconButton>
</Tooltip>
</>
)}
</Box>
</Box>
)
}