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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user