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:
Benjamin SutterandClaude Opus 5 committed 2026-08-05 14:43:16 +02:00
1 parent cc5582b7fd
commit d200d4e930
352 files changed
+8069 -8512

No files matched your search

@@ -0,0 +1,162 @@
import { memo } from 'react'
import { Box, Checkbox, Chip, Typography } from '@mui/material'
import { Building2, ExternalLink } from 'lucide-react'
import type { MarketLead } from '../../hooks/useMarketLeads'
import type { Property } from '../../domain/property'
import type { ExtractedContact } from '../../domain/futureSignal'
import { ObjectDeepLink } from '../team'
import { CONTACT_ICONS } from './marketLeadHelpers'
import { DS_BG, DS_BRAND, DS_SLATE, DS_TEXT } from '../../lib/ds'
/**
* Signalliste und Signaldetail der Nora-Seite.
*
* Ausgezogen aus `MarketIntelligence.tsx` — die Seite hält jetzt nur noch
* Reiter, Auswahl und Rahmen.
*/
// ── Left pane list item ──────────────────────────────────────────────────────
/**
* Kompakte Signalkarte (Runde 4, §7.2).
*
* Prozentsatz und Objektanzahl sind entfallen: beide Zahlen wurden auf der
* Karte gelesen, ohne dass man etwas mit ihnen anfangen konnte — die
* Entscheidung fällt in der Detailansicht. Übrig bleiben die Kerninformationen.
*/
const LeadListItem = memo(function LeadListItem({
lead, selected, onClick,
}: {
lead: MarketLead
selected: boolean
onClick: () => void
}) {
const { signal } = lead
return (
<Box
onClick={onClick}
sx={{
px: 2, py: 1.5,
borderBottom: '1px solid #f1f5f9',
borderLeft: `3px solid ${selected ? '#152642' : 'transparent'}`,
bgcolor: selected ? '#f0f9ff' : 'white',
cursor: 'pointer',
'&:hover': { bgcolor: selected ? '#f0f9ff' : '#f8fafc' },
transition: 'background 0.1s',
}}
>
<Typography variant="body2" sx={{ fontWeight: 600, color: DS_SLATE[900], lineHeight: 1.3, mb: 0.25 }}>
{signal.companyName ?? signal.locationHint}
</Typography>
{signal.title && (
<Typography variant="caption" sx={{ color: DS_SLATE[500], display: 'block', mb: 0.5, lineHeight: 1.3 }}>
{signal.title}
</Typography>
)}
<Typography variant="caption" sx={{ color: DS_SLATE[400], fontSize: '0.65rem' }}>
{signal.locationHint} · {signal.timeHorizonMonths} Mo.
</Typography>
</Box>
)
})
// ── Contact row ──────────────────────────────────────────────────────────────
/**
* Extrahierte Kontaktdaten (Runde 4, §7.3).
*
* Ohne Konfidenzstufe und ohne Farbpunkt: gezeigt wird nur noch, was klar
* belegbar ist — siehe `confirmableContacts`. Ein Wert mit dem Hinweis
* «spekulativ» hilft niemandem; entweder man kann ihn anschreiben oder nicht.
*/
function ContactRow({ contact }: { contact: ExtractedContact }) {
const icon = CONTACT_ICONS[contact.type]
const isClickable = contact.type === 'WEBSITE' || contact.type === 'LINKEDIN' || contact.type === 'EMAIL'
const href = contact.type === 'EMAIL' ? `mailto:${contact.value}` : contact.type === 'WEBSITE' || contact.type === 'LINKEDIN' ? `https://${contact.value.replace(/^https?:\/\//, '')}` : undefined
return (
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1.25, py: 0.75, borderBottom: '1px solid #f8fafc' }}>
<Box sx={{ color: DS_SLATE[500], mt: 0.125, flexShrink: 0 }}>{icon}</Box>
<Box sx={{ flex: 1, minWidth: 0 }}>
{isClickable && href ? (
<Box
component="a"
href={href}
target="_blank"
rel="noopener noreferrer"
sx={{ color: DS_BRAND.main, fontWeight: 500, fontSize: '0.8125rem', textDecoration: 'none', '&:hover': { textDecoration: 'underline' }, display: 'flex', alignItems: 'center', gap: 0.5 }}
>
{contact.value}
<ExternalLink size={11} />
</Box>
) : (
<Typography variant="body2" sx={{ fontWeight: 500, fontSize: '0.8125rem', color: DS_SLATE[900] }}>
{contact.value}
</Typography>
)}
{contact.label && (
<Typography variant="caption" sx={{ color: DS_SLATE[400], fontSize: '0.7rem' }}>
{contact.label}
</Typography>
)}
</Box>
</Box>
)
}
// ── Objektzeile mit Mehrfachauswahl ──────────────────────────────────────────
/**
* Ein passendes Portfolioobjekt zu einem Signal.
*
* Der Anschreiben-Entwurf und die Reminder-Aktion sind entfallen (Runde 4, §7.3):
* Nora erkennt die Nachfrage, das Dokument macht Livia. Geblieben ist die
* Auswahl — mehrere Objekte lassen sich gemeinsam für dasselbe Signal
* weiterreichen, statt für jedes einzeln denselben Weg zu gehen.
*/
function SignalPropertyRow({
p, fit, selected, onToggle,
}: {
p: Property
fit: number | null
selected: boolean
onToggle: () => void
}) {
return (
<Box
sx={{
display: 'flex', alignItems: 'center', gap: 1,
px: 1.5, py: 1.25, mb: 1,
border: `1px solid ${selected ? '#152642' : '#e2e8f0'}`,
borderRadius: 1.5,
bgcolor: selected ? '#f8fafc' : 'white',
}}
>
<Checkbox
size="small"
checked={selected}
onChange={onToggle}
slotProps={{ input: { 'aria-label': `${p.title} auswählen` } }}
sx={{ p: 0.5 }}
/>
<Building2 size={14} color="#152642" style={{ flexShrink: 0 }} />
<Box sx={{ flex: 1, minWidth: 0 }}>
<ObjectDeepLink propertyId={p.id} label={p.title} fontWeight={600} />
<Typography variant="caption" sx={{ color: DS_SLATE[500] }}>
{p.location.city} · {p.areaSqm.toLocaleString('de-CH')} m² · CHF {p.rentPricePerSqm}/m²/J
</Typography>
</Box>
{fit !== null && (
<Chip
label={`${fit}% Flächenmatch`}
size="small"
sx={{ height: 20, fontSize: '0.65rem', fontWeight: 600, flexShrink: 0, bgcolor: DS_BG.subtle, color: DS_TEXT.secondary }}
/>
)}
</Box>
)
}
export { LeadListItem, ContactRow, SignalPropertyRow }