diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-13 17:29:33 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-13 17:29:33 +0900 |
| commit | e84cf02a1cb4959a9d3bb5bbf37885c13a447f78 (patch) | |
| tree | cfb2817e3bd8f5ef08b4428b9e6fc619ef3884a1 /lib/po | |
| parent | 89274bffa596ffdfc4275fb8d11cdb02ff9a2d02 (diff) | |
(김준회) SHI/벤더 PO 구현
Diffstat (limited to 'lib/po')
| -rw-r--r-- | lib/po/vendor-table/service.ts | 573 | ||||
| -rw-r--r-- | lib/po/vendor-table/shi-vendor-po-columns.tsx | 59 | ||||
| -rw-r--r-- | lib/po/vendor-table/types.ts | 7 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-actions.tsx | 273 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-columns.tsx | 158 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-items-dialog.tsx | 13 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-note-dialog.tsx | 162 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-table.tsx | 20 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-toolbar-actions.tsx | 61 |
9 files changed, 998 insertions, 328 deletions
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<unknown>[] = []; 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<VendorPO | null> { + 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<VendorPO | null> { - 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<VendorPO | null> { */ 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: () => <div className="text-center">Actions</div>, 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 ( <div className="flex items-center justify-center gap-2"> + {/* 계약 상세보기 버튼 */} + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="outline" + size="sm" + className="h-8 px-2" + onClick={() => router.push(`/${lng}/evcp/po/${row.original.id}`)} + > + <EyeIcon className="h-3.5 w-3.5 mr-1" aria-hidden="true" /> + 상세 + </Button> + </TooltipTrigger> + <TooltipContent> + 계약 상세 정보 보기 + </TooltipContent> + </Tooltip> + </TooltipProvider> + {/* 서명 요청 버튼 */} <TooltipProvider> <Tooltip> @@ -425,38 +453,11 @@ export function getShiVendorColumns({ </TooltipContent> </Tooltip> </TooltipProvider> - - {/* 드롭다운 메뉴 (추가 액션) - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button variant="ghost" className="h-8 w-8 p-0"> - <span className="sr-only">Open menu</span> - <MoreHorizontalIcon className="h-4 w-4" /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="end"> - <DropdownMenuLabel>액션</DropdownMenuLabel> - <DropdownMenuItem - onClick={() => setRowAction({ row, type: "view-items" })} - > - <FileTextIcon className="mr-2 h-4 w-4" /> - 상세품목 보기 - </DropdownMenuItem> - <DropdownMenuSeparator /> - <DropdownMenuItem - onClick={() => setRowAction({ row, type: "signature-request" })} - className="text-blue-600" - > - <SendIcon className="mr-2 h-4 w-4" /> - 서명 요청 - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> */} </div> ) }, - 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<React.SetStateAction<{ row: { original: VendorPO }, type: VendorPOActionType } | null>> +} + +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 ( + <div className="flex justify-center"> + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" className="h-8 w-8 p-0" disabled={isLoading}> + <span className="sr-only">Open menu</span> + <MoreHorizontalIcon className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuLabel>액션</DropdownMenuLabel> + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "view-items" })} + > + <FileTextIcon className="mr-2 h-4 w-4" /> + 상세품목 보기 + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + onClick={handlePcrCreate} + disabled={isLoading || !canCreatePcr} + > + <PlusIcon className="mr-2 h-4 w-4" /> + PCR생성 + </DropdownMenuItem> + + <DropdownMenuSeparator /> + + <DropdownMenuItem + onClick={handleApprove} + disabled={isLoading || !canApprove} + > + 승인 + </DropdownMenuItem> + + <DropdownMenuItem + onClick={handleCancelApprove} + disabled={isLoading || !canCancelApprove} + > + 승인취소 + </DropdownMenuItem> + + <DropdownMenuItem + onClick={handleRejectClick} + className="text-red-600" + disabled={isLoading || !canReject} + > + <FileXIcon className="mr-2 h-4 w-4" /> + 계약거절 + </DropdownMenuItem> + + <DropdownMenuSeparator /> + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "print-contract" })} + disabled={isLoading} + > + <PrinterIcon className="mr-2 h-4 w-4" /> + 계약서출력 + </DropdownMenuItem> + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "contract-detail" })} + disabled={isLoading} + > + <EyeIcon className="mr-2 h-4 w-4" /> + 계약상세 + </DropdownMenuItem> + + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "price-index" })} + disabled={isLoading} + > + 연동표입력 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + + {/* 계약 거절 다이얼로그 */} + <Dialog open={rejectDialogOpen} onOpenChange={setRejectDialogOpen}> + <DialogContent className="sm:max-w-[425px]"> + <DialogHeader> + <DialogTitle>계약 거절</DialogTitle> + <DialogDescription> + 계약을 거절하는 사유를 입력해주세요. + </DialogDescription> + </DialogHeader> + <div className="grid gap-4 py-4"> + <div className="grid gap-2"> + <Label htmlFor="rejection-reason">거절 사유</Label> + <Textarea + id="rejection-reason" + placeholder="거절 사유를 상세히 입력해주세요..." + value={rejectionReason} + onChange={(e) => setRejectionReason(e.target.value)} + rows={4} + /> + </div> + </div> + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={handleRejectCancel} + disabled={isLoading} + > + 취소 + </Button> + <Button + type="button" + variant="destructive" + onClick={handleRejectConfirm} + disabled={isLoading || !rejectionReason.trim()} + > + {isLoading ? "처리 중..." : "거절하기"} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + </div> + ) +} diff --git a/lib/po/vendor-table/vendor-po-columns.tsx b/lib/po/vendor-table/vendor-po-columns.tsx index 0910eaf8..c954b872 100644 --- a/lib/po/vendor-table/vendor-po-columns.tsx +++ b/lib/po/vendor-table/vendor-po-columns.tsx @@ -2,43 +2,16 @@ import * as React from "react" import { type ColumnDef } from "@tanstack/react-table" -import { - FileTextIcon, - MoreHorizontalIcon, - EyeIcon, - PrinterIcon, - FileXIcon, - PlusIcon, - EditIcon -} from "lucide-react" +import { FileTextIcon } from "lucide-react" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Checkbox } from "@/components/ui/checkbox" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" import { VendorPO, VendorPOActionType } from "./types" - -// 벤더 PO용 행 액션 타입 -type VendorPORowAction = { - row: { original: VendorPO } - type: VendorPOActionType -} +import { VendorPOActions } from "./vendor-po-actions" interface GetVendorColumnsProps { - setRowAction: React.Dispatch<React.SetStateAction<VendorPORowAction | null>> + setRowAction: React.Dispatch<React.SetStateAction<{ row: { original: VendorPO }; type: VendorPOActionType } | null>> selectedRows?: number[] onRowSelect?: (id: number, selected: boolean) => void } @@ -333,18 +306,19 @@ export function getVendorColumns({ setRowAction, selectedRows = [], onRowSelect size: 120, }, + // 데이터 및 룰이 없어 구현불가한 컬럼 주석 처리 --> 안내됨 // L/C No. - { - accessorKey: "lcNo", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="L/C No." /> - ), - cell: ({ row }) => { - const lcNo = row.getValue("lcNo") as string - return <div className="text-sm">{lcNo || '-'}</div> - }, - size: 120, - }, + // { + // accessorKey: "lcNo", + // header: ({ column }) => ( + // <DataTableColumnHeaderSimple column={column} title="L/C No." /> + // ), + // cell: ({ row }) => { + // const lcNo = row.getValue("lcNo") as string + // return <div className="text-sm">{lcNo || '-'}</div> + // }, + // size: 120, + // }, // 납품대금 연동제 대상 { @@ -403,107 +377,7 @@ export function getVendorColumns({ setRowAction, selectedRows = [], onRowSelect id: "actions", enableHiding: false, header: () => <div className="text-center">액션</div>, - cell: function Cell({ row }) { - return ( - <div className="flex gap-1"> - {/* 상세품목 버튼 */} - <TooltipProvider> - <Tooltip> - <TooltipTrigger asChild> - <Button - variant="outline" - size="sm" - className="h-8 px-2" - onClick={() => setRowAction({ row, type: "view-items" })} - > - <FileTextIcon className="h-3.5 w-3.5" aria-hidden="true" /> - </Button> - </TooltipTrigger> - <TooltipContent> - 상세품목 보기 - </TooltipContent> - </Tooltip> - </TooltipProvider> - - {/* 드롭다운 메뉴 - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button variant="ghost" className="h-8 w-8 p-0"> - <span className="sr-only">Open menu</span> - <MoreHorizontalIcon className="h-4 w-4" /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="end"> - <DropdownMenuLabel>액션</DropdownMenuLabel> - <DropdownMenuItem - onClick={() => setRowAction({ row, type: "view-items" })} - > - <FileTextIcon className="mr-2 h-4 w-4" /> - 상세품목 보기 - </DropdownMenuItem> - <DropdownMenuSeparator /> - <DropdownMenuItem - onClick={() => setRowAction({ row, type: "pcr-create" })} - > - <PlusIcon className="mr-2 h-4 w-4" /> - PCR생성 - </DropdownMenuItem> - - <DropdownMenuSeparator /> - - <DropdownMenuItem - onClick={() => setRowAction({ row, type: "approve" })} - > - 승인 - </DropdownMenuItem> - - <DropdownMenuItem - onClick={() => setRowAction({ row, type: "cancel-approve" })} - > - 승인취소 - </DropdownMenuItem> - - <DropdownMenuItem - onClick={() => setRowAction({ row, type: "reject-contract" })} - className="text-red-600" - > - <FileXIcon className="mr-2 h-4 w-4" /> - 계약거절 - </DropdownMenuItem> - - <DropdownMenuSeparator /> - - <DropdownMenuItem - onClick={() => setRowAction({ row, type: "print-contract" })} - > - <PrinterIcon className="mr-2 h-4 w-4" /> - 계약서출력 - </DropdownMenuItem> - - <DropdownMenuItem - onClick={() => setRowAction({ row, type: "contract-detail" })} - > - <EyeIcon className="mr-2 h-4 w-4" /> - 계약상세 - </DropdownMenuItem> - - <DropdownMenuItem - onClick={() => setRowAction({ row, type: "po-note" })} - > - <EditIcon className="mr-2 h-4 w-4" /> - PO Note - </DropdownMenuItem> - - <DropdownMenuItem - onClick={() => setRowAction({ row, type: "price-index" })} - > - 연동표입력 - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> */} - </div> - ); - }, + cell: ({ row }) => <VendorPOActions row={row} setRowAction={setRowAction} />, size: 120, minSize: 100, }, diff --git a/lib/po/vendor-table/vendor-po-items-dialog.tsx b/lib/po/vendor-table/vendor-po-items-dialog.tsx index d3b33371..647950c4 100644 --- a/lib/po/vendor-table/vendor-po-items-dialog.tsx +++ b/lib/po/vendor-table/vendor-po-items-dialog.tsx @@ -103,12 +103,12 @@ export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDia <TableHead className="min-w-[100px] whitespace-nowrap">단가기준</TableHead> <TableHead className="min-w-[100px] whitespace-nowrap">자재번호</TableHead> <TableHead className="min-w-[200px] whitespace-nowrap">품목/자재내역</TableHead> - <TableHead className="min-w-[200px] whitespace-nowrap">자재내역사양</TableHead> + {/* <TableHead className="min-w-[200px] whitespace-nowrap">자재내역사양</TableHead> <TableHead className="min-w-[120px] whitespace-nowrap">설계자재번호</TableHead> <TableHead className="min-w-[100px] whitespace-nowrap">Fitting No.</TableHead> <TableHead className="min-w-[80px] whitespace-nowrap">Cert.</TableHead> <TableHead className="min-w-[80px] whitespace-nowrap">재질</TableHead> - <TableHead className="min-w-[150px] whitespace-nowrap">규격</TableHead> + <TableHead className="min-w-[150px] whitespace-nowrap">규격</TableHead> */} <TableHead className="min-w-[80px] text-right whitespace-nowrap">수량</TableHead> <TableHead className="min-w-[80px] whitespace-nowrap">수량단위</TableHead> <TableHead className="min-w-[80px] text-right whitespace-nowrap">중량</TableHead> @@ -121,7 +121,7 @@ export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDia <TableHead className="min-w-[100px] text-right whitespace-nowrap">조정금액</TableHead> <TableHead className="min-w-[100px] whitespace-nowrap">납기일자</TableHead> <TableHead className="min-w-[80px] whitespace-nowrap">VAT구분</TableHead> - <TableHead className="min-w-[120px] whitespace-nowrap">철의장 SPEC</TableHead> + {/* <TableHead className="min-w-[120px] whitespace-nowrap">철의장 SPEC</TableHead> */} <TableHead className="min-w-[100px] whitespace-nowrap">P/R 담당자</TableHead> </TableRow> </TableHeader> @@ -139,7 +139,8 @@ export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDia {item.itemDescription || '-'} </div> </TableCell> - <TableCell className="max-w-[200px]"> + {/* 자재내역사양~규격 까지 받은 정보 없음 */} + {/* <TableCell className="max-w-[200px]"> <div className="truncate" title={item.materialSpec || ''}> {item.materialSpec || '-'} </div> @@ -148,7 +149,7 @@ export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDia <TableCell>{item.fittingNo || '-'}</TableCell> <TableCell>{item.cert || '-'}</TableCell> <TableCell>{item.material || '-'}</TableCell> - <TableCell>{item.specification || '-'}</TableCell> + <TableCell>{item.specification || '-'}</TableCell> */} <TableCell className="text-right font-mono"> {item.quantity?.toLocaleString() || '-'} </TableCell> @@ -173,7 +174,7 @@ export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDia </TableCell> <TableCell>{item.deliveryDate || '-'}</TableCell> <TableCell>{item.vatType || '-'}</TableCell> - <TableCell>{item.steelSpec || '-'}</TableCell> + {/* <TableCell>{item.steelSpec || '-'}</TableCell> */} <TableCell>{item.prManager || '-'}</TableCell> </TableRow> ))} diff --git a/lib/po/vendor-table/vendor-po-note-dialog.tsx b/lib/po/vendor-table/vendor-po-note-dialog.tsx new file mode 100644 index 00000000..fbc14563 --- /dev/null +++ b/lib/po/vendor-table/vendor-po-note-dialog.tsx @@ -0,0 +1,162 @@ +"use client" + +import * as React from "react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Badge } from "@/components/ui/badge" +import { Separator } from "@/components/ui/separator" +import { ScrollArea } from "@/components/ui/scroll-area" +import { FileTextIcon, MessageSquareIcon } from "lucide-react" +import { VendorPO } from "./types" + +interface VendorPONoteDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + po: VendorPO | null +} + +interface PONoteItem { + itemNo: string + description: string + remark: string +} + +export function VendorPONoteDialog({ + open, + onOpenChange, + po, +}: VendorPONoteDialogProps) { + // 계약서 내용 및 노트 데이터를 추출하는 함수 + const extractContent = React.useCallback(() => { + if (!po) return { contractContent: null, remarks: null, itemNotes: [] } + + const contractContent = po.contractContent || null // contracts.contractContent (ZMM_NOTE에서 추출) + const remarks = po.remarks || null // contracts.remarks (추가 비고) + const itemNotes: PONoteItem[] = [] + + // items 배열에서 remark이 있는 항목들 추출 + if (po.items && po.items.length > 0) { + po.items.forEach((item, index) => { + if (item.remark && item.remark.trim()) { + itemNotes.push({ + itemNo: item.itemNo || `Item ${index + 1}`, + description: item.itemDescription || item.materialSpec || '', + remark: item.remark, + }) + } + }) + } + + return { contractContent, remarks, itemNotes } + }, [po]) + + const { contractContent, remarks, itemNotes } = extractContent() + const hasAnyContent = contractContent || remarks || itemNotes.length > 0 + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-[600px] max-h-[80vh]"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <FileTextIcon className="h-5 w-5" /> + 계약서 내용 - {po?.contractNo} + </DialogTitle> + <DialogDescription> + {po?.contractName} 계약의 계약서 내용 및 관련 노트입니다. + </DialogDescription> + </DialogHeader> + + <ScrollArea className="max-h-[60vh] pr-4"> + <div className="space-y-6"> + {/* 계약서 내용 (메인) */} + {contractContent && ( + <div className="space-y-3"> + <div className="flex items-center gap-2"> + <FileTextIcon className="h-4 w-4 text-blue-600" /> + <h3 className="text-sm font-semibold text-blue-600"> + 계약서 내용 + </h3> + </div> + <div className="bg-blue-50 border border-blue-200 rounded-lg p-4"> + <p className="text-sm text-gray-700 whitespace-pre-wrap leading-relaxed"> + {contractContent} + </p> + </div> + </div> + )} + + {/* 구분선 */} + {contractContent && (remarks || itemNotes.length > 0) && <Separator />} + + {/* 계약 비고 */} + {remarks && ( + <div className="space-y-3"> + <div className="flex items-center gap-2"> + <MessageSquareIcon className="h-4 w-4 text-orange-600" /> + <h3 className="text-sm font-semibold text-orange-600"> + 계약 비고 + </h3> + </div> + <div className="bg-orange-50 border border-orange-200 rounded-lg p-4"> + <p className="text-sm text-gray-700 whitespace-pre-wrap"> + {remarks} + </p> + </div> + </div> + )} + + {/* 구분선 */} + {(contractContent || remarks) && itemNotes.length > 0 && <Separator />} + + {/* 아이템별 노트들 */} + {itemNotes.length > 0 && ( + <div className="space-y-4"> + <div className="flex items-center gap-2"> + <FileTextIcon className="h-4 w-4 text-green-600" /> + <h3 className="text-sm font-semibold text-green-600"> + 품목별 노트 ({itemNotes.length}개) + </h3> + </div> + + <div className="space-y-3"> + {itemNotes.map((item, index) => ( + <div key={index} className="bg-green-50 border border-green-200 rounded-lg p-4"> + <div className="flex items-start gap-3"> + <Badge variant="outline" className="text-green-700 border-green-300"> + {item.itemNo} + </Badge> + <div className="flex-1 min-w-0"> + {item.description && ( + <p className="text-sm font-medium text-gray-900 mb-1 truncate" title={item.description}> + {item.description} + </p> + )} + <p className="text-sm text-gray-700 whitespace-pre-wrap"> + {item.remark} + </p> + </div> + </div> + </div> + ))} + </div> + </div> + )} + + {/* 내용이 없는 경우 */} + {!hasAnyContent && ( + <div className="text-center py-8"> + <FileTextIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" /> + <p className="text-gray-500">등록된 계약서 내용이 없습니다.</p> + </div> + )} + </div> + </ScrollArea> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file diff --git a/lib/po/vendor-table/vendor-po-table.tsx b/lib/po/vendor-table/vendor-po-table.tsx index a3ad4949..99b0e5eb 100644 --- a/lib/po/vendor-table/vendor-po-table.tsx +++ b/lib/po/vendor-table/vendor-po-table.tsx @@ -4,18 +4,19 @@ import * as React from "react" import type { DataTableAdvancedFilterField, DataTableFilterField, - DataTableRowAction, } from "@/types/table" import { useDataTable } from "@/hooks/use-data-table" import { DataTable } from "@/components/data-table/data-table" import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" import { toast } from "sonner" +import { useRouter, useParams } from "next/navigation" import { getVendorPOs, handleVendorPOAction } from "./service" import { getVendorColumns } from "./vendor-po-columns" import { VendorPO, VendorPOActionType } from "./types" import { VendorPOItemsDialog } from "./vendor-po-items-dialog" +import { VendorPONoteDialog } from "./vendor-po-note-dialog" import { VendorPOToolbarActions } from "./vendor-po-toolbar-actions" interface VendorPoTableProps { @@ -27,6 +28,10 @@ interface VendorPoTableProps { } export function VendorPoTable({ promises }: VendorPoTableProps) { + const router = useRouter() + const params = useParams() + const lng = params.lng as string + const [data, setData] = React.useState<{ data: VendorPO[]; pageCount: number; @@ -43,10 +48,11 @@ export function VendorPoTable({ promises }: VendorPoTableProps) { }, [promises]); const [rowAction, setRowAction] = - React.useState<DataTableRowAction<VendorPO> | null>(null) + React.useState<{ row: { original: VendorPO }; type: VendorPOActionType } | null>(null) // 다이얼로그 상태 const [itemsDialogOpen, setItemsDialogOpen] = React.useState(false) + const [noteDialogOpen, setNoteDialogOpen] = React.useState(false) const [selectedPO, setSelectedPO] = React.useState<VendorPO | null>(null) // 행 선택 처리 (1개만 선택 가능) @@ -88,10 +94,10 @@ export function VendorPoTable({ promises }: VendorPoTableProps) { setItemsDialogOpen(true) break case "contract-detail": - toast.info("계약상세 기능은 개발 중입니다.") + router.push(`/${lng}/partners/po/${po.id}`) break case "po-note": - toast.info("PO Note 기능은 개발 중입니다.") + setNoteDialogOpen(true) break case "price-index": toast.info("연동표입력 기능은 개발 중입니다.") @@ -242,6 +248,12 @@ export function VendorPoTable({ promises }: VendorPoTableProps) { onOpenChange={setItemsDialogOpen} po={selectedPO} /> + + <VendorPONoteDialog + open={noteDialogOpen} + onOpenChange={setNoteDialogOpen} + po={selectedPO} + /> </> ) }
\ No newline at end of file diff --git a/lib/po/vendor-table/vendor-po-toolbar-actions.tsx b/lib/po/vendor-table/vendor-po-toolbar-actions.tsx index 800a9e40..86e4379f 100644 --- a/lib/po/vendor-table/vendor-po-toolbar-actions.tsx +++ b/lib/po/vendor-table/vendor-po-toolbar-actions.tsx @@ -34,15 +34,15 @@ interface VendorPOToolbarActionsProps { onViewItems?: (po: VendorPO) => void } -export function VendorPOToolbarActions({ - table, - selectedRows, +export function VendorPOToolbarActions({ + table, + selectedRows, onAction, onViewItems }: VendorPOToolbarActionsProps) { const hasSelectedRow = selectedRows.length === 1 - const selectedPO = hasSelectedRow ? - table.getRowModel().rows.find(row => selectedRows.includes(row.original.id))?.original + const selectedPO = hasSelectedRow ? + table.getRowModel().rows.find(row => selectedRows.includes(row.original.id))?.original : null const handleToolbarAction = async (action: string) => { @@ -65,18 +65,18 @@ export function VendorPOToolbarActions({ return ( <div className="flex items-center gap-2"> {/* 주요 액션 버튼들 */} - <TooltipProvider> + {/* <TooltipProvider> <Tooltip> <TooltipTrigger asChild> - <Button - variant="default" - size="sm" - onClick={() => handleToolbarAction("pcr-create")} - disabled={!hasSelectedRow} - className="h-8" - > - PCR생성 - </Button> + <Button + variant="default" + size="sm" + onClick={() => handleToolbarAction("pcr-create")} + disabled={!hasSelectedRow} + className="h-8" + > + PCR생성 + </Button> </TooltipTrigger> <TooltipContent>선택된 PO에 대한 PCR을 생성합니다</TooltipContent> </Tooltip> @@ -97,10 +97,10 @@ export function VendorPOToolbarActions({ </TooltipTrigger> <TooltipContent>상세품목 현황을 확인합니다</TooltipContent> </Tooltip> - </TooltipProvider> + </TooltipProvider> */} {/* 승인 관련 액션 */} - {selectedPO?.contractStatus !== "승인완료" && ( + {/* {selectedPO?.contractStatus !== "승인완료" && ( <TooltipProvider> <Tooltip> <TooltipTrigger asChild> @@ -136,10 +136,10 @@ export function VendorPOToolbarActions({ <TooltipContent>승인을 취소합니다</TooltipContent> </Tooltip> </TooltipProvider> - )} + )} */} {/* 더 많은 액션 드롭다운 */} - <DropdownMenu> + {/* <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="outline" @@ -153,7 +153,7 @@ export function VendorPOToolbarActions({ <DropdownMenuContent align="end" className="w-[200px]"> <DropdownMenuLabel>계약 관련</DropdownMenuLabel> <DropdownMenuSeparator /> - + <DropdownMenuItem onClick={() => handleToolbarAction("contract-detail")} disabled={!hasSelectedRow} @@ -161,7 +161,7 @@ export function VendorPOToolbarActions({ <EyeIcon className="mr-2 h-4 w-4" /> 계약상세 </DropdownMenuItem> - + <DropdownMenuItem onClick={() => handleToolbarAction("po-note")} disabled={!hasSelectedRow} @@ -169,7 +169,7 @@ export function VendorPOToolbarActions({ <EditIcon className="mr-2 h-4 w-4" /> PO Note </DropdownMenuItem> - + <DropdownMenuItem onClick={() => handleToolbarAction("price-index")} disabled={!hasSelectedRow} @@ -179,7 +179,7 @@ export function VendorPOToolbarActions({ </DropdownMenuItem> <DropdownMenuSeparator /> - + <DropdownMenuItem onClick={() => handleToolbarAction("reject-contract")} disabled={!hasSelectedRow} @@ -188,9 +188,9 @@ export function VendorPOToolbarActions({ <FileXIcon className="mr-2 h-4 w-4" /> 계약거절 </DropdownMenuItem> - + <DropdownMenuSeparator /> - + <DropdownMenuItem onClick={() => handleToolbarAction("print-contract")} disabled={!hasSelectedRow} @@ -199,16 +199,7 @@ export function VendorPOToolbarActions({ 계약서출력 </DropdownMenuItem> </DropdownMenuContent> - </DropdownMenu> - - {/* 선택된 행 정보 표시 - {hasSelectedRow && selectedPO && ( - <div className="flex items-center gap-2 ml-4 text-sm text-muted-foreground"> - <span>선택됨:</span> - <span className="font-medium">{selectedPO.contractNo}</span> - <span>({selectedPO.contractName})</span> - </div> - )} */} + </DropdownMenu> */} </div> ) } |
