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>
191 lines
6.7 KiB
TypeScript
191 lines
6.7 KiB
TypeScript
import { useMemo, useCallback } from 'react'
|
|
import { Box, Chip, Typography } from '@mui/material'
|
|
import { useReminders } from '../../hooks/useReminders'
|
|
import { useShallow } from 'zustand/react/shallow'
|
|
import { useReminderStore } from '../../stores/reminderStore'
|
|
import { ReminderListRow } from './ReminderListRow'
|
|
import { ReminderCard } from './ReminderCard'
|
|
import { ReminderSkeleton } from './ReminderSkeleton'
|
|
import { ReminderEmptyState } from './ReminderEmptyState'
|
|
import type { Reminder } from '../../domain/reminder'
|
|
import { ReminderStatus } from '../../domain/reminder'
|
|
import type { FilterHorizon } from '../../stores/reminderStore'
|
|
|
|
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: '#e8e7e4' },
|
|
]
|
|
|
|
const LIST_COLS = '90px 130px 1fr 140px 80px 90px'
|
|
|
|
function applyFilters(
|
|
reminders: Reminder[],
|
|
filterType: string,
|
|
filterStatus: string,
|
|
filterHorizon: string,
|
|
searchQuery: string,
|
|
): Reminder[] {
|
|
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 (filterHorizon !== 'ALL' && getHorizon(r.dueDate) !== filterHorizon) return false
|
|
if (searchQuery) {
|
|
const q = searchQuery.toLowerCase()
|
|
const hay = `${r.propertyTitle} ${r.propertyCity} ${r.tenantName}`.toLowerCase()
|
|
if (!hay.includes(q)) return false
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
export function ReminderFeed() {
|
|
const { data, isLoading } = useReminders()
|
|
const {
|
|
filterType, filterStatus, filterHorizon, searchQuery, viewMode,
|
|
setFilterType, setFilterStatus, setFilterHorizon, setSearchQuery,
|
|
} = useReminderStore(useShallow(s => ({
|
|
filterType: s.filterType, filterStatus: s.filterStatus,
|
|
filterHorizon: s.filterHorizon, searchQuery: s.searchQuery,
|
|
viewMode: s.viewMode, setFilterType: s.setFilterType,
|
|
setFilterStatus: s.setFilterStatus, setFilterHorizon: s.setFilterHorizon,
|
|
setSearchQuery: s.setSearchQuery,
|
|
})))
|
|
|
|
const reminders = data ?? []
|
|
|
|
const filtered = useMemo(
|
|
() => applyFilters(reminders, filterType, filterStatus, filterHorizon, 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(() => {
|
|
setFilterType('ALL')
|
|
setFilterStatus('ACTIVE')
|
|
setFilterHorizon('ALL')
|
|
setSearchQuery('')
|
|
}, [setFilterType, setFilterStatus, setFilterHorizon, setSearchQuery])
|
|
|
|
if (isLoading) return <ReminderSkeleton />
|
|
if (filtered.length === 0) return <ReminderEmptyState onReset={resetFilters} />
|
|
|
|
if (viewMode === 'card') {
|
|
return (
|
|
<Box className="flex flex-col gap-4">
|
|
{HORIZON_CONFIG.map(({ key, label, color }) => {
|
|
const group = grouped[key]
|
|
if (!group || group.length === 0) return null
|
|
return (
|
|
<Box key={key}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5, px: 0.5 }}>
|
|
<Box sx={{ width: 3, height: 16, borderRadius: 1, bgcolor: color, flexShrink: 0 }} />
|
|
<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>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ border: '1px solid #e2e8f0', borderRadius: 1.5, overflow: 'hidden' }}>
|
|
{/* Table header */}
|
|
<Box
|
|
sx={{
|
|
display: 'grid',
|
|
gridTemplateColumns: LIST_COLS,
|
|
gap: 1,
|
|
px: 2,
|
|
py: 1,
|
|
bgcolor: '#f8fafc',
|
|
borderBottom: '1px solid #e2e8f0',
|
|
}}
|
|
>
|
|
{['Priorität', 'Typ', 'Objekt / Mieter', 'Fälligkeit', 'Status', 'Aktionen'].map(h => (
|
|
<Typography key={h} variant="caption" sx={{ fontWeight: 600, color: '#64748b', fontSize: '0.7rem' }}>
|
|
{h}
|
|
</Typography>
|
|
))}
|
|
</Box>
|
|
|
|
{/* Grouped rows */}
|
|
{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>
|
|
)
|
|
}
|