"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 } 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 } from "../service" import { BasicContractSignDialog } from "../vendor-table/basic-contract-sign-dialog" import { SSLVWPurInqReqDialog } from "@/components/common/legal/sslvw-pur-inq-req-dialog" interface BasicContractDetailTableToolbarActionsProps { table: Table gtcData?: Record } export function BasicContractDetailTableToolbarActions({ table, gtcData = {} }: 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 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; } if (contract.legalReviewRequestedAt && !contract.legalReviewCompletedAt) { return false; } return true; }); // 법무검토 요청 가능 여부 // 1. 협의 완료됨 (negotiationCompletedAt 있음) OR // 2. 협의 없음 (코멘트 없음, hasComments: false) // 협의 중 (negotiationCompletedAt 없고 코멘트 있음)은 불가 const canRequestLegalReview = hasSelectedRows && selectedRows.some(row => { const contract = row.original; // 이미 법무검토 요청된 계약서는 제외 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 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 } 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) // 테이블 데이터 갱신 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) } } // 빠른 승인 (서명 없이) 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 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 handleRequestLegalReview = () => { setLegalReviewDialog(true) } // 법무검토 링크 클릭 핸들러 const handleLegalReviewLinkClick = (url: string) => { window.open(url, '_blank', 'noopener,noreferrer') setLegalReviewDialog(false) } return ( <>
{/* 일괄 다운로드 버튼 */} {/* 재요청 버튼 */} {/* 법무검토 버튼 (SSLVW 데이터 조회) */} {/* 법무검토 요청 버튼 */} {/* 최종승인 버튼 */} {/* 실제 구매자 서명을 위한 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}
))}
{/* 법무검토 요청 다이얼로그 */} 법무검토 요청 법무검토 요청 유형을 선택하세요. 선택한 링크가 새 창에서 열립니다.
삼성중공업 법무관리시스템
아래 링크 중 해당하는 유형을 선택하여 법무검토를 요청하세요.
{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}
) })}
) }