diff options
Diffstat (limited to 'lib/rfq-last/service.ts')
| -rw-r--r-- | lib/rfq-last/service.ts | 480 |
1 files changed, 478 insertions, 2 deletions
diff --git a/lib/rfq-last/service.ts b/lib/rfq-last/service.ts index 67cb901f..0c75e72f 100644 --- a/lib/rfq-last/service.ts +++ b/lib/rfq-last/service.ts @@ -571,9 +571,9 @@ export async function getRfqItemsAction(rfqId: number) { materialDescription: item.materialDescription, size: item.size, deliveryDate: item.deliveryDate, - quantity: item.quantity, + quantity: Number(item.quantity) || 0, // 여기서 숫자로 변환 uom: item.uom, - grossWeight: item.grossWeight, + grossWeight: Number(item.grossWeight) || 0, // 여기서 숫자로 변환 gwUom: item.gwUom, specNo: item.specNo, specUrl: item.specUrl, @@ -1835,4 +1835,480 @@ export async function getRfqWithDetails(rfqId: number) { console.error("Get RFQ with details error:", error); return { success: false, error: "데이터 조회 중 오류가 발생했습니다." }; } +} + + +// RFQ 정보 타입 +export interface RfqFullInfo { + // 기본 RFQ 정보 + id: number; + rfqCode: string; + rfqType: string | null; + rfqTitle: string | null; + series: string | null; + rfqSealedYn: boolean | null; + + // ITB 관련 + projectCompany: string | null; + projectFlag: string | null; + projectSite: string | null; + smCode: string | null; + + // RFQ 추가 필드 + prNumber: string | null; + prIssueDate: Date | null; + + // 프로젝트 정보 + projectId: number | null; + projectCode: string | null; + projectName: string | null; + + // 아이템 정보 + itemCode: string | null; + itemName: string | null; + + // 패키지 정보 + packageNo: string | null; + packageName: string | null; + + // 날짜 정보 + dueDate: Date | null; + rfqSendDate: Date | null; + + // 상태 + status: string; + + // 담당자 정보 + picId: number | null; + picCode: string | null; + picName: string | null; + picUserName: string | null; + picTeam: string | null; + + // 설계담당자 + engPicName: string | null; + designTeam: string | null; + + // 자재그룹 정보 (PR Items에서) + materialGroup: string | null; + materialGroupDesc: string | null; + + // 카운트 정보 + vendorCount: number; + shortListedVendorCount: number; + quotationReceivedCount: number; + prItemsCount: number; + majorItemsCount: number; + + // 감사 정보 + createdBy: number; + createdByUserName: string | null; + createdAt: Date; + updatedBy: number; + updatedByUserName: string | null; + updatedAt: Date; + + sentBy: number | null; + sentByUserName: string | null; + + remark: string | null; + + // 평가 적용 여부 (추가 필드) + evaluationApply?: boolean; + quotationType?: string; + contractType?: string; + + // 연관 데이터 + vendors: VendorDetail[]; + attachments: AttachmentInfo[]; +} + +// 벤더 상세 정보 +export interface VendorDetail { + detailId: number; + vendorId: number | null; + vendorName: string | null; + vendorCode: string | null; + vendorCountry: string | null; + vendorEmail?: string | null; + vendorCategory?: string | null; + vendorGrade?: string | null; + basicContract?: string | null; + + // RFQ 조건 + currency: string | null; + paymentTermsCode: string | null; + paymentTermsDescription: string | null; + incotermsCode: string | null; + incotermsDescription: string | null; + incotermsDetail: string | null; + deliveryDate: Date | null; + contractDuration: string | null; + taxCode: string | null; + placeOfShipping: string | null; + placeOfDestination: string | null; + + // 상태 + shortList: boolean; + returnYn: boolean; + returnedAt: Date | null; + + // GTC/NDA + prjectGtcYn: boolean; + generalGtcYn: boolean; + ndaYn: boolean; + agreementYn: boolean; + + // 추가 조건 + materialPriceRelatedYn: boolean | null; + sparepartYn: boolean | null; + firstYn: boolean | null; + firstDescription: string | null; + sparepartDescription: string | null; + + remark: string | null; + cancelReason: string | null; + + // 회신 상태 + quotationStatus?: string | null; + quotationSubmittedAt?: Date | null; + + // 업데이트 정보 + updatedBy: number; + updatedByUserName: string | null; + updatedAt: Date | null; +} + +// 첨부파일 정보 +export interface AttachmentInfo { + id: number; + attachmentType: string; + serialNo: string; + currentRevision: string; + description: string | null; + + // 최신 리비전 정보 + fileName: string | null; + originalFileName: string | null; + filePath: string | null; + fileSize: number | null; + fileType: string | null; + + createdBy: number; + createdByUserName: string | null; + createdAt: Date; + updatedAt: Date; +} + +/** + * RFQ 전체 정보 조회 + */ +export async function getRfqFullInfo(rfqId: number): Promise<RfqFullInfo> { + try { + // 1. RFQ 기본 정보 조회 + const rfqData = await db + .select({ + rfq: rfqsLast, + picUser: users, + }) + .from(rfqsLast) + .leftJoin(users, eq(rfqsLast.pic, users.id)) + .where(eq(rfqsLast.id, rfqId)) + .limit(1); + + if (!rfqData.length) { + throw new Error(`RFQ ID ${rfqId}를 찾을 수 없습니다.`); + } + + const rfq = rfqData[0].rfq; + const picUser = rfqData[0].picUser; + + // 2. PR Items에서 자재그룹 정보 조회 (Major Item) + const prItemsData = await db + .select({ + materialCategory: rfqPrItems.materialCategory, + materialDescription: rfqPrItems.materialDescription, + prItemsCount: eq(rfqPrItems.majorYn, true), + }) + .from(rfqPrItems) + .where(and( + eq(rfqPrItems.rfqsLastId, rfqId), + eq(rfqPrItems.majorYn, true) + )) + .limit(1); + + const majorItem = prItemsData[0]; + + // 3. 벤더 정보 조회 + const vendorsData = await db + .select({ + detail: rfqLastDetails, + vendor: vendors, + paymentTerms: paymentTerms, + incoterms: incoterms, + updatedByUser: users, + }) + .from(rfqLastDetails) + .leftJoin(vendors, eq(rfqLastDetails.vendorsId, vendors.id)) + .leftJoin(paymentTerms, eq(rfqLastDetails.paymentTermsCode, paymentTerms.code)) + .leftJoin(incoterms, eq(rfqLastDetails.incotermsCode, incoterms.code)) + .leftJoin(users, eq(rfqLastDetails.updatedBy, users.id)) + .where(eq(rfqLastDetails.rfqsLastId, rfqId)); + + const vendorDetails: VendorDetail[] = vendorsData.map(v => ({ + detailId: v.detail.id, + vendorId: v.vendor?.id ?? null, + vendorName: v.vendor?.vendorName ?? null, + vendorCode: v.vendor?.vendorCode ?? null, + vendorCountry: v.vendor?.country ?? null, + vendorEmail: v.vendor?.email ?? null, + vendorCategory: v.vendor?.vendorCategory ?? null, + vendorGrade: v.vendor?.vendorGrade ?? null, + basicContract: v.vendor?.basicContract ?? null, + + currency: v.detail.currency, + paymentTermsCode: v.detail.paymentTermsCode, + paymentTermsDescription: v.paymentTerms?.description ?? null, + incotermsCode: v.detail.incotermsCode, + incotermsDescription: v.incoterms?.description ?? null, + incotermsDetail: v.detail.incotermsDetail, + deliveryDate: v.detail.deliveryDate, + contractDuration: v.detail.contractDuration, + taxCode: v.detail.taxCode, + placeOfShipping: v.detail.placeOfShipping, + placeOfDestination: v.detail.placeOfDestination, + + shortList: v.detail.shortList, + returnYn: v.detail.returnYn, + returnedAt: v.detail.returnedAt, + + prjectGtcYn: v.detail.prjectGtcYn, + generalGtcYn: v.detail.generalGtcYn, + ndaYn: v.detail.ndaYn, + agreementYn: v.detail.agreementYn, + + materialPriceRelatedYn: v.detail.materialPriceRelatedYn, + sparepartYn: v.detail.sparepartYn, + firstYn: v.detail.firstYn, + firstDescription: v.detail.firstDescription, + sparepartDescription: v.detail.sparepartDescription, + + remark: v.detail.remark, + cancelReason: v.detail.cancelReason, + + updatedBy: v.detail.updatedBy, + updatedByUserName: v.updatedByUser?.name ?? null, + updatedAt: v.detail.updatedAt, + })); + + // 4. 첨부파일 정보 조회 + const attachmentsData = await db + .select({ + attachment: rfqLastAttachments, + revision: rfqLastAttachmentRevisions, + createdByUser: users, + }) + .from(rfqLastAttachments) + .leftJoin( + rfqLastAttachmentRevisions, + and( + eq(rfqLastAttachments.latestRevisionId, rfqLastAttachmentRevisions.id), + eq(rfqLastAttachmentRevisions.isLatest, true) + ) + ) + .leftJoin(users, eq(rfqLastAttachments.createdBy, users.id)) + .where(eq(rfqLastAttachments.rfqId, rfqId)); + + const attachments: AttachmentInfo[] = attachmentsData.map(a => ({ + id: a.attachment.id, + attachmentType: a.attachment.attachmentType, + serialNo: a.attachment.serialNo, + currentRevision: a.attachment.currentRevision, + description: a.attachment.description, + + fileName: a.revision?.fileName ?? null, + originalFileName: a.revision?.originalFileName ?? null, + filePath: a.revision?.filePath ?? null, + fileSize: a.revision?.fileSize ?? null, + fileType: a.revision?.fileType ?? null, + + createdBy: a.attachment.createdBy, + createdByUserName: a.createdByUser?.name ?? null, + createdAt: a.attachment.createdAt, + updatedAt: a.attachment.updatedAt, + })); + + // 5. 카운트 정보 계산 + const vendorCount = vendorDetails.length; + const shortListedVendorCount = vendorDetails.filter(v => v.shortList).length; + const quotationReceivedCount = vendorDetails.filter(v => v.quotationSubmittedAt).length; + + // PR Items 카운트 (별도 쿼리 필요) + const prItemsCount = await db + .select({ count: sql<number>`COUNT(*)` }) + .from(rfqPrItems) + .where(eq(rfqPrItems.rfqsLastId, rfqId)); + + const majorItemsCount = await db + .select({ count: sql<number>`COUNT(*)` }) + .from(rfqPrItems) + .where(and( + eq(rfqPrItems.rfqsLastId, rfqId), + eq(rfqPrItems.majorYn, true) + )); + + // 6. 사용자 정보 조회 (createdBy, updatedBy, sentBy) + const [createdByUser] = await db + .select({ name: users.name }) + .from(users) + .where(eq(users.id, rfq.createdBy)) + .limit(1); + + const [updatedByUser] = await db + .select({ name: users.name }) + .from(users) + .where(eq(users.id, rfq.updatedBy)) + .limit(1); + + const [sentByUser] = rfq.sentBy + ? await db + .select({ name: users.name }) + .from(users) + .where(eq(users.id, rfq.sentBy)) + .limit(1) + : [null]; + + // 7. 전체 정보 조합 + const rfqFullInfo: RfqFullInfo = { + // 기본 정보 + id: rfq.id, + rfqCode: rfq.rfqCode ?? '', + rfqType: rfq.rfqType, + rfqTitle: rfq.rfqTitle, + series: rfq.series, + rfqSealedYn: rfq.rfqSealedYn, + + // ITB 관련 + projectCompany: rfq.projectCompany, + projectFlag: rfq.projectFlag, + projectSite: rfq.projectSite, + smCode: rfq.smCode, + + // RFQ 추가 필드 + prNumber: rfq.prNumber, + prIssueDate: rfq.prIssueDate, + + // 프로젝트 + projectId: rfq.projectId, + projectCode: null, // 프로젝트 조인 필요시 추가 + projectName: null, // 프로젝트 조인 필요시 추가 + + // 아이템 + itemCode: rfq.itemCode, + itemName: rfq.itemName, + + // 패키지 + packageNo: rfq.packageNo, + packageName: rfq.packageName, + + // 날짜 + dueDate: rfq.dueDate, + rfqSendDate: rfq.rfqSendDate, + + // 상태 + status: rfq.status, + + // 구매 담당자 + picId: rfq.pic, + picCode: rfq.picCode, + picName: rfq.picName, + picUserName: picUser?.name ?? null, + picTeam: picUser?.department ?? null, // users 테이블에 department 필드가 있다고 가정 + + // 설계 담당자 + engPicName: rfq.EngPicName, + designTeam: null, // 추가 정보 필요시 입력 + + // 자재그룹 (PR Items에서) + materialGroup: majorItem?.materialCategory ?? null, + materialGroupDesc: majorItem?.materialDescription ?? null, + + // 카운트 + vendorCount, + shortListedVendorCount, + quotationReceivedCount, + prItemsCount: prItemsCount[0]?.count ?? 0, + majorItemsCount: majorItemsCount[0]?.count ?? 0, + + // 감사 정보 + createdBy: rfq.createdBy, + createdByUserName: createdByUser?.name ?? null, + createdAt: rfq.createdAt, + updatedBy: rfq.updatedBy, + updatedByUserName: updatedByUser?.name ?? null, + updatedAt: rfq.updatedAt, + sentBy: rfq.sentBy, + sentByUserName: sentByUser?.name ?? null, + + remark: rfq.remark, + + // 추가 필드 (필요시) + evaluationApply: true, // 기본값 또는 별도 로직 + quotationType: rfq.rfqType ?? undefined, + contractType: undefined, // 별도 필드 필요 + + // 연관 데이터 + vendors: vendorDetails, + attachments: attachments, + }; + + return rfqFullInfo; + } catch (error) { + console.error("RFQ 정보 조회 실패:", error); + throw error; + } +} + +/** + * SendRfqDialog용 간단한 정보 조회 + */ +export async function getRfqInfoForSend(rfqId: number) { + const fullInfo = await getRfqFullInfo(rfqId); + + return { + rfqCode: fullInfo.rfqCode, + rfqTitle: fullInfo.rfqTitle || '', + rfqType: fullInfo.rfqType || '', + projectCode: fullInfo.projectCode, + projectName: fullInfo.projectName, + picName: fullInfo.picName, + picCode: fullInfo.picCode, + picTeam: fullInfo.picTeam, + packageNo: fullInfo.packageNo, + packageName: fullInfo.packageName, + designPicName: fullInfo.engPicName, // EngPicName이 설계담당자 + designTeam: fullInfo.designTeam, + materialGroup: fullInfo.materialGroup, + materialGroupDesc: fullInfo.materialGroupDesc, + dueDate: fullInfo.dueDate || new Date(), + quotationType: fullInfo.quotationType, + evaluationApply: fullInfo.evaluationApply, + contractType: fullInfo.contractType, + }; +} + +/** + * 벤더 정보만 조회 + */ +export async function getRfqVendors(rfqId: number) { + const fullInfo = await getRfqFullInfo(rfqId); + return fullInfo.vendors; +} + +/** + * 첨부파일 정보만 조회 + */ +export async function getRfqAttachments(rfqId: number) { + const fullInfo = await getRfqFullInfo(rfqId); + return fullInfo.attachments; }
\ No newline at end of file |
