36570c5bdc
- Fix MUI v9 API: PaperProps/InputLabelProps/inputProps → slotProps in 6 components - Add ExternalMarketResult alias, PropertyUnit import, OPERATIONS workspace config - Fix TradeOff.description → .concern, ScoreFactor.label → .criterion - Make schattenmarktRelease.leadTimeMonths optional, fix mock-data enum values - Fix useMatchDetailData query typing, weightingService missing WeightProfile keys - Split Pipeline/Compare/MatchDetail/IntelligenceMatchCard into sub-components - Fix all test fixtures (CreateNeedInput, CreatePropertyInput, TradeOffInput, etc.) - Add vercel.json for deployment, zero tsc errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
281 lines
11 KiB
TypeScript
281 lines
11 KiB
TypeScript
import { useState } from 'react'
|
|
import {
|
|
Drawer,
|
|
Box,
|
|
Typography,
|
|
IconButton,
|
|
Chip,
|
|
Divider,
|
|
TextField,
|
|
Button,
|
|
Switch,
|
|
FormControlLabel,
|
|
Tooltip,
|
|
} from '@mui/material'
|
|
import { X, MapPin, Calendar, DollarSign, Eye, Activity, FileText, ExternalLink } from 'lucide-react'
|
|
import { useReminderStore } from '../../stores/reminderStore'
|
|
import { useReminder, useCompleteReminder, useDismissReminder, useSnoozeReminder } from '../../hooks/useReminders'
|
|
import { usePropertyById } 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'
|
|
|
|
export function ReminderDetailDrawer() {
|
|
const { selectedId, drawerOpen, setDrawerOpen, setSelectedId } = useReminderStore()
|
|
const { data: reminder } = useReminder(selectedId ?? '')
|
|
const { data: reminderProperty } = usePropertyById(reminder?.propertyId ?? '')
|
|
const complete = useCompleteReminder()
|
|
const dismiss = useDismissReminder()
|
|
const snooze = useSnoozeReminder()
|
|
|
|
const [noteValue, setNoteValue] = useState('')
|
|
const [snoozeDate, setSnoozeDate] = useState('')
|
|
|
|
function handleClose() {
|
|
setDrawerOpen(false)
|
|
setSelectedId(null)
|
|
}
|
|
|
|
const isNew = !selectedId
|
|
|
|
return (
|
|
<Drawer
|
|
anchor="right"
|
|
open={drawerOpen}
|
|
onClose={handleClose}
|
|
slotProps={{ paper: { sx: { width: 480, display: 'flex', flexDirection: 'column' } } }}
|
|
>
|
|
{/* Header */}
|
|
<Box
|
|
sx={{
|
|
px: 2.5,
|
|
py: 2,
|
|
borderBottom: '1px solid #e2e8f0',
|
|
display: 'flex',
|
|
alignItems: 'flex-start',
|
|
gap: 1,
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0 }}>
|
|
{isNew ? (
|
|
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>Neuer Reminder</Typography>
|
|
) : reminder ? (
|
|
<>
|
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', alignItems: 'center' }}>
|
|
<ReminderTypeBadge type={reminder.type} />
|
|
<ReminderPriorityBadge priority={reminder.priority} />
|
|
<Chip
|
|
label={STATUS_LABEL[reminder.status]}
|
|
color={STATUS_CHIP_COLOR[reminder.status]}
|
|
size="small"
|
|
sx={{ height: 20, fontSize: '0.7rem' }}
|
|
/>
|
|
</Box>
|
|
<ReminderDaysIndicator dueDate={reminder.dueDate} />
|
|
</>
|
|
) : null}
|
|
</Box>
|
|
<IconButton size="small" onClick={handleClose} sx={{ flexShrink: 0 }}>
|
|
<X size={18} />
|
|
</IconButton>
|
|
</Box>
|
|
|
|
{/* Scrollable body */}
|
|
<Box sx={{ flex: 1, overflowY: 'auto', p: 2.5 }} className="flex flex-col gap-5">
|
|
{isNew && (
|
|
<Typography variant="body2" color="text.secondary">
|
|
Neue Reminder-Erstellung noch nicht implementiert.
|
|
</Typography>
|
|
)}
|
|
|
|
{reminder && (
|
|
<>
|
|
{/* 2. Property */}
|
|
<Box>
|
|
<SectionTitle icon={<MapPin size={15} color="#64748b" />}>Objekt</SectionTitle>
|
|
<Box className="flex flex-col gap-1">
|
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>{reminder.propertyTitle}</Typography>
|
|
<Typography variant="caption" color="text.secondary">
|
|
{reminder.propertyCity}{reminder.propertyDistrict ? ` · ${reminder.propertyDistrict}` : ''}
|
|
</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>
|
|
{reminderProperty?.leaseContractUrl ? (
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mt: 0.5 }}>
|
|
<FileText size={12} color="#1e3a5f" />
|
|
<Button
|
|
size="small"
|
|
variant="outlined"
|
|
endIcon={<ExternalLink size={10} />}
|
|
href={reminderProperty.leaseContractUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
sx={{ textTransform: 'none', fontSize: '0.68rem', py: 0.125, px: 0.75, borderColor: '#cbd5e1', color: '#1e3a5f' }}
|
|
>
|
|
{reminderProperty.leaseContractName ?? 'Mietvertrag'}
|
|
</Button>
|
|
</Box>
|
|
) : (
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.5 }}>
|
|
<FileText size={12} color="#cbd5e1" />
|
|
<Typography variant="caption" sx={{ color: '#cbd5e1' }}>Kein Mietvertrag hinterlegt</Typography>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
|
|
<Divider />
|
|
|
|
{/* 3. Dates */}
|
|
<Box>
|
|
<SectionTitle icon={<Calendar size={15} color="#64748b" />}>Daten & Fristen</SectionTitle>
|
|
<Box className="flex flex-col gap-1.5">
|
|
<DateRow label="Fälligkeitsdatum" value={reminder.dueDate} />
|
|
<DateRow label="Ereignisdatum" value={reminder.eventDate} />
|
|
<DateRow label="Vertragsende" value={reminder.contractEndDate} />
|
|
<DateRow label="Break-Option" value={reminder.breakOptionDate} />
|
|
{reminder.snoozedUntil && (
|
|
<DateRow label="Schlummern bis" value={reminder.snoozedUntil} />
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
|
|
<Divider />
|
|
|
|
{/* 4. Financials */}
|
|
<Box>
|
|
<SectionTitle icon={<DollarSign size={15} color="#64748b" />}>Finanzen</SectionTitle>
|
|
<Box className="flex flex-col gap-1.5">
|
|
<Box sx={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: 1 }}>
|
|
<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
|
|
multiline
|
|
rows={3}
|
|
fullWidth
|
|
size="small"
|
|
value={noteValue || (reminder.note ?? '')}
|
|
onChange={e => setNoteValue(e.target.value)}
|
|
placeholder="Notiz hinzufügen…"
|
|
sx={{ '& .MuiInputBase-root': { fontSize: '0.8125rem' } }}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Actions */}
|
|
{(reminder.status === ReminderStatus.ACTIVE || reminder.status === ReminderStatus.SNOOZED) && (
|
|
<>
|
|
<Divider />
|
|
<Box className="flex flex-col gap-2">
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.8125rem' }}>Aktionen</Typography>
|
|
<Box className="flex gap-2">
|
|
<Button
|
|
size="small"
|
|
variant="contained"
|
|
color="success"
|
|
sx={{ textTransform: 'none', flex: 1 }}
|
|
onClick={() => complete.mutate({ id: reminder.id, note: noteValue || undefined })}
|
|
>
|
|
Erledigt
|
|
</Button>
|
|
<Button
|
|
size="small"
|
|
variant="outlined"
|
|
color="error"
|
|
sx={{ textTransform: 'none', flex: 1 }}
|
|
onClick={() => dismiss.mutate({ id: reminder.id, note: noteValue || undefined })}
|
|
>
|
|
Verwerfen
|
|
</Button>
|
|
</Box>
|
|
<Box className="flex gap-2 items-center">
|
|
<TextField
|
|
type="date"
|
|
size="small"
|
|
value={snoozeDate}
|
|
onChange={e => setSnoozeDate(e.target.value)}
|
|
sx={{ flex: 1, '& .MuiInputBase-root': { fontSize: '0.8125rem' } }}
|
|
/>
|
|
<Button
|
|
size="small"
|
|
variant="outlined"
|
|
disabled={!snoozeDate}
|
|
sx={{ textTransform: 'none', whiteSpace: 'nowrap' }}
|
|
onClick={() => snoozeDate && snooze.mutate({ id: reminder.id, until: snoozeDate })}
|
|
>
|
|
Schlummern bis
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
</>
|
|
)}
|
|
|
|
<Divider />
|
|
|
|
{/* 7. Activity */}
|
|
<Box>
|
|
<SectionTitle icon={<Activity size={15} color="#64748b" />}>Aktivitätslog</SectionTitle>
|
|
<Box>
|
|
{[...reminder.activity].reverse().map((entry, i) => (
|
|
<ActivityEntry key={i} entry={entry} />
|
|
))}
|
|
</Box>
|
|
</Box>
|
|
</>
|
|
)}
|
|
</Box>
|
|
</Drawer>
|
|
)
|
|
}
|