diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index f32cdf6..9532d69 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -21,16 +21,21 @@ export function AppShell() { const sidebarCollapsed = useLayoutStore(s => s.sidebarCollapsed) const setActiveWorkspace = useLayoutStore(s => s.setActiveWorkspace) const toggleSidebar = useLayoutStore(s => s.toggleSidebar) + const setSidebarCollapsed = useLayoutStore(s => s.setSidebarCollapsed) const { currentUser } = useSessionStore() const navigate = useNavigate() const location = useLocation() 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) // Close mobile menu on route change useEffect(() => { setMobileOpen(false) }, [location.pathname]) + // Auto-collapse sidebar on compact screens (laptops < 1536px) + useEffect(() => { setSidebarCollapsed(isCompact) }, [isCompact, setSidebarCollapsed]) + // Sync active workspace with URL useEffect(() => { const detected = getWorkspaceFromPath(location.pathname) diff --git a/src/components/layout/RightContextPanel.tsx b/src/components/layout/RightContextPanel.tsx index 6222df3..d67c5b3 100644 --- a/src/components/layout/RightContextPanel.tsx +++ b/src/components/layout/RightContextPanel.tsx @@ -32,8 +32,8 @@ export function RightContextPanel() { right: 0, top: 56, height: 'calc(100vh - 56px)', - width: 320, - transform: isRightPanelOpen ? 'translateX(0)' : 'translateX(320px)', + width: { xs: '85vw', sm: 300, lg: 320 }, + transform: isRightPanelOpen ? 'translateX(0)' : 'translateX(110%)', transition: 'transform 0.25s ease', bgcolor: '#fff', borderLeft: '1px solid #e2e8f0', diff --git a/src/components/pipeline/PipelineDetailPanel.tsx b/src/components/pipeline/PipelineDetailPanel.tsx index 8c74fb5..b69ea4a 100644 --- a/src/components/pipeline/PipelineDetailPanel.tsx +++ b/src/components/pipeline/PipelineDetailPanel.tsx @@ -32,7 +32,7 @@ export function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: () return ( diff --git a/src/components/supply/ReminderDetailDrawer.tsx b/src/components/supply/ReminderDetailDrawer.tsx index 618d0d8..cc26178 100644 --- a/src/components/supply/ReminderDetailDrawer.tsx +++ b/src/components/supply/ReminderDetailDrawer.tsx @@ -1,26 +1,185 @@ import { useState } from 'react' import { - Drawer, - Box, - Typography, - IconButton, - Chip, - Divider, - TextField, - Button, - Switch, - FormControlLabel, - Tooltip, + Drawer, Box, Typography, IconButton, Chip, Divider, + TextField, Button, Select, MenuItem, FormControl, InputLabel, Autocomplete, } 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 { useReminder, useCompleteReminder, useDismissReminder, useSnoozeReminder } from '../../hooks/useReminders' -import { usePropertyById } from '../../hooks/useProperties' +import { + useReminder, useCompleteReminder, useDismissReminder, + useSnoozeReminder, useCreateReminder, +} from '../../hooks/useReminders' +import { usePropertyById, useProperties } from '../../hooks/useProperties' import { ReminderPriorityBadge } from './ReminderPriorityBadge' import { ReminderTypeBadge } from './ReminderTypeBadge' import { ReminderDaysIndicator } from './ReminderDaysIndicator' -import { ReminderStatus } from '../../domain/reminder' -import { SHADOW_RISK_COLOR, SHADOW_RISK_LABEL, STATUS_CHIP_COLOR, STATUS_LABEL, SectionTitle, DateRow, ActivityEntry } from './reminderDetailHelpers' +import { ReminderStatus, ReminderType, ReminderPriority } from '../../domain/reminder' +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 = { + 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> = { + [ReminderType.LEASE_EXPIRY]: 'contractEndDate', + [ReminderType.BREAK_OPTION]: 'breakOptionDate', + [ReminderType.RENT_REVIEW]: 'eventDate', + [ReminderType.SCHATTENMARKT_RELEASE]: 'eventDate', +} + +const TYPE_SECOND_LABEL: Partial> = { + [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.LEASE_EXPIRY) + const [selectedProperty, setSelectedProperty] = useState(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 ( + + {/* Typ */} + + Typ + + + + {/* Objekt */} + p.title} + value={selectedProperty} + onChange={handlePropertyChange} + size="small" + renderInput={params => ( + + )} + /> + + {/* Mieter — nur wenn Objekt ausgewählt */} + {selectedProperty && ( + setTenantName(e.target.value)} + placeholder="Mietername eingeben…" + /> + )} + + {/* Fälligkeitsdatum */} + setDueDate(e.target.value)} + slotProps={{ inputLabel: { shrink: true } }} + /> + + {/* Notiz */} + setNote(e.target.value)} + placeholder="Kontext oder Hinweise…" + /> + + + + ) +} + +// ── Main Drawer ─────────────────────────────────────────────────────────────── export function ReminderDetailDrawer() { const { selectedId, drawerOpen, setDrawerOpen, setSelectedId } = useReminderStore() @@ -38,74 +197,79 @@ export function ReminderDetailDrawer() { 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 ( - {/* Header */} - - - {isNew ? ( - Neuer Reminder - ) : reminder ? ( - <> - - - - - - - - ) : null} + {/* ── Header ── */} + + + + {isCreateMode ? ( + + Neuer Reminder + + ) : reminder ? ( + <> + + + + + + + + ) : null} + + + + - - - - {/* Scrollable body */} - - {isNew && ( - - Neue Reminder-Erstellung noch nicht implementiert. - - )} + {/* ── Body ── */} + + {/* Create mode */} + {isCreateMode && } + + {/* Detail mode */} {reminder && ( - <> - {/* 2. Property */} - - }>Objekt - - {reminder.propertyTitle} - - {reminder.propertyCity}{reminder.propertyDistrict ? ` · ${reminder.propertyDistrict}` : ''} - - Mieter: {reminder.tenantName} - Fläche: {reminder.areaSqm.toLocaleString('de-CH')} m² + + {/* 1. Objekt */} + + + {reminder.propertyTitle} + + + {reminder.propertyCity}{reminder.propertyDistrict ? ` · ${reminder.propertyDistrict}` : ''} · {reminder.tenantName} + + + {reminderProperty?.leaseContractUrl ? ( - - + + ) : ( - + - Kein Mietvertrag hinterlegt + Kein Vertrag + + )} + + {showPreMarket && ( + + + + Pre-Mkt: {SHADOW_RISK_LABEL[reminder.shadowMarketRisk]} + )} - + - {/* 3. Dates */} - - }>Daten & Fristen - - - - - + {/* 2. Fristen */} + + }>Fristen + + + {showSecondDate && ( + + )} {reminder.snoozedUntil && ( - + )} - + - {/* 4. Financials */} - - }>Finanzen - - - Miete/m² - - {reminder.currency} {reminder.currentRentPerSqm.toLocaleString('de-CH')} / Monat - - - - Total / Monat - - {reminder.currency} {(reminder.currentRentPerSqm * reminder.areaSqm).toLocaleString('de-CH')} - - - - - - - - {/* 5. Pre-Market */} - - }>Pre-Market Risiko - - - - - {SHADOW_RISK_LABEL[reminder.shadowMarketRisk]} - - - - } - label={ - - Pre-Market aktiviert - - } - /> - - - - - - - {/* 6. Note */} - - Notiz + {/* 3. Notiz */} + + + Notiz + - {/* Actions */} - {(reminder.status === ReminderStatus.ACTIVE || reminder.status === ReminderStatus.SNOOZED) && ( + {/* 4. Aktionen */} + {isActionable && ( <> - - - Aktionen - + + + + Aktionen + +