feat(team): Datenschicht für Property On

Property On wird als Funktionsbereich "Teamübersicht" in Property Match
eingebettet — sieben digitale Mitarbeiter, die ausführen, während der Mensch
freigibt, anpasst oder zurückweist.

Durchgehend Provider → Service → Hook, kein übersprungener Layer:
- domain/: teamAgent, agentWorkItem, agentProtocol, agentConnection, agentFilters
- mock-data/: 7 Personalblätter aus dem Agenten-Katalog (Aufgabenzahlen exakt:
  Ferdi 9, Sina 7, Lea 11, Bruno 9, Reto 10, Livia 11, Nora 8), 14 Vorgänge,
  38 Protokolleinträge, 9 Verbindungen
- provider/: vier Mockup-Provider mit versionierter localStorage-Persistenz
- services/: Fachlogik inkl. Freigabe, Zurückweisung, Kennzahlen, Demo-Reset
- hooks/: React Query, Mutationen entwerten Vorgänge, Kennzahlen und Protokoll
  gemeinsam — sonst zeigt die Oberfläche widersprüchliche Zahlen

Namensgebung: Der Entitätstyp heisst TeamAgent, nicht Agent. Im Repo existiert
bereits PowerOn als KI-Backend-Proxy, und Claude Code legt Arbeitskopien unter
agent-* ab; ein nackter Typ Agent wäre in Suchen nicht auffindbar. Alle
Satellitentypen sind Agent*-präfixiert, weil domain/index.ts per export *
bündelt und generische Namen dort kollidieren.

Eigener Protokolltyp statt ActivityEvent: den Namen gibt es im Repo dreifach
und gegenseitig inkompatibel (domain/activityEvent.ts, governanceService.ts,
PropertyActivityLogPanel.tsx). Ein Anschluss hätte den Konflikt zementiert.

