summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/ship/swp-workflow-panel.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-document-list/ship/swp-workflow-panel.tsx')
-rw-r--r--lib/vendor-document-list/ship/swp-workflow-panel.tsx370
1 files changed, 370 insertions, 0 deletions
diff --git a/lib/vendor-document-list/ship/swp-workflow-panel.tsx b/lib/vendor-document-list/ship/swp-workflow-panel.tsx
new file mode 100644
index 00000000..ded306e7
--- /dev/null
+++ b/lib/vendor-document-list/ship/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