'use client' /** * BatchModeSelector - 批次處理模式選擇器 * ======================================= * Phase 11: #50 批次處理功能 * * Features: * - 全部接受: 批次核准所有低/中風險 * - 逐一審核: 依序審核 * - 僅顯示 CRITICAL: 過濾高風險 * * 安全限制: * - CRITICAL + DESTRUCTIVE 禁止批次核准 * * i18n: 100% next-intl */ import { useState, useMemo, useCallback } from 'react' import { useTranslations } from 'next-intl' import { cn } from '@/lib/utils' import { CURRENT_USER } from '@/lib/constants' import { useApprovalStore, usePendingApprovals, toFrontendApproval, } from '@/stores/approval.store' import { StatusOrb } from '@/components/ui/status-orb' import { CheckCircle2, ListFilter, AlertTriangle, Loader2, Shield, } from 'lucide-react' // ApprovalRequest type used implicitly via toFrontendApproval // ============================================================================= // Types // ============================================================================= export type BatchMode = 'sequential' | 'filter_critical' | 'bulk_approve' interface BatchModeSelectorProps { className?: string /** 批次核准完成後的回調 */ onBulkComplete?: (succeeded: number, failed: number, skipped: number) => void /** 簽核者資訊 */ signerId?: string signerName?: string } interface BulkApproveResult { approval_id: string success: boolean message: string execution_triggered: boolean } interface BulkApproveResponse { total: number succeeded: number failed: number skipped: number results: BulkApproveResult[] } // ============================================================================= // Component // ============================================================================= export function BatchModeSelector({ className, onBulkComplete, signerId = CURRENT_USER.id, signerName = CURRENT_USER.name, }: BatchModeSelectorProps) { const t = useTranslations('approval') const tBatch = useTranslations('approval.batch') const pendingApprovals = usePendingApprovals() const { fetchPending } = useApprovalStore() const [mode, setMode] = useState('sequential') const [isProcessing, setIsProcessing] = useState(false) // 轉換為前端格式 const approvals = useMemo(() => { return pendingApprovals.map((a) => toFrontendApproval(a)) }, [pendingApprovals]) // 統計各風險等級數量 const stats = useMemo(() => { const result = { total: approvals.length, low: 0, medium: 0, high: 0, critical: 0, canBulkApprove: 0, } approvals.forEach((a) => { switch (a.riskLevel) { case 'low': result.low++ result.canBulkApprove++ break case 'medium': result.medium++ result.canBulkApprove++ break case 'high': result.high++ // HIGH 風險允許批次,但 DESTRUCTIVE 不允許 if (a.blastRadius.dataImpact !== 'DESTRUCTIVE') { result.canBulkApprove++ } break case 'critical': result.critical++ // CRITICAL 禁止批次 break } }) return result }, [approvals]) // 批次核准處理 const handleBulkApprove = useCallback(async () => { if (stats.canBulkApprove === 0) return setIsProcessing(true) try { // 取得可批次核准的 ID 列表 const bulkApproveIds = approvals .filter((a) => { // 排除 CRITICAL if (a.riskLevel === 'critical') return false // 排除 DESTRUCTIVE if (a.blastRadius.dataImpact === 'DESTRUCTIVE') return false return true }) .map((a) => a.id) const response = await fetch('/api/v1/approvals/bulk-approve', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ approval_ids: bulkApproveIds, signer_id: signerId, signer_name: signerName, comment: 'Bulk approved via ConversationalView', }), }) if (!response.ok) { throw new Error(`HTTP ${response.status}`) } const result: BulkApproveResponse = await response.json() // 刷新列表 await fetchPending() // 回調通知 onBulkComplete?.(result.succeeded, result.failed, result.skipped) } catch (error) { console.error('Bulk approve failed:', error) } finally { setIsProcessing(false) } }, [approvals, stats.canBulkApprove, signerId, signerName, fetchPending, onBulkComplete]) return (
{/* 統計概覽 */}

{tBatch('title')}

{stats.total} {t('pendingApprovals')}
{/* 風險等級分佈 */}
LOW: {stats.low}
MED: {stats.medium}
HIGH: {stats.high}
CRIT: {stats.critical}
{/* 模式選擇按鈕 */}
{/* 全部接受 (批次) */} {/* 逐一審核 */} {/* 僅顯示 CRITICAL */}
{/* 安全提示 */}

{tBatch('securityNote')}

) }