Demo-Uhr in lib/teamClock.ts: die Mockdaten sind auf den 20.05.2026 verankert.
Gegen die Systemzeit gerechnet wären die Filter "heute / diese Woche / dieser
Monat" an jedem anderen Kalendertag leer und die Auswertung sähe kaputt aus,
obwohl sie korrekt rechnet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin SutterandClaude Opus 5 committed 2026-08-03 16:55:40 +02:00
1 parent 36ce548169
commit 52dc7de25e
38 files changed
+9055

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { agentConnectionService } from '../services/agentConnectionService'
import type { CreateAgentConnectionInput } from '../domain/agentConnection'
import { useToastStore } from '../stores/toastStore'
import { STALE_AGENT_CONNECTIONS } from '../lib/constants'
import { QK_AGENT_PROTOCOL } from './useAgentProtocol'
export const QK_AGENT_CONNECTIONS = 'agentConnections'
export function useAgentConnections() {
return useQuery({
queryKey: [QK_AGENT_CONNECTIONS],
queryFn: () => agentConnectionService.getAll(),
staleTime: STALE_AGENT_CONNECTIONS,
select: (res) => res.data ?? [],
})
}
function useConnectionInvalidation() {
const queryClient = useQueryClient()
return () => {
queryClient.invalidateQueries({ queryKey: [QK_AGENT_CONNECTIONS] })
queryClient.invalidateQueries({ queryKey: [QK_AGENT_PROTOCOL] })
}
}
export function useConnectAgentConnection() {
const invalidate = useConnectionInvalidation()
return useMutation({
mutationFn: (input: CreateAgentConnectionInput) => agentConnectionService.connect(input),
onSuccess: (res) => {
invalidate()
useToastStore
.getState()
.showToast(`${res.data.name} verbunden. Frontend-Simulation, kein echtes System.`, 'success')
},
onError: () => {
useToastStore.getState().showToast('Verbindung konnte nicht angelegt werden.', 'error')
},
})
}
export function useDisconnectAgentConnection() {
const invalidate = useConnectionInvalidation()
return useMutation({
mutationFn: (id: string) => agentConnectionService.disconnect(id),
onSuccess: (res) => {
invalidate()
useToastStore.getState().showToast(`${res.data.name} getrennt.`, 'warning')
},
onError: () => {
useToastStore.getState().showToast('Verbindung konnte nicht getrennt werden.', 'error')
},
})
}
/**
* «Verbindung testen». Ein fehlgeschlagener Test ist ein gültiges fachliches
* Ergebnis, kein Fehler — deshalb landet er in `onSuccess` mit Warnton und wird
* nicht als roter Fehler-Toast ausgegeben.
*/
export function useTestAgentConnection() {
const invalidate = useConnectionInvalidation()
return useMutation({
mutationFn: (id: string) => agentConnectionService.test(id),
onSuccess: (res) => {
invalidate()
useToastStore.getState().showToast(res.data.message, res.data.ok ? 'success' : 'warning')
},
onError: () => {
useToastStore.getState().showToast('Test konnte nicht ausgeführt werden.', 'error')
},
})
}
+15
View File
@@ -0,0 +1,15 @@
import { useQuery } from '@tanstack/react-query'
import { agentProtocolService } from '../services/agentProtocolService'
import type { AgentProtocolFilters } from '../provider/IAgentProtocolProvider'
import { STALE_AGENT_PROTOCOL } from '../lib/constants'
export const QK_AGENT_PROTOCOL = 'agentProtocol'
export function useAgentProtocol(filters?: AgentProtocolFilters) {
return useQuery({
queryKey: [QK_AGENT_PROTOCOL, filters ?? {}],
queryFn: () => agentProtocolService.getAll(filters),
staleTime: STALE_AGENT_PROTOCOL,
select: (res) => res.data ?? [],
})
}
+128
View File
@@ -0,0 +1,128 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { agentWorkItemService } from '../services/agentWorkItemService'
import type { AgentWorkItemFilters } from '../provider/IAgentWorkItemProvider'
import type { AgentEditableField } from '../domain/agentWorkItem'
import { useToastStore } from '../stores/toastStore'
import { STALE_AGENT_WORK_ITEMS } from '../lib/constants'
import { QK_AGENT_PROTOCOL } from './useAgentProtocol'
export const QK_AGENT_WORK_ITEMS = 'agentWorkItems'
export const QK_AGENT_KPIS = 'agentKpis'
export function useAgentWorkItems(filters?: AgentWorkItemFilters) {
return useQuery({
queryKey: [QK_AGENT_WORK_ITEMS, filters ?? {}],
queryFn: () => agentWorkItemService.getAll(filters),
staleTime: STALE_AGENT_WORK_ITEMS,
select: (res) => res.data ?? [],
})
}
export function useAgentWorkItem(id: string | null) {
return useQuery({
queryKey: [QK_AGENT_WORK_ITEMS, 'item', id],
queryFn: () => agentWorkItemService.getById(id!),
staleTime: STALE_AGENT_WORK_ITEMS,
enabled: !!id,
select: (res) => res.data ?? null,
})
}
/** Die drei Kennzahlen der Auswertung — reagieren auf dieselben Filter wie der Verlauf. */
export function useAgentKpis(filters?: AgentWorkItemFilters) {
return useQuery({
queryKey: [QK_AGENT_KPIS, filters ?? {}],
queryFn: () => agentWorkItemService.getKpis(filters),
staleTime: STALE_AGENT_WORK_ITEMS,
select: (res) => res.data,
})
}
/**
* Jede Entscheidung verschiebt einen Vorgang zwischen den Reitern, verändert die
* Kennzahlen und schreibt ins Protokoll — alle drei Bestände müssen zusammen
* entwertet werden, sonst zeigt die Oberfläche widersprüchliche Zahlen.
*/
function useWorkItemInvalidation() {
const queryClient = useQueryClient()
return () => {
queryClient.invalidateQueries({ queryKey: [QK_AGENT_WORK_ITEMS] })
queryClient.invalidateQueries({ queryKey: [QK_AGENT_KPIS] })
queryClient.invalidateQueries({ queryKey: [QK_AGENT_PROTOCOL] })
}
}
export function useApproveWorkItem() {
const invalidate = useWorkItemInvalidation()
return useMutation({
mutationFn: (id: string) => agentWorkItemService.approve(id),
onSuccess: () => {
invalidate()
useToastStore.getState().showToast('Vorgang freigegeben.', 'success')
},
onError: () => {
useToastStore.getState().showToast('Freigabe fehlgeschlagen.', 'error')
},
})
}
export function useRejectWorkItem() {
const invalidate = useWorkItemInvalidation()
return useMutation({
mutationFn: ({ id, reason }: { id: string; reason?: string }) =>
agentWorkItemService.reject(id, reason),
onSuccess: () => {
invalidate()
useToastStore.getState().showToast('Vorgang zurückgewiesen.', 'warning')
},
onError: () => {
useToastStore.getState().showToast('Zurückweisung fehlgeschlagen.', 'error')
},
})
}
export function useSaveWorkItemEdit() {
const invalidate = useWorkItemInvalidation()
return useMutation({
mutationFn: ({ id, fields }: { id: string; fields: AgentEditableField[] }) =>
agentWorkItemService.saveEdit(id, fields),
onSuccess: () => {
invalidate()
useToastStore
.getState()
.showToast('Änderungen gespeichert. Der Vorgang wartet weiterhin auf die Entscheidung.', 'info')
},
onError: () => {
useToastStore.getState().showToast('Änderungen konnten nicht gespeichert werden.', 'error')
},
})
}
export function useAnswerWorkItemQuery() {
const invalidate = useWorkItemInvalidation()
return useMutation({
mutationFn: ({ id, answer }: { id: string; answer: string }) =>
agentWorkItemService.answerQuery(id, answer),
onSuccess: () => {
invalidate()
useToastStore.getState().showToast('Rückfrage beantwortet.', 'success')
},
onError: () => {
useToastStore.getState().showToast('Antwort konnte nicht gespeichert werden.', 'error')
},
})
}
export function useMarkWorkItemDone() {
const invalidate = useWorkItemInvalidation()
return useMutation({
mutationFn: (id: string) => agentWorkItemService.markDone(id),
onSuccess: () => {
invalidate()
useToastStore.getState().showToast('Vorgang als erledigt markiert.', 'success')
},
onError: () => {
useToastStore.getState().showToast('Vorgang konnte nicht abgeschlossen werden.', 'error')
},
})
}
+102
View File
@@ -0,0 +1,102 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { teamAgentService } from '../services/teamAgentService'
import type { AgentTask, AgentChannel, AgentSetting } from '../domain/teamAgent'
import { useToastStore } from '../stores/toastStore'
import { STALE_TEAM_AGENTS } from '../lib/constants'
import { QK_AGENT_PROTOCOL } from './useAgentProtocol'
export const QK_TEAM_AGENTS = 'teamAgents'
export function useTeamAgents() {
return useQuery({
queryKey: [QK_TEAM_AGENTS],
queryFn: () => teamAgentService.getAll(),
staleTime: STALE_TEAM_AGENTS,
select: (res) => res.data ?? [],
})
}
export function useTeamAgent(id: string | null) {
return useQuery({
queryKey: [QK_TEAM_AGENTS, id],
queryFn: () => teamAgentService.getById(id!),
staleTime: STALE_TEAM_AGENTS,
enabled: !!id,
select: (res) => res.data ?? null,
})
}
/** Jede Mutation berührt auch das Protokoll — beide Bestände zusammen entwerten. */
function useAgentMutationInvalidation() {
const queryClient = useQueryClient()
return () => {
queryClient.invalidateQueries({ queryKey: [QK_TEAM_AGENTS] })
queryClient.invalidateQueries({ queryKey: [QK_AGENT_PROTOCOL] })
}
}
export function useSetAgentActive() {
const invalidate = useAgentMutationInvalidation()
return useMutation({
mutationFn: ({ id, active }: { id: string; active: boolean }) =>
teamAgentService.setActive(id, active),
onSuccess: (res, variables) => {
invalidate()
useToastStore
.getState()
.showToast(
variables.active
? `${res.data.name} ist wieder aktiv.`
: `${res.data.name} wurde pausiert.`,
variables.active ? 'success' : 'warning',
)
},
onError: () => {
useToastStore.getState().showToast('Status konnte nicht geändert werden.', 'error')
},
})
}
export function useSaveAgentTasks() {
const invalidate = useAgentMutationInvalidation()
return useMutation({
mutationFn: ({ id, tasks }: { id: string; tasks: AgentTask[] }) =>
teamAgentService.saveTasks(id, tasks),
onSuccess: () => {
invalidate()
useToastStore.getState().showToast('Aufgaben gespeichert.', 'success')
},
onError: () => {
useToastStore.getState().showToast('Aufgaben konnten nicht gespeichert werden.', 'error')
},
})
}
export function useSaveAgentSettings() {
const invalidate = useAgentMutationInvalidation()
return useMutation({
mutationFn: ({ id, settings }: { id: string; settings: AgentSetting[] }) =>
teamAgentService.saveSettings(id, settings),
onSuccess: () => {
invalidate()
useToastStore.getState().showToast('Einstellungen gespeichert.', 'success')
},
onError: () => {
useToastStore.getState().showToast('Einstellungen konnten nicht gespeichert werden.', 'error')
},
})
}
export function useSaveAgentChannels() {
const invalidate = useAgentMutationInvalidation()
return useMutation({
mutationFn: ({ id, channels }: { id: string; channels: AgentChannel[] }) =>
teamAgentService.saveChannels(id, channels),
onSuccess: () => {
invalidate()
},
onError: () => {
useToastStore.getState().showToast('Kanal konnte nicht geändert werden.', 'error')
},
})
}
+28
View File
@@ -0,0 +1,28 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { teamDemoService } from '../services/teamDemoService'
import { useToastStore } from '../stores/toastStore'
import { QK_TEAM_AGENTS } from './useTeamAgents'
import { QK_AGENT_WORK_ITEMS, QK_AGENT_KPIS } from './useAgentWorkItems'
import { QK_AGENT_PROTOCOL } from './useAgentProtocol'
import { QK_AGENT_CONNECTIONS } from './useAgentConnections'
/**
* «Demo zurücksetzen». Entwertet alle vier Property-On-Bestände, damit die
* Oberfläche unmittelbar den Auslieferungszustand zeigt — ein Reload ist nicht
* nötig.
*/
export function useResetTeamDemo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: () => teamDemoService.reset(),
onSuccess: () => {
for (const key of [QK_TEAM_AGENTS, QK_AGENT_WORK_ITEMS, QK_AGENT_KPIS, QK_AGENT_PROTOCOL, QK_AGENT_CONNECTIONS]) {
queryClient.invalidateQueries({ queryKey: [key] })
}
useToastStore.getState().showToast('Demo zurückgesetzt. Alle lokalen Änderungen wurden verworfen.', 'success')
},
onError: () => {
useToastStore.getState().showToast('Demo konnte nicht zurückgesetzt werden.', 'error')
},
})
}