Files
KArtSell.Aegis/frontend/src/features/approval/composables/useApprovalRequests.ts
T
kjh2064 f7b290d6c0 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>
2026-08-15 10:48:02 +09:00

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,
}
}