feat: Approval Queue page (T03 transaction template) — third KBX v60 page
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>
This commit is contained in:
@@ -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<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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user