Files
property-match/src/components/supply/ReminderDetailDrawer.tsx
T
Benjamin SutterandClaude Opus 5 d200d4e930 feat(agenten): Runde 4 — «Meine Agenten» mit fünf digitalen Mitarbeitenden
Setzt das Umsetzungsbriefing Runde 4 im bestehenden Frontend um.

Informationsarchitektur
- «Teamübersicht» heisst «Meine Agenten»; ihr bisheriger Inhalt (Kreisgrafik,
  Auswertung, Verbindungszusammenfassung) ist entfallen.
- Personalverwaltung, Bearbeitungsverlauf und Kanäle & Systeme sind jetzt
  Reiter derselben Seite (?section=), gestaltet wie die Auswahl im
  Bearbeitungsverlauf. Es wird nur der gewählte Bereich gerendert.
- Die alten Unterseitenpfade leiten um, damit Lesezeichen nicht brechen.

Agentenbestand
- Reto und Lea vollständig entfernt — aus Navigation, Dossiers, Protokoll,
  Verbindungen, Vorgängen und Porträtbestand.
- Retos Aufgaben liegen bei Bruno: Nachbereitung, WhatsApp-Anruf,
  Protokoll und Kundennotiz, Ablage im CRM.
- Livia ist «Exposé Master»: Lageberichte, Inserate, Angebotsbroschüren.
- Sidebar führt Ferdi, Bruno, Livia, Nora, Sina mit Porträt und Funktion.

Agentenseiten
- Ein gemeinsamer AgentWorkspaceHero auf allen fünf Seiten.
- Ferdi: Auswertungskarten, Priorität und Typfarben entfallen; Fälligkeit nur
  bei fünf Tagen oder weniger rot; Objektlinks nach «Meine Objekte»; neu die
  Terminplanung im verbundenen Kalender mit typgerechtem PDF-Ausschnitt.
- Sina: reduzierte, filter- und sortierbare Objektübersicht; Detailansicht
  direkt editierbar, leere Pflichtfelder rot umrandet.
- Nora: Signale ohne Prozentsätze und Konfidenzstufen, nur belegbare Angaben;
  Mehrfachauswahl leitet Objekte an Livia weiter.
- Livia: aktive und archivierte Leads, Arbeitsbereich gleitet an den oberen
  Rand; dreistufiger Exposé-Prozess Hochladen → Exposé → Export.
- Bruno: neue Seite mit Auftragsliste und Vor-/Nachbereitungs-Drawer;
  Glocke warnt bei Besichtigung unter 24 Stunden ohne Bericht.

Datenschicht
- Neu: Kalender, Exposé-Leads, Exposé-Entwürfe, Besichtigungsaufträge —
  je Domain, Provider, Service und Hook.
- IAIService um generateExposeText erweitert; der Entwurf nutzt ausschliesslich
  erfasste Objektdaten und meldet Lücken, statt sie zu füllen.
- Alle Objektverweise zeigen auf reale Einträge aus «Meine Objekte»; neue
  Detailroute /supply/properties/:propertyId.

Offen: Chat, Kalender, CRM und DMS sind Frontend-Simulation ohne Anbindung.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 14:43:16 +02:00

190 lines
8.3 KiB
TypeScript

