feat: F018 human review queue — 2-panel governance workspace

Replaces the old ad-hoc ReviewQueue page with a full governance-compliant
workflow: filterable task list, detail panel with confidence/risk context,
role-aware approve/reject/escalate/more-data actions, and persistent notes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-17 13:03:34 +02:00
parent 1d0cb8b154
commit 5d3323617c
18 changed files with 1231 additions and 432 deletions
+57 -8
View File
@@ -1,26 +1,77 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { reviewService } from '../services/reviewService'
import { useSessionStore } from '../stores/sessionStore'
import type { ReviewFilters } from '../provider/IReviewProvider'
import type { ReviewTaskStatus } from '../domain/review'
const STALE_REVIEW = 30_000
const QK = 'reviewQueue'
export function useReviewQueue(filters?: ReviewFilters) {
return useQuery({
queryKey: ['reviewQueue', filters ?? {}],
queryFn: () => reviewService.getQueue(filters),
queryKey: [QK, filters ?? {}],
queryFn: () => reviewService.getTasks(filters),
staleTime: STALE_REVIEW,
select: (res) => res.data ?? [],
})
}
export function useReviewTask(id: string | null) {
return useQuery({
queryKey: [QK, 'task', id],
queryFn: () => reviewService.getTask(id!),
staleTime: STALE_REVIEW,
enabled: !!id,
select: (res) => res.data ?? null,
})
}
export function useUpdateReviewStatus() {
const queryClient = useQueryClient()
const { currentUser } = useSessionStore.getState()
const userId = currentUser?.email ?? 'unknown'
return useMutation({
mutationFn: ({ id, status, note }: { id: string; status: ReviewTaskStatus; note?: string }) =>
reviewService.updateStatus(id, status, userId, note),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QK] })
},
})
}
export function useAssignReviewTask() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ id, assignTo }: { id: string; assignTo: string }) =>
reviewService.assign(id, assignTo),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QK] })
},
})
}
export function useAddReviewNote() {
const queryClient = useQueryClient()
const { currentUser } = useSessionStore.getState()
const userId = currentUser?.email ?? 'unknown'
return useMutation({
mutationFn: ({ id, content }: { id: string; content: string }) =>
reviewService.addNote(id, { content, createdBy: userId, createdAt: new Date().toISOString() }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QK] })
},
})
}
// Legacy exports
export function useApproveReviewItem() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ id, notes }: { id: string; notes?: string }) =>
reviewService.approve(id, 'current-user', notes),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reviewQueue'] })
},
onSuccess: () => queryClient.invalidateQueries({ queryKey: [QK] }),
})
}
@@ -29,8 +80,6 @@ export function useRejectReviewItem() {
return useMutation({
mutationFn: ({ id, notes }: { id: string; notes?: string }) =>
reviewService.reject(id, 'current-user', notes),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reviewQueue'] })
},
onSuccess: () => queryClient.invalidateQueries({ queryKey: [QK] }),
})
}