feat: Reminder Manager UX — timeline-first grouped layout

Replace 3-row filter panel with a grouped feed (Überfällig / Diese Woche /
Dieser Monat / Später). KPI cards are now clickable shortcuts that filter to
their time horizon or type (Pre-Market). Filter bar condenses to one row:
search + type dropdown + archive toggle. Rows get a left priority-color
border (red/orange/yellow). Default status filter is ACTIVE+SNOOZED.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 22:42:37 +02:00
parent a9e038a64f
commit d804d1c923
6 changed files with 263 additions and 162 deletions
+116 -35
View File
@@ -1,5 +1,5 @@
import { useMemo, useCallback } from 'react' import { useMemo, useCallback } from 'react'
import { Box, Typography } from '@mui/material' import { Box, Chip, Typography } from '@mui/material'
import { useReminders } from '../../hooks/useReminders' import { useReminders } from '../../hooks/useReminders'
import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
import { useReminderStore } from '../../stores/reminderStore' import { useReminderStore } from '../../stores/reminderStore'
@@ -8,24 +8,47 @@ import { ReminderCard } from './ReminderCard'
import { ReminderSkeleton } from './ReminderSkeleton' import { ReminderSkeleton } from './ReminderSkeleton'
import { ReminderEmptyState } from './ReminderEmptyState' import { ReminderEmptyState } from './ReminderEmptyState'
import type { Reminder } from '../../domain/reminder' import type { Reminder } from '../../domain/reminder'
import { ReminderStatus } from '../../domain/reminder'
import type { FilterHorizon } from '../../stores/reminderStore'
const LIST_HEADER_COLS = '100px 130px 1fr 140px 90px 100px 90px' const MOCK_TODAY = new Date('2026-05-20')
function getHorizon(dueDate: string): FilterHorizon {
const days = Math.ceil((new Date(dueDate).getTime() - MOCK_TODAY.getTime()) / (1000 * 60 * 60 * 24))
if (days <= 0) return 'OVERDUE'
if (days <= 7) return 'THIS_WEEK'
if (days <= 30) return 'THIS_MONTH'
return 'LATER'
}
const HORIZON_CONFIG: { key: FilterHorizon; label: string; color: string; bg: string; border: string }[] = [
{ key: 'OVERDUE', label: 'Überfällig', color: '#dc2626', bg: '#fef2f2', border: '#fecaca' },
{ key: 'THIS_WEEK', label: 'Diese Woche', color: '#ea580c', bg: '#fff7ed', border: '#fed7aa' },
{ key: 'THIS_MONTH', label: 'Dieser Monat', color: '#0369a1', bg: '#f0f9ff', border: '#bae6fd' },
{ key: 'LATER', label: 'Später', color: '#64748b', bg: '#f8fafc', border: '#e2e8f0' },
]
const LIST_COLS = '100px 130px 1fr 140px 90px 100px 90px'
function applyFilters( function applyFilters(
reminders: Reminder[], reminders: Reminder[],
filterType: string, filterType: string,
filterStatus: string, filterStatus: string,
filterPriority: string, filterHorizon: string,
searchQuery: string, searchQuery: string,
): Reminder[] { ): Reminder[] {
return reminders.filter(r => { return reminders.filter(r => {
if (filterStatus === 'ACTIVE') {
if (r.status !== ReminderStatus.ACTIVE && r.status !== ReminderStatus.SNOOZED) return false
} else if (filterStatus !== 'ALL') {
if (r.status !== filterStatus) return false
}
if (filterType !== 'ALL' && r.type !== filterType) return false if (filterType !== 'ALL' && r.type !== filterType) return false
if (filterStatus !== 'ALL' && r.status !== filterStatus) return false if (filterHorizon !== 'ALL' && getHorizon(r.dueDate) !== filterHorizon) return false
if (filterPriority !== 'ALL' && r.priority !== filterPriority) return false
if (searchQuery) { if (searchQuery) {
const q = searchQuery.toLowerCase() const q = searchQuery.toLowerCase()
const haystack = `${r.propertyTitle} ${r.propertyCity} ${r.tenantName}`.toLowerCase() const hay = `${r.propertyTitle} ${r.propertyCity} ${r.tenantName}`.toLowerCase()
if (!haystack.includes(q)) return false if (!hay.includes(q)) return false
} }
return true return true
}) })
@@ -34,59 +57,83 @@ function applyFilters(
export function ReminderFeed() { export function ReminderFeed() {
const { data, isLoading } = useReminders() const { data, isLoading } = useReminders()
const { const {
filterType, filterStatus, filterPriority, searchQuery, viewMode, filterType, filterStatus, filterHorizon, searchQuery, viewMode,
setFilterType, setFilterStatus, setFilterPriority, setSearchQuery, setFilterType, setFilterStatus, setFilterHorizon, setSearchQuery,
} = useReminderStore(useShallow(s => ({ } = useReminderStore(useShallow(s => ({
filterType: s.filterType, filterStatus: s.filterStatus, filterType: s.filterType, filterStatus: s.filterStatus,
filterPriority: s.filterPriority, searchQuery: s.searchQuery, filterHorizon: s.filterHorizon, searchQuery: s.searchQuery,
viewMode: s.viewMode, setFilterType: s.setFilterType, viewMode: s.viewMode, setFilterType: s.setFilterType,
setFilterStatus: s.setFilterStatus, setFilterPriority: s.setFilterPriority, setFilterStatus: s.setFilterStatus, setFilterHorizon: s.setFilterHorizon,
setSearchQuery: s.setSearchQuery, setSearchQuery: s.setSearchQuery,
}))) })))
const reminders = data ?? [] const reminders = data ?? []
const filtered = useMemo( const filtered = useMemo(
() => applyFilters(reminders, filterType, filterStatus, filterPriority, searchQuery), () => applyFilters(reminders, filterType, filterStatus, filterHorizon, searchQuery),
[reminders, filterType, filterStatus, filterPriority, searchQuery], [reminders, filterType, filterStatus, filterHorizon, searchQuery],
) )
const grouped = useMemo(() => {
const map: Record<string, Reminder[]> = {
OVERDUE: [], THIS_WEEK: [], THIS_MONTH: [], LATER: [],
}
filtered.forEach(r => map[getHorizon(r.dueDate)].push(r))
return map
}, [filtered])
const resetFilters = useCallback(() => { const resetFilters = useCallback(() => {
setFilterType('ALL') setFilterType('ALL')
setFilterStatus('ALL') setFilterStatus('ACTIVE')
setFilterPriority('ALL') setFilterHorizon('ALL')
setSearchQuery('') setSearchQuery('')
}, [setFilterType, setFilterStatus, setFilterPriority, setSearchQuery]) }, [setFilterType, setFilterStatus, setFilterHorizon, setSearchQuery])
if (isLoading) return <ReminderSkeleton /> if (isLoading) return <ReminderSkeleton />
if (filtered.length === 0) return <ReminderEmptyState onReset={resetFilters} />
if (filtered.length === 0) {
return <ReminderEmptyState onReset={resetFilters} />
}
if (viewMode === 'card') { if (viewMode === 'card') {
return ( return (
<Box <Box className="flex flex-col gap-4">
sx={{ {HORIZON_CONFIG.map(({ key, label, color, bg, border }) => {
display: 'grid', const group = grouped[key]
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', if (!group || group.length === 0) return null
gap: 2, return (
}} <Box key={key}>
> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5, px: 0.5 }}>
{filtered.map(r => ( <Box sx={{ width: 3, height: 16, borderRadius: 1, bgcolor: color, flexShrink: 0 }} />
<ReminderCard key={r.id} reminder={r} /> <Typography variant="body2" sx={{ fontWeight: 700, color, fontSize: '0.8125rem' }}>
))} {label}
</Typography>
<Chip
label={group.length}
size="small"
sx={{ height: 18, fontSize: '0.65rem', bgcolor: color, color: 'white', ml: 0.25 }}
/>
</Box>
<Box
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
gap: 2,
}}
>
{group.map(r => <ReminderCard key={r.id} reminder={r} />)}
</Box>
</Box>
)
})}
</Box> </Box>
) )
} }
return ( return (
<Box sx={{ border: '1px solid #e2e8f0', borderRadius: 1, overflow: 'hidden' }}> <Box sx={{ border: '1px solid #e2e8f0', borderRadius: 1.5, overflow: 'hidden' }}>
{/* Table header */} {/* Table header */}
<Box <Box
sx={{ sx={{
display: 'grid', display: 'grid',
gridTemplateColumns: LIST_HEADER_COLS, gridTemplateColumns: LIST_COLS,
gap: 1, gap: 1,
px: 2, px: 2,
py: 1, py: 1,
@@ -101,9 +148,43 @@ export function ReminderFeed() {
))} ))}
</Box> </Box>
{filtered.map(r => ( {/* Grouped rows */}
<ReminderListRow key={r.id} reminder={r} /> {HORIZON_CONFIG.map(({ key, label, color, bg, border }) => {
))} const group = grouped[key]
if (!group || group.length === 0) return null
return (
<Box key={key}>
{/* Section divider */}
<Box
sx={{
px: 2,
py: 0.625,
bgcolor: bg,
borderTop: `1px solid ${border}`,
borderBottom: `1px solid ${border}`,
display: 'flex',
alignItems: 'center',
gap: 1,
}}
>
<Typography
variant="caption"
sx={{ fontWeight: 700, color, textTransform: 'uppercase', letterSpacing: 0.6, fontSize: '0.65rem' }}
>
{label}
</Typography>
<Chip
label={group.length}
size="small"
sx={{ height: 16, fontSize: '0.6rem', bgcolor: color, color: 'white', px: 0 }}
/>
</Box>
{/* Rows */}
{group.map(r => <ReminderListRow key={r.id} reminder={r} />)}
</Box>
)
})}
</Box> </Box>
) )
} }
+62 -100
View File
@@ -1,9 +1,9 @@
import { Box, ToggleButtonGroup, ToggleButton, TextField, InputAdornment, Typography } from '@mui/material' import { Box, ToggleButtonGroup, ToggleButton, TextField, InputAdornment, Typography, Select, MenuItem, FormControl, FormControlLabel, Switch } from '@mui/material'
import { Search } from 'lucide-react' import { Search } from 'lucide-react'
import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
import { useReminderStore } from '../../stores/reminderStore' import { useReminderStore } from '../../stores/reminderStore'
import { ReminderType, ReminderStatus, ReminderPriority } from '../../domain/reminder' import { ReminderType } from '../../domain/reminder'
import type { ReminderType as ReminderTypeType, ReminderStatus as ReminderStatusType, ReminderPriority as ReminderPriorityType } from '../../domain/reminder' import type { ReminderType as ReminderTypeType } from '../../domain/reminder'
const TYPE_LABELS: Record<ReminderTypeType, string> = { const TYPE_LABELS: Record<ReminderTypeType, string> = {
LEASE_EXPIRY: 'Mietablauf', LEASE_EXPIRY: 'Mietablauf',
@@ -16,127 +16,89 @@ const TYPE_LABELS: Record<ReminderTypeType, string> = {
CUSTOM: 'Individuell', CUSTOM: 'Individuell',
} }
const STATUS_LABELS: Record<ReminderStatusType, string> = {
ACTIVE: 'Aktiv',
SNOOZED: 'Schlummern',
COMPLETED: 'Erledigt',
DISMISSED: 'Verworfen',
}
const PRIORITY_LABELS: Record<ReminderPriorityType, string> = {
URGENT: 'Dringend',
HIGH: 'Hoch',
MEDIUM: 'Mittel',
LOW: 'Niedrig',
}
export function ReminderFilterBar() { export function ReminderFilterBar() {
const { const {
filterType, setFilterType, filterType, setFilterType,
filterStatus, setFilterStatus, filterStatus, setFilterStatus,
filterPriority, setFilterPriority,
searchQuery, setSearchQuery, searchQuery, setSearchQuery,
viewMode, setViewMode, viewMode, setViewMode,
} = useReminderStore(useShallow(s => ({ } = useReminderStore(useShallow(s => ({
filterType: s.filterType, setFilterType: s.setFilterType, filterType: s.filterType, setFilterType: s.setFilterType,
filterStatus: s.filterStatus, setFilterStatus: s.setFilterStatus, filterStatus: s.filterStatus, setFilterStatus: s.setFilterStatus,
filterPriority: s.filterPriority, setFilterPriority: s.setFilterPriority,
searchQuery: s.searchQuery, setSearchQuery: s.setSearchQuery, searchQuery: s.searchQuery, setSearchQuery: s.setSearchQuery,
viewMode: s.viewMode, setViewMode: s.setViewMode, viewMode: s.viewMode, setViewMode: s.setViewMode,
}))) })))
const showArchive = filterStatus === 'ALL'
return ( return (
<Box className="flex flex-col gap-3"> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}>
<Box className="flex items-center gap-3 flex-wrap"> {/* Search */}
{/* Search */} <TextField
<TextField size="small"
size="small" placeholder="Suchen…"
placeholder="Suchen…" value={searchQuery}
value={searchQuery} onChange={e => setSearchQuery(e.target.value)}
onChange={e => setSearchQuery(e.target.value)} slotProps={{
slotProps={{ input: {
input: { startAdornment: (
startAdornment: ( <InputAdornment position="start">
<InputAdornment position="start"> <Search size={16} color="#94a3b8" />
<Search size={16} color="#94a3b8" /> </InputAdornment>
</InputAdornment> ),
), },
}, }}
}} sx={{ width: 200, '& .MuiInputBase-root': { fontSize: '0.8125rem' } }}
sx={{ width: 220, '& .MuiInputBase-root': { fontSize: '0.8125rem' } }} />
/>
{/* View mode */} {/* Type selector */}
<Box sx={{ ml: 'auto', display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="caption" color="text.secondary">Ansicht:</Typography> <Typography variant="caption" color="text.secondary" sx={{ whiteSpace: 'nowrap' }}>Typ:</Typography>
<ToggleButtonGroup <FormControl size="small">
value={viewMode} <Select
exclusive
onChange={(_, v) => v && setViewMode(v)}
size="small"
>
<ToggleButton value="list" sx={{ textTransform: 'none', fontSize: '0.75rem', px: 1.5, py: 0.5 }}>Liste</ToggleButton>
<ToggleButton value="card" sx={{ textTransform: 'none', fontSize: '0.75rem', px: 1.5, py: 0.5 }}>Karten</ToggleButton>
</ToggleButtonGroup>
</Box>
</Box>
<Box className="flex flex-wrap gap-3">
{/* Type filter */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<Typography variant="caption" color="text.secondary" sx={{ flexShrink: 0 }}>Typ:</Typography>
<ToggleButtonGroup
value={filterType} value={filterType}
exclusive onChange={e => setFilterType(e.target.value as ReminderTypeType | 'ALL')}
onChange={(_, v) => v && setFilterType(v)} sx={{ fontSize: '0.8125rem', minWidth: 150 }}
size="small"
> >
<ToggleButton value="ALL" sx={{ textTransform: 'none', fontSize: '0.7rem', px: 1, py: 0.375 }}>Alle</ToggleButton> <MenuItem value="ALL" sx={{ fontSize: '0.8125rem' }}>Alle Typen</MenuItem>
{Object.values(ReminderType).map(t => ( {Object.values(ReminderType).map(t => (
<ToggleButton key={t} value={t} sx={{ textTransform: 'none', fontSize: '0.7rem', px: 1, py: 0.375 }}> <MenuItem key={t} value={t} sx={{ fontSize: '0.8125rem' }}>
{TYPE_LABELS[t]} {TYPE_LABELS[t]}
</ToggleButton> </MenuItem>
))} ))}
</ToggleButtonGroup> </Select>
</Box> </FormControl>
</Box> </Box>
<Box className="flex flex-wrap gap-3"> {/* Archive toggle */}
{/* Status filter */} <FormControlLabel
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> control={
<Typography variant="caption" color="text.secondary" sx={{ flexShrink: 0 }}>Status:</Typography> <Switch
<ToggleButtonGroup
value={filterStatus}
exclusive
onChange={(_, v) => v && setFilterStatus(v)}
size="small" size="small"
> checked={showArchive}
<ToggleButton value="ALL" sx={{ textTransform: 'none', fontSize: '0.7rem', px: 1, py: 0.375 }}>Alle</ToggleButton> onChange={e => setFilterStatus(e.target.checked ? 'ALL' : 'ACTIVE')}
{Object.values(ReminderStatus).map(s => ( />
<ToggleButton key={s} value={s} sx={{ textTransform: 'none', fontSize: '0.7rem', px: 1, py: 0.375 }}> }
{STATUS_LABELS[s]} label={
</ToggleButton> <Typography variant="caption" color="text.secondary">
))} Abgeschlossene anzeigen
</ToggleButtonGroup> </Typography>
</Box> }
sx={{ m: 0 }}
/>
{/* Priority filter */} {/* View mode */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ ml: 'auto', display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ flexShrink: 0 }}>Priorität:</Typography> <Typography variant="caption" color="text.secondary">Ansicht:</Typography>
<ToggleButtonGroup <ToggleButtonGroup
value={filterPriority} value={viewMode}
exclusive exclusive
onChange={(_, v) => v && setFilterPriority(v)} onChange={(_, v) => v && setViewMode(v)}
size="small" size="small"
> >
<ToggleButton value="ALL" sx={{ textTransform: 'none', fontSize: '0.7rem', px: 1, py: 0.375 }}>Alle</ToggleButton> <ToggleButton value="list" sx={{ textTransform: 'none', fontSize: '0.75rem', px: 1.5, py: 0.5 }}>Liste</ToggleButton>
{Object.values(ReminderPriority).map(p => ( <ToggleButton value="card" sx={{ textTransform: 'none', fontSize: '0.75rem', px: 1.5, py: 0.5 }}>Karten</ToggleButton>
<ToggleButton key={p} value={p} sx={{ textTransform: 'none', fontSize: '0.7rem', px: 1, py: 0.375 }}> </ToggleButtonGroup>
{PRIORITY_LABELS[p]}
</ToggleButton>
))}
</ToggleButtonGroup>
</Box>
</Box> </Box>
</Box> </Box>
) )
+60 -22
View File
@@ -1,32 +1,46 @@
import { Box, Paper, Typography, Skeleton } from '@mui/material' import { Box, Typography, Skeleton } from '@mui/material'
import { AlertTriangle, Calendar, CalendarDays, Eye } from 'lucide-react' import { AlertOctagon, Calendar, CalendarDays, Eye } from 'lucide-react'
import { useShallow } from 'zustand/react/shallow'
import { useReminderInsights } from '../../hooks/useReminders' import { useReminderInsights } from '../../hooks/useReminders'
import { useReminderStore } from '../../stores/reminderStore'
import type { FilterHorizon } from '../../stores/reminderStore'
import { ReminderType } from '../../domain/reminder'
interface KpiItemProps { interface KpiCardProps {
icon: React.ReactNode icon: React.ReactNode
label: string label: string
value: number | string value: number | string
color: string color: string
active: boolean
onClick: () => void
} }
function KpiItem({ icon, label, value, color }: KpiItemProps) { function KpiCard({ icon, label, value, color, active, onClick }: KpiCardProps) {
return ( return (
<Paper <Box
variant="outlined" onClick={onClick}
sx={{ sx={{
flex: 1, flex: 1,
p: 1.5, p: 1.5,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 1.5, gap: 1.5,
borderColor: '#e2e8f0', border: active ? `2px solid ${color}` : '1px solid #e2e8f0',
borderRadius: 1.5,
bgcolor: active ? `${color}10` : 'white',
cursor: 'pointer',
transition: 'all 0.15s',
minWidth: 0, minWidth: 0,
'&:hover': {
bgcolor: `${color}0d`,
borderColor: color,
},
}} }}
> >
<Box <Box
sx={{ sx={{
width: 36, width: 34,
height: 36, height: 34,
borderRadius: 1, borderRadius: 1,
bgcolor: `${color}18`, bgcolor: `${color}18`,
display: 'flex', display: 'flex',
@@ -41,16 +55,32 @@ function KpiItem({ icon, label, value, color }: KpiItemProps) {
<Typography variant="h6" sx={{ fontWeight: 700, color, lineHeight: 1.2, fontSize: '1.25rem' }}> <Typography variant="h6" sx={{ fontWeight: 700, color, lineHeight: 1.2, fontSize: '1.25rem' }}>
{value} {value}
</Typography> </Typography>
<Typography variant="caption" color="text.secondary" sx={{ whiteSpace: 'nowrap' }}> <Typography variant="caption" color="text.secondary" sx={{ whiteSpace: 'nowrap', fontSize: '0.7rem' }}>
{label} {label}
</Typography> </Typography>
</Box> </Box>
</Paper> </Box>
) )
} }
export function ReminderKpiBar() { export function ReminderKpiBar() {
const { data, isLoading } = useReminderInsights() 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) {
setFilterHorizon(filterHorizon === h ? 'ALL' : h)
}
function togglePreMarket() {
setFilterType(filterType === ReminderType.SCHATTENMARKT_RELEASE ? 'ALL' : ReminderType.SCHATTENMARKT_RELEASE)
}
if (isLoading) { if (isLoading) {
return ( return (
@@ -62,33 +92,41 @@ export function ReminderKpiBar() {
) )
} }
const insights = data ?? { urgentCount: 0, dueThisWeek: 0, dueThisMonth: 0, schattenmarktReadyCount: 0 } const ins = data ?? { overdueCount: 0, dueThisWeek: 0, dueThisMonth: 0, schattenmarktReadyCount: 0 }
return ( return (
<Box className="flex gap-3"> <Box className="flex gap-3">
<KpiItem <KpiCard
icon={<AlertTriangle size={18} color="#dc2626" />} icon={<AlertOctagon size={18} color="#dc2626" />}
label="Dringend" label="Überfällig"
value={insights.urgentCount} value={ins.overdueCount}
color="#dc2626" color="#dc2626"
active={filterHorizon === 'OVERDUE'}
onClick={() => toggleHorizon('OVERDUE')}
/> />
<KpiItem <KpiCard
icon={<Calendar size={18} color="#ea580c" />} icon={<Calendar size={18} color="#ea580c" />}
label="Diese Woche" label="Diese Woche"
value={insights.dueThisWeek} value={ins.dueThisWeek}
color="#ea580c" color="#ea580c"
active={filterHorizon === 'THIS_WEEK'}
onClick={() => toggleHorizon('THIS_WEEK')}
/> />
<KpiItem <KpiCard
icon={<CalendarDays size={18} color="#0369a1" />} icon={<CalendarDays size={18} color="#0369a1" />}
label="Dieser Monat" label="Dieser Monat"
value={insights.dueThisMonth} value={ins.dueThisMonth}
color="#0369a1" color="#0369a1"
active={filterHorizon === 'THIS_MONTH'}
onClick={() => toggleHorizon('THIS_MONTH')}
/> />
<KpiItem <KpiCard
icon={<Eye size={18} color="#be185d" />} icon={<Eye size={18} color="#be185d" />}
label="Pre-Market Risiko" label="Pre-Market Risiko"
value={insights.schattenmarktReadyCount} value={ins.schattenmarktReadyCount}
color="#be185d" color="#be185d"
active={filterType === ReminderType.SCHATTENMARKT_RELEASE}
onClick={togglePreMarket}
/> />
</Box> </Box>
) )
+13 -2
View File
@@ -7,7 +7,14 @@ import { ReminderTypeBadge } from './ReminderTypeBadge'
import { ReminderDaysIndicator } from './ReminderDaysIndicator' import { ReminderDaysIndicator } from './ReminderDaysIndicator'
import { useCompleteReminder, useDismissReminder, useSnoozeReminder } from '../../hooks/useReminders' import { useCompleteReminder, useDismissReminder, useSnoozeReminder } from '../../hooks/useReminders'
import { useReminderStore } from '../../stores/reminderStore' import { useReminderStore } from '../../stores/reminderStore'
import { ReminderStatus } from '../../domain/reminder' import { ReminderStatus, ReminderPriority } from '../../domain/reminder'
const PRIORITY_BORDER: Record<string, string> = {
[ReminderPriority.URGENT]: '#dc2626',
[ReminderPriority.HIGH]: '#ea580c',
[ReminderPriority.MEDIUM]: '#ca8a04',
[ReminderPriority.LOW]: 'transparent',
}
const STATUS_CHIP_COLOR: Record<string, 'default' | 'success' | 'warning' | 'error' | 'info'> = { const STATUS_CHIP_COLOR: Record<string, 'default' | 'success' | 'warning' | 'error' | 'info'> = {
ACTIVE: 'info', ACTIVE: 'info',
@@ -56,6 +63,8 @@ export const ReminderListRow = memo(function ReminderListRow({ reminder }: Props
snooze.mutate({ id: reminder.id, until: '2026-05-27' }) snooze.mutate({ id: reminder.id, until: '2026-05-27' })
} }
const borderColor = PRIORITY_BORDER[reminder.priority] ?? 'transparent'
return ( return (
<Box <Box
onClick={handleRowClick} onClick={handleRowClick}
@@ -64,9 +73,11 @@ export const ReminderListRow = memo(function ReminderListRow({ reminder }: Props
gridTemplateColumns: '100px 130px 1fr 140px 90px 100px 90px', gridTemplateColumns: '100px 130px 1fr 140px 90px 100px 90px',
alignItems: 'center', alignItems: 'center',
gap: 1, gap: 1,
px: 2, pl: 1.5,
pr: 2,
py: 1.25, py: 1.25,
borderBottom: '1px solid #f1f5f9', borderBottom: '1px solid #f1f5f9',
borderLeft: `3px solid ${borderColor}`,
bgcolor: 'white', bgcolor: 'white',
cursor: 'pointer', cursor: 'pointer',
'&:hover': { bgcolor: '#f8fafc' }, '&:hover': { bgcolor: '#f8fafc' },
+3
View File
@@ -48,6 +48,8 @@ export const reminderService = {
(r.shadowMarketRisk === ShadowMarketRisk.HIGH), (r.shadowMarketRisk === ShadowMarketRisk.HIGH),
).length ).length
const overdueCount = active.filter(r => daysDiff(r.dueDate) <= 0).length
const activeDays = active const activeDays = active
.filter(r => daysDiff(r.dueDate) > 0) .filter(r => daysDiff(r.dueDate) > 0)
.map(r => daysDiff(r.dueDate)) .map(r => daysDiff(r.dueDate))
@@ -60,6 +62,7 @@ export const reminderService = {
dueThisWeek, dueThisWeek,
dueThisMonth, dueThisMonth,
schattenmarktReadyCount, schattenmarktReadyCount,
overdueCount,
avgDaysToAction, avgDaysToAction,
} }
}, },
+9 -3
View File
@@ -1,6 +1,8 @@
import { create } from 'zustand' import { create } from 'zustand'
import type { ReminderType, ReminderStatus, ReminderPriority } from '../domain/reminder' import type { ReminderType, ReminderStatus, ReminderPriority } from '../domain/reminder'
export type FilterHorizon = 'ALL' | 'OVERDUE' | 'THIS_WEEK' | 'THIS_MONTH' | 'LATER'
interface ReminderStore { interface ReminderStore {
selectedId: string | null selectedId: string | null
setSelectedId: (id: string | null) => void setSelectedId: (id: string | null) => void
@@ -8,10 +10,12 @@ interface ReminderStore {
setDrawerOpen: (open: boolean) => void setDrawerOpen: (open: boolean) => void
filterType: ReminderType | 'ALL' filterType: ReminderType | 'ALL'
setFilterType: (t: ReminderType | 'ALL') => void setFilterType: (t: ReminderType | 'ALL') => void
filterStatus: ReminderStatus | 'ALL' filterStatus: ReminderStatus | 'ALL' | 'ACTIVE'
setFilterStatus: (s: ReminderStatus | 'ALL') => void setFilterStatus: (s: ReminderStatus | 'ALL' | 'ACTIVE') => void
filterPriority: ReminderPriority | 'ALL' filterPriority: ReminderPriority | 'ALL'
setFilterPriority: (p: ReminderPriority | 'ALL') => void setFilterPriority: (p: ReminderPriority | 'ALL') => void
filterHorizon: FilterHorizon
setFilterHorizon: (h: FilterHorizon) => void
searchQuery: string searchQuery: string
setSearchQuery: (q: string) => void setSearchQuery: (q: string) => void
viewMode: 'list' | 'card' viewMode: 'list' | 'card'
@@ -25,10 +29,12 @@ export const useReminderStore = create<ReminderStore>((set) => ({
setDrawerOpen: (open) => set({ drawerOpen: open }), setDrawerOpen: (open) => set({ drawerOpen: open }),
filterType: 'ALL', filterType: 'ALL',
setFilterType: (t) => set({ filterType: t }), setFilterType: (t) => set({ filterType: t }),
filterStatus: 'ALL', filterStatus: 'ACTIVE',
setFilterStatus: (s) => set({ filterStatus: s }), setFilterStatus: (s) => set({ filterStatus: s }),
filterPriority: 'ALL', filterPriority: 'ALL',
setFilterPriority: (p) => set({ filterPriority: p }), setFilterPriority: (p) => set({ filterPriority: p }),
filterHorizon: 'ALL',
setFilterHorizon: (h) => set({ filterHorizon: h }),
searchQuery: '', searchQuery: '',
setSearchQuery: (q) => set({ searchQuery: q }), setSearchQuery: (q) => set({ searchQuery: q }),
viewMode: 'list', viewMode: 'list',