feat: responsive layout + Reminder Manager redesign

- Auto-collapse sidebar at <1536px (all laptops), expand at ≥1536px
- Responsive drawer/panel widths across Pipeline, Properties, MyListings, Anfragen, MatchDetail, AISearch
- Reminder Manager: dot+label priority badge, Fläche column removed, status only for non-active rows
- ReminderKpiBar: focal Überfällig card, three secondary KPIs
- ReminderDetailDrawer: Task Panel redesign — Finanzen removed, Notiz promoted, Pre-Market as inline badge, full create form
- useCreateReminder hook wired to reminderService.create with cache invalidation
- Mock data: contract-derived reminders (LEASE_EXPIRY, BREAK_OPTION, RENT_REVIEW, INSURANCE_RENEWAL, SCHATTENMARKT_RELEASE) now show auto-creation note in Verlauf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-06-16 14:30:06 +02:00
parent 5259579f85
commit 6aa4f96bd2
17 changed files with 463 additions and 273 deletions
+106 -64
View File
@@ -6,7 +6,7 @@ import { useReminderStore } from '../../stores/reminderStore'
import type { FilterHorizon } from '../../stores/reminderStore'
import { ReminderType } from '../../domain/reminder'
interface KpiCardProps {
interface SecondaryKpiProps {
icon: React.ReactNode
label: string
value: number | string
@@ -15,35 +15,34 @@ interface KpiCardProps {
onClick: () => void
}
function KpiCard({ icon, label, value, color, active, onClick }: KpiCardProps) {
function SecondaryKpi({ icon, label, value, color, active, onClick }: SecondaryKpiProps) {
return (
<Box
onClick={onClick}
sx={{
flex: 1,
px: 1.5,
py: 1.125,
px: 2,
py: 1.5,
display: 'flex',
alignItems: 'center',
gap: 1,
border: active ? `1.5px solid ${color}` : '1px solid #e8e7e4',
borderRadius: 1.5,
bgcolor: active ? `${color}08` : 'white',
flexDirection: 'column',
gap: 0.25,
cursor: 'pointer',
borderRadius: 1.5,
border: active ? `1.5px solid ${color}` : '1px solid #e8e7e4',
bgcolor: active ? `${color}08` : 'white',
transition: 'border-color 0.15s, background 0.15s',
minWidth: 0,
'&:hover': { bgcolor: `${color}06`, borderColor: color },
}}
>
<Box sx={{ opacity: active ? 1 : 0.55, flexShrink: 0 }}>{icon}</Box>
<Box sx={{ minWidth: 0 }}>
<Typography sx={{ fontWeight: 700, color, lineHeight: 1.1, fontSize: '1.1rem' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
<Box sx={{ opacity: active ? 1 : 0.5, flexShrink: 0 }}>{icon}</Box>
<Typography sx={{ fontWeight: 700, color, fontSize: '1.25rem', lineHeight: 1 }}>
{value}
</Typography>
<Typography variant="caption" sx={{ color: '#64748b', whiteSpace: 'nowrap', fontSize: '0.68rem' }}>
{label}
</Typography>
</Box>
<Typography variant="caption" sx={{ color: '#94a3b8', fontSize: '0.7rem', whiteSpace: 'nowrap' }}>
{label}
</Typography>
</Box>
)
}
@@ -60,69 +59,112 @@ export function ReminderKpiBar() {
)
function toggleHorizon(h: FilterHorizon) {
if (filterHorizon === h) {
setFilterHorizon('ALL')
} else {
setFilterHorizon(h)
setFilterType('ALL') // clear type filter so count matches
}
if (filterHorizon === h) setFilterHorizon('ALL')
else { setFilterHorizon(h); setFilterType('ALL') }
}
function togglePreMarket() {
if (filterType === ReminderType.SCHATTENMARKT_RELEASE) {
setFilterType('ALL')
} else {
setFilterType(ReminderType.SCHATTENMARKT_RELEASE)
setFilterHorizon('ALL') // clear horizon filter so count matches
}
if (filterType === ReminderType.SCHATTENMARKT_RELEASE) setFilterType('ALL')
else { setFilterType(ReminderType.SCHATTENMARKT_RELEASE); setFilterHorizon('ALL') }
}
if (isLoading) {
return (
<Box className="flex gap-3">
{[1, 2, 3, 4].map(i => (
<Skeleton key={i} variant="rounded" height={68} sx={{ flex: 1 }} />
))}
<Box sx={{ display: 'flex', gap: 2 }}>
<Skeleton variant="rounded" height={76} sx={{ flex: '0 0 220px' }} />
<Box sx={{ flex: 1, display: 'flex', gap: 2 }}>
{[1, 2, 3].map(i => <Skeleton key={i} variant="rounded" height={76} sx={{ flex: 1 }} />)}
</Box>
</Box>
)
}
const ins = data ?? { overdueCount: 0, dueThisWeek: 0, dueThisMonth: 0, schattenmarktReadyCount: 0 }
const isOverdue = ins.overdueCount > 0
const overdueActive = filterHorizon === 'OVERDUE'
return (
<Box className="flex gap-3">
<KpiCard
icon={<AlertOctagon size={18} color="#dc2626" />}
label="Überfällig"
value={ins.overdueCount}
color="#dc2626"
active={filterHorizon === 'OVERDUE'}
<Box sx={{ display: 'flex', gap: 2, alignItems: 'stretch' }}>
{/* Focal card — Überfällig */}
<Box
onClick={() => toggleHorizon('OVERDUE')}
/>
<KpiCard
icon={<Calendar size={18} color="#ea580c" />}
label="Diese Woche"
value={ins.dueThisWeek}
color="#ea580c"
active={filterHorizon === 'THIS_WEEK'}
onClick={() => toggleHorizon('THIS_WEEK')}
/>
<KpiCard
icon={<CalendarDays size={18} color="#0369a1" />}
label="Dieser Monat"
value={ins.dueThisMonth}
color="#0369a1"
active={filterHorizon === 'THIS_MONTH'}
onClick={() => toggleHorizon('THIS_MONTH')}
/>
<KpiCard
icon={<Eye size={18} color="#be185d" />}
label="Pre-Market Risiko"
value={ins.schattenmarktReadyCount}
color="#be185d"
active={filterType === ReminderType.SCHATTENMARKT_RELEASE}
onClick={togglePreMarket}
/>
sx={{
flex: '0 0 200px',
px: 2.5,
py: 2,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
gap: 0.5,
borderRadius: 1.5,
cursor: 'pointer',
border: overdueActive
? '1.5px solid #dc2626'
: isOverdue
? '1.5px solid #fca5a5'
: '1px solid #e8e7e4',
bgcolor: overdueActive
? '#fef2f2'
: isOverdue
? '#fff5f5'
: 'white',
transition: 'border-color 0.15s, background 0.15s',
'&:hover': { borderColor: '#dc2626', bgcolor: '#fef2f2' },
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<AlertOctagon size={16} color={isOverdue ? '#dc2626' : '#cbd5e1'} />
<Typography
sx={{
fontWeight: 800,
fontSize: '2rem',
lineHeight: 1,
color: isOverdue ? '#dc2626' : '#94a3b8',
letterSpacing: '-0.02em',
}}
>
{ins.overdueCount}
</Typography>
</Box>
<Typography
variant="caption"
sx={{
color: isOverdue ? '#dc2626' : '#94a3b8',
fontWeight: isOverdue ? 600 : 400,
fontSize: '0.75rem',
}}
>
Überfällig
</Typography>
</Box>
{/* Secondary KPIs */}
<Box sx={{ flex: 1, display: 'flex', gap: 2 }}>
<SecondaryKpi
icon={<Calendar size={14} color="#ea580c" />}
label="Diese Woche"
value={ins.dueThisWeek}
color="#ea580c"
active={filterHorizon === 'THIS_WEEK'}
onClick={() => toggleHorizon('THIS_WEEK')}
/>
<SecondaryKpi
icon={<CalendarDays size={14} color="#0369a1" />}
label="Dieser Monat"
value={ins.dueThisMonth}
color="#0369a1"
active={filterHorizon === 'THIS_MONTH'}
onClick={() => toggleHorizon('THIS_MONTH')}
/>
<SecondaryKpi
icon={<Eye size={14} color="#be185d" />}
label="Pre-Market Risiko"
value={ins.schattenmarktReadyCount}
color="#be185d"
active={filterType === ReminderType.SCHATTENMARKT_RELEASE}
onClick={togglePreMarket}
/>
</Box>
</Box>
)
}