diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-05 01:53:35 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-05 01:53:35 +0000 |
| commit | 610d3bccf1cb640e2a21df28d8d2a954c2bf337e (patch) | |
| tree | e7e6d72fecf14ddcff1b5b52263d14119b7c488c /lib/vendor-document-list/table/swp-workflow-panel.tsx | |
| parent | 15969dfedffc4e215c81d507164bc2bb383974e5 (diff) | |
(대표님) 변경사항 0604 - OCR 관련 및 drizzle generated sqls
Diffstat (limited to 'lib/vendor-document-list/table/swp-workflow-panel.tsx')
| -rw-r--r-- | lib/vendor-document-list/table/swp-workflow-panel.tsx | 370 |
1 files changed, 370 insertions, 0 deletions
diff --git a/lib/vendor-document-list/table/swp-workflow-panel.tsx b/lib/vendor-document-list/table/swp-workflow-panel.tsx new file mode 100644 index 00000000..ded306e7 --- /dev/null +++ b/lib/vendor-document-list/table/swp-workflow-panel.tsx @@ -0,0 +1,370 @@ +"use client" + +import * as React from "react" +import { Send, Eye, CheckCircle, Clock, RefreshCw, AlertTriangle, Loader2 } from "lucide-react" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Badge } from "@/components/ui/badge" +import { Separator } from "@/components/ui/separator" +import { Progress } from "@/components/ui/progress" +import type { EnhancedDocument } from "@/types/enhanced-documents" + +interface SWPWorkflowPanelProps { + contractId: number + documents: EnhancedDocument[] + onWorkflowUpdate?: () => void +} + +type WorkflowStatus = + | 'IDLE' // 대기 상태 + | 'SUBMITTED' // 목록 전송됨 + | 'UNDER_REVIEW' // 검토 중 + | 'CONFIRMED' // 컨펌됨 + | 'REVISION_REQUIRED' // 수정 요청됨 + | 'RESUBMITTED' // 재전송됨 + | 'APPROVED' // 최종 승인됨 + +interface WorkflowState { + status: WorkflowStatus + lastUpdatedAt?: string + pendingActions: string[] + confirmationData?: any + revisionComments?: string[] + approvalData?: any +} + +export function SWPWorkflowPanel({ + contractId, + documents, + onWorkflowUpdate +}: SWPWorkflowPanelProps) { + const [workflowState, setWorkflowState] = React.useState<WorkflowState | null>(null) + const [isLoading, setIsLoading] = React.useState(false) + const [actionProgress, setActionProgress] = React.useState(0) + + // 워크플로우 상태 조회 + const fetchWorkflowStatus = async () => { + setIsLoading(true) + try { + const response = await fetch(`/api/sync/workflow/status?contractId=${contractId}&targetSystem=SWP`) + if (!response.ok) throw new Error('Failed to fetch workflow status') + + const status = await response.json() + setWorkflowState(status) + } catch (error) { + console.error('Failed to fetch workflow status:', error) + toast.error('워크플로우 상태를 확인할 수 없습니다') + } finally { + setIsLoading(false) + } + } + + // 컴포넌트 마운트 시 상태 조회 + React.useEffect(() => { + fetchWorkflowStatus() + }, [contractId]) + + // 워크플로우 액션 실행 + const executeWorkflowAction = async (action: string) => { + setActionProgress(0) + setIsLoading(true) + + try { + // 진행률 시뮬레이션 + const progressInterval = setInterval(() => { + setActionProgress(prev => Math.min(prev + 20, 90)) + }, 200) + + const response = await fetch('/api/sync/workflow/action', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contractId, + targetSystem: 'SWP', + action, + documents: documents.map(doc => ({ id: doc.id, documentNo: doc.documentNo })) + }) + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.message || 'Workflow action failed') + } + + const result = await response.json() + + clearInterval(progressInterval) + setActionProgress(100) + + setTimeout(() => { + setActionProgress(0) + + if (result?.success) { + toast.success( + `${getActionLabel(action)} 완료`, + { description: result?.message || '워크플로우가 성공적으로 진행되었습니다.' } + ) + } else { + toast.error( + `${getActionLabel(action)} 실패`, + { description: result?.message || '워크플로우 실행에 실패했습니다.' } + ) + } + + fetchWorkflowStatus() // 상태 갱신 + onWorkflowUpdate?.() + }, 500) + + } catch (error) { + setActionProgress(0) + + toast.error(`${getActionLabel(action)} 실패`, { + description: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.' + }) + } finally { + setIsLoading(false) + } + } + + const getActionLabel = (action: string): string => { + switch (action) { + case 'SUBMIT_LIST': return '목록 전송' + case 'CHECK_CONFIRMATION': return '컨펌 확인' + case 'RESUBMIT_REVISED': return '수정본 재전송' + case 'CHECK_APPROVAL': return '승인 확인' + default: return action + } + } + + const getStatusBadge = () => { + if (isLoading) { + return <Badge variant="secondary">확인 중...</Badge> + } + + if (!workflowState) { + return <Badge variant="destructive">오류</Badge> + } + + switch (workflowState.status) { + case 'IDLE': + return <Badge variant="secondary">대기</Badge> + case 'SUBMITTED': + return ( + <Badge variant="default" className="gap-1 bg-blue-500"> + <Clock className="w-3 h-3" /> + 전송됨 + </Badge> + ) + case 'UNDER_REVIEW': + return ( + <Badge variant="default" className="gap-1 bg-yellow-500"> + <Eye className="w-3 h-3" /> + 검토 중 + </Badge> + ) + case 'CONFIRMED': + return ( + <Badge variant="default" className="gap-1 bg-green-500"> + <CheckCircle className="w-3 h-3" /> + 컨펌됨 + </Badge> + ) + case 'REVISION_REQUIRED': + return ( + <Badge variant="destructive" className="gap-1"> + <AlertTriangle className="w-3 h-3" /> + 수정 요청 + </Badge> + ) + case 'RESUBMITTED': + return ( + <Badge variant="default" className="gap-1 bg-orange-500"> + <RefreshCw className="w-3 h-3" /> + 재전송됨 + </Badge> + ) + case 'APPROVED': + return ( + <Badge variant="default" className="gap-1 bg-green-600"> + <CheckCircle className="w-3 h-3" /> + 승인 완료 + </Badge> + ) + default: + return <Badge variant="secondary">알 수 없음</Badge> + } + } + + const getAvailableActions = (): string[] => { + if (!workflowState) return [] + + switch (workflowState.status) { + case 'IDLE': + return ['SUBMIT_LIST'] + case 'SUBMITTED': + return ['CHECK_CONFIRMATION'] + case 'UNDER_REVIEW': + return ['CHECK_CONFIRMATION'] + case 'CONFIRMED': + return [] // 컨펌되면 자동으로 다음 단계로 + case 'REVISION_REQUIRED': + return ['RESUBMIT_REVISED'] + case 'RESUBMITTED': + return ['CHECK_APPROVAL'] + case 'APPROVED': + return [] // 완료 상태 + default: + return [] + } + } + + const availableActions = getAvailableActions() + + return ( + <Popover> + <PopoverTrigger asChild> + <div className="flex items-center gap-3"> + <Button + variant="outline" + size="sm" + className="flex items-center border-orange-200 hover:bg-orange-50" + disabled={isLoading} + > + {isLoading ? ( + <Loader2 className="w-4 h-4 animate-spin" /> + ) : ( + <RefreshCw className="w-4 h-4" /> + )} + <span className="hidden sm:inline">SWP 워크플로우</span> + {workflowState?.pendingActions && workflowState.pendingActions.length > 0 && ( + <Badge + variant="destructive" + className="h-5 w-5 p-0 text-xs flex items-center justify-center" + > + {workflowState.pendingActions.length} + </Badge> + )} + </Button> + </div> + </PopoverTrigger> + + <PopoverContent className="w-80"> + <div className="space-y-4"> + <div className="space-y-2"> + <h4 className="font-medium">SWP 워크플로우 상태</h4> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">현재 상태</span> + {getStatusBadge()} + </div> + </div> + + {workflowState && ( + <div className="space-y-3"> + <Separator /> + + {/* 대기 중인 액션들 */} + {workflowState.pendingActions && workflowState.pendingActions.length > 0 && ( + <div className="space-y-2"> + <div className="text-sm font-medium">대기 중인 작업</div> + {workflowState.pendingActions.map((action, index) => ( + <Badge key={index} variant="outline" className="mr-1"> + {getActionLabel(action)} + </Badge> + ))} + </div> + )} + + {/* 수정 요청 사항 */} + {workflowState.revisionComments && workflowState.revisionComments.length > 0 && ( + <div className="space-y-2"> + <div className="text-sm font-medium text-red-600">수정 요청 사항</div> + <div className="text-xs text-muted-foreground space-y-1"> + {workflowState.revisionComments.map((comment, index) => ( + <div key={index} className="p-2 bg-red-50 rounded text-red-700"> + {comment} + </div> + ))} + </div> + </div> + )} + + {/* 마지막 업데이트 시간 */} + {workflowState.lastUpdatedAt && ( + <div className="text-sm"> + <div className="text-muted-foreground">마지막 업데이트</div> + <div className="font-medium"> + {new Date(workflowState.lastUpdatedAt).toLocaleString()} + </div> + </div> + )} + + {/* 진행률 표시 */} + {isLoading && actionProgress > 0 && ( + <div className="space-y-2"> + <div className="flex items-center justify-between text-sm"> + <span>진행률</span> + <span>{actionProgress}%</span> + </div> + <Progress value={actionProgress} className="h-2" /> + </div> + )} + </div> + )} + + <Separator /> + + {/* 액션 버튼들 */} + <div className="space-y-2"> + {availableActions.length > 0 ? ( + availableActions.map((action) => ( + <Button + key={action} + onClick={() => executeWorkflowAction(action)} + disabled={isLoading} + className="w-full justify-start" + size="sm" + variant={action.includes('SUBMIT') || action.includes('RESUBMIT') ? 'default' : 'outline'} + > + {isLoading ? ( + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + ) : ( + <Send className="w-4 h-4 mr-2" /> + )} + {getActionLabel(action)} + </Button> + )) + ) : ( + <div className="text-sm text-muted-foreground text-center py-2"> + {workflowState?.status === 'APPROVED' + ? '워크플로우가 완료되었습니다.' + : '실행 가능한 작업이 없습니다.'} + </div> + )} + + {/* 상태 새로고침 버튼 */} + <Button + variant="outline" + size="sm" + onClick={fetchWorkflowStatus} + disabled={isLoading} + className="w-full" + > + {isLoading ? ( + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + ) : ( + <RefreshCw className="w-4 h-4 mr-2" /> + )} + 상태 새로고침 + </Button> + </div> + </div> + </PopoverContent> + </Popover> + ) +}
\ No newline at end of file |
