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>
This commit is contained in:
1 parent
cc5582b7fd
commit
d200d4e930
352 files changed
+8069
-8512
No files matched your search
@@ -0,0 +1,149 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user