diff --git a/frontend/src/features/approval/composables/useApprovalRequests.ts b/frontend/src/features/approval/composables/useApprovalRequests.ts new file mode 100644 index 00000000..8d349afc --- /dev/null +++ b/frontend/src/features/approval/composables/useApprovalRequests.ts @@ -0,0 +1,159 @@ +/** + * Approval Requests Composable + * Fetch and manage approval requests + */ + +import { ref, computed } from 'vue' +import type { ApprovalRequest, ApprovalFilter } from '../types' + +export function useApprovalRequests() { + const requests = ref([]) + const selectedRequestId = ref(null) + const isLoading = ref(false) + const error = ref(null) + const filter = ref({}) + + const mockRequests: ApprovalRequest[] = [ + { + requestId: 'APR-2026-001', + modelId: '00000000-0000-0000-0000-000000000001', + modelName: 'Hawkeye-Alpha v2.1', + action: 'activate', + metadata: { + pbo: 15.2, + dsr: 96.5, + oos: 1.8, + }, + status: 'pending', + requesterName: 'kjh2064', + requestedAt: '2026-08-14T10:30:00Z', + }, + { + requestId: 'APR-2026-002', + modelId: '00000000-0000-0000-0000-000000000002', + modelName: 'Falcon-Beta v1.8', + action: 'transition-phase', + metadata: { + currentPhase: 'Review', + targetPhase: 'Manual Activation', + pbo: 18.3, + dsr: 94.2, + oos: 2.1, + }, + status: 'approved', + requesterName: 'kjh2064', + requestedAt: '2026-08-12T14:22:00Z', + reviewerName: 'admin', + reviewedAt: '2026-08-13T09:15:00Z', + reviewComment: 'Metrics acceptable for transition. Approved.', + }, + { + requestId: 'APR-2026-003', + modelId: '00000000-0000-0000-0000-000000000003', + modelName: 'Eagle-Gamma v3.0', + action: 'transition-phase', + metadata: { + currentPhase: 'Validate', + targetPhase: 'Review', + pbo: 20.5, + dsr: 92.1, + oos: 2.8, + }, + status: 'rejected', + requesterName: 'kjh2064', + requestedAt: '2026-08-10T11:45:00Z', + reviewerName: 'admin', + reviewedAt: '2026-08-11T16:20:00Z', + reviewComment: 'PBO exceeds 20% threshold. Needs further optimization.', + }, + ] + + const selectedRequest = computed(() => { + return requests.value.find(r => r.requestId === selectedRequestId.value) || null + }) + + const filteredRequests = computed(() => { + let result = requests.value + + if (filter.value.status) { + result = result.filter(r => r.status === filter.value.status) + } + + if (filter.value.action) { + result = result.filter(r => r.action === filter.value.action) + } + + return result.sort( + (a, b) => new Date(b.requestedAt).getTime() - new Date(a.requestedAt).getTime() + ) + }) + + const statusStats = computed(() => ({ + pending: requests.value.filter(r => r.status === 'pending').length, + approved: requests.value.filter(r => r.status === 'approved').length, + rejected: requests.value.filter(r => r.status === 'rejected').length, + total: requests.value.length, + })) + + async function fetchRequests() { + isLoading.value = true + error.value = null + + try { + await new Promise(resolve => setTimeout(resolve, 600)) + requests.value = mockRequests + } catch (err) { + error.value = err instanceof Error ? err.message : 'Failed to fetch requests' + } finally { + isLoading.value = false + } + } + + function selectRequest(requestId: string) { + selectedRequestId.value = requestId + } + + function clearSelection() { + selectedRequestId.value = null + } + + function setFilter(newFilter: ApprovalFilter) { + filter.value = newFilter + } + + async function approveRequest(requestId: string, comment: string) { + const req = requests.value.find(r => r.requestId === requestId) + if (req) { + req.status = 'approved' + req.reviewerName = 'current-user' + req.reviewedAt = new Date().toISOString() + req.reviewComment = comment + } + } + + async function rejectRequest(requestId: string, comment: string) { + const req = requests.value.find(r => r.requestId === requestId) + if (req) { + req.status = 'rejected' + req.reviewerName = 'current-user' + req.reviewedAt = new Date().toISOString() + req.reviewComment = comment + } + } + + return { + requests, + filteredRequests, + selectedRequest, + selectedRequestId, + isLoading, + error, + statusStats, + fetchRequests, + selectRequest, + clearSelection, + setFilter, + approveRequest, + rejectRequest, + } +} diff --git a/frontend/src/features/approval/pages/ApprovalQueue.vue b/frontend/src/features/approval/pages/ApprovalQueue.vue new file mode 100644 index 00000000..477c8eaf --- /dev/null +++ b/frontend/src/features/approval/pages/ApprovalQueue.vue @@ -0,0 +1,635 @@ + + + + + diff --git a/frontend/src/features/approval/registry.ts b/frontend/src/features/approval/registry.ts new file mode 100644 index 00000000..f50edc72 --- /dev/null +++ b/frontend/src/features/approval/registry.ts @@ -0,0 +1,48 @@ +/** + * Approval Feature Screen Registry + */ + +import type { ScreenDefinition } from '@kbx/contracts' + +export const approvalQueueScreen: ScreenDefinition = { + screenId: 'governance.approval.queue', + title: 'Approval Queue', + module: 'ERP', + path: '/governance/approvals', + component: () => import('./pages/ApprovalQueue.vue'), + permissions: ['approval.review'], + template: 'T03', + + help: { + title: 'Approval Workflow', + sections: [ + { + title: 'What is Maker-Checker?', + content: + 'Maker-Checker enforces that critical model decisions require two parties: the requester and an independent reviewer.', + }, + { + title: 'How to Approve', + content: 'Select a pending request, review the metrics and comments, then approve or reject with your decision.', + }, + { + title: 'Decision Criteria', + content: 'Activation requires: PBO ≤ 20%, DSR ≥ 95%, OOS ≤ 2.5%, plus 252+ trading-day shadow run.', + }, + ], + relatedScreens: ['model-ops.models.list', 'model-ops.shadow-run.queue'], + }, + + grid: { + columnDefs: [ + { field: 'requestId', headerName: 'Request ID', width: 120 }, + { field: 'modelName', headerName: 'Model', width: 150 }, + { field: 'action', headerName: 'Action', width: 100 }, + { field: 'status', headerName: 'Status', width: 100 }, + { field: 'requesterName', headerName: 'Requester', width: 120 }, + { field: 'requestedAt', headerName: 'Date', width: 150 }, + ], + }, +} + +export default [approvalQueueScreen] diff --git a/frontend/src/features/approval/types/index.ts b/frontend/src/features/approval/types/index.ts new file mode 100644 index 00000000..44826793 --- /dev/null +++ b/frontend/src/features/approval/types/index.ts @@ -0,0 +1,29 @@ +/** + * Approval Feature Types + */ + +export interface ApprovalRequest { + requestId: string + modelId: string + modelName: string + action: 'activate' | 'retire' | 'transition-phase' + metadata: { + currentPhase?: string + targetPhase?: string + pbo?: number + dsr?: number + oos?: number + } + status: 'pending' | 'approved' | 'rejected' + requesterName: string + requestedAt: string + reviewerName?: string + reviewedAt?: string + reviewComment?: string +} + +export interface ApprovalFilter { + status?: string + modelId?: string + action?: string +}