Files
property-match/src/components/expose/ExposeFieldGroup.tsx
T
Benjamin Sutter 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

139 lines
4.3 KiB
TypeScript

import { Box, Chip, MenuItem, TextField, Typography } from '@mui/material'
import type { ExposeField, ExposeSection } from '../../lib/exposeFields'
import type { ExposeValues } from '../../domain/expose'
import { DS_SLATE, DS_TEXT } from '../../lib/ds'
interface Props {
section: ExposeSection
values: ExposeValues
missing: Set<string>
onChange: (key: string, value: string) => void
/** Zusatzinhalt am Ende des Abschnitts, z. B. der KI-Entwurfsknopf. */
footer?: React.ReactNode
}
/** Mehrfachauswahl als Wert: kommagetrennt, damit ein flaches Wörterbuch reicht. */
function toggleChip(current: string, option: string): string {
const set = new Set(current.split(',').map(s => s.trim()).filter(Boolean))
if (set.has(option)) set.delete(option)
else set.add(option)
return [...set].join(', ')
}
function isSelected(current: string, option: string): boolean {
return current.split(',').map(s => s.trim()).includes(option)
}
function FieldControl({
field, value, isMissing, onChange,
}: {
field: ExposeField
value: string
isMissing: boolean
onChange: (value: string) => void
}) {
const label = field.suffix ? `${field.label} (${field.suffix})` : field.label
if (field.type === 'chips') {
return (
<Box>
<Typography
variant="caption"
sx={{ display: 'block', mb: 0.75, color: DS_TEXT.secondary, textTransform: 'uppercase', letterSpacing: 0.4, fontSize: '0.65rem' }}
>
{label}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.75 }}>
{(field.options ?? []).map(o => {
const selected = isSelected(value, o)
return (
<Chip
key={o}
label={o}
size="small"
variant={selected ? 'filled' : 'outlined'}
onClick={() => onChange(toggleChip(value, o))}
sx={{
cursor: 'pointer',
bgcolor: selected ? '#152642' : 'transparent',
color: selected ? 'white' : DS_TEXT.secondary,
borderColor: DS_SLATE[200],
}}
/>
)
})}
</Box>
</Box>
)
}
return (
<TextField
select={field.type === 'select'}
multiline={field.type === 'multiline'}
rows={field.type === 'multiline' ? 3 : undefined}
type={field.type === 'number' ? 'number' : 'text'}
label={field.required ? `${label} *` : label}
size="small"
fullWidth
value={value}
onChange={e => onChange(e.target.value)}
sx={{
'& .MuiOutlinedInput-notchedOutline': isMissing
? { borderColor: DS_TEXT.error, borderWidth: 2 }
: {},
}}
>
{field.type === 'select' &&
(field.options ?? []).map(o => <MenuItem key={o} value={o}>{o}</MenuItem>)}
</TextField>
)
}
/**
* Ein fachlicher Bereich des Exposé-Dossiers.
*
* Rendert generisch aus `lib/exposeFields.ts` — die Bereiche sind Daten, keine
* elf handgeschriebenen Formulare. Leere Pflichtfelder sind rot umrandet
* (Runde 4, §8.5.2); sie werden nicht mit plausiblen Werten gefüllt.
*/
export function ExposeFieldGroup({ section, values, missing, onChange, footer }: Props) {
return (
<Box component="section" sx={{ mb: 4 }}>
<Typography sx={{ fontWeight: 700, fontSize: '1rem', color: DS_TEXT.primary }}>
{section.title}
</Typography>
{section.description && (
<Typography variant="caption" sx={{ color: DS_TEXT.muted, display: 'block', mb: 1.5 }}>
{section.description}
</Typography>
)}
<Box
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', md: 'repeat(3, minmax(0, 1fr))' },
gap: 1.5,
mt: section.description ? 0 : 1.5,
}}
>
{section.fields.map(field => (
<Box
key={field.key}
sx={{ gridColumn: field.wide || field.type === 'chips' || field.type === 'multiline' ? '1 / -1' : 'auto' }}
>
<FieldControl
field={field}
value={values[field.key] ?? ''}
isMissing={missing.has(field.key)}
onChange={v => onChange(field.key, v)}
/>
</Box>
))}
</Box>
{footer && <Box sx={{ mt: 1.5 }}>{footer}</Box>}
</Box>
)
}