From e84cf02a1cb4959a9d3bb5bbf37885c13a447f78 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 13 Oct 2025 17:29:33 +0900 Subject: (김준회) SHI/벤더 PO 구현 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/po/vendor-table/service.ts | 573 +++++++++++++++++----- lib/po/vendor-table/shi-vendor-po-columns.tsx | 59 +-- lib/po/vendor-table/types.ts | 7 +- lib/po/vendor-table/vendor-po-actions.tsx | 273 +++++++++++ lib/po/vendor-table/vendor-po-columns.tsx | 158 +----- lib/po/vendor-table/vendor-po-items-dialog.tsx | 13 +- lib/po/vendor-table/vendor-po-note-dialog.tsx | 162 ++++++ lib/po/vendor-table/vendor-po-table.tsx | 20 +- lib/po/vendor-table/vendor-po-toolbar-actions.tsx | 61 +-- lib/soap/ecc/mapper/po-mapper.ts | 22 +- 10 files changed, 1019 insertions(+), 329 deletions(-) create mode 100644 lib/po/vendor-table/vendor-po-actions.tsx create mode 100644 lib/po/vendor-table/vendor-po-note-dialog.tsx (limited to 'lib') diff --git a/lib/po/vendor-table/service.ts b/lib/po/vendor-table/service.ts index 195144a2..224dd2f1 100644 --- a/lib/po/vendor-table/service.ts +++ b/lib/po/vendor-table/service.ts @@ -1,14 +1,14 @@ "use server"; import { GetVendorPOSchema } from "./validations"; -import { getVendorPOsPage } from "./mock-data"; import { VendorPO, VendorPOItem } from "./types"; import db from "@/db/db"; -import { contracts, contractItems } from "@/db/schema/contract"; +import { contracts, contractItems, ContractStatus } from "@/db/schema/contract"; import { projects } from "@/db/schema/projects"; import { vendors } from "@/db/schema/vendors"; import { items } from "@/db/schema/items"; -import { eq, and, or, ilike, count, desc, asc } from "drizzle-orm"; +import { revalidatePath } from "next/cache"; +import { eq, and, or, ilike, count, desc, asc, SQL } from "drizzle-orm"; /** * 벤더 PO 목록 조회 @@ -20,17 +20,16 @@ export async function getVendorPOs(input: GetVendorPOSchema) { const offset = (input.page - 1) * input.perPage; // 검색 조건 구성 - let whereConditions = []; + const whereConditions: SQL[] = []; if (input.search) { const searchTerm = `%${input.search}%`; - whereConditions.push( - or( - ilike(contracts.contractNo, searchTerm), - ilike(contracts.contractName, searchTerm), - ilike(projects.name, searchTerm), - ilike(vendors.vendorName, searchTerm) - ) + const searchCondition = or( + ilike(contracts.contractNo, searchTerm), + ilike(contracts.contractName, searchTerm), + ilike(projects.name, searchTerm), + ilike(vendors.vendorName, searchTerm) ); + if (searchCondition) whereConditions.push(searchCondition); } // 벤더 필터링 (partners 페이지에서 사용) @@ -44,13 +43,15 @@ export async function getVendorPOs(input: GetVendorPOSchema) { if (filter.id && filter.value) { switch (filter.id) { case "contractStatus": - whereConditions.push(ilike(contracts.status, `%${filter.value}%`)); + const statusCondition = ilike(contracts.status, `%${filter.value}%`); + if (statusCondition) whereConditions.push(statusCondition); break; case "contractType": - whereConditions.push(ilike(contracts.purchaseDocType, `%${filter.value}%`)); + const typeCondition = ilike(contracts.purchaseDocType, `%${filter.value}%`); + if (typeCondition) whereConditions.push(typeCondition); break; case "currency": - whereConditions.push(eq(contracts.currency, filter.value)); + whereConditions.push(eq(contracts.currency, filter.value as string)); break; // 추가 필터 조건들... } @@ -117,7 +118,11 @@ export async function getVendorPOs(input: GetVendorPOSchema) { priceIndexYn: contracts.priceIndexYn, writtenContractNo: contracts.writtenContractNo, contractVersion: contracts.contractVersion, - + + // 계약서 내용 및 노트 + contractContent: contracts.contractContent, + remarks: contracts.remarks, + createdAt: contracts.createdAt, updatedAt: contracts.updatedAt, @@ -145,34 +150,146 @@ export async function getVendorPOs(input: GetVendorPOSchema) { // VendorPO 타입으로 변환 const data: VendorPO[] = rawData.map(row => ({ + id: row.id, + contractNo: row.contractNo || '', + revision: 'Rev.01', // mock 데이터용 기본값 + itemNo: 'ITM-AUTO', // mock 데이터용 기본값 + contractStatus: row.status || '', + contractType: row.purchaseDocType || '', + details: '상세보기', // mock 데이터용 기본값 + projectName: row.projectName || '', + contractName: row.contractName || '', + contractPeriod: row.startDate && row.endDate + ? `${row.startDate} ~ ${row.endDate}` + : '', + contractQuantity: '1 LOT', // 기본값 (실제로는 contract_items에서 계산 필요) + currency: row.currency || 'KRW', + paymentTerms: row.paymentTerms || '', + tax: '10%', // 기본값 (실제로는 contract_items에서 계산 필요) + exchangeRate: row.exchangeRate?.toString() || '', + deliveryTerms: row.deliveryTerms || '', + purchaseManager: '', // 사용자 테이블 조인 필요 + poReceiveDate: row.createdAt?.toISOString().split('T')[0] || '', + contractDate: row.startDate || '', + lcNo: undefined, + priceIndexTarget: row.priceIndexYn === 'Y', + linkedContractNo: undefined, + lastModifiedDate: row.updatedAt?.toISOString().split('T')[0] || '', + lastModifiedBy: '', // 사용자 테이블 조인 필요 + + // SAP ECC 추가 필드들 + poVersion: row.poVersion || undefined, + purchaseDocType: row.purchaseDocType || undefined, + purchaseOrg: row.purchaseOrg || undefined, + purchaseGroup: row.purchaseGroup || undefined, + poConfirmStatus: row.poConfirmStatus || undefined, + contractGuaranteeCode: row.contractGuaranteeCode || undefined, + defectGuaranteeCode: row.defectGuaranteeCode || undefined, + guaranteePeriodCode: row.guaranteePeriodCode || undefined, + advancePaymentYn: row.advancePaymentYn || undefined, + budgetAmount: row.budgetAmount ? Number(row.budgetAmount) : undefined, + budgetCurrency: row.budgetCurrency || undefined, + totalAmount: row.totalAmount ? Number(row.totalAmount) : undefined, + totalAmountKrw: row.totalAmountKrw ? Number(row.totalAmountKrw) : undefined, + electronicContractYn: row.electronicContractYn || undefined, + electronicApprovalDate: row.electronicApprovalDate || undefined, + electronicApprovalTime: row.electronicApprovalTime || undefined, + ownerApprovalYn: row.ownerApprovalYn || undefined, + plannedInOutFlag: row.plannedInOutFlag || undefined, + settlementStandard: row.settlementStandard || undefined, + weightSettlementFlag: row.weightSettlementFlag || undefined, + priceIndexYn: row.priceIndexYn || undefined, + writtenContractNo: row.writtenContractNo || undefined, + contractVersion: row.contractVersion || undefined, + })); + + return { + data, + pageCount + }; + } catch (err) { + console.error("Error in getVendorPOs:", err); + return { data: [], pageCount: 0 }; + } +} + +/** + * 벤더 PO 상세 정보 조회 + */ +export async function getVendorPOById(id: number): Promise { + try { + const [row] = await db + .select({ + id: contracts.id, + contractNo: contracts.contractNo, + contractName: contracts.contractName, + status: contracts.status, + startDate: contracts.startDate, + endDate: contracts.endDate, + currency: contracts.currency, + totalAmount: contracts.totalAmount, + totalAmountKrw: contracts.totalAmountKrw, + paymentTerms: contracts.paymentTerms, + deliveryTerms: contracts.deliveryTerms, + exchangeRate: contracts.exchangeRate, + poVersion: contracts.poVersion, + purchaseDocType: contracts.purchaseDocType, + purchaseOrg: contracts.purchaseOrg, + purchaseGroup: contracts.purchaseGroup, + poConfirmStatus: contracts.poConfirmStatus, + contractGuaranteeCode: contracts.contractGuaranteeCode, + defectGuaranteeCode: contracts.defectGuaranteeCode, + guaranteePeriodCode: contracts.guaranteePeriodCode, + advancePaymentYn: contracts.advancePaymentYn, + budgetAmount: contracts.budgetAmount, + budgetCurrency: contracts.budgetCurrency, + electronicContractYn: contracts.electronicContractYn, + electronicApprovalDate: contracts.electronicApprovalDate, + electronicApprovalTime: contracts.electronicApprovalTime, + ownerApprovalYn: contracts.ownerApprovalYn, + plannedInOutFlag: contracts.plannedInOutFlag, + settlementStandard: contracts.settlementStandard, + weightSettlementFlag: contracts.weightSettlementFlag, + priceIndexYn: contracts.priceIndexYn, + writtenContractNo: contracts.writtenContractNo, + contractVersion: contracts.contractVersion, + createdAt: contracts.createdAt, + updatedAt: contracts.updatedAt, + projectName: projects.name, + }) + .from(contracts) + .leftJoin(projects, eq(contracts.projectId, projects.id)) + .where(eq(contracts.id, id)) + .limit(1); + + if (!row) return null; + + // VendorPO 타입으로 변환 + const po: VendorPO = { id: row.id, contractNo: row.contractNo || '', - revision: 'Rev.01', // mock 데이터용 기본값 - itemNo: 'ITM-AUTO', // mock 데이터용 기본값 + revision: 'Rev.01', + itemNo: 'ITM-AUTO', contractStatus: row.status || '', contractType: row.purchaseDocType || '', - details: '상세보기', // mock 데이터용 기본값 + details: '상세보기', projectName: row.projectName || '', contractName: row.contractName || '', - contractPeriod: row.startDate && row.endDate - ? `${row.startDate} ~ ${row.endDate}` - : '', - contractQuantity: '1 LOT', // 기본값 (실제로는 contract_items에서 계산 필요) + contractPeriod: row.startDate && row.endDate ? `${row.startDate} ~ ${row.endDate}` : '', + contractQuantity: '1 LOT', currency: row.currency || 'KRW', paymentTerms: row.paymentTerms || '', - tax: '10%', // 기본값 (실제로는 contract_items에서 계산 필요) + tax: '10%', exchangeRate: row.exchangeRate?.toString() || '', deliveryTerms: row.deliveryTerms || '', - purchaseManager: '', // 사용자 테이블 조인 필요 + purchaseManager: '', poReceiveDate: row.createdAt?.toISOString().split('T')[0] || '', contractDate: row.startDate || '', lcNo: undefined, priceIndexTarget: row.priceIndexYn === 'Y', linkedContractNo: undefined, lastModifiedDate: row.updatedAt?.toISOString().split('T')[0] || '', - lastModifiedBy: '', // 사용자 테이블 조인 필요 - - // SAP ECC 추가 필드들 + lastModifiedBy: '', poVersion: row.poVersion || undefined, purchaseDocType: row.purchaseDocType || undefined, purchaseOrg: row.purchaseOrg || undefined, @@ -196,90 +313,9 @@ export async function getVendorPOs(input: GetVendorPOSchema) { priceIndexYn: row.priceIndexYn || undefined, writtenContractNo: row.writtenContractNo || undefined, contractVersion: row.contractVersion || undefined, - })); - - return { - data, - pageCount - }; - - // 목업 데이터 사용 (개발/테스트용) - // const result = getVendorPOsPage( - // input.page, - // input.perPage, - // input.search, - // input.filters - // ); - - // 실제 데이터베이스 연동시에는 아래와 같은 구조로 구현 - // const offset = (input.page - 1) * input.perPage; - // - // // 검색 조건 구성 - // let whereConditions = []; - // if (input.search) { - // const searchTerm = `%${input.search}%`; - // whereConditions.push( - // or( - // ilike(vendorPOTable.contractNo, searchTerm), - // ilike(vendorPOTable.contractName, searchTerm), - // ilike(vendorPOTable.projectName, searchTerm) - // ) - // ); - // } - // - // // 필터 조건 추가 - // if (input.contractStatus) { - // whereConditions.push(eq(vendorPOTable.contractStatus, input.contractStatus)); - // } - // - // const finalWhere = whereConditions.length > 0 ? and(...whereConditions) : undefined; - // - // // 정렬 조건 - // const orderBy = input.sort.length > 0 - // ? input.sort.map((item) => - // item.desc - // ? desc(vendorPOTable[item.id]) - // : asc(vendorPOTable[item.id]) - // ) - // : [desc(vendorPOTable.lastModifiedDate)]; - // - // // 데이터 조회 - // const data = await db - // .select() - // .from(vendorPOTable) - // .where(finalWhere) - // .orderBy(...orderBy) - // .offset(offset) - // .limit(input.perPage); - // - // // 총 개수 조회 - // const [{ count }] = await db - // .select({ count: count() }) - // .from(vendorPOTable) - // .where(finalWhere); - // - // const pageCount = Math.ceil(count / input.perPage); - - return { - data: result.data, - pageCount: result.pageCount }; - } catch (err) { - console.error("Error in getVendorPOs:", err); - return { data: [], pageCount: 0 }; - } -} -/** - * 벤더 PO 상세 정보 조회 - */ -export async function getVendorPOById(id: number): Promise { - try { - // 목업 데이터에서 조회 - const result = getVendorPOsPage(1, 100); // 모든 데이터 가져오기 - const po = result.data.find(item => item.id === id); - - return po || null; + return po; } catch (err) { console.error("Error in getVendorPOById:", err); return null; @@ -292,8 +328,7 @@ export async function getVendorPOById(id: number): Promise { */ export async function handleVendorPOAction( poId: number, - action: string, - data?: any + action: string ): Promise<{ success: boolean; message: string }> { try { // 목업에서는 성공 응답만 반환 @@ -415,3 +450,319 @@ export async function getVendorPOItemsByContractNo(contractNo: string): Promise< throw err; } } + +/** + * PCR 생성 요청: PCR 생성 요청 후, 상태 변경 + */ +export async function createPcrRequest(contractId: number) { + try { + // TODO PCR 생성 요청 로직 구현 + + // PCR 생성 요청 상태로 변경 + await db.update(contracts).set({ status: ContractStatus.PCR_REQUEST }).where(eq(contracts.id, contractId)); + + // 캐시 무효화하여 변경사항 즉시 반영 + revalidatePath("/partners/po"); + } catch (err) { + console.error("Error in createPcrRequest:", err); + throw err; + } + + return { + success: true, + message: "PCR 생성 요청이 성공적으로 완료되었습니다." + }; +} + +/** + * 계약 승인 처리: 상태만 변경 + */ +export async function acceptContract(contractId: number) { + try { + await db.update(contracts).set({ status: ContractStatus.COMPLETE_THE_CONTRACT }).where(eq(contracts.id, contractId)); + + // 캐시 무효화하여 변경사항 즉시 반영 + revalidatePath("/partners/po"); + } catch (err) { + console.error("Error in acceptContract:", err); + throw err; + } + + return { + success: true, + message: "계약이 성공적으로 승인되었습니다." + }; +} +/** + * 계약 승인 취소 처리: 상태만 변경 + */ +export async function cancelAcceptContract(contractId: number) { + try { + + // 계약 승인 상태에서만 취소 가능 + const contract = await db.query.contracts.findFirst({ + where: eq(contracts.id, contractId), + }); + if (!contract) { + throw new Error("계약을 찾을 수 없습니다."); + } + if (contract.status !== ContractStatus.COMPLETE_THE_CONTRACT) { + throw new Error("계약 승인 상태가 아닙니다."); + } + + // 취소 처리 + await db.update(contracts).set({ status: ContractStatus.CONTRACT_ACCEPT_REQUEST }).where(eq(contracts.id, contractId)); + + // 캐시 무효화하여 변경사항 즉시 반영 + revalidatePath("/partners/po"); + } catch (err) { + console.error("Error in cancelAcceptContract:", err); + throw err; + } + + return { + success: true, + message: "계약이 성공적으로 승인 취소되었습니다." + }; +} + +/** + * 계약 거절 처리: 거절 사유를 입력받고, 상태 변경 + */ +export async function rejectContract(contractId: number, rejectionReason: string) { + try { + await db.update(contracts).set({ status: ContractStatus.REJECT_TO_ACCEPT_CONTRACT, rejectionReason }).where(eq(contracts.id, contractId)); + + // 캐시 무효화하여 변경사항 즉시 반영 + revalidatePath("/partners/po"); + } catch (err) { + console.error("Error in rejectContract:", err); + throw err; + } + + return { + success: true, + message: "계약이 성공적으로 거절되었습니다." + }; +} + +/** + * 벤더 코멘트 저장 + */ +export async function saveVendorComment(contractId: number, vendorComment: string) { + try { + await db.update(contracts).set({ + vendorComment, + updatedAt: new Date() + }).where(eq(contracts.id, contractId)); + + // 캐시 무효화하여 변경사항 즉시 반영 + revalidatePath("/partners/po"); + } catch (err) { + console.error("Error in saveVendorComment:", err); + throw err; + } + + return { + success: true, + message: "의견이 성공적으로 저장되었습니다." + }; +} + +/** + * SHI 코멘트 저장 (EVCP용) + */ +export async function saveSHIComment(contractId: number, shiComment: string) { + try { + await db.update(contracts).set({ + shiComment, + updatedAt: new Date() + }).where(eq(contracts.id, contractId)); + + // 캐시 무효화하여 변경사항 즉시 반영 + revalidatePath("/evcp/po"); + revalidatePath(`/evcp/po/${contractId}`); + } catch (err) { + console.error("Error in saveSHIComment:", err); + throw err; + } + + return { + success: true, + message: "SHI 의견이 성공적으로 저장되었습니다." + }; +} + +/** + * 특정 계약의 상세 정보 조회 (EVCP/SHI용) + */ +export async function getContractDetail(contractId: number) { + try { + // contractId 유효성 검사 + if (!contractId || isNaN(contractId)) { + return { success: false, error: "유효하지 않은 계약 ID입니다." }; + } + + // 계약 기본 정보 조회 + const [contractData] = await db + .select({ + // contracts 테이블 필드들 + id: contracts.id, + contractNo: contracts.contractNo, + contractName: contracts.contractName, + status: contracts.status, + startDate: contracts.startDate, + endDate: contracts.endDate, + contractDate: contracts.createdAt, + currency: contracts.currency, + totalAmount: contracts.totalAmount, + totalAmountKrw: contracts.totalAmountKrw, + paymentTerms: contracts.paymentTerms, + deliveryTerms: contracts.deliveryTerms, + exchangeRate: contracts.exchangeRate, + rejectionReason: contracts.rejectionReason, + + // SAP ECC 추가 필드들 + poVersion: contracts.poVersion, + purchaseDocType: contracts.purchaseDocType, + purchaseOrg: contracts.purchaseOrg, + purchaseGroup: contracts.purchaseGroup, + poConfirmStatus: contracts.poConfirmStatus, + + // 계약/보증 관련 + contractGuaranteeCode: contracts.contractGuaranteeCode, + defectGuaranteeCode: contracts.defectGuaranteeCode, + guaranteePeriodCode: contracts.guaranteePeriodCode, + advancePaymentYn: contracts.advancePaymentYn, + + // 계약서 내용 및 노트 + contractContent: contracts.contractContent, + remarks: contracts.remarks, + vendorComment: contracts.vendorComment, + shiComment: contracts.shiComment, + + createdAt: contracts.createdAt, + updatedAt: contracts.updatedAt, + + // 조인된 테이블 필드들 + projectId: projects.id, + projectName: projects.name, + projectCode: projects.code, + vendorId: vendors.id, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, + }) + .from(contracts) + .leftJoin(projects, eq(contracts.projectId, projects.id)) + .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) + .where(eq(contracts.id, contractId)) + .limit(1); + + if (!contractData) { + return { success: false, error: "계약 정보를 찾을 수 없습니다." }; + } + + // 계약 품목 조회 + const items = await getVendorPOItems(contractId); + + return { + success: true, + data: { + ...contractData, + items, + }, + }; + } catch (error) { + console.error("Error fetching contract detail:", error); + return { success: false, error: "계약 상세 정보 조회 중 오류가 발생했습니다." }; + } +} + +/** + * 특정 계약의 상세 정보 조회 (벤더용 계약 상세 페이지) + */ +export async function getVendorContractDetail(contractId: number, vendorId: number) { + try { + // contractId 유효성 검사 + if (!contractId || isNaN(contractId)) { + return { success: false, error: "유효하지 않은 계약 ID입니다." }; + } + + // 계약 기본 정보 조회 (벤더 필터링 포함) + const [contractData] = await db + .select({ + // contracts 테이블 필드들 + id: contracts.id, + contractNo: contracts.contractNo, + contractName: contracts.contractName, + status: contracts.status, + startDate: contracts.startDate, + endDate: contracts.endDate, + contractDate: contracts.createdAt, + currency: contracts.currency, + totalAmount: contracts.totalAmount, + totalAmountKrw: contracts.totalAmountKrw, + paymentTerms: contracts.paymentTerms, + deliveryTerms: contracts.deliveryTerms, + exchangeRate: contracts.exchangeRate, + + // SAP ECC 추가 필드들 + poVersion: contracts.poVersion, + purchaseDocType: contracts.purchaseDocType, + purchaseOrg: contracts.purchaseOrg, + purchaseGroup: contracts.purchaseGroup, + poConfirmStatus: contracts.poConfirmStatus, + + // 계약/보증 관련 + contractGuaranteeCode: contracts.contractGuaranteeCode, + defectGuaranteeCode: contracts.defectGuaranteeCode, + guaranteePeriodCode: contracts.guaranteePeriodCode, + advancePaymentYn: contracts.advancePaymentYn, + + // 계약서 내용 및 노트 + contractContent: contracts.contractContent, + remarks: contracts.remarks, + vendorComment: contracts.vendorComment, + shiComment: contracts.shiComment, + + createdAt: contracts.createdAt, + updatedAt: contracts.updatedAt, + + // 조인된 테이블 필드들 + projectId: projects.id, + projectName: projects.name, + projectCode: projects.code, + vendorId: vendors.id, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, + }) + .from(contracts) + .leftJoin(projects, eq(contracts.projectId, projects.id)) + .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) + .where( + and( + eq(contracts.id, contractId), + eq(contracts.vendorId, vendorId) // 벤더 권한 체크 + ) + ) + .limit(1); + + if (!contractData) { + return { success: false, error: "계약 정보를 찾을 수 없거나 접근 권한이 없습니다." }; + } + + // 계약 품목 조회 + const items = await getVendorPOItems(contractId); + + return { + success: true, + data: { + ...contractData, + items, + }, + }; + } catch (error) { + console.error("Error fetching contract detail:", error); + return { success: false, error: "계약 상세 정보 조회 중 오류가 발생했습니다." }; + } +} \ No newline at end of file diff --git a/lib/po/vendor-table/shi-vendor-po-columns.tsx b/lib/po/vendor-table/shi-vendor-po-columns.tsx index 041e0c05..356b3dab 100644 --- a/lib/po/vendor-table/shi-vendor-po-columns.tsx +++ b/lib/po/vendor-table/shi-vendor-po-columns.tsx @@ -6,7 +6,9 @@ import { SendIcon, FileTextIcon, MoreHorizontalIcon, + EyeIcon, } from "lucide-react" +import { useRouter, useParams } from "next/navigation" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" @@ -404,8 +406,34 @@ export function getShiVendorColumns({ enableHiding: false, header: () =>
Actions
, cell: ({ row }) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const router = useRouter() + // eslint-disable-next-line react-hooks/rules-of-hooks + const params = useParams() + const lng = params.lng as string + return (
+ {/* 계약 상세보기 버튼 */} + + + + + + + 계약 상세 정보 보기 + + + + {/* 서명 요청 버튼 */} @@ -425,38 +453,11 @@ export function getShiVendorColumns({ - - {/* 드롭다운 메뉴 (추가 액션) - - - - - - 액션 - setRowAction({ row, type: "view-items" })} - > - - 상세품목 보기 - - - setRowAction({ row, type: "signature-request" })} - className="text-blue-600" - > - - 서명 요청 - - - */}
) }, - size: 160, - minSize: 160, + size: 200, + minSize: 200, }, ] } \ No newline at end of file diff --git a/lib/po/vendor-table/types.ts b/lib/po/vendor-table/types.ts index 97572ffc..f8bc3ea2 100644 --- a/lib/po/vendor-table/types.ts +++ b/lib/po/vendor-table/types.ts @@ -65,7 +65,11 @@ export interface VendorPO { priceIndexYn?: string // 납품대금연동제대상여부 (ZDLV_PRICE_T) writtenContractNo?: string // 서면계약번호 (ZWEBELN) contractVersion?: number // 서면계약차수 (ZVER_NO) - + + // 계약서 내용 및 노트 + contractContent?: string // 계약서 내용 (ZMM_NOTE에서 추출) + remarks?: string // 비고 (ECC에서 추가 정보) + // 상세품목 정보 (다이얼로그에서 표시) items?: VendorPOItem[] } @@ -98,6 +102,7 @@ export interface VendorPOItem { vatType: string // VAT구분 steelSpec?: string // 철의장 SPEC prManager: string // P/R 담당자 + remark?: string // 비고 (contract_items.remark) } // 파싱된 벤더 PO 타입 (JSON 필드들이 파싱된 상태) diff --git a/lib/po/vendor-table/vendor-po-actions.tsx b/lib/po/vendor-table/vendor-po-actions.tsx new file mode 100644 index 00000000..329d91fd --- /dev/null +++ b/lib/po/vendor-table/vendor-po-actions.tsx @@ -0,0 +1,273 @@ +"use client" + +import * as React from "react" +import { + FileTextIcon, + MoreHorizontalIcon, + EyeIcon, + PrinterIcon, + FileXIcon, + PlusIcon, + EditIcon +} from "lucide-react" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" +import { toast } from "sonner" +import { VendorPO, VendorPOActionType } from "./types" +import { createPcrRequest, acceptContract, rejectContract, cancelAcceptContract } from "./service" +import { ContractStatus } from "@/db/schema/contract" + +interface VendorPOActionsProps { + row: { original: VendorPO } + setRowAction: React.Dispatch> +} + +export function VendorPOActions({ row, setRowAction }: VendorPOActionsProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [rejectDialogOpen, setRejectDialogOpen] = React.useState(false) + const [rejectionReason, setRejectionReason] = React.useState("") + + // 계약 상태에 따른 버튼 활성화 조건 + const contractStatus = row.original.contractStatus + const canCreatePcr = contractStatus === ContractStatus.CONTRACT_ACCEPT_REQUEST + const canApprove = contractStatus === ContractStatus.CONTRACT_ACCEPT_REQUEST + const canCancelApprove = contractStatus === ContractStatus.COMPLETE_THE_CONTRACT + const canReject = contractStatus === ContractStatus.CONTRACT_ACCEPT_REQUEST + + // PCR 생성 핸들러 + const handlePcrCreate = async () => { + if (isLoading) return + + try { + setIsLoading(true) + const result = await createPcrRequest(row.original.id) + + if (result.success) { + toast.success(result.message) + // 필요한 경우 테이블 리프레시 로직 추가 + } + } catch (error) { + console.error("PCR 생성 실패:", error) + toast.error("PCR 생성에 실패했습니다.") + } finally { + setIsLoading(false) + } + } + + // 승인 핸들러 + const handleApprove = async () => { + if (isLoading) return + + try { + setIsLoading(true) + const result = await acceptContract(row.original.id) + + if (result.success) { + toast.success(result.message) + // 필요한 경우 테이블 리프레시 로직 추가 + } + } catch (error) { + console.error("계약 승인 실패:", error) + toast.error("계약 승인에 실패했습니다.") + } finally { + setIsLoading(false) + } + } + + // 승인 취소 핸들러 + const handleCancelApprove = async () => { + if (isLoading) return + + try { + setIsLoading(true) + const result = await cancelAcceptContract(row.original.id) + + if (result.success) { + toast.success(result.message) + // 필요한 경우 테이블 리프레시 로직 추가 + } + } catch (error) { + console.error("승인 취소 실패:", error) + toast.error("승인 취소에 실패했습니다.") + } finally { + setIsLoading(false) + } + } + + // 계약 거절 다이얼로그 열기 + const handleRejectClick = () => { + if (isLoading || !canReject) return + setRejectDialogOpen(true) + } + + // 계약 거절 확인 + const handleRejectConfirm = async () => { + if (!rejectionReason.trim()) { + toast.error("거절 사유를 입력해주세요.") + return + } + + try { + setIsLoading(true) + setRejectDialogOpen(false) + + const result = await rejectContract(row.original.id, rejectionReason.trim()) + + if (result.success) { + toast.success(result.message) + setRejectionReason("") // 입력값 초기화 + } + } catch (error) { + console.error("계약 거절 실패:", error) + toast.error("계약 거절에 실패했습니다.") + } finally { + setIsLoading(false) + } + } + + // 계약 거절 취소 + const handleRejectCancel = () => { + setRejectDialogOpen(false) + setRejectionReason("") + } + + return ( +
+ + + + + + 액션 + setRowAction({ row, type: "view-items" })} + > + + 상세품목 보기 + + + + + PCR생성 + + + + + + 승인 + + + + 승인취소 + + + + + 계약거절 + + + + + setRowAction({ row, type: "print-contract" })} + disabled={isLoading} + > + + 계약서출력 + + + setRowAction({ row, type: "contract-detail" })} + disabled={isLoading} + > + + 계약상세 + + + + setRowAction({ row, type: "price-index" })} + disabled={isLoading} + > + 연동표입력 + + + + + {/* 계약 거절 다이얼로그 */} + + + + 계약 거절 + + 계약을 거절하는 사유를 입력해주세요. + + +
+
+ +