diff options
Diffstat (limited to 'lib/pq')
| -rw-r--r-- | lib/pq/pq-review-table-new/vendors-table-columns.tsx | 12 | ||||
| -rw-r--r-- | lib/pq/service.ts | 310 |
2 files changed, 311 insertions, 11 deletions
diff --git a/lib/pq/pq-review-table-new/vendors-table-columns.tsx b/lib/pq/pq-review-table-new/vendors-table-columns.tsx index 30b1c83f..b4d7d038 100644 --- a/lib/pq/pq-review-table-new/vendors-table-columns.tsx +++ b/lib/pq/pq-review-table-new/vendors-table-columns.tsx @@ -82,7 +82,7 @@ export interface PQSubmission { completedAt: Date | null
forecastedAt: Date | null
evaluationScore: number | null
- evaluationResult: "APPROVED" | "SUPPLEMENT" | "REJECTED" | "RESULT_SENT" | null
+ evaluationResult: "APPROVED" | "SUPPLEMENT" | "SUPPLEMENT_REINSPECT" | "SUPPLEMENT_DOCUMENT" | "REJECTED" | "RESULT_SENT" | null
investigationNotes: string | null
} | null
// 통합 상태를 위한 새 필드
@@ -327,6 +327,10 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC return { status: "INVESTIGATION_APPROVED", label: "실사 승인", variant: "success" as const };
case "SUPPLEMENT":
return { status: "INVESTIGATION_SUPPLEMENT", label: "실사 보완필요", variant: "secondary" as const };
+ case "SUPPLEMENT_REINSPECT":
+ return { status: "INVESTIGATION_SUPPLEMENT_REINSPECT", label: "실사 보완-재실사", variant: "secondary" as const };
+ case "SUPPLEMENT_DOCUMENT":
+ return { status: "INVESTIGATION_SUPPLEMENT_DOCUMENT", label: "실사 보완-서류제출", variant: "secondary" as const };
case "REJECTED":
return { status: "INVESTIGATION_REJECTED", label: "실사 불가", variant: "destructive" as const };
default:
@@ -336,6 +340,8 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC return { status: "INVESTIGATION_COMPLETED", label: "실사 완료", variant: "default" as const };
case "CANCELED":
return { status: "INVESTIGATION_CANCELED", label: "실사 취소됨", variant: "destructive" as const };
+ case "SUPPLEMENT_REQUIRED":
+ return { status: "INVESTIGATION_SUPPLEMENT_REQUIRED", label: "실사 보완 요구됨", variant: "secondary" as const };
case "RESULT_SENT":
return { status: "INVESTIGATION_RESULT_SENT", label: "실사 결과 발송", variant: "success" as const };
default:
@@ -398,6 +404,10 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC return <Badge variant="success">승인</Badge>;
case "SUPPLEMENT":
return <Badge variant="secondary">보완</Badge>;
+ case "SUPPLEMENT_REINSPECT":
+ return <Badge variant="secondary">보완-재실사</Badge>;
+ case "SUPPLEMENT_DOCUMENT":
+ return <Badge variant="secondary">보완-서류제출</Badge>;
case "REJECTED":
return <Badge variant="destructive">불가</Badge>;
default:
diff --git a/lib/pq/service.ts b/lib/pq/service.ts index b6640453..8b1986ce 100644 --- a/lib/pq/service.ts +++ b/lib/pq/service.ts @@ -1238,7 +1238,7 @@ export async function requestPqChangesAction({ await db
.update(vendorPQSubmissions)
.set({
- status: "IN_PROGRESS", // 변경 요청 상태로 설정
+ status: "SUBMITTED", // 변경 요청 상태로 설정
updatedAt: new Date(),
})
.where(
@@ -2210,22 +2210,21 @@ export async function approvePQAction({ projectName = projectData?.name || 'Unknown Project';
}
- // 5. PQ 상태 업데이트
+ // 5. PQ 상태를 QM_REVIEWING으로 업데이트 (TO-BE: QM 검토 단계 추가)
await db
.update(vendorPQSubmissions)
.set({
- status: "APPROVED",
- approvedAt: currentDate,
+ status: "QM_REVIEWING",
updatedAt: currentDate,
})
.where(eq(vendorPQSubmissions.id, pqSubmissionId));
- // 6. 일반 PQ인 경우 벤더 상태 업데이트 (선택사항)
+ // 6. 일반 PQ인 경우 벤더 상태를 IN_PQ로 업데이트 (QM 검토 중)
if (pqSubmission.type === "GENERAL") {
await db
.update(vendors)
.set({
- status: "PQ_APPROVED",
+ status: "IN_PQ",
updatedAt: currentDate,
})
.where(eq(vendors.id, vendorId));
@@ -2235,21 +2234,21 @@ export async function approvePQAction({ if (vendor.email) {
try {
const emailSubject = pqSubmission.projectId
- ? `[eVCP] Project PQ Approved for ${projectName}`
- : "[eVCP] General PQ Approved";
+ ? `[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-approved-vendor",
+ template: "pq-qm-review-vendor",
context: {
vendorName: vendor.vendorName,
projectId: pqSubmission.projectId,
projectName: projectName,
isProjectPQ: !!pqSubmission.projectId,
- approvedDate: currentDate.toLocaleString(),
+ reviewDate: currentDate.toLocaleString(),
portalUrl,
}
});
@@ -2277,6 +2276,297 @@ export async function approvePQAction({ }
}
+// QM 검토 승인 액션
+export async function approveQMReviewAction({
+ pqSubmissionId,
+ vendorId,
+}: {
+ pqSubmissionId: number;
+ vendorId: number;
+}) {
+ unstable_noStore();
+
+ try {
+ const headersList = await headers();
+ const host = headersList.get('host') || 'localhost:3000';
+ const currentDate = new Date();
+
+ // 1. PQ 제출 정보 조회
+ const pqSubmission = await db
+ .select({
+ id: vendorPQSubmissions.id,
+ vendorId: vendorPQSubmissions.vendorId,
+ projectId: vendorPQSubmissions.projectId,
+ type: vendorPQSubmissions.type,
+ status: vendorPQSubmissions.status,
+ })
+ .from(vendorPQSubmissions)
+ .where(
+ and(
+ eq(vendorPQSubmissions.id, pqSubmissionId),
+ eq(vendorPQSubmissions.vendorId, vendorId)
+ )
+ )
+ .then(rows => rows[0]);
+
+ if (!pqSubmission) {
+ return { ok: false, error: "PQ submission not found" };
+ }
+
+ // 2. 상태 확인 (QM_REVIEWING 상태만 승인 가능)
+ if (pqSubmission.status !== "QM_REVIEWING") {
+ return {
+ ok: false,
+ error: `Cannot approve QM review in current status: ${pqSubmission.status}`
+ };
+ }
+
+ // 3. 벤더 정보 조회
+ const vendor = await db
+ .select({
+ id: vendors.id,
+ vendorName: vendors.vendorName,
+ email: vendors.email,
+ status: vendors.status,
+ })
+ .from(vendors)
+ .where(eq(vendors.id, vendorId))
+ .then(rows => rows[0]);
+
+ if (!vendor) {
+ return { ok: false, error: "Vendor not found" };
+ }
+
+ // 4. 프로젝트 정보 (프로젝트 PQ인 경우)
+ let projectName = '';
+ if (pqSubmission.projectId) {
+ const projectData = await db
+ .select({
+ id: projects.id,
+ name: projects.name,
+ })
+ .from(projects)
+ .where(eq(projects.id, pqSubmission.projectId))
+ .then(rows => rows[0]);
+
+ projectName = projectData?.name || 'Unknown Project';
+ }
+
+ // 5. PQ 상태를 QM_APPROVED로 업데이트
+ await db
+ .update(vendorPQSubmissions)
+ .set({
+ status: "QM_APPROVED",
+ approvedAt: currentDate,
+ updatedAt: currentDate,
+ })
+ .where(eq(vendorPQSubmissions.id, pqSubmissionId));
+
+ // 6. 일반 PQ인 경우 벤더 상태를 PQ_APPROVED로 업데이트
+ if (pqSubmission.type === "GENERAL") {
+ await db
+ .update(vendors)
+ .set({
+ status: "PQ_APPROVED",
+ updatedAt: currentDate,
+ })
+ .where(eq(vendors.id, vendorId));
+ }
+
+ // 7. 실사 요청 생성 (QM 승인 후 실사 프로세스 시작)
+ await db
+ .insert(vendorInvestigations)
+ .values({
+ vendorId: vendorId,
+ pqSubmissionId: pqSubmissionId,
+ investigationStatus: "PLANNED",
+ investigationMethod: "DOCUMENT_EVAL", // 기본값, 나중에 변경 가능
+ });
+
+ // 8. 벤더에게 이메일 알림 발송
+ if (vendor.email) {
+ 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);
+ }
+ }
+
+ // 9. 캐시 무효화
+ revalidateTag("vendors");
+ revalidateTag("vendor-status-counts");
+ revalidateTag("pq-submissions");
+ revalidateTag("vendor-pq-submissions");
+ revalidateTag("vendor-investigations");
+ revalidatePath("/evcp/pq_new");
+
+ return { ok: true };
+ } catch (error) {
+ console.error("QM review approve error:", error);
+ return { ok: false, error: getErrorMessage(error) };
+ }
+}
+
+// QM 검토 거절 액션
+export async function rejectQMReviewAction({
+ pqSubmissionId,
+ vendorId,
+ rejectReason
+}: {
+ pqSubmissionId: number;
+ vendorId: number;
+ rejectReason: string;
+}) {
+ unstable_noStore();
+
+ try {
+ const headersList = await headers();
+ const host = headersList.get('host') || 'localhost:3000';
+ const currentDate = new Date();
+
+ // 1. PQ 제출 정보 조회
+ const pqSubmission = await db
+ .select({
+ id: vendorPQSubmissions.id,
+ vendorId: vendorPQSubmissions.vendorId,
+ projectId: vendorPQSubmissions.projectId,
+ type: vendorPQSubmissions.type,
+ status: vendorPQSubmissions.status,
+ })
+ .from(vendorPQSubmissions)
+ .where(
+ and(
+ eq(vendorPQSubmissions.id, pqSubmissionId),
+ eq(vendorPQSubmissions.vendorId, vendorId)
+ )
+ )
+ .then(rows => rows[0]);
+
+ if (!pqSubmission) {
+ return { ok: false, error: "PQ submission not found" };
+ }
+
+ // 2. 상태 확인 (QM_REVIEWING 상태만 거절 가능)
+ if (pqSubmission.status !== "QM_REVIEWING") {
+ return {
+ ok: false,
+ error: `Cannot reject QM review in current status: ${pqSubmission.status}`
+ };
+ }
+
+ // 3. 벤더 정보 조회
+ const vendor = await db
+ .select({
+ id: vendors.id,
+ vendorName: vendors.vendorName,
+ email: vendors.email,
+ status: vendors.status,
+ })
+ .from(vendors)
+ .where(eq(vendors.id, vendorId))
+ .then(rows => rows[0]);
+
+ if (!vendor) {
+ return { ok: false, error: "Vendor not found" };
+ }
+
+ // 4. 프로젝트 정보 (프로젝트 PQ인 경우)
+ let projectName = '';
+ if (pqSubmission.projectId) {
+ const projectData = await db
+ .select({
+ id: projects.id,
+ name: projects.name,
+ })
+ .from(projects)
+ .where(eq(projects.id, pqSubmission.projectId))
+ .then(rows => rows[0]);
+
+ projectName = projectData?.name || 'Unknown Project';
+ }
+
+ // 5. PQ 상태를 QM_REJECTED로 업데이트
+ await db
+ .update(vendorPQSubmissions)
+ .set({
+ status: "QM_REJECTED",
+ rejectedAt: currentDate,
+ rejectReason: rejectReason,
+ updatedAt: currentDate,
+ })
+ .where(eq(vendorPQSubmissions.id, pqSubmissionId));
+
+ // 6. 일반 PQ인 경우 벤더 상태를 PQ_FAILED로 업데이트
+ if (pqSubmission.type === "GENERAL") {
+ await db
+ .update(vendors)
+ .set({
+ status: "PQ_FAILED",
+ updatedAt: currentDate,
+ })
+ .where(eq(vendors.id, vendorId));
+ }
+
+ // 7. 벤더에게 이메일 알림 발송
+ if (vendor.email) {
+ try {
+ const emailSubject = pqSubmission.projectId
+ ? `[eVCP] Project PQ Rejected for ${projectName}`
+ : "[eVCP] General PQ Rejected";
+
+ const portalUrl = `${host}/partners/pq`;
+
+ await sendEmail({
+ to: vendor.email,
+ subject: emailSubject,
+ template: "pq-rejected-vendor",
+ context: {
+ vendorName: vendor.vendorName,
+ projectId: pqSubmission.projectId,
+ projectName: projectName,
+ isProjectPQ: !!pqSubmission.projectId,
+ rejectedDate: currentDate.toLocaleString(),
+ rejectReason: rejectReason,
+ portalUrl,
+ }
+ });
+ } catch (emailError) {
+ console.error("Failed to send vendor notification:", emailError);
+ }
+ }
+
+ // 8. 캐시 무효화
+ revalidateTag("vendors");
+ revalidateTag("vendor-status-counts");
+ revalidateTag("pq-submissions");
+ revalidateTag("vendor-pq-submissions");
+ revalidatePath("/evcp/pq_new");
+
+ return { ok: true };
+ } catch (error) {
+ console.error("QM review reject error:", error);
+ return { ok: false, error: getErrorMessage(error) };
+ }
+}
+
// PQ 거부 액션
export async function rejectPQAction({
pqSubmissionId,
|
