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

@@ -1,69 +1,85 @@
/**
* Property On — Position und Struktur des Navigationseintrags (§3.3, §3.4).
* Property On — Position und Struktur des Navigationseintrags «Meine Agenten».
*
* Die Spezifikation ist hier wörtlich: «Teamübersicht» steht direkt unter
* «Meine Objekte» und oberhalb von «Reminder Manager». Eine spätere Umsortierung
* der Navigation würde das still brechen — dieser Test hält es fest.
* Seit Runde 4 ist «Meine Agenten» der einzige Eintrag mit Unterpunkten, und
* diese Unterpunkte sind die fünf digitalen Mitarbeitenden in verbindlicher
* Reihenfolge. Eine spätere Umsortierung würde das still brechen — dieser Test
* hält es fest.
*/
import { describe, it, expect } from 'vitest'
import { WORKSPACE_CONFIG, getPageNameFromPath } from '../appShellConfig'
import { WorkspaceType } from '../../../domain/enums'
import { ROUTES } from '../../../lib/constants'
import { MY_AGENTS_LABEL, ROUTES } from '../../../lib/constants'
import { AGENT_WORKSPACES } from '../../../lib/agentWorkspaces'
const supplyNav = WORKSPACE_CONFIG[WorkspaceType.SUPPLY].navItems
describe('Navigationseintrag «Teamübersicht»', () => {
it('steht direkt zwischen «Meine Objekte» und «Reminder Manager»', () => {
describe('Navigationseintrag «Meine Agenten»', () => {
it('steht direkt unter «Meine Objekte»', () => {
const labels = supplyNav.map(item => item.label)
const objekte = labels.indexOf('Meine Objekte')
const team = labels.indexOf('Teamübersicht')
const reminder = labels.indexOf('Reminder Manager')
const agenten = labels.indexOf(MY_AGENTS_LABEL)
expect(objekte).toBeGreaterThanOrEqual(0)
expect(team).toBe(objekte + 1)
expect(reminder).toBe(team + 1)
expect(agenten).toBe(objekte + 1)
})
it('führt auf den Basispfad des Funktionsbereichs', () => {
const team = supplyNav.find(item => item.label === 'Teamübersicht')
expect(team?.path).toBe(ROUTES.SUPPLY.TEAM)
const agenten = supplyNav.find(item => item.label === MY_AGENTS_LABEL)
expect(agenten?.path).toBe(ROUTES.SUPPLY.TEAM)
})
it('trägt die drei Subreiter in verbindlicher Reihenfolge', () => {
const team = supplyNav.find(item => item.label === 'Teamübersicht')
expect(team?.children?.map(c => c.label)).toEqual([
'Personalverwaltung',
'Bearbeitungsverlauf',
'Kanäle & Systeme',
])
it('trägt die fünf Agenten in verbindlicher Reihenfolge', () => {
const agenten = supplyNav.find(item => item.label === MY_AGENTS_LABEL)
expect(agenten?.children?.map(c => c.label)).toEqual(['Ferdi', 'Bruno', 'Livia', 'Nora', 'Sina'])
})
it('verweist mit jedem Subreiter auf einen Unterpfad der Teamübersicht', () => {
const team = supplyNav.find(item => item.label === 'Teamübersicht')
for (const child of team?.children ?? []) {
expect(child.path.startsWith(`${ROUTES.SUPPLY.TEAM}/`)).toBe(true)
it('gibt jedem Agenten Porträt-ID und Funktionsbezeichnung mit', () => {
const agenten = supplyNav.find(item => item.label === MY_AGENTS_LABEL)
for (const child of agenten?.children ?? []) {
expect(child.agentId).toBeTruthy()
expect(child.role).toBeTruthy()
}
})
it('verweist mit jedem Agenten auf eine eigene Seite', () => {
const agenten = supplyNav.find(item => item.label === MY_AGENTS_LABEL)
const paths = agenten?.children?.map(c => c.path) ?? []
expect(paths).toEqual(AGENT_WORKSPACES.map(a => a.path))
expect(new Set(paths).size).toBe(paths.length)
})
it('ist der einzige Hauptreiter mit Subreitern — sonst wäre die Sidebar uneinheitlich', () => {
const withChildren = supplyNav.filter(item => (item.children?.length ?? 0) > 0)
expect(withChildren.map(item => item.label)).toEqual(['Teamübersicht'])
expect(withChildren.map(item => item.label)).toEqual([MY_AGENTS_LABEL])
})
it('führt Reto und Lea nicht mehr', () => {
const labels = supplyNav.flatMap(item => [item.label, ...(item.children ?? []).map(c => c.label)])
expect(labels).not.toContain('Reto')
expect(labels).not.toContain('Lea')
})
})
describe('Seitentitel aus dem Pfad', () => {
it('löst Haupt- und Subreiter über die Navigation auf', () => {
expect(getPageNameFromPath(ROUTES.SUPPLY.TEAM)).toBe('Teamübersicht')
expect(getPageNameFromPath(ROUTES.SUPPLY.TEAM_PERSONNEL)).toBe('Personalverwaltung')
expect(getPageNameFromPath(ROUTES.SUPPLY.TEAM_HISTORY)).toBe('Bearbeitungsverlauf')
expect(getPageNameFromPath(ROUTES.SUPPLY.TEAM_CONNECTIONS)).toBe('Kanäle & Systeme')
it('löst Haupt- und Agentenreiter über die Navigation auf', () => {
expect(getPageNameFromPath(ROUTES.SUPPLY.TEAM)).toBe(MY_AGENTS_LABEL)
expect(getPageNameFromPath(ROUTES.SUPPLY.AGENT_FERDI)).toBe('Ferdi')
expect(getPageNameFromPath(ROUTES.SUPPLY.AGENT_BRUNO)).toBe('Bruno')
expect(getPageNameFromPath(ROUTES.SUPPLY.AGENT_LIVIA)).toBe('Livia')
expect(getPageNameFromPath(ROUTES.SUPPLY.AGENT_NORA)).toBe('Nora')
expect(getPageNameFromPath(ROUTES.SUPPLY.AGENT_SINA)).toBe('Sina')
})
it('fängt Deep-Links ab, statt in den Segment-Fallback zu fallen', () => {
// Ohne eigene Regel ergäbe der Fallback «Erledigte auftraege» bzw. «Ferdi».
expect(getPageNameFromPath(`${ROUTES.SUPPLY.TEAM_PERSONNEL}/ferdi/aufgaben`)).toBe('Personalblatt')
expect(getPageNameFromPath(ROUTES.SUPPLY.TEAM_HISTORY_DONE)).toBe('Bearbeitungsverlauf')
expect(getPageNameFromPath(ROUTES.SUPPLY.TEAM_HISTORY_PENDING)).toBe('Bearbeitungsverlauf')
it('trägt die alten Unterseitenpfade bis zur Umleitung unter «Meine Agenten»', () => {
// Ohne eigene Regel ergäbe der Fallback «Personalverwaltung» bzw. «Erledigte auftraege».
expect(getPageNameFromPath(`${ROUTES.SUPPLY.TEAM_PERSONNEL}/ferdi/aufgaben`)).toBe(MY_AGENTS_LABEL)
expect(getPageNameFromPath(ROUTES.SUPPLY.TEAM_HISTORY_DONE)).toBe(MY_AGENTS_LABEL)
expect(getPageNameFromPath(ROUTES.SUPPLY.TEAM_CONNECTIONS)).toBe(MY_AGENTS_LABEL)
})
it('erkennt die Objekt-Detailroute', () => {
expect(getPageNameFromPath(`${ROUTES.SUPPLY.PROPERTIES}/prop-001`)).toBe('Objekt Detail')
})
})