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>
150 lines
4.6 KiB
TypeScript
150 lines
4.6 KiB
TypeScript
import { memo, useCallback, useRef } from 'react'
|
||
import { Box, Typography } from '@mui/material'
|
||
import type { ExposeLead } from '../../domain/exposeLead'
|
||
import type { Property } from '../../domain/property'
|
||
import { ObjectDeepLink } from '../team'
|
||
import { LeadWorkspace } from './LeadWorkspace'
|
||
import { DS_SLATE, DS_TEXT } from '../../lib/ds'
|
||
|
||
export const LEAD_COLS = '110px 1.2fr 1.4fr 1fr 1.4fr'
|
||
|
||
const COLUMN_LABELS = ['Datum', 'Möglicher Interessent', 'Kontaktdaten', 'Raum', 'Objektempfehlung']
|
||
|
||
interface RowProps {
|
||
lead: ExposeLead
|
||
properties: Property[]
|
||
selected: boolean
|
||
onSelect: (id: string, row: HTMLElement | null) => void
|
||
}
|
||
|
||
const LeadRow = memo(function LeadRow({ lead, properties, selected, onSelect }: RowProps) {
|
||
const ref = useRef<HTMLDivElement>(null)
|
||
|
||
const recommended = lead.propertyIds
|
||
.map(id => properties.find(p => p.id === id))
|
||
.filter((p): p is Property => Boolean(p))
|
||
|
||
return (
|
||
<Box
|
||
ref={ref}
|
||
onClick={() => onSelect(lead.id, ref.current)}
|
||
sx={{
|
||
display: 'grid',
|
||
gridTemplateColumns: LEAD_COLS,
|
||
alignItems: 'flex-start',
|
||
gap: 1,
|
||
px: 2, py: 1.5,
|
||
borderBottom: '1px solid #f1f5f9',
|
||
bgcolor: selected ? '#f8fafc' : 'white',
|
||
cursor: 'pointer',
|
||
'&:hover': { bgcolor: DS_SLATE[50] },
|
||
transition: 'background-color 0.1s',
|
||
}}
|
||
>
|
||
<Typography variant="caption" sx={{ color: DS_TEXT.secondary, pt: 0.25 }}>
|
||
{new Date(lead.receivedAt).toLocaleDateString('de-CH')}
|
||
</Typography>
|
||
|
||
<Typography variant="body2" sx={{ fontWeight: 600, color: DS_TEXT.primary }}>
|
||
{lead.prospect}
|
||
</Typography>
|
||
|
||
<Box sx={{ minWidth: 0 }}>
|
||
{lead.contacts.length === 0 ? (
|
||
<Typography variant="caption" sx={{ color: DS_TEXT.disabled }}>–</Typography>
|
||
) : (
|
||
lead.contacts.map(c => (
|
||
<Typography
|
||
key={c}
|
||
variant="caption"
|
||
sx={{ display: 'block', color: DS_TEXT.secondary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
|
||
>
|
||
{c}
|
||
</Typography>
|
||
))
|
||
)}
|
||
</Box>
|
||
|
||
<Typography variant="body2" sx={{ color: DS_TEXT.secondary }}>
|
||
{lead.locationHint}
|
||
</Typography>
|
||
|
||
{/* Mehrere Objekte je Lead — jedes verlinkt nach «Meine Objekte». */}
|
||
<Box sx={{ minWidth: 0 }} onClick={e => e.stopPropagation()}>
|
||
{recommended.length === 0 ? (
|
||
<Typography variant="caption" sx={{ color: DS_TEXT.disabled }}>–</Typography>
|
||
) : (
|
||
recommended.map(p => (
|
||
<ObjectDeepLink key={p.id} propertyId={p.id} label={p.title} />
|
||
))
|
||
)}
|
||
</Box>
|
||
</Box>
|
||
)
|
||
})
|
||
|
||
interface Props {
|
||
leads: ExposeLead[]
|
||
properties: Property[]
|
||
selectedId: string | null
|
||
onSelect: (id: string, row: HTMLElement | null) => void
|
||
/** Arbeitsbereich nur bei aktiven Leads — Archiviertes wird nicht bearbeitet. */
|
||
withWorkspace: boolean
|
||
emptyText: string
|
||
}
|
||
|
||
/**
|
||
* Livias Leadliste im Grunddesign der Ferdi-Liste (Runde 4, §8.3).
|
||
*
|
||
* Der Arbeitsbereich öffnet sich zwischen der gewählten Zeile und den übrigen
|
||
* Leads — nicht in einem Drawer. Das hält den Bezug zum Lead sichtbar, während
|
||
* gearbeitet wird.
|
||
*/
|
||
export function LeadTable({ leads, properties, selectedId, onSelect, withWorkspace, emptyText }: Props) {
|
||
const handleSelect = useCallback(
|
||
(id: string, row: HTMLElement | null) => onSelect(id, row),
|
||
[onSelect],
|
||
)
|
||
|
||
if (leads.length === 0) {
|
||
return (
|
||
<Box sx={{ py: 6, textAlign: 'center' }}>
|
||
<Typography variant="body2" sx={{ color: DS_TEXT.muted }}>{emptyText}</Typography>
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<Box sx={{ border: '1px solid #e2e8f0', borderRadius: 1.5, overflow: 'hidden' }}>
|
||
<Box
|
||
sx={{
|
||
display: 'grid',
|
||
gridTemplateColumns: LEAD_COLS,
|
||
gap: 1,
|
||
px: 2, py: 1,
|
||
bgcolor: DS_SLATE[50],
|
||
borderBottom: '1px solid #e2e8f0',
|
||
}}
|
||
>
|
||
{COLUMN_LABELS.map(h => (
|
||
<Typography key={h} variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.secondary, fontSize: '0.7rem' }}>
|
||
{h}
|
||
</Typography>
|
||
))}
|
||
</Box>
|
||
|
||
{leads.map(lead => (
|
||
<Box key={lead.id}>
|
||
<LeadRow
|
||
lead={lead}
|
||
properties={properties}
|
||
selected={lead.id === selectedId}
|
||
onSelect={handleSelect}
|
||
/>
|
||
{withWorkspace && lead.id === selectedId && <LeadWorkspace lead={lead} />}
|
||
</Box>
|
||
))}
|
||
</Box>
|
||
)
|
||
}
|