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:
1 parent
36ce548169
commit
52dc7de25e
38 files changed
+9055
No files matched your search
@@ -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')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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 ?? [],
|
||||
})
|
||||
}
|
||||
@@ -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')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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')
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user