summaryrefslogtreecommitdiff
path: root/lib/po
diff options
context:
space:
mode:
Diffstat (limited to 'lib/po')
-rw-r--r--lib/po/vendor-table/service.ts573
-rw-r--r--lib/po/vendor-table/shi-vendor-po-columns.tsx59
-rw-r--r--lib/po/vendor-table/types.ts7
-rw-r--r--lib/po/vendor-table/vendor-po-actions.tsx273
-rw-r--r--lib/po/vendor-table/vendor-po-columns.tsx158
-rw-r--r--lib/po/vendor-table/vendor-po-items-dialog.tsx13
-rw-r--r--lib/po/vendor-table/vendor-po-note-dialog.tsx162
-rw-r--r--lib/po/vendor-table/vendor-po-table.tsx20
-rw-r--r--lib/po/vendor-table/vendor-po-toolbar-actions.tsx61
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>
)
}