Files
property-match/src/components/supply/ReminderKpiBar.tsx
T
Benjamin Sutter 6aa4f96bd2 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>
2026-06-16 14:30:06 +02:00

171 lines
5.4 KiB
TypeScript

import { Box, Typography, Skeleton } from '@mui/material'
import { AlertOctagon, Calendar, CalendarDays, Eye } from 'lucide-react'
import { useShallow } from 'zustand/react/shallow'
import { useReminderInsights } from '../../hooks/useReminders'
import { useReminderStore } from '../../stores/reminderStore'
import type { FilterHorizon } from '../../stores/reminderStore'
import { ReminderType } from '../../domain/reminder'
interface SecondaryKpiProps {
icon: React.ReactNode
label: string
value: number | string
color: string
active: boolean
onClick: () => void
}
function SecondaryKpi({ icon, label, value, color, active, onClick }: SecondaryKpiProps) {
return (
<Box
onClick={onClick}
sx={{
flex: 1,
px: 2,
py: 1.5,
display: 'flex',
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',
'&:hover': { bgcolor: `${color}06`, borderColor: color },
}}
>
<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>
</Box>
<Typography variant="caption" sx={{ color: '#94a3b8', fontSize: '0.7rem', whiteSpace: 'nowrap' }}>
{label}
</Typography>
</Box>
)
}
export function ReminderKpiBar() {
const { data, isLoading } = useReminderInsights()
const { filterHorizon, setFilterHorizon, filterType, setFilterType } = useReminderStore(
useShallow(s => ({
filterHorizon: s.filterHorizon,
setFilterHorizon: s.setFilterHorizon,
filterType: s.filterType,
setFilterType: s.setFilterType,
}))
)
function toggleHorizon(h: FilterHorizon) {
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') }
}
if (isLoading) {
return (
<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 sx={{ display: 'flex', gap: 2, alignItems: 'stretch' }}>
{/* Focal card — Überfällig */}
<Box
onClick={() => toggleHorizon('OVERDUE')}
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>
)
}