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>
190 lines
8.3 KiB
TypeScript
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}` : ''} · {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>
|
|
)
|
|
}
|