f7b290d6c0
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 <noreply@anthropic.com>
160 lines
4.2 KiB
TypeScript
160 lines
4.2 KiB
TypeScript
/**
|
|
* 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<ApprovalRequest[]>([])
|
|
const selectedRequestId = ref<string | null>(null)
|
|
const isLoading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const filter = ref<ApprovalFilter>({})
|
|
|
|
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,
|
|
}
|
|
}
|