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:
@@ -0,0 +1,124 @@
|
||||
import { Card, CardContent, Box, Typography, Chip, IconButton, Tooltip, Divider } from '@mui/material'
|
||||
import { Check, Bell, X, MapPin, Calendar } 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 ReminderCard({ 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
|
||||
|
||||
return (
|
||||
<Card
|
||||
variant="outlined"
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
'&:hover': { boxShadow: 2 },
|
||||
transition: 'box-shadow 0.15s',
|
||||
}}
|
||||
onClick={() => { setSelectedId(reminder.id); setDrawerOpen(true) }}
|
||||
>
|
||||
<CardContent sx={{ p: 2, '&:last-child': { pb: 2 } }}>
|
||||
{/* Top row */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<ReminderTypeBadge type={reminder.type} />
|
||||
<ReminderPriorityBadge priority={reminder.priority} />
|
||||
</Box>
|
||||
<Chip
|
||||
label={STATUS_LABEL[reminder.status]}
|
||||
color={STATUS_CHIP_COLOR[reminder.status]}
|
||||
size="small"
|
||||
sx={{ height: 20, fontSize: '0.7rem' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Property */}
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.25 }}>
|
||||
{reminder.propertyTitle}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }}>
|
||||
<MapPin size={12} color="#94a3b8" />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{reminder.propertyCity}{reminder.propertyDistrict ? ` · ${reminder.propertyDistrict}` : ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1 }}>
|
||||
{reminder.tenantName} · {reminder.areaSqm.toLocaleString('de-CH')} m²
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: 1 }} />
|
||||
|
||||
{/* Due date */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 1 }}>
|
||||
<Calendar size={13} color="#64748b" />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Fällig: {new Date(reminder.dueDate).toLocaleDateString('de-CH')}
|
||||
</Typography>
|
||||
<Box sx={{ ml: 'auto' }}>
|
||||
<ReminderDaysIndicator dueDate={reminder.dueDate} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Actions */}
|
||||
{isActionable && (
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }} onClick={e => e.stopPropagation()}>
|
||||
<Tooltip title="Erledigen">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => complete.mutate({ id: reminder.id })}
|
||||
sx={{ color: '#16a34a', border: '1px solid #dcfce7', borderRadius: 1 }}
|
||||
>
|
||||
<Check size={14} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="7 Tage schlummern">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => snooze.mutate({ id: reminder.id, until: '2026-05-27' })}
|
||||
sx={{ color: '#ca8a04', border: '1px solid #fef9c3', borderRadius: 1 }}
|
||||
>
|
||||
<Bell size={14} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Verwerfen">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => dismiss.mutate({ id: reminder.id })}
|
||||
sx={{ color: '#dc2626', border: '1px solid #fee2e2', borderRadius: 1 }}
|
||||
>
|
||||
<X size={14} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user