diff options
Diffstat (limited to 'lib/basic-contract')
6 files changed, 103 insertions, 118 deletions
diff --git a/lib/basic-contract/actions/check-gtc-comments.ts b/lib/basic-contract/actions/check-gtc-comments.ts index 1ce998bc..6d523cb0 100644 --- a/lib/basic-contract/actions/check-gtc-comments.ts +++ b/lib/basic-contract/actions/check-gtc-comments.ts @@ -4,11 +4,9 @@ import db from "@/db/db"; import { eq, and, desc, isNull, isNotNull, ne, or } from "drizzle-orm"; import { gtcDocuments, - gtcVendorDocuments, - gtcVendorClauses, - gtcNegotiationHistory, projects, - BasicContractView + BasicContractView, + agreementComments } from "@/db/schema"; /** @@ -27,7 +25,7 @@ function extractProjectCodeFromTemplateName(templateName: string): string | null // 프로젝트 코드는 보통 첫 번째 단어 if (words.length > 1 && words[words.length - 1].toLowerCase() === 'gtc') { - return words[words.length - 1]; + return words[0]; // 첫 번째 단어가 프로젝트 코드 } return null; @@ -105,14 +103,16 @@ async function checkGTCCommentsForContract( // gtcDocumentId가 없어도 새로운 코멘트 시스템은 작동해야 함 if (basicContractId) { console.log(`🔍 [checkGTCCommentsForContract] basicContractId: ${basicContractId} 로 코멘트 조회`); - const { agreementComments } = await import("@/db/schema"); + // 기존 방식과 동일하게 빈 코멘트는 제외 const newComments = await db .select({ id: agreementComments.id }) .from(agreementComments) .where( and( eq(agreementComments.basicContractId, basicContractId), - eq(agreementComments.isDeleted, false) + eq(agreementComments.isDeleted, false), + isNotNull(agreementComments.comment), + ne(agreementComments.comment, '') ) ) .limit(1); @@ -130,51 +130,9 @@ async function checkGTCCommentsForContract( console.log(`⚠️ [checkGTCCommentsForContract] basicContractId ${basicContractId}: agreementComments 없음`); } - // GTC Document를 찾지 못한 경우 (기존 방식도 체크할 수 없음) - if (!gtcDocumentId) { - console.log(`⚠️ [checkGTCCommentsForContract] gtcDocumentId null - 기존 방식 체크 불가`); - return { gtcDocumentId: null, hasComments: false }; - } - - // 2-2. 기존 방식: gtcDocumentId로 해당 벤더의 vendor documents 찾기 - const vendorDocuments = await db - .select({ id: gtcVendorDocuments.id }) - .from(gtcVendorDocuments) - .where( - and( - eq(gtcVendorDocuments.baseDocumentId, gtcDocumentId), - eq(gtcVendorDocuments.vendorId, vendorId), - eq(gtcVendorDocuments.isActive, true) - ) - ) - .limit(1) - - if (vendorDocuments.length === 0) { - return { gtcDocumentId, hasComments: false }; - } - - // vendor document에 연결된 clauses에서 negotiation history 확인 - const commentsExist = await db - .select({ count: gtcNegotiationHistory.id }) - .from(gtcNegotiationHistory) - .innerJoin( - gtcVendorClauses, - eq(gtcNegotiationHistory.vendorClauseId, gtcVendorClauses.id) - ) - .where( - and( - eq(gtcVendorClauses.vendorDocumentId, vendorDocuments[0].id), - eq(gtcVendorClauses.isActive, true), - isNotNull(gtcNegotiationHistory.comment), - ne(gtcNegotiationHistory.comment, '') - ) - ) - .limit(1) - - return { - gtcDocumentId, - hasComments: commentsExist.length > 0 - }; + // GTC 조항 기능은 더 이상 사용하지 않으므로, agreement_comments만 체크 + // agreement_comments에 코멘트가 없으면 hasComments = false 반환 + return { gtcDocumentId, hasComments: false }; } catch (error) { console.error('Error checking GTC comments for contract:', error); diff --git a/lib/basic-contract/agreement-comments/actions.ts b/lib/basic-contract/agreement-comments/actions.ts index 2f60c3d8..32e9ce4c 100644 --- a/lib/basic-contract/agreement-comments/actions.ts +++ b/lib/basic-contract/agreement-comments/actions.ts @@ -2,7 +2,7 @@ import { revalidateTag } from "next/cache"; import db from "@/db/db"; -import { eq, and, desc, inArray, sql } from "drizzle-orm"; +import { eq, and, desc, inArray, sql, isNotNull, ne } from "drizzle-orm"; import { agreementComments, basicContract, vendors, users } from "@/db/schema"; import { saveFile, deleteFile } from "@/lib/file-stroage"; import { sendEmail } from "@/lib/mail/sendEmail"; @@ -62,7 +62,9 @@ export async function getAgreementComments( .where( and( eq(agreementComments.basicContractId, basicContractId), - eq(agreementComments.isDeleted, false) + eq(agreementComments.isDeleted, false), + isNotNull(agreementComments.comment), + ne(agreementComments.comment, '') ) ) .orderBy(desc(agreementComments.createdAt)); diff --git a/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx b/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx index d03d0720..047866f7 100644 --- a/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx +++ b/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx @@ -172,25 +172,8 @@ export function getDetailColumns({ <Mail className="mr-2 h-4 w-4" /> 재발송 </DropdownMenuItem> - <DropdownMenuItem onClick={async () => { - // 준법서약 템플릿인 경우 compliance 응답 페이지로 이동 - if (contract.templateName?.includes('준법')) { - try { - const response = await getComplianceResponseByBasicContractId(contract.id); - - if (response) { - router.push(`/evcp/compliance/${response.templateId}/responses/${response.id}`); - } else { - toast.error("준법서약 응답을 찾을 수 없습니다."); - setRowAction({ type: "view", row }); - } - } catch (error) { - console.error("Error fetching compliance response:", error); - toast.error("응답 정보를 가져오는데 실패했습니다."); - } - } else { - setRowAction({ type: "view", row }); - } + <DropdownMenuItem onClick={() => { + setRowAction({ type: "view", row }); }}> <FileText className="mr-2 h-4 w-4" /> 상세 정보 @@ -327,19 +310,20 @@ export function getDetailColumns({ const handleOpenGTC = (e: React.MouseEvent) => { e.stopPropagation() - // gtcDocumentId가 있으면 그걸 사용, 없으면 templateId 사용 - const documentIdToUse = contractGtcData?.gtcDocumentId || contract.templateId - - if (documentIdToUse && contract.vendorId) { - const gtcUrl = `/evcp/basic-contract/vendor-gtc/${documentIdToUse}?vendorId=${contract.vendorId}&vendorName=${encodeURIComponent(contract.vendorName || '')}&contractId=${contract.id}&templateId=${contract.templateId}` - window.open(gtcUrl, '_blank') - } else { - console.error('GTC 페이지를 열 수 없습니다:', { - gtcDocumentId: contractGtcData?.gtcDocumentId, - templateId: contract.templateId, - vendorId: contract.vendorId - }) + // 상세보기와 동일하게 contract.id를 경로 파라미터로 사용 + const params = new URLSearchParams(); + if (contract.templateId) { + params.set("templateId", contract.templateId.toString()); + } + if (contract.vendorId) { + params.set("vendorId", contract.vendorId.toString()); + } + if (contract.vendorName) { + params.set("vendorName", contract.vendorName); } + const query = params.toString(); + const gtcUrl = `/evcp/basic-contract/vendor-gtc/${contract.id}${query ? `?${query}` : ""}`; + window.open(gtcUrl, '_blank'); } return ( diff --git a/lib/basic-contract/status-detail/basic-contracts-detail-table.tsx b/lib/basic-contract/status-detail/basic-contracts-detail-table.tsx index a2e1c5e4..9c5da894 100644 --- a/lib/basic-contract/status-detail/basic-contracts-detail-table.tsx +++ b/lib/basic-contract/status-detail/basic-contracts-detail-table.tsx @@ -235,13 +235,36 @@ type RedFlagResolutionState = { // GTC 템플릿인 경우 GTC 협의 페이지로 이동 const contract = rowAction.row.original; if (contract.templateName?.includes('GTC')) { - const contractGtcData = gtcData[contract.id]; - if (contractGtcData?.gtcDocumentId) { - const gtcUrl = `/evcp/basic-contract/vendor-gtc/${contractGtcData.gtcDocumentId}?vendorId=${contract.vendorId}&vendorName=${encodeURIComponent(contract.vendorName || '')}&contractId=${contract.id}&templateId=${contract.templateId}`; - router.push(gtcUrl); - } else { - toast.error("GTC 문서 정보를 찾을 수 없습니다."); + // GTC 템플릿인 경우 GTC 협의 페이지로 이동 + // 준법과 동일하게 contract.id를 경로 파라미터로 사용 + const params = new URLSearchParams(); + if (contract.templateId) { + params.set("templateId", contract.templateId.toString()); } + if (contract.vendorId) { + params.set("vendorId", contract.vendorId.toString()); + } + if (contract.vendorName) { + params.set("vendorName", contract.vendorName); + } + const query = params.toString(); + const gtcUrl = `/evcp/basic-contract/vendor-gtc/${contract.id}${query ? `?${query}` : ""}`; + router.push(gtcUrl); + } else if (contract.templateName?.includes('준법')) { + // 준법 템플릿인 경우 준법 코멘트 페이지로 이동 + const params = new URLSearchParams(); + if (contract.templateId) { + params.set("templateId", contract.templateId.toString()); + } + if (contract.vendorId) { + params.set("vendorId", contract.vendorId.toString()); + } + if (contract.vendorName) { + params.set("vendorName", contract.vendorName); + } + const query = params.toString(); + const complianceUrl = `/evcp/basic-contract/compliance-comments/${contract.id}${query ? `?${query}` : ""}`; + router.push(complianceUrl); } else { // 일반 계약서인 경우: 상세 정보를 보여주는 기능 // 현재는 준비 중이지만, 향후 다이얼로그나 시트를 열 수 있음 diff --git a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx index e5aab10d..407a3c4d 100644 --- a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx +++ b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx @@ -142,13 +142,20 @@ const canCompleteCurrentContract = React.useMemo(() => { // 계약서별 상태 초기화 + // Vendor signed 상태의 계약서도 포함하여 초기화 React.useEffect(() => { if (contracts.length > 0 && contractStatuses.length === 0) { setContractStatuses( - contracts.map(contract => ({ - id: contract.id, - status: 'pending' as const - })) + contracts.map(contract => { + // 이미 서명된 계약서는 vendor_signed 상태로 초기화 + const isSigned = contract.vendorSignedAt || + contract.status === "COMPLETED" || + contract.status === "VENDOR_SIGNED"; + return { + id: contract.id, + status: isSigned ? ('vendor_signed' as const) : ('pending' as const) + }; + }) ); } }, [contracts, contractStatuses.length]); @@ -705,7 +712,7 @@ const canCompleteCurrentContract = React.useMemo(() => { <div className="space-y-2"> {filteredContracts.map((contract) => { const contractStatus = contractStatuses.find(status => status.id === contract.id); - const isCompleted = contractStatus?.status === 'completed'; + const isCompleted = contractStatus?.status === 'completed' || contractStatus?.status === 'vendor_signed'; const hasError = contractStatus?.status === 'error'; // 계약서별 완료 상태 확인 @@ -732,7 +739,8 @@ const canCompleteCurrentContract = React.useMemo(() => { !isCompleted && !hasError && "hover:bg-blue-50 hover:border-blue-200" )} onClick={() => handleSelectContract(contract)} - disabled={isCompleted} + // Vendor signed 상태에서도 코멘트를 볼 수 있도록 비활성화하지 않음 + disabled={false} > <div className="flex flex-col w-full space-y-1"> {/* 첫 번째 줄: 제목 + 상태 */} @@ -851,7 +859,7 @@ const canCompleteCurrentContract = React.useMemo(() => { {selectedContract.templateName || t("basicContracts.dialog.document")} {/* 현재 계약서 상태 표시 */} - {currentContractStatus?.status === 'completed' ? ( + {(currentContractStatus?.status === 'completed' || currentContractStatus?.status === 'vendor_signed') ? ( <Badge variant="outline" className="ml-2 bg-green-50 text-green-700 border-green-200"> <CheckCircle2 className="h-3 w-3 mr-1" /> {isBuyerMode ? "승인 완료" : "서명 완료"} @@ -931,10 +939,10 @@ const canCompleteCurrentContract = React.useMemo(() => { <div className="p-4 flex justify-between items-center bg-gray-50 border-t flex-shrink-0"> <div className="flex items-center space-x-4"> {/* 현재 계약서가 완료된 경우 */} - {currentContractStatus?.status === 'completed' ? ( + {currentContractStatus?.status === 'completed' || currentContractStatus?.status === 'vendor_signed' ? ( <p className="text-sm text-green-600 flex items-center"> <CheckCircle2 className="h-4 w-4 text-green-500 mr-1" /> - 이 계약서는 이미 {isBuyerMode ? "승인이" : "서명이"} 완료되었습니다 + 이 계약서는 이미 {isBuyerMode ? "승인이" : "서명이"} 완료되었습니다. 코멘트를 확인할 수 있습니다. </p> ) : currentContractStatus?.status === 'error' ? ( <p className="text-sm text-red-600 flex items-center"> @@ -1015,22 +1023,27 @@ const canCompleteCurrentContract = React.useMemo(() => { <Trophy className="h-4 w-4" /> 모든 {isBuyerMode ? "승인" : "서명"} 완료 </Button> - ) : currentContractStatus?.status === 'completed' ? ( - // 현재 계약서가 완료된 경우 - <Button - variant="outline" - className="gap-2" - onClick={() => { - const nextContract = getNextPendingContract(); - if (nextContract) { - setSelectedContract(nextContract); - } - }} - disabled={!getNextPendingContract()} - > - <ArrowRight className="h-4 w-4" /> - 다음 계약서 - </Button> + ) : (currentContractStatus?.status === 'completed' || currentContractStatus?.status === 'vendor_signed') ? ( + // 현재 계약서가 완료된 경우 - 코멘트 확인만 가능 + <div className="flex items-center gap-2"> + <Button + variant="outline" + className="gap-2" + onClick={() => { + const nextContract = getNextPendingContract(); + if (nextContract) { + setSelectedContract(nextContract); + } + }} + disabled={!getNextPendingContract()} + > + <ArrowRight className="h-4 w-4" /> + 다음 계약서 + </Button> + <p className="text-sm text-gray-500"> + 서명 완료 - 코멘트 확인 가능 + </p> + </div> ) : ( // 현재 계약서를 서명해야 하는 경우 <Button diff --git a/lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx b/lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx index 1fc6fe6b..7bab0ad5 100644 --- a/lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx +++ b/lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx @@ -27,13 +27,18 @@ export function BasicContractTableToolbarActions({ table }: TemplateTableToolbar return translated === key ? fallback : translated; }, [t, ready]); - // PENDING 상태인 선택된 계약서들 + // PENDING 또는 COMPLETED(VENDOR_SIGNED) 상태인 선택된 계약서들 + // Vendor signed 상태에서도 코멘트를 확인할 수 있도록 포함 const pendingContracts = React.useMemo(() => { return table .getFilteredSelectedRowModel() .rows .map(row => row.original) - .filter(contract => contract.status === "PENDING"); + .filter(contract => + contract.status === "PENDING" || + contract.status === "COMPLETED" || + contract.status === "VENDOR_SIGNED" + ); }, [table.getFilteredSelectedRowModel().rows]); // 선택된 행이 있는지 확인 |
