6aa4f96bd2
- 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>
171 lines
5.4 KiB
TypeScript
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>
|
|
)
|
|
}
|