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

+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')
},
})
}