From f7b290d6c0ddb37df1f6e75620b0e90e019541f0 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sat, 15 Aug 2026 10:48:02 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Approval=20Queue=20page=20(T03=20transa?= =?UTF-8?q?ction=20template)=20=E2=80=94=20third=20KBX=20v60=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement ApprovalQueue.vue using KBX Foundation v60 transaction pattern: - T03 Transaction template for maker-checker approval workflow - KbxScreenFrame wrapper with breadcrumb/title - Summary stats (Pending, Approved, Rejected counts) - Status and action type filters - Header with model name, action type, status badge - Request details grid (Request ID, Requester, Requested At, Status) - Validation metrics display (PBO, DSR, OOS, Target Phase) - Review & approval section with textarea for comments - Approve/Reject buttons with submit state - Review history display (reviewer, date, decision, comment) - Side panel with request list (fixed position on desktop, stacked on mobile) - Dark mode and responsive layout New files: - features/approval/pages/ApprovalQueue.vue (T03 transaction page) - features/approval/composables/useApprovalRequests.ts (approval data) - features/approval/types/index.ts (type definitions) - features/approval/registry.ts (screen definition) Demo data: 3 approval requests (pending, approved, rejected) with full workflow. Mock approval/rejection methods with comment capture. Ready for API integration and real backend workflow. Co-Authored-By: Claude Haiku 4.5 --- .../composables/useApprovalRequests.ts | 159 +++++ .../features/approval/pages/ApprovalQueue.vue | 635 ++++++++++++++++++ frontend/src/features/approval/registry.ts | 48 ++ frontend/src/features/approval/types/index.ts | 29 + 4 files changed, 871 insertions(+) create mode 100644 frontend/src/features/approval/composables/useApprovalRequests.ts create mode 100644 frontend/src/features/approval/pages/ApprovalQueue.vue create mode 100644 frontend/src/features/approval/registry.ts create mode 100644 frontend/src/features/approval/types/index.ts 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 +}