"use client" import * as React from "react" import { type Table } from "@tanstack/react-table" import { Download, FileDown, Mail, CheckCircle, AlertTriangle, Send, Check, FileSignature, FileText, ExternalLink, Globe, Flag } from "lucide-react" import { exportTableToExcel } from "@/lib/export" import { downloadFile } from "@/lib/file-download" import { Button } from "@/components/ui/button" import { BasicContractView } from "@/db/schema" import { toast } from "sonner" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Badge } from "@/components/ui/badge" import { prepareFinalApprovalAction, quickFinalApprovalAction, resendContractsAction, updateLegalReviewStatusFromSSLVW, updateComplianceReviewStatusFromCPVW, requestComplianceInquiryAction } from "../service" import { BasicContractSignDialog } from "../vendor-table/basic-contract-sign-dialog" import { SSLVWPurInqReqDialog } from "@/components/common/legal/sslvw-pur-inq-req-dialog" import { CPVWWabQustListViewDialog } from "@/components/common/legal/cpvw-wab-qust-list-view-dialog" import { prepareRedFlagResolutionApproval, requestRedFlagResolution } from "@/lib/compliance/red-flag-resolution" import { useRouter } from "next/navigation" import { useSession } from "next-auth/react" import { ApprovalPreviewDialog } from "@/lib/approval/client" interface RedFlagResolutionState { resolved: boolean resolvedAt: Date | null pendingApprovalId: string | null } interface BasicContractDetailTableToolbarActionsProps { table: Table gtcData?: Record agreementCommentData?: Record redFlagData?: Record redFlagResolutionData?: Record isComplianceTemplate?: boolean } export function BasicContractDetailTableToolbarActions({ table, gtcData = {}, agreementCommentData = {}, redFlagData = {}, redFlagResolutionData = {}, isComplianceTemplate = false }: BasicContractDetailTableToolbarActionsProps) { // 선택된 행들 가져오기 const selectedRows = table.getSelectedRowModel().rows const hasSelectedRows = selectedRows.length > 0 // 다이얼로그 상태 const [resendDialog, setResendDialog] = React.useState(false) const [finalApproveDialog, setFinalApproveDialog] = React.useState(false) const [legalReviewDialog, setLegalReviewDialog] = React.useState(false) const [loading, setLoading] = React.useState(false) const [buyerSignDialog, setBuyerSignDialog] = React.useState(false) const [contractsToSign, setContractsToSign] = React.useState([]) const [redFlagApprovalPreview, setRedFlagApprovalPreview] = React.useState<{ contractIds: number[] templateName: string variables: Record title: string defaultApprovers?: string[] } | null>(null) const [showRedFlagApprovalDialog, setShowRedFlagApprovalDialog] = React.useState(false) const router = useRouter() const { data: session } = useSession() // 각 버튼별 활성화 조건 계산 const canBulkDownload = hasSelectedRows && selectedRows.some(row => row.original.signedFilePath && row.original.signedFileName && row.original.vendorSignedAt ) const canBulkResend = hasSelectedRows const canFinalApprove = hasSelectedRows && selectedRows.some(row => { const contract = row.original; if (contract.completedAt !== null || !contract.signedFilePath) { return false; } // ⚠️ 법무/준법문의 완료 여부는 SSLVW/CPVW 상태 및 완료 시간에 의존하므로, // 여기서는 legalReviewCompletedAt / complianceReviewCompletedAt 기반으로 // 최종 승인 버튼을 막지 않습니다. (상태/시간은 UI 참고용으로만 사용) return true; }); // 법무검토 요청 가능 여부 (준법서약 템플릿이 아닐 때만) // 1. 협력업체 서명 완료 (vendorSignedAt 있음) // 2. 협의 완료됨 (negotiationCompletedAt 있음) OR // 3. 협의 없음 (코멘트 없음, hasComments: false) // 협의 중 (negotiationCompletedAt 없고 코멘트 있음)은 불가 const canRequestLegalReview = !isComplianceTemplate && hasSelectedRows && selectedRows.some(row => { const contract = row.original; // 필수 조건 확인: 최종승인 미완료, 법무검토 미요청, 협력업체 서명 완료 if ( contract.legalReviewRequestedAt || contract.completedAt || !contract.vendorSignedAt ) { return false; } // 협의 완료된 경우 → 가능 if (contract.negotiationCompletedAt) { return true; } // 협의 완료되지 않은 경우 // GTC 템플릿인 경우 코멘트 존재 여부 확인 if (contract.templateName?.includes('GTC')) { const contractGtcData = gtcData[contract.id]; // 코멘트가 없으면 가능 (협의 없음) if (contractGtcData && !contractGtcData.hasComments) { return true; } // 코멘트가 있으면 불가 (협의 중) return false; } // GTC가 아닌 경우는 협의 완료 여부만 확인 return false; }); // 준법문의 버튼 활성화 가능 여부 // 1. 협력업체 서명 완료 (vendorSignedAt 있음) // 2. 협의 완료됨 (negotiationCompletedAt 있음) OR 협의 없음 (코멘트 없음) // 3. 레드플래그 해소됨 (redFlagResolutionData에서 resolved 상태) // 4. 이미 준법문의 요청되지 않음 (complianceReviewRequestedAt 없음) const canRequestComplianceInquiry = hasSelectedRows && selectedRows.some(row => { const contract = row.original; // 필수 조건 확인: 준법서약 템플릿, 최종승인 미완료, 협력업체 서명 완료, 준법문의 미요청 if ( !isComplianceTemplate || contract.completedAt || !contract.vendorSignedAt || contract.complianceReviewRequestedAt ) { return false; } // 협의 완료 확인 // 협의 완료된 경우 → 가능 if (contract.negotiationCompletedAt) { // 협의 완료됨, 레드플래그만 확인하면 됨 } else { // 협의 완료되지 않은 경우: 코멘트가 없으면 협의 없음으로 간주하여 가능 const commentData = agreementCommentData[contract.id]; if (commentData && commentData.hasComments) { // 코멘트가 있으면 협의 중이므로 불가 return false; } // 코멘트가 없으면 협의 없음으로 간주하여 가능 } // 레드플래그 해소 확인 const resolution = redFlagResolutionData[contract.id]; // 레드플래그가 있는 경우, 해소되어야 함 if (redFlagData[contract.id] === true && !resolution?.resolved) { return false; } return true; }); // 필터링된 계약서들 계산 const resendContracts = selectedRows.map(row => row.original) const finalApproveContracts = selectedRows .map(row => row.original) .filter(contract => { if (contract.completedAt !== null || !contract.signedFilePath) { return false; } if (contract.legalReviewRequestedAt && !contract.legalReviewCompletedAt) { return false; } return true; }); const contractsWithoutLegalReview = finalApproveContracts.filter(contract => !contract.legalReviewRequestedAt && !contract.legalReviewCompletedAt ); // 법무검토 요청 가능한 계약서들 const legalReviewContracts = selectedRows .map(row => row.original) .filter(contract => { // 이미 법무검토 요청됨 if (contract.legalReviewRequestedAt) { return false; } // 이미 최종승인 완료됨 if (contract.completedAt) { return false; } // 협의 완료된 경우 if (contract.negotiationCompletedAt) { return true; } // 협의 완료되지 않은 경우 // GTC 템플릿인 경우 코멘트 없으면 가능 if (contract.templateName?.includes('GTC')) { const contractGtcData = gtcData[contract.id]; // 코멘트가 없으면 가능 (협의 없음) if (contractGtcData && !contractGtcData.hasComments) { return true; } // 코멘트가 있으면 불가 (협의 중) return false; } // GTC가 아닌 경우는 협의 완료 여부만 확인 return false; }); // 대량 재발송 const handleBulkResend = async () => { if (!hasSelectedRows) { toast.error("재발송할 계약서를 선택해주세요") return } setResendDialog(true) } // 선택된 계약서들 일괄 다운로드 const handleBulkDownload = async () => { if (!canBulkDownload) { toast.error("다운로드할 파일이 있는 계약서를 선택해주세요") return } const selectedContracts = selectedRows .map(row => row.original) .filter(contract => contract.signedFilePath && contract.signedFileName) if (selectedContracts.length === 0) { toast.error("다운로드할 파일이 없습니다") return } // 다운로드 시작 알림 toast.success(`${selectedContracts.length}건의 파일 다운로드를 시작합니다`) let successCount = 0 let failedCount = 0 const failedFiles: string[] = [] // 순차적으로 다운로드 (병렬 다운로드는 브라우저 제한으로 인해 문제가 될 수 있음) for (let i = 0; i < selectedContracts.length; i++) { const contract = selectedContracts[i] try { // 진행 상황 표시 if (selectedContracts.length > 3) { toast.loading(`다운로드 중... (${i + 1}/${selectedContracts.length})`, { id: 'bulk-download-progress' }) } const result = await downloadFile( contract.signedFilePath!, contract.signedFileName!, { action: 'download', showToast: false, // 개별 토스트는 비활성화 onError: (error) => { console.error(`다운로드 실패 - ${contract.signedFileName}:`, error) failedFiles.push(`${contract.vendorName || '업체명 없음'} (${contract.signedFileName})`) failedCount++ }, onSuccess: (fileName) => { console.log(`다운로드 성공 - ${fileName}`) successCount++ } } ) if (result.success) { successCount++ } else { failedCount++ failedFiles.push(`${contract.vendorName || '업체명 없음'} (${contract.signedFileName})`) } // 다운로드 간격 (브라우저 부하 방지) if (i < selectedContracts.length - 1) { await new Promise(resolve => setTimeout(resolve, 300)) } } catch (error) { console.error(`다운로드 에러 - ${contract.signedFileName}:`, error) failedCount++ failedFiles.push(`${contract.vendorName || '업체명 없음'} (${contract.signedFileName})`) } } // 진행 상황 토스트 제거 toast.dismiss('bulk-download-progress') // 최종 결과 표시 if (successCount === selectedContracts.length) { toast.success(`모든 파일 다운로드 완료 (${successCount}건)`) } else if (successCount > 0) { toast.warning( `일부 파일 다운로드 완료\n성공: ${successCount}건, 실패: ${failedCount}건`, { duration: 5000, description: failedFiles.length > 0 ? `실패한 파일: ${failedFiles.slice(0, 3).join(', ')}${failedFiles.length > 3 ? ` 외 ${failedFiles.length - 3}건` : ''}` : undefined } ) } else { toast.error( `모든 파일 다운로드 실패 (${failedCount}건)`, { duration: 5000, description: failedFiles.length > 0 ? `실패한 파일: ${failedFiles.slice(0, 3).join(', ')}${failedFiles.length > 3 ? ` 외 ${failedFiles.length - 3}건` : ''}` : undefined } ) } console.log("일괄 다운로드 완료:", { total: selectedContracts.length, success: successCount, failed: failedCount, failedFiles }) } // 최종승인 const handleFinalApprove = async () => { if (!canFinalApprove) { toast.error("최종승인 가능한 계약서를 선택해주세요") return } setFinalApproveDialog(true) } // 재요청 확인 const confirmResend = async () => { setLoading(true) try { // TODO: 서버액션 호출 await resendContractsAction(resendContracts.map(c => c.id)) console.log("대량 재발송:", resendContracts) toast.success(`${resendContracts.length}건의 계약서 재발송을 완료했습니다`) setResendDialog(false) table.toggleAllPageRowsSelected(false) // 선택 해제 } catch (error) { toast.error("재발송 중 오류가 발생했습니다") console.error(error) } finally { setLoading(false) } } // 최종승인 확인 (수정됨) const confirmFinalApprove = async () => { setLoading(true) try { // 먼저 서명 가능한 계약서들을 준비 const prepareResult = await prepareFinalApprovalAction( finalApproveContracts.map(c => c.id) ) if (prepareResult.success && prepareResult.contracts) { // 서명이 필요한 경우 서명 다이얼로그 열기 setContractsToSign(prepareResult.contracts) setFinalApproveDialog(false) // 기존 다이얼로그는 닫기 // buyerSignDialog는 더 이상 필요 없으므로 제거 } else { toast.error(prepareResult.message) } } catch (error) { toast.error("최종승인 준비 중 오류가 발생했습니다") console.error(error) } finally { setLoading(false) } } // 구매자 서명 완료 콜백 const handleBuyerSignComplete = () => { setContractsToSign([]) // 계약서 목록 초기화하여 BasicContractSignDialog 언마운트 table.toggleAllPageRowsSelected(false) toast.success("모든 계약서의 최종승인이 완료되었습니다!") } // SSLVW 데이터 선택 확인 핸들러 const handleSSLVWConfirm = async (selectedSSLVWData: any[]) => { if (!selectedSSLVWData || selectedSSLVWData.length === 0) { toast.error("선택된 데이터가 없습니다.") return } if (selectedRows.length !== 1) { toast.error("계약서 한 건을 선택해주세요.") return } try { setLoading(true) // 선택된 계약서 ID들 추출 const selectedContractIds = selectedRows.map(row => row.original.id) // 서버 액션 호출 const result = await updateLegalReviewStatusFromSSLVW(selectedSSLVWData, selectedContractIds) if (result.success) { toast.success(result.message) router.refresh() table.toggleAllPageRowsSelected(false) } else { toast.error(result.message) } if (result.errors && result.errors.length > 0) { toast.warning(`일부 처리 실패: ${result.errors.join(', ')}`) } } catch (error) { console.error('SSLVW 확인 처리 실패:', error) toast.error('법무검토 상태 업데이트 중 오류가 발생했습니다.') } finally { setLoading(false) } } // CPVW 데이터 선택 확인 핸들러 const handleCPVWConfirm = async (selectedCPVWData: any[]) => { if (!selectedCPVWData || selectedCPVWData.length === 0) { toast.error("선택된 데이터가 없습니다.") return } if (selectedRows.length !== 1) { toast.error("계약서 한 건을 선택해주세요.") return } try { setLoading(true) // 선택된 계약서 ID들 추출 const selectedContractIds = selectedRows.map(row => row.original.id) // 서버 액션 호출 const result = await updateComplianceReviewStatusFromCPVW(selectedCPVWData, selectedContractIds) if (result.success) { toast.success(result.message) router.refresh() table.toggleAllPageRowsSelected(false) } else { toast.error(result.message) } if (result.errors && result.errors.length > 0) { toast.warning(`일부 처리 실패: ${result.errors.join(', ')}`) } } catch (error) { console.error('CPVW 확인 처리 실패:', error) toast.error('준법문의 상태 업데이트 중 오류가 발생했습니다.') } finally { setLoading(false) } } // 빠른 승인 (서명 없이) const confirmQuickApproval = async () => { setLoading(true) try { const result = await quickFinalApprovalAction( finalApproveContracts.map(c => c.id) ) if (result.success) { toast.success(result.message) setFinalApproveDialog(false) table.toggleAllPageRowsSelected(false) } else { toast.error(result.message) } } catch (error) { toast.error("최종승인 중 오류가 발생했습니다") console.error(error) } finally { setLoading(false) } } const hasPendingResolution = (contractId: number) => { const state = redFlagResolutionData[contractId] return Boolean(state?.pendingApprovalId && !state?.resolved) } const redFlagEligibleContracts = selectedRows .map(row => row.original) .filter(contract => { if (redFlagData[contract.id] !== true) return false return !hasPendingResolution(contract.id) }) const redFlagPendingContracts = selectedRows .map(row => row.original) .filter(contract => hasPendingResolution(contract.id)) const canRequestRedFlagResolution = hasSelectedRows && isComplianceTemplate && redFlagEligibleContracts.length > 0 // RED FLAG 해소요청 const handleRequestRedFlagResolution = async () => { if (!canRequestRedFlagResolution) { toast.error("해소요청 가능한 RED FLAG 계약서를 선택해주세요") return } if (redFlagPendingContracts.length > 0) { const preview = redFlagPendingContracts .map((contract) => contract.vendorName || `계약 ${contract.id}`) .slice(0, 2) .join(", ") toast.info( `${preview}${redFlagPendingContracts.length > 2 ? ` 외 ${redFlagPendingContracts.length - 2}건` : ""}은 해소요청이 이미 진행 중입니다.`, { description: "진행 중인 계약서는 자동으로 제외하고 요청합니다.", } ) } setLoading(true) try { const contractIds = redFlagEligibleContracts.map(c => c.id) const preview = await prepareRedFlagResolutionApproval(contractIds) setRedFlagApprovalPreview(preview) setShowRedFlagApprovalDialog(true) } catch (error) { console.error("RED FLAG 해소요청 준비 오류:", error) toast.error( error instanceof Error ? error.message : "RED FLAG 해소요청 정보를 준비하는 중 오류가 발생했습니다." ) } finally { setLoading(false) } } const handleRedFlagApprovalConfirm = async (approvalData: { approvers: string[] title: string attachments?: File[] }) => { if (!redFlagApprovalPreview) { toast.error("결재 정보를 찾을 수 없습니다. 다시 시도해주세요.") return } setLoading(true) try { const result = await requestRedFlagResolution({ contractIds: redFlagApprovalPreview.contractIds, approvers: approvalData.approvers, title: approvalData.title, }) toast.success("RED FLAG 해소요청 결재가 상신되었습니다.", { description: `결재 ID: ${result.approvalId}`, }) table.toggleAllPageRowsSelected(false) setShowRedFlagApprovalDialog(false) setRedFlagApprovalPreview(null) } catch (error) { console.error("RED FLAG 해소요청 오류:", error) toast.error( error instanceof Error ? error.message : "RED FLAG 해소요청 중 오류가 발생했습니다." ) } finally { setLoading(false) } } // 법무검토 요청 링크 목록 const legalReviewLinks = [ { id: 'domestic-contract', label: '국내계약', url: 'http://60.101.208.95:8080/#/pjt/register-inquiry/domestic-contract', description: '삼성중공업 법무관리시스템 - 국내계약' }, { id: 'domestic-advice', label: '국내자문', url: 'http://60.101.208.95:8080/#/pjt/register-inquiry/domestic-advice', description: '삼성중공업 법무관리시스템 - 국내자문' }, { id: 'overseas-contract', label: '해외계약', url: 'http://60.101.208.95:8080/#/pjt/register-inquiry/overseas-contract', description: '삼성중공업 법무관리시스템 - 해외계약' }, { id: 'overseas-advice', label: '해외자문', url: 'http://60.101.208.95:8080/#/pjt/register-inquiry/overseas-advice', description: '삼성중공업 법무관리시스템 - 해외자문' } ] const complianceInquiryUrl = 'http://60.101.207.55/Inquiry/Write/InquiryWrite.aspx' // 법무검토 요청 / 준법문의 const handleRequestLegalReview = async () => { if (isComplianceTemplate) { // 준법문의: 요청일 기록 후 외부 URL 열기 const selectedContractIds = selectedRows.map(row => row.original.id) try { setLoading(true) const result = await requestComplianceInquiryAction(selectedContractIds) if (result.success) { toast.success(result.message) router.refresh() window.open(complianceInquiryUrl, '_blank', 'noopener,noreferrer') } else { toast.error(result.message) } } catch (error) { console.error('준법문의 요청 처리 실패:', error) toast.error('준법문의 요청 중 오류가 발생했습니다.') } finally { setLoading(false) } return } setLegalReviewDialog(true) } // 법무검토 링크 클릭 핸들러 const handleLegalReviewLinkClick = (url: string) => { window.open(url, '_blank', 'noopener,noreferrer') setLegalReviewDialog(false) } return ( <>
{/* 일괄 다운로드 버튼 */} {/* RED FLAG 해소요청 버튼 (준법서약 템플릿만) */} {isComplianceTemplate && ( )} {/* 재요청 버튼 */} {/* 법무검토 버튼 (SSLVW 데이터 조회) - 준법서약 템플릿이 아닐 때만 표시 */} {!isComplianceTemplate && ( )} {/* 준법문의 요청 데이터 조회 버튼 (준법서약 템플릿만) */} {isComplianceTemplate && ( )} {/* 법무검토 요청 / 준법문의 버튼 */} {isComplianceTemplate ? ( ) : ( )} {/* 최종승인 버튼 */} {/* 실제 구매자 서명을 위한 BasicContractSignDialog */} {contractsToSign.length > 0 && ( 0} mode="buyer" // 구매자 모드 prop t={(key) => key} /> )} {/* Export 버튼 */}
{/* 재발송 다이얼로그 */} 계약서 재발송 확인 선택한 {resendContracts.length}건의 계약서를 재발송합니다.
{resendContracts.map((contract, index) => (
{contract.vendorName || '업체명 없음'}
{contract.vendorCode || '코드 없음'} | {contract.templateName || '템플릿명 없음'}
{contract.status}
))}
{/* 법무검토 요청 다이얼로그 (준법 템플릿 제외) */} {!isComplianceTemplate && ( 법무검토 요청 법무검토 요청 유형을 선택하세요. 선택한 링크가 새 창에서 열립니다.
삼성중공업 법무관리시스템
아래 링크 중 해당하는 유형을 선택하여 법무검토를 요청하세요.
{legalReviewLinks.map((link) => ( ))}
)} {/* 최종승인 다이얼로그 */} 최종승인 전 확인 선택한 {finalApproveContracts.length}건의 계약서를 최종승인을 위해 서명을 호출합니다.
{contractsWithoutLegalReview.length > 0 && (
법무검토 없이 승인되는 계약서
{contractsWithoutLegalReview.length}건의 계약서가 법무검토 없이 승인됩니다. 승인 후에는 되돌릴 수 없으니 신중히 검토해주세요.
)}
{finalApproveContracts.map((contract) => { const hasLegalReview = contract.legalReviewRequestedAt && contract.legalReviewCompletedAt const noLegalReview = !contract.legalReviewRequestedAt && !contract.legalReviewCompletedAt return (
{contract.vendorName || '업체명 없음'}
{contract.vendorCode || '코드 없음'} | {contract.templateName || '템플릿명 없음'}
{noLegalReview && (
법무검토 없음
)} {hasLegalReview && (
법무검토 완료
)}
{contract.status}
) })}
{redFlagApprovalPreview && session?.user?.epId && ( { setShowRedFlagApprovalDialog(open) if (!open) { setRedFlagApprovalPreview(null) } }} templateName={redFlagApprovalPreview.templateName} variables={redFlagApprovalPreview.variables} title={redFlagApprovalPreview.title} defaultApprovers={redFlagApprovalPreview.defaultApprovers} currentUser={{ id: Number(session.user.id), epId: session.user.epId, name: session.user.name || undefined, email: session.user.email || undefined, }} onConfirm={handleRedFlagApprovalConfirm} /> )} ) }