From c72d0897f7b37843109c86f61d97eba05ba3ca0d Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Fri, 13 Jun 2025 07:08:01 +0000
Subject: (대표님) 20250613 16시 08분 b-rfq, document 등
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../ship/swp-workflow-panel.tsx                    | 370 +++++++++++++++++++++
 1 file changed, 370 insertions(+)
 create mode 100644 lib/vendor-document-list/ship/swp-workflow-panel.tsx

(limited to 'lib/vendor-document-list/ship/swp-workflow-panel.tsx')

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
-- 
cgit v1.2.3

