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

+3 -21
View File
@@ -1,19 +1,7 @@
import { createContext, useContext, type ReactNode } from 'react'
import type { ReactNode } from 'react'
import { useSessionStore } from '../stores/sessionStore'
import type { MockUser } from '../stores/sessionStore'
// Placeholder AuthContext — swap for real auth (Supabase, Auth0, etc.) later.
// All call sites use this context; no component imports sessionStore directly.
interface AuthContextValue {
user: MockUser | null
isAuthenticated: boolean
isLoading: boolean
login: (user: MockUser) => void
logout: () => void
}
const AuthContext = createContext<AuthContextValue | null>(null)
import { AuthContext } from './authContext'
import type { AuthContextValue } from './authContext'
export function AuthProvider({ children }: { children: ReactNode }) {
const currentUser = useSessionStore(s => s.currentUser)
@@ -31,9 +19,3 @@ export function AuthProvider({ children }: { children: ReactNode }) {
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext)
if (!ctx) throw new Error('useAuth must be used within AuthProvider')
return ctx
}
+6
View File
@@ -0,0 +1,6 @@
import type { AgentDirectoryEntry } from '../domain/agentDirectory'
export interface IAgentDirectoryProvider {
getAll(): Promise<AgentDirectoryEntry[]>
getByIds(ids: string[]): Promise<AgentDirectoryEntry[]>
}
+10
View File
@@ -0,0 +1,10 @@
import type { CalendarEvent, CreateCalendarEventInput } from '../domain/calendarEvent'
export interface ICalendarProvider {
/** Ist ein Kalender verbunden? Steuert, ob die Terminplanung angeboten wird. */
isConnected(): Promise<boolean>
/** Anzeigename des verbundenen Kalenders, z. B. «Microsoft 365 Kalender». */
getConnectionName(): Promise<string>
getEventsByReminder(reminderId: string): Promise<CalendarEvent[]>
create(input: CreateCalendarEventInput): Promise<CalendarEvent>
}
+8
View File
@@ -0,0 +1,8 @@
import type { CreateExposeLeadInput, ExposeLead } from '../domain/exposeLead'
export interface IExposeLeadProvider {
getAll(): Promise<ExposeLead[]>
getById(id: string): Promise<ExposeLead | null>
create(input: CreateExposeLeadInput): Promise<ExposeLead>
archive(id: string): Promise<ExposeLead>
}
+9
View File
@@ -0,0 +1,9 @@
import type { ExposeDraft } from '../domain/expose'
export interface IExposeProvider {
/** Entwurf zu einem Lead und Objekt — null, solange keiner besteht. */
getDraft(leadId: string, propertyId: string): Promise<ExposeDraft | null>
save(draft: ExposeDraft): Promise<ExposeDraft>
/** Setzt den Erstellungszeitpunkt — trennt «gespeichert» von «erstellt». */
markGenerated(draftId: string): Promise<ExposeDraft>
}
+8
View File
@@ -0,0 +1,8 @@
import type { VisitAssignment } from '../domain/visitAssignment'
export interface IVisitAssignmentProvider {
getAll(): Promise<VisitAssignment[]>
getById(id: string): Promise<VisitAssignment | null>
/** Bericht anfordern — beendet die Warnung «Besichtigung in weniger als 24 h». */
requestReport(id: string): Promise<VisitAssignment>
}
@@ -0,0 +1,17 @@
import { agentDirectory } from '../mock-data/agentDirectory'
import type { AgentDirectoryEntry } from '../domain/agentDirectory'
import type { IAgentDirectoryProvider } from './IAgentDirectoryProvider'
export const MockupAgentDirectoryProvider: IAgentDirectoryProvider = {
async getAll() {
return [...agentDirectory]
},
async getByIds(ids) {
// Reihenfolge der angefragten IDs beibehalten — die Oberfläche zeigt sie so,
// wie der aufrufende Datensatz sie führt, nicht alphabetisch.
return ids
.map(id => agentDirectory.find(a => a.id === id))
.filter((a): a is AgentDirectoryEntry => Boolean(a))
},
}
+45
View File
@@ -0,0 +1,45 @@
import type { CalendarEvent, CreateCalendarEventInput } from '../domain/calendarEvent'
import type { ICalendarProvider } from './ICalendarProvider'
import { mockAgentConnections } from '../mock-data/agentConnections'
import { AgentConnectionCategory } from '../domain/agentConnection'
import { AgentConnectionStatus } from '../domain/teamAgent'
/**
* Kalender-Ablage der Demo.
*
* Es wird kein echter Kalender kontaktiert. Verbindungsname und Verbindungs-
* zustand kommen aus dem bestehenden Bestand «Kanäle & Systeme» — so zeigt die
* Terminplanung denselben Kalender an, den der Nutzer dort konfiguriert hat,
* statt einen zweiten, erfundenen zu behaupten.
*/
let store: CalendarEvent[] = []
let sequence = 0
function calendarConnection() {
return mockAgentConnections.find(c => c.category === AgentConnectionCategory.CALENDAR)
}
export const MockupCalendarProvider: ICalendarProvider = {
async isConnected() {
return calendarConnection()?.status === AgentConnectionStatus.CONNECTED
},
async getConnectionName() {
return calendarConnection()?.name ?? 'Kalender'
},
async getEventsByReminder(reminderId) {
return store.filter(e => e.reminderId === reminderId)
},
async create(input: CreateCalendarEventInput) {
sequence += 1
const event: CalendarEvent = {
...input,
id: `cal-${String(sequence).padStart(3, '0')}`,
createdAt: new Date().toISOString(),
}
store = [...store, event]
return event
},
}
-48
View File
@@ -1,48 +0,0 @@
import type { IDashboardProvider, DashboardStats } from './IDashboardProvider'
import { mockProperties } from '../mock-data/properties'
import { mockMatches } from '../mock-data/matches'
import { mockNeeds } from '../mock-data/needs'
import { mockFutureSignals } from '../mock-data/futureSignals'
import { mockReviewQueue } from '../mock-data/reviewQueue'
import { mockDelay } from '../lib/mockUtils'
export const MockupDashboardProvider: IDashboardProvider = {
async getStats(organizationId?) {
await mockDelay()
let props = mockProperties
let matches = mockMatches
let needs = mockNeeds
let signals = mockFutureSignals
let queue = mockReviewQueue
if (organizationId) {
props = props.filter(p => p.organizationId === organizationId)
matches = matches.filter(m => m.organizationId === organizationId)
needs = needs.filter(n => n.organizationId === organizationId)
signals = signals.filter(s => s.organizationId === organizationId)
queue = queue.filter(r => r.relatedOrganizationId === organizationId)
}
const avgScore =
matches.length > 0
? matches.reduce((sum, m) => sum + m.matchScore, 0) / matches.length
: 0
const stats: DashboardStats = {
totalProperties: props.length,
verifiedProperties: props.filter(p => p.resultType === 'VERIFIED_PORTFOLIO').length,
verifiedPortfolioProperties: props.filter(p => p.resultType === 'VERIFIED_PORTFOLIO').length,
futureSignalProperties: props.filter(p => p.resultType === 'FUTURE_AVAILABILITY').length,
totalMatches: matches.length,
pendingReviews: queue.filter(r => r.status === 'PENDING').length,
approvedMatches: matches.filter(m => m.isApproved === true).length,
activeNeeds: needs.length,
totalSignals: signals.length,
verifiedSignals: signals.filter(s => s.isVerified).length,
averageMatchScore: Math.round(avgScore),
highConfidenceMatches: matches.filter(m => m.confidenceLevel >= 0.75).length,
}
return stats
},
}
+36
View File
@@ -0,0 +1,36 @@
import { mockExposeLeads } from '../mock-data/exposeLeads'
import type { ExposeLead } from '../domain/exposeLead'
import { ExposeLeadStatus } from '../domain/exposeLead'
import type { IExposeLeadProvider } from './IExposeLeadProvider'
let store: ExposeLead[] = [...mockExposeLeads]
let sequence = store.length
export const MockupExposeLeadProvider: IExposeLeadProvider = {
async getAll() {
return [...store]
},
async getById(id) {
return store.find(l => l.id === id) ?? null
},
async create(input) {
sequence += 1
const lead: ExposeLead = {
...input,
id: `lead-${String(sequence).padStart(3, '0')}`,
receivedAt: input.receivedAt ?? new Date().toISOString(),
status: input.status ?? ExposeLeadStatus.ACTIVE,
}
store = [lead, ...store]
return lead
},
async archive(id) {
const idx = store.findIndex(l => l.id === id)
if (idx === -1) throw new Error(`Lead ${id} nicht gefunden`)
store[idx] = { ...store[idx], status: ExposeLeadStatus.ARCHIVED }
return store[idx]
},
}
+37
View File
@@ -0,0 +1,37 @@
import type { ExposeDraft } from '../domain/expose'
import type { IExposeProvider } from './IExposeProvider'
/**
* Entwurfsablage der Demo — in-memory, wie alle Mockup-Provider.
*
* Der Schlüssel ist das Paar aus Lead und Objekt: derselbe Lead kann mehrere
* Objekte empfehlen, und jedes bekommt sein eigenes Exposé.
*/
const store = new Map<string, ExposeDraft>()
function key(leadId: string, propertyId: string): string {
return `${leadId}::${propertyId}`
}
export const MockupExposeProvider: IExposeProvider = {
async getDraft(leadId, propertyId) {
return store.get(key(leadId, propertyId)) ?? null
},
async save(draft) {
const saved: ExposeDraft = { ...draft, updatedAt: new Date().toISOString() }
store.set(key(draft.leadId, draft.propertyId), saved)
return saved
},
async markGenerated(draftId) {
for (const [k, d] of store) {
if (d.id === draftId) {
const updated: ExposeDraft = { ...d, generatedAt: new Date().toISOString() }
store.set(k, updated)
return updated
}
}
throw new Error(`Exposé-Entwurf ${draftId} nicht gefunden`)
},
}
@@ -3,7 +3,7 @@ import type { MarketSignal, MarketSignalFilters, SignalProcessingStatus } from '
import { MOCK_MARKET_SIGNALS } from '../mock-data/marketSignals'
// Mutable in-memory copy for status updates
let signals: MarketSignal[] = [...MOCK_MARKET_SIGNALS]
const signals: MarketSignal[] = [...MOCK_MARKET_SIGNALS]
function applyFilters(data: MarketSignal[], filters?: MarketSignalFilters): MarketSignal[] {
if (!filters) return data
+1 -1
View File
@@ -3,7 +3,7 @@ import type { Reminder, ReminderActivity } from '../domain/reminder'
import { ReminderStatus } from '../domain/reminder'
import type { IReminderProvider } from './IReminderProvider'
let store: Reminder[] = [...mockReminders]
const store: Reminder[] = [...mockReminders]
function now(): string {
return new Date().toISOString()
+1 -1
View File
@@ -4,7 +4,7 @@ import { GateStatus, PipelineStage } from '../domain/signalPipeline'
import type { GateType } from '../domain/signalPipeline'
import { MOCK_PIPELINE_STATES, MOCK_AUDIT_TRAILS } from '../mock-data/signalPipelines'
let states: PipelineState[] = [...MOCK_PIPELINE_STATES]
const states: PipelineState[] = [...MOCK_PIPELINE_STATES]
export const MockupSignalPipelineProvider: ISignalPipelineProvider = {
async getPipelineState(signalId) {
@@ -0,0 +1,24 @@
import { mockVisitAssignments } from '../mock-data/visitAssignments'
import type { VisitAssignment } from '../domain/visitAssignment'
import type { IVisitAssignmentProvider } from './IVisitAssignmentProvider'
const store: VisitAssignment[] = [...mockVisitAssignments]
export const MockupVisitAssignmentProvider: IVisitAssignmentProvider = {
async getAll() {
return [...store]
},
async getById(id) {
return store.find(a => a.id === id) ?? null
},
async requestReport(id) {
const idx = store.findIndex(a => a.id === id)
if (idx === -1) throw new Error(`Auftrag ${id} nicht gefunden`)
const current = store[idx]
if (!current.preparation) throw new Error('Nur Vorbereitungsaufträge kennen einen Bericht.')
store[idx] = { ...current, preparation: { ...current.preparation, reportRequested: true } }
return store[idx]
},
}
+21
View File
@@ -0,0 +1,21 @@
import { createContext } from 'react'
import type { MockUser } from '../stores/sessionStore'
/**
* Platzhalter-Auth-Kontext — wird später gegen eine echte Anmeldung getauscht
* (Supabase, Auth0 o. ä.). Alle Aufrufer gehen über diesen Kontext; keine
* Komponente greift direkt auf den `sessionStore` zu.
*
* Kontext und Provider liegen in getrennten Dateien, weil eine `.tsx`-Datei,
* die eine Komponente und einen Nicht-Komponenten-Wert exportiert, im
* Entwicklungsmodus bei jeder Änderung ihren Zustand verliert.
*/
export interface AuthContextValue {
user: MockUser | null
isAuthenticated: boolean
isLoading: boolean
login: (user: MockUser) => void
logout: () => void
}
export const AuthContext = createContext<AuthContextValue | null>(null)