From 871a6d46a769cbe9e87146434f4bcb2d6792ab81 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 30 Oct 2025 10:44:47 +0000 Subject: (최겸) 구매 PQ/실사 재개발(테스트 필요), 정규업체등록 결재 개발, 실사 의뢰 결재 후처리 등 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pq/service.ts | 266 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 200 insertions(+), 66 deletions(-) (limited to 'lib/pq/service.ts') diff --git a/lib/pq/service.ts b/lib/pq/service.ts index 8b1986ce..0bc575a6 100644 --- a/lib/pq/service.ts +++ b/lib/pq/service.ts @@ -555,6 +555,28 @@ export async function submitPQAction({ const targetSubmissionId = existingSubmission?.id || ''; const targetRequesterId = existingSubmission?.requesterId || null; + // QM 담당자 이메일 조회 (해당 PQ와 연결된 실사에 배정된 경우) + let qmManagerEmail: string | null = null; + if (targetSubmissionId) { + try { + const inv = await db + .select({ qmManagerId: vendorInvestigations.qmManagerId }) + .from(vendorInvestigations) + .where(eq(vendorInvestigations.pqSubmissionId, Number(targetSubmissionId))) + .then(rows => rows[0]); + if (inv?.qmManagerId) { + const qmUser = await db + .select({ email: users.email }) + .from(users) + .where(eq(users.id, inv.qmManagerId)) + .then(rows => rows[0]); + qmManagerEmail = qmUser?.email || null; + } + } catch (e) { + console.warn("Failed to fetch QM manager email for PQ submission", e); + } + } + if (targetRequesterId !== null) { try { // 요청자 정보 조회 @@ -577,6 +599,7 @@ export async function submitPQAction({ await sendEmail({ to: requester.email, + cc: qmManagerEmail ? [qmManagerEmail] : undefined, subject: emailSubject, template: "pq-submitted-admin", context: { @@ -1238,7 +1261,7 @@ export async function requestPqChangesAction({ await db .update(vendorPQSubmissions) .set({ - status: "SUBMITTED", // 변경 요청 상태로 설정 + status: "IN_PROGRESS", // 변경 요청 상태로 설정 updatedAt: new Date(), }) .where( @@ -2209,53 +2232,54 @@ export async function approvePQAction({ projectName = projectData?.name || 'Unknown Project'; } - - // 5. PQ 상태를 QM_REVIEWING으로 업데이트 (TO-BE: QM 검토 단계 추가) + + // 5. PQ 상태 업데이트 await db - .update(vendorPQSubmissions) + .update(vendorPQSubmissions) + .set({ + status: "APPROVED", + approvedAt: currentDate, + updatedAt: currentDate, + }) + .where(eq(vendorPQSubmissions.id, pqSubmissionId)); + + // 6. 일반 PQ인 경우 벤더 상태 업데이트 (선택사항) + if (pqSubmission.type === "GENERAL") { + await db + .update(vendors) .set({ - status: "QM_REVIEWING", + status: "PQ_APPROVED", updatedAt: currentDate, }) - .where(eq(vendorPQSubmissions.id, pqSubmissionId)); - - // 6. 일반 PQ인 경우 벤더 상태를 IN_PQ로 업데이트 (QM 검토 중) - if (pqSubmission.type === "GENERAL") { - await db - .update(vendors) - .set({ - status: "IN_PQ", - updatedAt: currentDate, - }) - .where(eq(vendors.id, vendorId)); + .where(eq(vendors.id, vendorId)); } - + // 7. 벤더에게 이메일 알림 발송 if (vendor.email) { - try { - const emailSubject = pqSubmission.projectId - ? `[eVCP] Project PQ Under QM Review for ${projectName}` - : "[eVCP] General PQ Under QM Review"; - - const portalUrl = `${host}/partners/pq`; - - await sendEmail({ - to: vendor.email, - subject: emailSubject, - template: "pq-qm-review-vendor", - context: { - vendorName: vendor.vendorName, - projectId: pqSubmission.projectId, - projectName: projectName, - isProjectPQ: !!pqSubmission.projectId, - reviewDate: currentDate.toLocaleString(), - portalUrl, - } - }); - } catch (emailError) { - console.error("Failed to send vendor notification:", emailError); - // 이메일 발송 실패가 전체 프로세스를 중단하지 않음 - } + try { + const emailSubject = pqSubmission.projectId + ? `[eVCP] Project PQ Approved for ${projectName}` + : "[eVCP] General PQ Approved"; + + const portalUrl = `${host}/partners/pq`; + + await sendEmail({ + to: vendor.email, + subject: emailSubject, + template: "pq-approved-vendor", + context: { + vendorName: vendor.vendorName, + projectId: pqSubmission.projectId, + projectName: projectName, + isProjectPQ: !!pqSubmission.projectId, + approvedDate: currentDate.toLocaleString(), + portalUrl, + } + }); + } catch (emailError) { + console.error("Failed to send vendor notification:", emailError); + // 이메일 발송 실패가 전체 프로세스를 중단하지 않음 + } } // 8. 캐시 무효화 @@ -2314,12 +2338,12 @@ export async function approveQMReviewAction({ } // 2. 상태 확인 (QM_REVIEWING 상태만 승인 가능) - if (pqSubmission.status !== "QM_REVIEWING") { - return { - ok: false, - error: `Cannot approve QM review in current status: ${pqSubmission.status}` - }; - } + // if (pqSubmission.status !== "QM_REVIEWING") { + // return { + // ok: false, + // error: `Cannot approve QM review in current status: ${pqSubmission.status}` + // }; + // } // 3. 벤더 정보 조회 const vendor = await db @@ -2373,22 +2397,41 @@ export async function approveQMReviewAction({ .where(eq(vendors.id, vendorId)); } - // 7. 실사 요청 생성 (QM 승인 후 실사 프로세스 시작) - await db - .insert(vendorInvestigations) - .values({ - vendorId: vendorId, - pqSubmissionId: pqSubmissionId, - investigationStatus: "PLANNED", - investigationMethod: "DOCUMENT_EVAL", // 기본값, 나중에 변경 가능 - }); + // 7. 실사 상태 변경: QM 승인 시 QM_REVIEW_CONFIRMED로 전환 + try { + const existingInvestigation = await db + .select({ id: vendorInvestigations.id }) + .from(vendorInvestigations) + .where(eq(vendorInvestigations.pqSubmissionId, pqSubmissionId)) + .then(rows => rows[0]); + + if (existingInvestigation) { + await db + .update(vendorInvestigations) + .set({ investigationStatus: "QM_REVIEW_CONFIRMED", updatedAt: currentDate }) + .where(eq(vendorInvestigations.id, existingInvestigation.id)); + } else { + await db + .insert(vendorInvestigations) + .values({ + vendorId: vendorId, + pqSubmissionId: pqSubmissionId, + investigationStatus: "QM_REVIEW_CONFIRMED", + investigationMethod: "DOCUMENT_EVAL", + requestedAt: currentDate, + updatedAt: currentDate, + }); + } + } catch (e) { + console.error("Failed to set investigation QM_REVIEW_CONFIRMED on QM approve", e); + } // 8. 벤더에게 이메일 알림 발송 if (vendor.email) { try { const emailSubject = pqSubmission.projectId - ? `[eVCP] Project PQ Approved for ${projectName}` - : "[eVCP] General PQ Approved"; + ? `[eVCP] Project PQ QM Approved for ${projectName}` + : "[eVCP] General PQ QM Approved"; const portalUrl = `${host}/partners/pq`; @@ -2525,7 +2568,25 @@ export async function rejectQMReviewAction({ .where(eq(vendors.id, vendorId)); } - // 7. 벤더에게 이메일 알림 발송 + // 7. 실사 상태 변경: QM 거절 시 CANCELED로 전환 + try { + const existingInvestigation = await db + .select({ id: vendorInvestigations.id }) + .from(vendorInvestigations) + .where(eq(vendorInvestigations.pqSubmissionId, pqSubmissionId)) + .then(rows => rows[0]); + + if (existingInvestigation) { + await db + .update(vendorInvestigations) + .set({ investigationStatus: "CANCELED", updatedAt: currentDate }) + .where(eq(vendorInvestigations.id, existingInvestigation.id)); + } + } catch (e) { + console.error("Failed to set investigation CANCELED on QM reject", e); + } + + // 8. 벤더에게 이메일 알림 발송 if (vendor.email) { try { const emailSubject = pqSubmission.projectId @@ -2553,11 +2614,12 @@ export async function rejectQMReviewAction({ } } - // 8. 캐시 무효화 + // 9. 캐시 무효화 revalidateTag("vendors"); revalidateTag("vendor-status-counts"); revalidateTag("pq-submissions"); revalidateTag("vendor-pq-submissions"); + revalidateTag("vendor-investigations"); revalidatePath("/evcp/pq_new"); return { ok: true }; @@ -2714,10 +2776,76 @@ export async function rejectPQAction({ } } +// PQ 보완요청 메일 발송 액션 +export async function requestPqSupplementAction({ + pqSubmissionId, + vendorId, + comment, +}: { + pqSubmissionId: number; + vendorId: number; + comment: string; +}) { + unstable_noStore(); + try { + const session = await getServerSession(authOptions); + const currentUserEmail = session?.user?.email || null; + const headersList = await headers(); + const host = headersList.get('host') || 'localhost:3000'; + + // PQ/벤더/요청자 정보 조회 + const pq = await db + .select({ id: vendorPQSubmissions.id, pqNumber: vendorPQSubmissions.pqNumber, requesterId: vendorPQSubmissions.requesterId, projectId: vendorPQSubmissions.projectId }) + .from(vendorPQSubmissions) + .where(and(eq(vendorPQSubmissions.id, pqSubmissionId), eq(vendorPQSubmissions.vendorId, vendorId))) + .then(rows => rows[0]); + if (!pq) return { ok: false, error: 'PQ submission not found' }; + + const vendor = await db + .select({ vendorName: vendors.vendorName, email: vendors.email }) + .from(vendors) + .where(eq(vendors.id, vendorId)) + .then(rows => rows[0]); + if (!vendor?.email) return { ok: false, error: 'Vendor email not found' }; + + let requesterEmail: string | null = null; + if (pq.requesterId) { + const requester = await db + .select({ email: users.email }) + .from(users) + .where(eq(users.id, pq.requesterId)) + .then(rows => rows[0]); + requesterEmail = requester?.email || null; + } + + const reviewUrl = `http://${host}/evcp/pq/${vendorId}/${pqSubmissionId}`; + + await sendEmail({ + to: vendor.email, + cc: [currentUserEmail, requesterEmail].filter(Boolean) as string[], + subject: `[eVCP] PQ 보완 요청: ${vendor.vendorName}`, + template: 'pq-supplement-request', + context: { + vendorName: vendor.vendorName, + pqNumber: pq.pqNumber, + comment, + reviewUrl, + }, + }); + + revalidateTag('pq-submissions'); + return { ok: true }; + } catch (error) { + console.error('PQ supplement request error:', error); + return { ok: false, error: getErrorMessage(error) }; + } +} + // 실사 의뢰 생성 서버 액션 export async function requestInvestigationAction( pqSubmissionIds: number[], + currentUser: { id: number; epId: string | null; email?: string }, data: { qmManagerId: number, forecastedAt: Date, @@ -2727,10 +2855,7 @@ export async function requestInvestigationAction( ) { try { // 세션에서 요청자 정보 가져오기 - const session = await getServerSession(authOptions); - const requesterId = session?.user?.id ? Number(session.user.id) : null; - - if (!requesterId) { + if (!currentUser.id) { return { success: false, error: "인증된 사용자만 실사를 의뢰할 수 있습니다." }; } @@ -2755,7 +2880,7 @@ export async function requestInvestigationAction( const now = new Date(); - // 각 PQ에 대한 실사 요청 생성 - 타입이 정확히 맞는지 확인 + // 실사 요청 생성 const investigations = pqSubmissions.map((pq) => { return { vendorId: pq.vendorId, @@ -2765,12 +2890,21 @@ export async function requestInvestigationAction( forecastedAt: data.forecastedAt, investigationAddress: data.investigationAddress, investigationNotes: data.investigationNotes || null, - requesterId: requesterId, + requesterId: currentUser.id, requestedAt: now, createdAt: now, updatedAt: now, }; }); + //PQ 제출 정보 업데이트, status를 QM_REVIEWING로 업데이트 + await tx + .update(vendorPQSubmissions) + .set({ + status: "QM_REVIEWING", + updatedAt: now, + }) + .where(inArray(vendorPQSubmissions.id, pqSubmissionIds)); + // 실사 요청 저장 const created = await tx -- cgit v1.2.3