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:
Benjamin Sutter
2026-06-16 14:30:06 +02:00
parent 5259579f85
commit 6aa4f96bd2
17 changed files with 463 additions and 273 deletions
+6 -1
View File
@@ -21,16 +21,21 @@ export function AppShell() {
const sidebarCollapsed = useLayoutStore(s => s.sidebarCollapsed) const sidebarCollapsed = useLayoutStore(s => s.sidebarCollapsed)
const setActiveWorkspace = useLayoutStore(s => s.setActiveWorkspace) const setActiveWorkspace = useLayoutStore(s => s.setActiveWorkspace)
const toggleSidebar = useLayoutStore(s => s.toggleSidebar) const toggleSidebar = useLayoutStore(s => s.toggleSidebar)
const setSidebarCollapsed = useLayoutStore(s => s.setSidebarCollapsed)
const { currentUser } = useSessionStore() const { currentUser } = useSessionStore()
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const theme = useTheme() const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('md')) const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
const isCompact = useMediaQuery(theme.breakpoints.down('xl'))
const [mobileOpen, setMobileOpen] = useState(false) const [mobileOpen, setMobileOpen] = useState(false)
// Close mobile menu on route change // Close mobile menu on route change
useEffect(() => { setMobileOpen(false) }, [location.pathname]) useEffect(() => { setMobileOpen(false) }, [location.pathname])
// Auto-collapse sidebar on compact screens (laptops < 1536px)
useEffect(() => { setSidebarCollapsed(isCompact) }, [isCompact, setSidebarCollapsed])
// Sync active workspace with URL // Sync active workspace with URL
useEffect(() => { useEffect(() => {
const detected = getWorkspaceFromPath(location.pathname) const detected = getWorkspaceFromPath(location.pathname)
+2 -2
View File
@@ -32,8 +32,8 @@ export function RightContextPanel() {
right: 0, right: 0,
top: 56, top: 56,
height: 'calc(100vh - 56px)', height: 'calc(100vh - 56px)',
width: 320, width: { xs: '85vw', sm: 300, lg: 320 },
transform: isRightPanelOpen ? 'translateX(0)' : 'translateX(320px)', transform: isRightPanelOpen ? 'translateX(0)' : 'translateX(110%)',
transition: 'transform 0.25s ease', transition: 'transform 0.25s ease',
bgcolor: '#fff', bgcolor: '#fff',
borderLeft: '1px solid #e2e8f0', borderLeft: '1px solid #e2e8f0',
@@ -32,7 +32,7 @@ export function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: ()
return ( return (
<Box sx={{ <Box sx={{
width: { xs: '100%', md: 340 }, flexShrink: 0, width: { xs: '100%', md: 300, lg: 320, xl: 340 }, flexShrink: 0,
display: 'flex', flexDirection: 'column', display: 'flex', flexDirection: 'column',
bgcolor: 'white', borderLeft: `1px solid ${DS_BORDER.default}`, overflow: 'hidden', bgcolor: 'white', borderLeft: `1px solid ${DS_BORDER.default}`, overflow: 'hidden',
}}> }}>
+285 -157
View File
@@ -1,26 +1,185 @@
import { useState } from 'react' import { useState } from 'react'
import { import {
Drawer, Drawer, Box, Typography, IconButton, Chip, Divider,
Box, TextField, Button, Select, MenuItem, FormControl, InputLabel, Autocomplete,
Typography,
IconButton,
Chip,
Divider,
TextField,
Button,
Switch,
FormControlLabel,
Tooltip,
} from '@mui/material' } from '@mui/material'
import { X, MapPin, Calendar, DollarSign, Eye, Activity, FileText, ExternalLink } from 'lucide-react' import { X, Calendar, Activity, FileText, ExternalLink, Eye } from 'lucide-react'
import { useReminderStore } from '../../stores/reminderStore' import { useReminderStore } from '../../stores/reminderStore'
import { useReminder, useCompleteReminder, useDismissReminder, useSnoozeReminder } from '../../hooks/useReminders' import {
import { usePropertyById } from '../../hooks/useProperties' useReminder, useCompleteReminder, useDismissReminder,
useSnoozeReminder, useCreateReminder,
} from '../../hooks/useReminders'
import { usePropertyById, useProperties } from '../../hooks/useProperties'
import { ReminderPriorityBadge } from './ReminderPriorityBadge' import { ReminderPriorityBadge } from './ReminderPriorityBadge'
import { ReminderTypeBadge } from './ReminderTypeBadge' import { ReminderTypeBadge } from './ReminderTypeBadge'
import { ReminderDaysIndicator } from './ReminderDaysIndicator' import { ReminderDaysIndicator } from './ReminderDaysIndicator'
import { ReminderStatus } from '../../domain/reminder' import { ReminderStatus, ReminderType, ReminderPriority } from '../../domain/reminder'
import { SHADOW_RISK_COLOR, SHADOW_RISK_LABEL, STATUS_CHIP_COLOR, STATUS_LABEL, SectionTitle, DateRow, ActivityEntry } from './reminderDetailHelpers' import type { Property } from '../../domain/property'
import {
SHADOW_RISK_COLOR, SHADOW_RISK_LABEL,
STATUS_CHIP_COLOR, STATUS_LABEL,
SectionTitle, DateRow, ActivityEntry,
} from './reminderDetailHelpers'
// ── Constants ────────────────────────────────────────────────────────────────
const TYPE_LABELS: Record<ReminderType, string> = {
LEASE_EXPIRY: 'Mietablauf',
BREAK_OPTION: 'Break-Option',
RENT_REVIEW: 'Mietanpassung',
INSPECTION: 'Inspektion',
INSURANCE_RENEWAL: 'Versicherung',
MAINTENANCE: 'Unterhalt',
SCHATTENMARKT_RELEASE: 'Pre-Market',
CUSTOM: 'Individuell',
}
const TYPE_SECOND_DATE: Partial<Record<ReminderType, 'contractEndDate' | 'breakOptionDate' | 'eventDate'>> = {
[ReminderType.LEASE_EXPIRY]: 'contractEndDate',
[ReminderType.BREAK_OPTION]: 'breakOptionDate',
[ReminderType.RENT_REVIEW]: 'eventDate',
[ReminderType.SCHATTENMARKT_RELEASE]: 'eventDate',
}
const TYPE_SECOND_LABEL: Partial<Record<ReminderType, string>> = {
[ReminderType.LEASE_EXPIRY]: 'Vertragsende',
[ReminderType.BREAK_OPTION]: 'Break-Option',
[ReminderType.RENT_REVIEW]: 'Ereignisdatum',
[ReminderType.SCHATTENMARKT_RELEASE]: 'Ereignisdatum',
}
const MOCK_TODAY = new Date('2026-05-20')
function calcPriority(dueDateStr: string): ReminderPriority {
const days = Math.ceil((new Date(dueDateStr).getTime() - MOCK_TODAY.getTime()) / (1000 * 60 * 60 * 24))
if (days <= 14) return ReminderPriority.URGENT
if (days <= 30) return ReminderPriority.HIGH
if (days <= 60) return ReminderPriority.MEDIUM
return ReminderPriority.LOW
}
// ── Create Form ───────────────────────────────────────────────────────────────
function CreateForm({ onClose }: { onClose: () => void }) {
const { data: properties = [] } = useProperties()
const create = useCreateReminder()
const [type, setType] = useState<ReminderType>(ReminderType.LEASE_EXPIRY)
const [selectedProperty, setSelectedProperty] = useState<Property | null>(null)
const [tenantName, setTenantName] = useState('')
const [dueDate, setDueDate] = useState('')
const [note, setNote] = useState('')
function handlePropertyChange(_: unknown, prop: Property | null) {
setSelectedProperty(prop)
setTenantName(prop?.currentTenant ?? '')
}
function handleSubmit() {
if (!selectedProperty || !dueDate) return
create.mutate(
{
type,
priority: calcPriority(dueDate),
status: ReminderStatus.ACTIVE,
propertyId: selectedProperty.id,
propertyTitle: selectedProperty.title,
propertyCity: selectedProperty.location.city,
propertyDistrict: selectedProperty.location.district,
tenantName: tenantName || '—',
dueDate,
eventDate: dueDate,
areaSqm: selectedProperty.areaSqm,
currentRentPerSqm: selectedProperty.rentPricePerSqm,
currency: 'CHF',
shadowMarketRisk: 'NONE',
schattenmarktEnabled: false,
note: note || undefined,
organizationId: selectedProperty.organizationId ?? 'org-1',
},
{ onSuccess: onClose },
)
}
const canSubmit = !!selectedProperty && !!dueDate && !create.isPending
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}>
{/* Typ */}
<FormControl size="small" fullWidth>
<InputLabel>Typ</InputLabel>
<Select
value={type}
label="Typ"
onChange={e => setType(e.target.value as ReminderType)}
>
{Object.values(ReminderType).map(t => (
<MenuItem key={t} value={t}>{TYPE_LABELS[t]}</MenuItem>
))}
</Select>
</FormControl>
{/* Objekt */}
<Autocomplete
options={properties}
getOptionLabel={p => p.title}
value={selectedProperty}
onChange={handlePropertyChange}
size="small"
renderInput={params => (
<TextField {...params} label="Objekt" placeholder="Objekt auswählen…" />
)}
/>
{/* Mieter — nur wenn Objekt ausgewählt */}
{selectedProperty && (
<TextField
label="Mieter"
size="small"
fullWidth
value={tenantName}
onChange={e => setTenantName(e.target.value)}
placeholder="Mietername eingeben…"
/>
)}
{/* Fälligkeitsdatum */}
<TextField
label="Fälligkeitsdatum"
type="date"
size="small"
fullWidth
value={dueDate}
onChange={e => setDueDate(e.target.value)}
slotProps={{ inputLabel: { shrink: true } }}
/>
{/* Notiz */}
<TextField
label="Notiz (optional)"
multiline
rows={3}
size="small"
fullWidth
value={note}
onChange={e => setNote(e.target.value)}
placeholder="Kontext oder Hinweise…"
/>
<Button
variant="contained"
fullWidth
disabled={!canSubmit}
onClick={handleSubmit}
sx={{ textTransform: 'none', fontWeight: 600 }}
>
{create.isPending ? 'Wird erstellt…' : 'Reminder erstellen'}
</Button>
</Box>
)
}
// ── Main Drawer ───────────────────────────────────────────────────────────────
export function ReminderDetailDrawer() { export function ReminderDetailDrawer() {
const { selectedId, drawerOpen, setDrawerOpen, setSelectedId } = useReminderStore() const { selectedId, drawerOpen, setDrawerOpen, setSelectedId } = useReminderStore()
@@ -38,74 +197,79 @@ export function ReminderDetailDrawer() {
setSelectedId(null) setSelectedId(null)
} }
const isNew = !selectedId const isCreateMode = !selectedId
const secondDateKey = reminder ? TYPE_SECOND_DATE[reminder.type] : undefined
const secondDateLabel = reminder ? TYPE_SECOND_LABEL[reminder.type] : undefined
const secondDateValue = secondDateKey && reminder ? reminder[secondDateKey] : undefined
const showSecondDate = secondDateValue && reminder && secondDateValue !== reminder.dueDate
const showPreMarket = reminder &&
reminder.shadowMarketRisk !== 'NONE' &&
reminder.shadowMarketRisk !== 'LOW'
const isActionable = reminder &&
(reminder.status === ReminderStatus.ACTIVE || reminder.status === ReminderStatus.SNOOZED)
return ( return (
<Drawer <Drawer
anchor="right" anchor="right"
open={drawerOpen} open={drawerOpen}
onClose={handleClose} onClose={handleClose}
slotProps={{ paper: { sx: { width: 480, display: 'flex', flexDirection: 'column' } } }} slotProps={{ paper: { sx: { width: 460, display: 'flex', flexDirection: 'column' } } }}
> >
{/* Header */} {/* ── Header ── */}
<Box <Box sx={{ px: 2.5, py: 2, borderBottom: '1px solid #e2e8f0', flexShrink: 0 }}>
sx={{ <Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
px: 2.5, <Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0 }}>
py: 2, {isCreateMode ? (
borderBottom: '1px solid #e2e8f0', <Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#0f172a' }}>
display: 'flex', Neuer Reminder
alignItems: 'flex-start', </Typography>
gap: 1, ) : reminder ? (
flexShrink: 0, <>
}} <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', alignItems: 'center' }}>
> <ReminderTypeBadge type={reminder.type} />
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0 }}> <ReminderPriorityBadge priority={reminder.priority} />
{isNew ? ( <Chip
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>Neuer Reminder</Typography> label={STATUS_LABEL[reminder.status]}
) : reminder ? ( color={STATUS_CHIP_COLOR[reminder.status]}
<> size="small"
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', alignItems: 'center' }}> sx={{ height: 20, fontSize: '0.7rem' }}
<ReminderTypeBadge type={reminder.type} /> />
<ReminderPriorityBadge priority={reminder.priority} /> </Box>
<Chip <ReminderDaysIndicator dueDate={reminder.dueDate} />
label={STATUS_LABEL[reminder.status]} </>
color={STATUS_CHIP_COLOR[reminder.status]} ) : null}
size="small" </Box>
sx={{ height: 20, fontSize: '0.7rem' }} <IconButton size="small" onClick={handleClose} sx={{ flexShrink: 0, color: '#94a3b8' }}>
/> <X size={18} />
</Box> </IconButton>
<ReminderDaysIndicator dueDate={reminder.dueDate} />
</>
) : null}
</Box> </Box>
<IconButton size="small" onClick={handleClose} sx={{ flexShrink: 0 }}>
<X size={18} />
</IconButton>
</Box> </Box>
{/* Scrollable body */} {/* ── Body ── */}
<Box sx={{ flex: 1, overflowY: 'auto', p: 2.5 }} className="flex flex-col gap-5"> <Box sx={{ flex: 1, overflowY: 'auto', p: 2.5, display: 'flex', flexDirection: 'column' }}>
{isNew && (
<Typography variant="body2" color="text.secondary">
Neue Reminder-Erstellung noch nicht implementiert.
</Typography>
)}
{/* Create mode */}
{isCreateMode && <CreateForm onClose={handleClose} />}
{/* Detail mode */}
{reminder && ( {reminder && (
<> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
{/* 2. Property */} {/* 1. Objekt */}
<Box> <Box sx={{ mb: 2.5 }}>
<SectionTitle icon={<MapPin size={15} color="#64748b" />}>Objekt</SectionTitle> <Typography variant="body1" sx={{ fontWeight: 700, lineHeight: 1.3, mb: 0.25 }}>
<Box className="flex flex-col gap-1"> {reminder.propertyTitle}
<Typography variant="body2" sx={{ fontWeight: 600 }}>{reminder.propertyTitle}</Typography> </Typography>
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1 }}>
{reminder.propertyCity}{reminder.propertyDistrict ? ` · ${reminder.propertyDistrict}` : ''} {reminder.propertyCity}{reminder.propertyDistrict ? ` · ${reminder.propertyDistrict}` : ''}&nbsp;·&nbsp;{reminder.tenantName}
</Typography> </Typography>
<Typography variant="caption" color="text.secondary">Mieter: {reminder.tenantName}</Typography>
<Typography variant="caption" color="text.secondary">Fläche: {reminder.areaSqm.toLocaleString('de-CH')} m²</Typography> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap' }}>
{reminderProperty?.leaseContractUrl ? ( {reminderProperty?.leaseContractUrl ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mt: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<FileText size={12} color="#152642" /> <FileText size={12} color="#64748b" />
<Button <Button
size="small" size="small"
variant="outlined" variant="outlined"
@@ -113,94 +277,58 @@ export function ReminderDetailDrawer() {
href={reminderProperty.leaseContractUrl} href={reminderProperty.leaseContractUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
sx={{ textTransform: 'none', fontSize: '0.68rem', py: 0.125, px: 0.75, borderColor: '#cbd5e1', color: '#152642' }} sx={{
textTransform: 'none', fontSize: '0.68rem',
py: 0.125, px: 0.75, borderColor: '#cbd5e1', color: '#334155',
}}
> >
{reminderProperty.leaseContractName ?? 'Mietvertrag'} {reminderProperty.leaseContractName ?? 'Mietvertrag'}
</Button> </Button>
</Box> </Box>
) : ( ) : (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<FileText size={12} color="#cbd5e1" /> <FileText size={12} color="#cbd5e1" />
<Typography variant="caption" sx={{ color: '#cbd5e1' }}>Kein Mietvertrag hinterlegt</Typography> <Typography variant="caption" sx={{ color: '#cbd5e1' }}>Kein Vertrag</Typography>
</Box>
)}
{showPreMarket && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.4 }}>
<Eye size={11} color={SHADOW_RISK_COLOR[reminder.shadowMarketRisk]} />
<Typography variant="caption" sx={{
color: SHADOW_RISK_COLOR[reminder.shadowMarketRisk],
fontWeight: 600, fontSize: '0.68rem',
}}>
Pre-Mkt: {SHADOW_RISK_LABEL[reminder.shadowMarketRisk]}
</Typography>
</Box> </Box>
)} )}
</Box> </Box>
</Box> </Box>
<Divider /> <Divider sx={{ mb: 2.5 }} />
{/* 3. Dates */} {/* 2. Fristen */}
<Box> <Box sx={{ mb: 2.5 }}>
<SectionTitle icon={<Calendar size={15} color="#64748b" />}>Daten &amp; Fristen</SectionTitle> <SectionTitle icon={<Calendar size={14} color="#64748b" />}>Fristen</SectionTitle>
<Box className="flex flex-col gap-1.5"> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<DateRow label="Fälligkeitsdatum" value={reminder.dueDate} /> <DateRow label="Fälligkeit" value={reminder.dueDate} />
<DateRow label="Ereignisdatum" value={reminder.eventDate} /> {showSecondDate && (
<DateRow label="Vertragsende" value={reminder.contractEndDate} /> <DateRow label={secondDateLabel!} value={secondDateValue} />
<DateRow label="Break-Option" value={reminder.breakOptionDate} /> )}
{reminder.snoozedUntil && ( {reminder.snoozedUntil && (
<DateRow label="Schlummern bis" value={reminder.snoozedUntil} /> <DateRow label="Schlummert bis" value={reminder.snoozedUntil} />
)} )}
</Box> </Box>
</Box> </Box>
<Divider /> <Divider sx={{ mb: 2.5 }} />
{/* 4. Financials */} {/* 3. Notiz */}
<Box> <Box sx={{ mb: isActionable ? 0 : 2.5 }}>
<SectionTitle icon={<DollarSign size={15} color="#64748b" />}>Finanzen</SectionTitle> <Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1, fontSize: '0.8125rem' }}>
<Box className="flex flex-col gap-1.5"> Notiz
<Box sx={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: 1 }}> </Typography>
<Typography variant="caption" color="text.secondary">Miete/m²</Typography>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{reminder.currency} {reminder.currentRentPerSqm.toLocaleString('de-CH')} / Monat
</Typography>
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: 1 }}>
<Typography variant="caption" color="text.secondary">Total / Monat</Typography>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{reminder.currency} {(reminder.currentRentPerSqm * reminder.areaSqm).toLocaleString('de-CH')}
</Typography>
</Box>
</Box>
</Box>
<Divider />
{/* 5. Pre-Market */}
<Box>
<SectionTitle icon={<Eye size={15} color="#64748b" />}>Pre-Market Risiko</SectionTitle>
<Box className="flex flex-col gap-2">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box
sx={{
width: 10,
height: 10,
borderRadius: '50%',
bgcolor: SHADOW_RISK_COLOR[reminder.shadowMarketRisk],
}}
/>
<Typography variant="body2" sx={{ fontWeight: 600, color: SHADOW_RISK_COLOR[reminder.shadowMarketRisk] }}>
{SHADOW_RISK_LABEL[reminder.shadowMarketRisk]}
</Typography>
</Box>
<Tooltip title="Nur Anzeige — Änderung über Objektverwaltung">
<FormControlLabel
control={<Switch checked={reminder.schattenmarktEnabled} size="small" readOnly />}
label={
<Typography variant="caption">
Pre-Market aktiviert
</Typography>
}
/>
</Tooltip>
</Box>
</Box>
<Divider />
{/* 6. Note */}
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1, fontSize: '0.8125rem' }}>Notiz</Typography>
<TextField <TextField
multiline multiline
rows={3} rows={3}
@@ -213,24 +341,24 @@ export function ReminderDetailDrawer() {
/> />
</Box> </Box>
{/* Actions */} {/* 4. Aktionen */}
{(reminder.status === ReminderStatus.ACTIVE || reminder.status === ReminderStatus.SNOOZED) && ( {isActionable && (
<> <>
<Divider /> <Divider sx={{ my: 2.5 }} />
<Box className="flex flex-col gap-2"> <Box sx={{ mb: 2.5, display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.8125rem' }}>Aktionen</Typography> <Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.8125rem' }}>
<Box className="flex gap-2"> Aktionen
</Typography>
<Box sx={{ display: 'flex', gap: 1.5 }}>
<Button <Button
size="small"
variant="contained" variant="contained"
color="success" color="success"
sx={{ textTransform: 'none', flex: 1 }} sx={{ textTransform: 'none', flex: 1, fontWeight: 600 }}
onClick={() => complete.mutate({ id: reminder.id, note: noteValue || undefined })} onClick={() => complete.mutate({ id: reminder.id, note: noteValue || undefined })}
> >
Erledigt Erledigt
</Button> </Button>
<Button <Button
size="small"
variant="outlined" variant="outlined"
color="error" color="error"
sx={{ textTransform: 'none', flex: 1 }} sx={{ textTransform: 'none', flex: 1 }}
@@ -239,7 +367,7 @@ export function ReminderDetailDrawer() {
Verwerfen Verwerfen
</Button> </Button>
</Box> </Box>
<Box className="flex gap-2 items-center"> <Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<TextField <TextField
type="date" type="date"
size="small" size="small"
@@ -248,10 +376,10 @@ export function ReminderDetailDrawer() {
sx={{ flex: 1, '& .MuiInputBase-root': { fontSize: '0.8125rem' } }} sx={{ flex: 1, '& .MuiInputBase-root': { fontSize: '0.8125rem' } }}
/> />
<Button <Button
size="small"
variant="outlined" variant="outlined"
size="small"
disabled={!snoozeDate} disabled={!snoozeDate}
sx={{ textTransform: 'none', whiteSpace: 'nowrap' }} sx={{ textTransform: 'none', whiteSpace: 'nowrap', color: '#64748b', borderColor: '#e2e8f0' }}
onClick={() => snoozeDate && snooze.mutate({ id: reminder.id, until: snoozeDate })} onClick={() => snoozeDate && snooze.mutate({ id: reminder.id, until: snoozeDate })}
> >
Schlummern bis Schlummern bis
@@ -261,18 +389,18 @@ export function ReminderDetailDrawer() {
</> </>
)} )}
<Divider /> <Divider sx={{ mb: 2.5 }} />
{/* 7. Activity */} {/* 5. Verlauf */}
<Box> <Box>
<SectionTitle icon={<Activity size={15} color="#64748b" />}>Aktivitätslog</SectionTitle> <SectionTitle icon={<Activity size={14} color="#64748b" />}>Verlauf</SectionTitle>
<Box> <Box>
{[...reminder.activity].reverse().map((entry, i) => ( {[...reminder.activity].reverse().map((entry, i) => (
<ActivityEntry key={i} entry={entry} /> <ActivityEntry key={i} entry={entry} />
))} ))}
</Box> </Box>
</Box> </Box>
</> </Box>
)} )}
</Box> </Box>
</Drawer> </Drawer>
+2 -2
View File
@@ -28,7 +28,7 @@ const HORIZON_CONFIG: { key: FilterHorizon; label: string; color: string; bg: st
{ key: 'LATER', label: 'Später', color: '#64748b', bg: '#f8fafc', border: '#e8e7e4' }, { key: 'LATER', label: 'Später', color: '#64748b', bg: '#f8fafc', border: '#e8e7e4' },
] ]
const LIST_COLS = '100px 130px 1fr 140px 90px 100px 90px' const LIST_COLS = '90px 130px 1fr 140px 80px 90px'
function applyFilters( function applyFilters(
reminders: Reminder[], reminders: Reminder[],
@@ -141,7 +141,7 @@ export function ReminderFeed() {
borderBottom: '1px solid #e2e8f0', borderBottom: '1px solid #e2e8f0',
}} }}
> >
{['Priorität', 'Typ', 'Objekt / Mieter', 'Fälligkeit', 'Fläche', 'Status', 'Aktionen'].map(h => ( {['Priorität', 'Typ', 'Objekt / Mieter', 'Fälligkeit', 'Status', 'Aktionen'].map(h => (
<Typography key={h} variant="caption" sx={{ fontWeight: 600, color: '#64748b', fontSize: '0.7rem' }}> <Typography key={h} variant="caption" sx={{ fontWeight: 600, color: '#64748b', fontSize: '0.7rem' }}>
{h} {h}
</Typography> </Typography>
+106 -64
View File
@@ -6,7 +6,7 @@ import { useReminderStore } from '../../stores/reminderStore'
import type { FilterHorizon } from '../../stores/reminderStore' import type { FilterHorizon } from '../../stores/reminderStore'
import { ReminderType } from '../../domain/reminder' import { ReminderType } from '../../domain/reminder'
interface KpiCardProps { interface SecondaryKpiProps {
icon: React.ReactNode icon: React.ReactNode
label: string label: string
value: number | string value: number | string
@@ -15,35 +15,34 @@ interface KpiCardProps {
onClick: () => void onClick: () => void
} }
function KpiCard({ icon, label, value, color, active, onClick }: KpiCardProps) { function SecondaryKpi({ icon, label, value, color, active, onClick }: SecondaryKpiProps) {
return ( return (
<Box <Box
onClick={onClick} onClick={onClick}
sx={{ sx={{
flex: 1, flex: 1,
px: 1.5, px: 2,
py: 1.125, py: 1.5,
display: 'flex', display: 'flex',
alignItems: 'center', flexDirection: 'column',
gap: 1, gap: 0.25,
border: active ? `1.5px solid ${color}` : '1px solid #e8e7e4',
borderRadius: 1.5,
bgcolor: active ? `${color}08` : 'white',
cursor: 'pointer', 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', transition: 'border-color 0.15s, background 0.15s',
minWidth: 0,
'&:hover': { bgcolor: `${color}06`, borderColor: color }, '&:hover': { bgcolor: `${color}06`, borderColor: color },
}} }}
> >
<Box sx={{ opacity: active ? 1 : 0.55, flexShrink: 0 }}>{icon}</Box> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
<Box sx={{ minWidth: 0 }}> <Box sx={{ opacity: active ? 1 : 0.5, flexShrink: 0 }}>{icon}</Box>
<Typography sx={{ fontWeight: 700, color, lineHeight: 1.1, fontSize: '1.1rem' }}> <Typography sx={{ fontWeight: 700, color, fontSize: '1.25rem', lineHeight: 1 }}>
{value} {value}
</Typography> </Typography>
<Typography variant="caption" sx={{ color: '#64748b', whiteSpace: 'nowrap', fontSize: '0.68rem' }}>
{label}
</Typography>
</Box> </Box>
<Typography variant="caption" sx={{ color: '#94a3b8', fontSize: '0.7rem', whiteSpace: 'nowrap' }}>
{label}
</Typography>
</Box> </Box>
) )
} }
@@ -60,69 +59,112 @@ export function ReminderKpiBar() {
) )
function toggleHorizon(h: FilterHorizon) { function toggleHorizon(h: FilterHorizon) {
if (filterHorizon === h) { if (filterHorizon === h) setFilterHorizon('ALL')
setFilterHorizon('ALL') else { setFilterHorizon(h); setFilterType('ALL') }
} else {
setFilterHorizon(h)
setFilterType('ALL') // clear type filter so count matches
}
} }
function togglePreMarket() { function togglePreMarket() {
if (filterType === ReminderType.SCHATTENMARKT_RELEASE) { if (filterType === ReminderType.SCHATTENMARKT_RELEASE) setFilterType('ALL')
setFilterType('ALL') else { setFilterType(ReminderType.SCHATTENMARKT_RELEASE); setFilterHorizon('ALL') }
} else {
setFilterType(ReminderType.SCHATTENMARKT_RELEASE)
setFilterHorizon('ALL') // clear horizon filter so count matches
}
} }
if (isLoading) { if (isLoading) {
return ( return (
<Box className="flex gap-3"> <Box sx={{ display: 'flex', gap: 2 }}>
{[1, 2, 3, 4].map(i => ( <Skeleton variant="rounded" height={76} sx={{ flex: '0 0 220px' }} />
<Skeleton key={i} variant="rounded" height={68} sx={{ flex: 1 }} /> <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> </Box>
) )
} }
const ins = data ?? { overdueCount: 0, dueThisWeek: 0, dueThisMonth: 0, schattenmarktReadyCount: 0 } const ins = data ?? { overdueCount: 0, dueThisWeek: 0, dueThisMonth: 0, schattenmarktReadyCount: 0 }
const isOverdue = ins.overdueCount > 0
const overdueActive = filterHorizon === 'OVERDUE'
return ( return (
<Box className="flex gap-3"> <Box sx={{ display: 'flex', gap: 2, alignItems: 'stretch' }}>
<KpiCard {/* Focal card — Überfällig */}
icon={<AlertOctagon size={18} color="#dc2626" />} <Box
label="Überfällig"
value={ins.overdueCount}
color="#dc2626"
active={filterHorizon === 'OVERDUE'}
onClick={() => toggleHorizon('OVERDUE')} onClick={() => toggleHorizon('OVERDUE')}
/> sx={{
<KpiCard flex: '0 0 200px',
icon={<Calendar size={18} color="#ea580c" />} px: 2.5,
label="Diese Woche" py: 2,
value={ins.dueThisWeek} display: 'flex',
color="#ea580c" flexDirection: 'column',
active={filterHorizon === 'THIS_WEEK'} justifyContent: 'center',
onClick={() => toggleHorizon('THIS_WEEK')} gap: 0.5,
/> borderRadius: 1.5,
<KpiCard cursor: 'pointer',
icon={<CalendarDays size={18} color="#0369a1" />} border: overdueActive
label="Dieser Monat" ? '1.5px solid #dc2626'
value={ins.dueThisMonth} : isOverdue
color="#0369a1" ? '1.5px solid #fca5a5'
active={filterHorizon === 'THIS_MONTH'} : '1px solid #e8e7e4',
onClick={() => toggleHorizon('THIS_MONTH')} bgcolor: overdueActive
/> ? '#fef2f2'
<KpiCard : isOverdue
icon={<Eye size={18} color="#be185d" />} ? '#fff5f5'
label="Pre-Market Risiko" : 'white',
value={ins.schattenmarktReadyCount} transition: 'border-color 0.15s, background 0.15s',
color="#be185d" '&:hover': { borderColor: '#dc2626', bgcolor: '#fef2f2' },
active={filterType === ReminderType.SCHATTENMARKT_RELEASE} }}
onClick={togglePreMarket} >
/> <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> </Box>
) )
} }
+19 -22
View File
@@ -70,10 +70,10 @@ export const ReminderListRow = memo(function ReminderListRow({ reminder }: Props
onClick={handleRowClick} onClick={handleRowClick}
sx={{ sx={{
display: 'grid', display: 'grid',
gridTemplateColumns: '100px 130px 1fr 140px 90px 100px 90px', gridTemplateColumns: '90px 130px 1fr 140px 80px 90px',
alignItems: 'center', alignItems: 'center',
gap: 1, gap: 1,
pl: 1.5, pl: 2,
pr: 2, pr: 2,
py: 1.25, py: 1.25,
borderBottom: '1px solid #f1f5f9', borderBottom: '1px solid #f1f5f9',
@@ -84,8 +84,8 @@ export const ReminderListRow = memo(function ReminderListRow({ reminder }: Props
transition: 'background-color 0.1s', transition: 'background-color 0.1s',
}} }}
> >
{/* Priority badge */} {/* Priority dot */}
<Box> <Box sx={{ display: 'flex', alignItems: 'center' }}>
<ReminderPriorityBadge priority={reminder.priority} /> <ReminderPriorityBadge priority={reminder.priority} />
</Box> </Box>
@@ -112,25 +112,22 @@ export const ReminderListRow = memo(function ReminderListRow({ reminder }: Props
<ReminderDaysIndicator dueDate={reminder.dueDate} /> <ReminderDaysIndicator dueDate={reminder.dueDate} />
</Box> </Box>
{/* Area */} {/* Status — only shown for non-default states */}
<Typography variant="caption" color="text.secondary" sx={{ whiteSpace: 'nowrap' }}>
{reminder.areaSqm.toLocaleString('de-CH')} m²
</Typography>
{/* Status chip */}
<Box> <Box>
<Chip {reminder.status !== ReminderStatus.ACTIVE && (
label={STATUS_LABEL[reminder.status]} <Chip
size="small" label={STATUS_LABEL[reminder.status]}
sx={{ size="small"
height: 20, sx={{
fontSize: '0.7rem', height: 20,
fontWeight: 500, fontSize: '0.7rem',
bgcolor: STATUS_STYLE[reminder.status]?.bg ?? '#f8fafc', fontWeight: 500,
color: STATUS_STYLE[reminder.status]?.color ?? '#64748b', bgcolor: STATUS_STYLE[reminder.status]?.bg ?? '#f8fafc',
border: `1px solid ${STATUS_STYLE[reminder.status]?.border ?? '#e2e8f0'}`, color: STATUS_STYLE[reminder.status]?.color ?? '#64748b',
}} border: `1px solid ${STATUS_STYLE[reminder.status]?.border ?? '#e2e8f0'}`,
/> }}
/>
)}
</Box> </Box>
{/* Actions */} {/* Actions */}
@@ -5,7 +5,7 @@ const CONFIG: Record<ReminderPriority, { color: string; label: string }> = {
URGENT: { color: '#dc2626', label: 'Dringend' }, URGENT: { color: '#dc2626', label: 'Dringend' },
HIGH: { color: '#ea580c', label: 'Hoch' }, HIGH: { color: '#ea580c', label: 'Hoch' },
MEDIUM: { color: '#ca8a04', label: 'Mittel' }, MEDIUM: { color: '#ca8a04', label: 'Mittel' },
LOW: { color: '#64748b', label: 'Niedrig' }, LOW: { color: '#94a3b8', label: 'Niedrig' },
} }
interface Props { interface Props {
@@ -15,9 +15,9 @@ interface Props {
export function ReminderPriorityBadge({ priority }: Props) { export function ReminderPriorityBadge({ priority }: Props) {
const { color, label } = CONFIG[priority] const { color, label } = CONFIG[priority]
return ( return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.6 }}>
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: color, flexShrink: 0 }} /> <Box sx={{ width: 7, height: 7, borderRadius: '50%', bgcolor: color, flexShrink: 0 }} />
<Typography variant="caption" sx={{ color, fontWeight: 600, fontSize: '0.7rem' }}> <Typography sx={{ color, fontWeight: 600, fontSize: '0.7rem', lineHeight: 1, whiteSpace: 'nowrap' }}>
{label} {label}
</Typography> </Typography>
</Box> </Box>
+16
View File
@@ -1,5 +1,6 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { reminderService } from '../services/reminderService' import { reminderService } from '../services/reminderService'
import type { Reminder } from '../domain/reminder'
import { useToastStore } from '../stores/toastStore' import { useToastStore } from '../stores/toastStore'
export function useReminders() { export function useReminders() {
@@ -69,6 +70,21 @@ export function useSnoozeReminder() {
}) })
} }
export function useCreateReminder() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: Omit<Reminder, 'id' | 'activity' | 'createdAt' | 'updatedAt'>) =>
reminderService.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reminders'] })
queryClient.invalidateQueries({ queryKey: ['reminder-insights'] })
},
onError: () => {
useToastStore.getState().showToast('Reminder konnte nicht erstellt werden.', 'error')
},
})
}
export function useUpdateReminder() { export function useUpdateReminder() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useMutation({ return useMutation({
+13 -13
View File
@@ -27,7 +27,7 @@ export const mockReminders: Reminder[] = [
schattenmarktEnabled: false, schattenmarktEnabled: false,
note: 'Mieter hat bisher keine Verlängerungsabsicht signalisiert. Erstkontakt dringend.', note: 'Mieter hat bisher keine Verlängerungsabsicht signalisiert. Erstkontakt dringend.',
activity: [ activity: [
{ at: '2026-04-01T08:00:00Z', by: 'Anna Meier', action: 'CREATED', note: 'Automatisch aus Vertragsdaten erstellt' }, { at: '2026-04-01T08:00:00Z', by: 'System', action: 'CREATED', note: 'Automatisch aus Vertragsdaten erstellt' },
{ at: '2026-05-10T14:30:00Z', by: 'Thomas Huber', action: 'NOTED', note: 'Mieter angerufen, kein Rückruf erhalten' }, { at: '2026-05-10T14:30:00Z', by: 'Thomas Huber', action: 'NOTED', note: 'Mieter angerufen, kein Rückruf erhalten' },
], ],
organizationId: 'org-wincasa', organizationId: 'org-wincasa',
@@ -56,7 +56,7 @@ export const mockReminders: Reminder[] = [
schattenmarktEnabled: true, schattenmarktEnabled: true,
note: 'Break-Option läuft am 01.09 ab — Frist zur Ausübung ist 90 Tage vorher, also bis 03.06.', note: 'Break-Option läuft am 01.09 ab — Frist zur Ausübung ist 90 Tage vorher, also bis 03.06.',
activity: [ activity: [
{ at: '2026-03-15T09:00:00Z', by: 'Anna Meier', action: 'CREATED' }, { at: '2026-03-15T09:00:00Z', by: 'System', action: 'CREATED', note: 'Automatisch aus Vertragsdaten erstellt' },
{ at: '2026-05-05T11:00:00Z', by: 'Anna Meier', action: 'NOTED', note: 'Mieterdossier vorbereitet' }, { at: '2026-05-05T11:00:00Z', by: 'Anna Meier', action: 'NOTED', note: 'Mieterdossier vorbereitet' },
], ],
organizationId: 'org-wincasa', organizationId: 'org-wincasa',
@@ -83,7 +83,7 @@ export const mockReminders: Reminder[] = [
schattenmarktEnabled: false, schattenmarktEnabled: false,
note: 'Gebäudeversicherung läuft am 30.06 ab. Police-Nummer ZG-2022-9912.', note: 'Gebäudeversicherung läuft am 30.06 ab. Police-Nummer ZG-2022-9912.',
activity: [ activity: [
{ at: '2026-04-20T10:00:00Z', by: 'Sandra Wyss', action: 'CREATED' }, { at: '2026-04-20T10:00:00Z', by: 'System', action: 'CREATED', note: 'Automatisch aus Vertragsdaten erstellt' },
{ at: '2026-05-12T15:00:00Z', by: 'Sandra Wyss', action: 'NOTED', note: 'Offerte von Mobiliar angefordert' }, { at: '2026-05-12T15:00:00Z', by: 'Sandra Wyss', action: 'NOTED', note: 'Offerte von Mobiliar angefordert' },
], ],
organizationId: 'org-wincasa', organizationId: 'org-wincasa',
@@ -140,7 +140,7 @@ export const mockReminders: Reminder[] = [
schattenmarktEnabled: true, schattenmarktEnabled: true,
note: 'Indexierte Mietanpassung per 01.10 möglich. LIK-Index prüfen.', note: 'Indexierte Mietanpassung per 01.10 möglich. LIK-Index prüfen.',
activity: [ activity: [
{ at: '2026-04-05T08:00:00Z', by: 'Anna Meier', action: 'CREATED' }, { at: '2026-04-05T08:00:00Z', by: 'System', action: 'CREATED', note: 'Automatisch aus Vertragsdaten erstellt' },
{ at: '2026-05-18T09:00:00Z', by: 'Anna Meier', action: 'NOTED', note: 'LIK-Daten für Q1 2026 abrufbar' }, { at: '2026-05-18T09:00:00Z', by: 'Anna Meier', action: 'NOTED', note: 'LIK-Daten für Q1 2026 abrufbar' },
], ],
organizationId: 'org-wincasa', organizationId: 'org-wincasa',
@@ -168,7 +168,7 @@ export const mockReminders: Reminder[] = [
schattenmarktEnabled: false, schattenmarktEnabled: false,
note: 'Frist für Vertragsverhandlung: 9 Monate vor Ablauf. Markt Bern Industrie angespannt.', note: 'Frist für Vertragsverhandlung: 9 Monate vor Ablauf. Markt Bern Industrie angespannt.',
activity: [ activity: [
{ at: '2026-03-01T08:00:00Z', by: 'Sandra Wyss', action: 'CREATED' }, { at: '2026-03-01T08:00:00Z', by: 'System', action: 'CREATED', note: 'Automatisch aus Vertragsdaten erstellt' },
], ],
organizationId: 'org-wincasa', organizationId: 'org-wincasa',
createdAt: '2026-03-01T08:00:00Z', createdAt: '2026-03-01T08:00:00Z',
@@ -195,7 +195,7 @@ export const mockReminders: Reminder[] = [
schattenmarktEnabled: false, schattenmarktEnabled: false,
note: 'Einheit noch nicht für Pre-Market freigegeben. 14 Monate Lead Time empfohlen.', note: 'Einheit noch nicht für Pre-Market freigegeben. 14 Monate Lead Time empfohlen.',
activity: [ activity: [
{ at: '2026-04-18T10:00:00Z', by: 'Thomas Huber', action: 'CREATED', note: 'Pre-Market-Freigabe ausstehend' }, { at: '2026-04-18T10:00:00Z', by: 'System', action: 'CREATED', note: 'Automatisch aus Vertragsdaten & Marktdaten erstellt' },
{ at: '2026-05-02T11:00:00Z', by: 'Thomas Huber', action: 'NOTED', note: 'Eigentümer informiert, Freigabe ausstehend' }, { at: '2026-05-02T11:00:00Z', by: 'Thomas Huber', action: 'NOTED', note: 'Eigentümer informiert, Freigabe ausstehend' },
], ],
organizationId: 'org-wincasa', organizationId: 'org-wincasa',
@@ -250,7 +250,7 @@ export const mockReminders: Reminder[] = [
snoozedUntil: '2026-05-27', snoozedUntil: '2026-05-27',
note: 'Mieter erwägt Flächenreduktion. Gespräch vereinbart für 27.05.', note: 'Mieter erwägt Flächenreduktion. Gespräch vereinbart für 27.05.',
activity: [ activity: [
{ at: '2026-04-02T09:00:00Z', by: 'Anna Meier', action: 'CREATED' }, { at: '2026-04-02T09:00:00Z', by: 'System', action: 'CREATED', note: 'Automatisch aus Vertragsdaten erstellt' },
{ at: '2026-05-14T16:00:00Z', by: 'Anna Meier', action: 'SNOOZED', note: 'Bis nach Gespräch mit Mieter zurückgestellt' }, { at: '2026-05-14T16:00:00Z', by: 'Anna Meier', action: 'SNOOZED', note: 'Bis nach Gespräch mit Mieter zurückgestellt' },
], ],
organizationId: 'org-wincasa', organizationId: 'org-wincasa',
@@ -279,7 +279,7 @@ export const mockReminders: Reminder[] = [
shadowMarketRisk: ShadowMarketRisk.LOW, shadowMarketRisk: ShadowMarketRisk.LOW,
schattenmarktEnabled: true, schattenmarktEnabled: true,
activity: [ activity: [
{ at: '2026-03-20T10:00:00Z', by: 'Sandra Wyss', action: 'CREATED' }, { at: '2026-03-20T10:00:00Z', by: 'System', action: 'CREATED', note: 'Automatisch aus Vertragsdaten erstellt' },
], ],
organizationId: 'org-wincasa', organizationId: 'org-wincasa',
createdAt: '2026-03-20T10:00:00Z', createdAt: '2026-03-20T10:00:00Z',
@@ -306,7 +306,7 @@ export const mockReminders: Reminder[] = [
schattenmarktEnabled: false, schattenmarktEnabled: false,
note: 'Erstgespräch über Verlängerung bis 01.07 einleiten.', note: 'Erstgespräch über Verlängerung bis 01.07 einleiten.',
activity: [ activity: [
{ at: '2026-03-10T08:00:00Z', by: 'Thomas Huber', action: 'CREATED' }, { at: '2026-03-10T08:00:00Z', by: 'System', action: 'CREATED', note: 'Automatisch aus Vertragsdaten erstellt' },
{ at: '2026-05-02T09:00:00Z', by: 'Thomas Huber', action: 'NOTED', note: 'Vermieterseite wünscht Mietpreiserhöhung +5%' }, { at: '2026-05-02T09:00:00Z', by: 'Thomas Huber', action: 'NOTED', note: 'Vermieterseite wünscht Mietpreiserhöhung +5%' },
], ],
organizationId: 'org-wincasa', organizationId: 'org-wincasa',
@@ -359,7 +359,7 @@ export const mockReminders: Reminder[] = [
schattenmarktEnabled: false, schattenmarktEnabled: false,
note: 'Pre-Market Freigabe 9 Monate vor Vertragsende. Eigentümer-Freigabe einholen.', note: 'Pre-Market Freigabe 9 Monate vor Vertragsende. Eigentümer-Freigabe einholen.',
activity: [ activity: [
{ at: '2026-04-25T11:00:00Z', by: 'Anna Meier', action: 'CREATED' }, { at: '2026-04-25T11:00:00Z', by: 'System', action: 'CREATED', note: 'Automatisch aus Vertragsdaten & Marktdaten erstellt' },
], ],
organizationId: 'org-wincasa', organizationId: 'org-wincasa',
createdAt: '2026-04-25T11:00:00Z', createdAt: '2026-04-25T11:00:00Z',
@@ -415,7 +415,7 @@ export const mockReminders: Reminder[] = [
shadowMarketRisk: ShadowMarketRisk.MEDIUM, shadowMarketRisk: ShadowMarketRisk.MEDIUM,
schattenmarktEnabled: true, schattenmarktEnabled: true,
activity: [ activity: [
{ at: '2026-02-01T08:00:00Z', by: 'Anna Meier', action: 'CREATED', note: 'Vertragsablauf-Erinnerung (4 Monate)' }, { at: '2026-02-01T08:00:00Z', by: 'System', action: 'CREATED', note: 'Automatisch aus Vertragsdaten erstellt' },
], ],
organizationId: 'org-wincasa', organizationId: 'org-wincasa',
createdAt: '2026-02-01T08:00:00Z', createdAt: '2026-02-01T08:00:00Z',
@@ -440,7 +440,7 @@ export const mockReminders: Reminder[] = [
shadowMarketRisk: ShadowMarketRisk.NONE, shadowMarketRisk: ShadowMarketRisk.NONE,
schattenmarktEnabled: false, schattenmarktEnabled: false,
activity: [ activity: [
{ at: '2026-04-20T08:00:00Z', by: 'Sandra Wyss', action: 'CREATED' }, { at: '2026-04-20T08:00:00Z', by: 'System', action: 'CREATED', note: 'Automatisch aus Vertragsdaten erstellt' },
], ],
organizationId: 'org-wincasa', organizationId: 'org-wincasa',
createdAt: '2026-04-20T08:00:00Z', createdAt: '2026-04-20T08:00:00Z',
@@ -522,7 +522,7 @@ export const mockReminders: Reminder[] = [
schattenmarktEnabled: false, schattenmarktEnabled: false,
note: 'Mietpreisanpassung +2.8% vereinbart, ab 01.09 gültig.', note: 'Mietpreisanpassung +2.8% vereinbart, ab 01.09 gültig.',
activity: [ activity: [
{ at: '2026-02-15T08:00:00Z', by: 'Anna Meier', action: 'CREATED' }, { at: '2026-02-15T08:00:00Z', by: 'System', action: 'CREATED', note: 'Automatisch aus Vertragsdaten erstellt' },
{ at: '2026-04-14T11:00:00Z', by: 'Anna Meier', action: 'NOTED', note: 'Mieter hat Anpassung akzeptiert' }, { at: '2026-04-14T11:00:00Z', by: 'Anna Meier', action: 'NOTED', note: 'Mieter hat Anpassung akzeptiert' },
{ at: '2026-04-15T14:00:00Z', by: 'Anna Meier', action: 'COMPLETED', note: 'Nachtrag unterzeichnet' }, { at: '2026-04-15T14:00:00Z', by: 'Anna Meier', action: 'COMPLETED', note: 'Nachtrag unterzeichnet' },
], ],
+1 -1
View File
@@ -221,7 +221,7 @@ export default function AISearch() {
{/* IDLE: full form */} {/* IDLE: full form */}
{(step === NeedBuilderStep.IDLE || step === NeedBuilderStep.PARSING) && ( {(step === NeedBuilderStep.IDLE || step === NeedBuilderStep.PARSING) && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, maxWidth: 1200, mx: 'auto' }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, maxWidth: { md: 900, xl: 1200 }, mx: 'auto' }}>
<VoiceNeedInput <VoiceNeedInput
text={inputText} text={inputText}
onTextChange={handleTextChange} onTextChange={handleTextChange}
+2 -2
View File
@@ -134,8 +134,8 @@ export default function Anfragen() {
{/* ── Left panel ── */} {/* ── Left panel ── */}
<Box <Box
sx={{ sx={{
width: { xs: mobileShowChat ? 0 : '100%', md: 320 }, width: { xs: mobileShowChat ? 0 : '100%', md: 280, lg: 300, xl: 320 },
minWidth: { md: 320 }, minWidth: { md: 280, lg: 300, xl: 320 },
flexShrink: 0, flexShrink: 0,
display: 'flex', flexDirection: 'column', display: 'flex', flexDirection: 'column',
borderRight: `1px solid ${DS_BORDER.default}`, borderRight: `1px solid ${DS_BORDER.default}`,
+1 -1
View File
@@ -171,7 +171,7 @@ export default function MatchDetail() {
/> />
{/* ── Main content ── */} {/* ── Main content ── */}
<Box sx={{ maxWidth: { sm: 780, md: 960, lg: 1100 }, mx: 'auto', px: { xs: 1.5, sm: 3 }, py: { xs: 2, sm: 3 }, display: 'flex', flexDirection: 'column', gap: 2 }}> <Box sx={{ maxWidth: { sm: 700, md: 860, lg: 1000, xl: 1100 }, mx: 'auto', px: { xs: 1.5, sm: 3 }, py: { xs: 2, sm: 3 }, display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* ── Section A: Warum dieser Match? ── */} {/* ── Section A: Warum dieser Match? ── */}
<Paper sx={{ p: 2.5 }}> <Paper sx={{ p: 2.5 }}>
+1 -1
View File
@@ -109,7 +109,7 @@ export default function Pipeline() {
return ( return (
<Box <Box
key={stage.key} key={stage.key}
sx={{ minWidth: syncedSelected ? 190 : 230, maxWidth: syncedSelected ? 230 : 270, flexShrink: 0, display: 'flex', flexDirection: 'column' }} sx={{ minWidth: syncedSelected ? 170 : 200, maxWidth: syncedSelected ? 210 : 260, flexShrink: 0, display: 'flex', flexDirection: 'column' }}
> >
<Box sx={{ py: 1, display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ py: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography sx={{ fontWeight: 700, color: stage.color, fontSize: '0.8125rem' }}> <Typography sx={{ fontWeight: 700, color: stage.color, fontSize: '0.8125rem' }}>
+1 -1
View File
@@ -425,7 +425,7 @@ export default function MyListings() {
anchor="right" anchor="right"
open={!!detailId} open={!!detailId}
onClose={() => setDetailId(null)} onClose={() => setDetailId(null)}
slotProps={{ paper: { sx: { width: { xs: '100%', sm: 560 } } } }} slotProps={{ paper: { sx: { width: { xs: '100%', sm: '90vw', md: 480, lg: 540, xl: 560 } } } }}
> >
{detailId && ( {detailId && (
<PropertyDetailView propertyId={detailId} onClose={() => setDetailId(null)} hideTabs={['Matchability', 'Marktsignale']} /> <PropertyDetailView propertyId={detailId} onClose={() => setDetailId(null)} hideTabs={['Matchability', 'Marktsignale']} />
+1 -1
View File
@@ -247,7 +247,7 @@ export default function Properties() {
anchor="right" anchor="right"
open={!!selectedId} open={!!selectedId}
onClose={() => setSelectedId(null)} onClose={() => setSelectedId(null)}
slotProps={{ paper: { sx: { width: isMobile ? '100vw' : 650, boxShadow: '-4px 0 24px rgba(0,0,0,0.10)' } } }} slotProps={{ paper: { sx: { width: isMobile ? '100vw' : { md: 520, lg: 580, xl: 640 }, boxShadow: '-4px 0 24px rgba(0,0,0,0.10)' } } }}
> >
{selectedId && ( {selectedId && (
<PropertyDetailView propertyId={selectedId} onClose={() => setSelectedId(null)} /> <PropertyDetailView propertyId={selectedId} onClose={() => setSelectedId(null)} />
+2
View File
@@ -17,6 +17,7 @@ interface LayoutState {
// Actions // Actions
setActiveWorkspace: (workspace: WorkspaceType) => void setActiveWorkspace: (workspace: WorkspaceType) => void
toggleSidebar: () => void toggleSidebar: () => void
setSidebarCollapsed: (collapsed: boolean) => void
openRightPanel: (type: RightPanelContentType) => void openRightPanel: (type: RightPanelContentType) => void
closeRightPanel: () => void closeRightPanel: () => void
toggleRightPanel: (type: RightPanelContentType) => void toggleRightPanel: (type: RightPanelContentType) => void
@@ -30,6 +31,7 @@ export const useLayoutStore = create<LayoutState>((set, get) => ({
setActiveWorkspace: (workspace) => set({ activeWorkspace: workspace }), setActiveWorkspace: (workspace) => set({ activeWorkspace: workspace }),
toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })), toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),
setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }),
openRightPanel: (type) => set({ isRightPanelOpen: true, rightPanelContentType: type }), openRightPanel: (type) => set({ isRightPanelOpen: true, rightPanelContentType: type }),
closeRightPanel: () => set({ isRightPanelOpen: false, rightPanelContentType: null }), closeRightPanel: () => set({ isRightPanelOpen: false, rightPanelContentType: null }),
toggleRightPanel: (type) => { toggleRightPanel: (type) => {