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:
@@ -1,16 +1,20 @@
|
||||
import type { ReviewQueueItem, ReviewPriority, ReviewQueueStatus } from '../domain/review'
|
||||
import type { ReviewTask, ReviewPriority, ReviewTaskStatus, ReviewEntityType, ReviewNote } from '../domain/review'
|
||||
|
||||
export interface ReviewFilters {
|
||||
priority?: ReviewPriority
|
||||
status?: ReviewQueueStatus
|
||||
status?: ReviewTaskStatus
|
||||
entityType?: ReviewEntityType
|
||||
assignedTo?: string
|
||||
organizationId?: string
|
||||
}
|
||||
|
||||
export interface IReviewProvider {
|
||||
getQueue(filters?: ReviewFilters): Promise<ReviewQueueItem[]>
|
||||
getById(id: string): Promise<ReviewQueueItem | null>
|
||||
approve(id: string, reviewedBy: string, notes?: string): Promise<ReviewQueueItem>
|
||||
reject(id: string, reviewedBy: string, notes?: string): Promise<ReviewQueueItem>
|
||||
assign(id: string, assignTo: string): Promise<ReviewQueueItem>
|
||||
getQueue(filters?: ReviewFilters): Promise<ReviewTask[]>
|
||||
getById(id: string): Promise<ReviewTask | null>
|
||||
updateStatus(id: string, status: ReviewTaskStatus, userId: string, note?: string): Promise<ReviewTask>
|
||||
addNote(id: string, note: Omit<ReviewNote, 'id'>): Promise<ReviewTask>
|
||||
assign(id: string, assignTo: string): Promise<ReviewTask>
|
||||
// Legacy actions (delegate to updateStatus internally)
|
||||
approve(id: string, reviewedBy: string, notes?: string): Promise<ReviewTask>
|
||||
reject(id: string, reviewedBy: string, notes?: string): Promise<ReviewTask>
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export const MockupDashboardProvider: IDashboardProvider = {
|
||||
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.organizationId === organizationId)
|
||||
queue = queue.filter(r => r.relatedOrganizationId === organizationId)
|
||||
}
|
||||
|
||||
const avgScore =
|
||||
|
||||
@@ -1,42 +1,76 @@
|
||||
import type { IReviewProvider, ReviewFilters } from './IReviewProvider'
|
||||
import type { ReviewQueueItem } from '../domain/review'
|
||||
import type { ReviewTask, ReviewNote } from '../domain/review'
|
||||
import { mockReviewQueue } from '../mock-data/reviewQueue'
|
||||
import { mockDelay } from '../lib/mockUtils'
|
||||
|
||||
const store: ReviewQueueItem[] = [...mockReviewQueue]
|
||||
const store: ReviewTask[] = [...mockReviewQueue]
|
||||
|
||||
const priorityOrder: Record<string, number> = { HIGH: 0, MEDIUM: 1, LOW: 2 }
|
||||
const priorityOrder: Record<string, number> = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
|
||||
|
||||
function makeNote(content: string, createdBy: string): ReviewNote {
|
||||
return {
|
||||
id: `note-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
content,
|
||||
createdBy,
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
export const MockupReviewProvider: IReviewProvider = {
|
||||
async getQueue(filters?: ReviewFilters) {
|
||||
await mockDelay()
|
||||
let results = [...store]
|
||||
if (filters?.priority) results = results.filter(r => r.priority === filters.priority)
|
||||
if (filters?.status) results = results.filter(r => r.status === filters.status)
|
||||
if (filters?.assignedTo) results = results.filter(r => r.assignedTo === filters.assignedTo)
|
||||
if (filters?.organizationId) results = results.filter(r => r.organizationId === filters.organizationId)
|
||||
if (filters?.priority) results = results.filter(r => r.priority === filters.priority)
|
||||
if (filters?.status) results = results.filter(r => r.status === filters.status)
|
||||
if (filters?.entityType) results = results.filter(r => r.entityType === filters.entityType)
|
||||
if (filters?.assignedTo) results = results.filter(r => r.assignedTo === filters.assignedTo)
|
||||
if (filters?.organizationId) results = results.filter(r => r.relatedOrganizationId === filters.organizationId)
|
||||
return results.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9))
|
||||
},
|
||||
|
||||
async getById(id) {
|
||||
await mockDelay()
|
||||
return store.find(r => r.id === id) ?? null
|
||||
},
|
||||
async approve(id, _reviewedBy, notes?) {
|
||||
|
||||
async updateStatus(id, status, userId, note?) {
|
||||
await mockDelay()
|
||||
const idx = store.findIndex(r => r.id === id)
|
||||
store[idx] = { ...store[idx], status: 'COMPLETED', notes, updatedAt: new Date().toISOString() }
|
||||
const notes = [...store[idx].reviewNotes]
|
||||
if (note) notes.push(makeNote(note, userId))
|
||||
store[idx] = { ...store[idx], status, reviewNotes: notes, updatedAt: new Date().toISOString() }
|
||||
return store[idx]
|
||||
},
|
||||
async reject(id, _reviewedBy, notes?) {
|
||||
|
||||
async addNote(id, note) {
|
||||
await mockDelay()
|
||||
const idx = store.findIndex(r => r.id === id)
|
||||
store[idx] = { ...store[idx], status: 'COMPLETED', notes, updatedAt: new Date().toISOString() }
|
||||
const newNote: ReviewNote = { id: `note-${Date.now()}`, ...note }
|
||||
store[idx] = {
|
||||
...store[idx],
|
||||
reviewNotes: [...store[idx].reviewNotes, newNote],
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
return store[idx]
|
||||
},
|
||||
|
||||
async assign(id, assignTo) {
|
||||
await mockDelay()
|
||||
const idx = store.findIndex(r => r.id === id)
|
||||
store[idx] = { ...store[idx], assignedTo: assignTo, status: 'IN_REVIEW', updatedAt: new Date().toISOString() }
|
||||
store[idx] = {
|
||||
...store[idx],
|
||||
assignedTo: assignTo,
|
||||
status: store[idx].status === 'PENDING' ? 'IN_REVIEW' : store[idx].status,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
return store[idx]
|
||||
},
|
||||
|
||||
async approve(id, reviewedBy, notes?) {
|
||||
return this.updateStatus(id, 'APPROVED', reviewedBy, notes)
|
||||
},
|
||||
|
||||
async reject(id, reviewedBy, notes?) {
|
||||
return this.updateStatus(id, 'REJECTED', reviewedBy, notes)
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user