import { useState } from 'react'
import {
Drawer, Box, Typography, IconButton, Divider, TextField, Button,
} from '@mui/material'
import { X, Calendar } from 'lucide-react'
import { useReminderStore } from '../../stores/reminderStore'
import {
useReminder, useCompleteReminder, useDismissReminder, useSnoozeReminder,
} from '../../hooks/useReminders'
import { usePropertyById } from '../../hooks/useProperties'
import { ObjectDeepLink } from '../team'
import { ReminderTypeBadge } from './ReminderTypeBadge'
import { ReminderDaysIndicator } from './ReminderDaysIndicator'
import { ReminderStatusBadge } from './ReminderStatusBadge'
import { ReminderCalendarPlanner } from './ReminderCalendarPlanner'
import { ReminderStatus, ReminderType } from '../../domain/reminder'
import { SectionTitle, DateRow } from './reminderDetailHelpers'
import { DS_BRAND, DS_SLATE, DS_TEXT } from '../../lib/ds'
// ── Constants ────────────────────────────────────────────────────────────────
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',
}
// ── Drawer ───────────────────────────────────────────────────────────────────
/**
* Reminder-Detailansicht (Runde 4, §5.5 und §5.6).
*
* Entfallen sind: Priorität, das «Aktiv»-Etikett, die zusätzlichen Status- und
* Typfarben, der Verlauf am Ende, der Vertrags- und Pre-Market-Hinweis unter
* dem Objekt sowie der Notizbereich. Das Anlegen neuer Reminder ist mit dem
* Knopf «Reminder erstellen» weggefallen — der Drawer zeigt nur noch Bestehendes.
*
* Neu an der Stelle des Notizbereichs: die Terminplanung im verbundenen Kalender.
*/
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 [snoozeDate, setSnoozeDate] = useState('')
function handleClose() {
setDrawerOpen(false)
setSelectedId(null)
}
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 isActionable = reminder &&
(reminder.status === ReminderStatus.ACTIVE || reminder.status === ReminderStatus.SNOOZED)
return (
<Drawer
anchor="right"
open={drawerOpen}
onClose={handleClose}
slotProps={{ paper: { sx: { width: 460, display: 'flex', flexDirection: 'column' } } }}
>
{/* ── Header ── */}
<Box sx={{ px: 2.5, py: 2, borderBottom: '1px solid #e2e8f0', flexShrink: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0 }}>
{reminder && (
<>
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', alignItems: 'center' }}>
<ReminderTypeBadge type={reminder.type} />
<ReminderStatusBadge status={reminder.status} />
</Box>
<ReminderDaysIndicator dueDate={reminder.dueDate} />
</>
)}
</Box>
<IconButton size="small" onClick={handleClose} sx={{ flexShrink: 0, color: DS_SLATE[400] }}>
<X size={18} />
</IconButton>
</Box>
</Box>
{/* ── Body ── */}
<Box sx={{ flex: 1, overflowY: 'auto', p: 2.5, display: 'flex', flexDirection: 'column' }}>
{reminder && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
{/* 1. Objekt — Name führt nach «Meine Objekte» */}
<Box sx={{ mb: 2.5 }}>
<ObjectDeepLink
propertyId={reminder.propertyId}
label={reminder.propertyTitle}
variant="body1"
fontWeight={700}
noWrap={false}
/>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.25 }}>
{reminder.propertyCity}{reminder.propertyDistrict ? ` · ${reminder.propertyDistrict}` : ''}&nbsp;·&nbsp;{reminder.tenantName}
</Typography>
</Box>
<Divider sx={{ mb: 2.5 }} />
{/* 2. Fristen */}
<Box sx={{ mb: 2.5 }}>
<SectionTitle icon={<Calendar size={14} color="#64748b" />}>Fristen</SectionTitle>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<DateRow label="Fälligkeit" value={reminder.dueDate} />
{showSecondDate && (
<DateRow label={secondDateLabel!} value={secondDateValue} />
)}
{reminder.snoozedUntil && (
<DateRow label="Schlummert bis" value={reminder.snoozedUntil} />
)}
</Box>
</Box>
<Divider sx={{ mb: 2.5 }} />
{/* 3. Terminplanung — steht, wo bis Runde 3 der Notizbereich war */}
<Box sx={{ mb: 2.5 }}>
<ReminderCalendarPlanner reminder={reminder} property={reminderProperty} />
</Box>
{/* 4. Aktionen */}
{isActionable && (
<>
<Divider sx={{ mb: 2.5 }} />
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.8125rem' }}>
Aktionen
</Typography>
<Box sx={{ display: 'flex', gap: 1.5 }}>
<Button
variant="contained"
sx={{ textTransform: 'none', flex: 1, fontWeight: 600, bgcolor: DS_BRAND.main, '&:hover': { bgcolor: DS_BRAND.hover } }}
onClick={() => complete.mutate({ id: reminder.id })}
>
Erledigt
</Button>
<Button
variant="outlined"
sx={{ textTransform: 'none', flex: 1, borderColor: DS_SLATE[200], color: DS_TEXT.secondary }}
onClick={() => dismiss.mutate({ id: reminder.id })}
>
Verwerfen
</Button>
</Box>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<TextField
type="date"
size="small"
value={snoozeDate}
onChange={e => setSnoozeDate(e.target.value)}
sx={{ flex: 1, '& .MuiInputBase-root': { fontSize: '0.8125rem' } }}
/>
<Button
variant="outlined"
size="small"
disabled={!snoozeDate}
sx={{ textTransform: 'none', whiteSpace: 'nowrap', color: DS_TEXT.secondary, borderColor: DS_SLATE[200] }}
onClick={() => snoozeDate && snooze.mutate({ id: reminder.id, until: snoozeDate })}
>
Schlummern bis
</Button>
</Box>
</Box>
</>
)}
</Box>
)}
</Box>
</Drawer>
)
}