diff options
Diffstat (limited to 'lib/pq')
| -rw-r--r-- | lib/pq/pq-review-table-new/edit-investigation-dialog.tsx | 6 | ||||
| -rw-r--r-- | lib/pq/pq-review-table-new/site-visit-dialog.tsx | 135 | ||||
| -rw-r--r-- | lib/pq/pq-review-table-new/vendors-table-columns.tsx | 43 | ||||
| -rw-r--r-- | lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx | 91 | ||||
| -rw-r--r-- | lib/pq/pq-review-table-new/vendors-table.tsx | 5 | ||||
| -rw-r--r-- | lib/pq/service.ts | 266 |
6 files changed, 397 insertions, 149 deletions
diff --git a/lib/pq/pq-review-table-new/edit-investigation-dialog.tsx b/lib/pq/pq-review-table-new/edit-investigation-dialog.tsx index c4057798..8e139b79 100644 --- a/lib/pq/pq-review-table-new/edit-investigation-dialog.tsx +++ b/lib/pq/pq-review-table-new/edit-investigation-dialog.tsx @@ -51,7 +51,7 @@ const editInvestigationSchema = z.object({ z.string().transform((str) => str ? new Date(str) : undefined)
]).optional(),
evaluationResult: z.enum(["APPROVED", "SUPPLEMENT", "REJECTED"]).optional(),
- investigationNotes: z.string().max(1000, "QM 의견은 1000자 이내로 입력해주세요.").optional(),
+ investigationNotes: z.string().max(1000, "구매 의견은 1000자 이내로 입력해주세요.").optional(),
attachments: z.array(z.instanceof(File)).optional(),
})
@@ -210,9 +210,9 @@ export function EditInvestigationDialog({ <Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
- <DialogTitle>실사 정보 수정</DialogTitle>
+ <DialogTitle>구매자체평가 실사 결과 수정</DialogTitle>
<DialogDescription>
- 구매자체평가 실사 정보를 수정합니다.
+ 구매자체평가 실사 결과를 수정합니다.
</DialogDescription>
</DialogHeader>
diff --git a/lib/pq/pq-review-table-new/site-visit-dialog.tsx b/lib/pq/pq-review-table-new/site-visit-dialog.tsx index 2b65d03e..b1474150 100644 --- a/lib/pq/pq-review-table-new/site-visit-dialog.tsx +++ b/lib/pq/pq-review-table-new/site-visit-dialog.tsx @@ -140,6 +140,7 @@ interface SiteVisitDialogProps { projectCode?: string
pqItems?: Array<{itemCode: string, itemName: string}> | null
}
+ isReinspection?: boolean // 재실사 모드 플래그
}
export function SiteVisitDialog({
@@ -147,6 +148,7 @@ export function SiteVisitDialog({ onClose,
onSubmit,
investigation,
+ isReinspection = false,
}: SiteVisitDialogProps) {
const [isPending, setIsPending] = React.useState(false)
const [selectedFiles, setSelectedFiles] = React.useState<File[]>([])
@@ -184,58 +186,88 @@ export function SiteVisitDialog({ },
})
- // Dialog가 열릴 때마다 폼 재설정 및 기존 요청 확인
+ // Dialog가 열릴 때마다 폼 재설정 및 기존 요청 로딩
React.useEffect(() => {
if (isOpen) {
- // 기존 방문실사 요청이 있는지 확인
- const checkExistingRequest = async () => {
+ const loadExistingRequest = async () => {
try {
+ // 기존 방문실사 요청이 있는지 확인하고 최신 것을 로드
const existingRequest = await getSiteVisitRequestAction(investigation.id)
-
+
if (existingRequest.success && existingRequest.data) {
- toast.error("이미 방문실사 요청이 존재합니다. 추가 요청은 불가능합니다.")
- onClose()
+ // 기존 데이터를 form에 로드
+ const data = existingRequest.data
+ form.reset({
+ inspectionDuration: data.inspectionDuration || 1.0,
+ requestedStartDate: data.requestedStartDate ? new Date(data.requestedStartDate) : undefined,
+ requestedEndDate: data.requestedEndDate ? new Date(data.requestedEndDate) : undefined,
+ shiAttendees: data.shiAttendees || {
+ technicalSales: { checked: false, count: 0, details: "" },
+ design: { checked: false, count: 0, details: "" },
+ procurement: { checked: false, count: 0, details: "" },
+ quality: { checked: false, count: 0, details: "" },
+ production: { checked: false, count: 0, details: "" },
+ commissioning: { checked: false, count: 0, details: "" },
+ other: { checked: false, count: 0, details: "" },
+ },
+ shiAttendeeDetails: data.shiAttendeeDetails || "",
+ vendorRequests: data.vendorRequests || {
+ availableDates: false,
+ factoryName: false,
+ factoryLocation: false,
+ factoryAddress: false,
+ factoryPicName: false,
+ factoryPicPhone: false,
+ factoryPicEmail: false,
+ factoryDirections: false,
+ accessProcedure: false,
+ other: false,
+ },
+ otherVendorRequests: data.otherVendorRequests || "",
+ additionalRequests: data.additionalRequests || "",
+ })
return
}
+
+ // 기본값으로 폼 초기화 (기존 요청이 없는 경우)
+ form.reset({
+ inspectionDuration: 1.0,
+ requestedStartDate: undefined,
+ requestedEndDate: undefined,
+ shiAttendees: {
+ technicalSales: { checked: false, count: 0, details: "" },
+ design: { checked: false, count: 0, details: "" },
+ procurement: { checked: false, count: 0, details: "" },
+ quality: { checked: false, count: 0, details: "" },
+ production: { checked: false, count: 0, details: "" },
+ commissioning: { checked: false, count: 0, details: "" },
+ other: { checked: false, count: 0, details: "" },
+ },
+ shiAttendeeDetails: "",
+ vendorRequests: {
+ availableDates: false,
+ factoryName: false,
+ factoryLocation: false,
+ factoryAddress: false,
+ factoryPicName: false,
+ factoryPicPhone: false,
+ factoryPicEmail: false,
+ factoryDirections: false,
+ accessProcedure: false,
+ other: false,
+ },
+ otherVendorRequests: "",
+ additionalRequests: "",
+ })
} catch (error) {
- console.error("방문실사 요청 상태 확인 중 오류:", error)
- toast.error("방문실사 요청 상태 확인 중 오류가 발생했습니다.")
+ console.error("방문실사 요청 데이터 로드 중 오류:", error)
+ toast.error("방문실사 요청 데이터 로드 중 오류가 발생했습니다.")
onClose()
return
}
}
-
- checkExistingRequest()
-
- form.reset({
- inspectionDuration: 1.0,
- requestedStartDate: undefined,
- requestedEndDate: undefined,
- shiAttendees: {
- technicalSales: { checked: false, count: 0, details: "" },
- design: { checked: false, count: 0, details: "" },
- procurement: { checked: false, count: 0, details: "" },
- quality: { checked: false, count: 0, details: "" },
- production: { checked: false, count: 0, details: "" },
- commissioning: { checked: false, count: 0, details: "" },
- other: { checked: false, count: 0, details: "" },
- },
- shiAttendeeDetails: "",
- vendorRequests: {
- availableDates: false,
- factoryName: false,
- factoryLocation: false,
- factoryAddress: false,
- factoryPicName: false,
- factoryPicPhone: false,
- factoryPicEmail: false,
- factoryDirections: false,
- accessProcedure: false,
- other: false,
- },
- otherVendorRequests: "",
- additionalRequests: "",
- })
+
+ loadExistingRequest()
setSelectedFiles([])
}
}, [isOpen, form, investigation.id, onClose])
@@ -243,19 +275,11 @@ export function SiteVisitDialog({ async function handleSubmit(data: SiteVisitRequestFormValues) {
setIsPending(true)
try {
- // 제출 전에 한 번 더 기존 요청이 있는지 확인
- const existingRequest = await getSiteVisitRequestAction(investigation.id)
-
- if (existingRequest.success && existingRequest.data) {
- toast.error("이미 방문실사 요청이 존재합니다. 추가 요청은 불가능합니다.")
- onClose()
- return
- }
-
+
await onSubmit(data, selectedFiles)
- toast.success("방문실사 요청이 성공적으로 발송되었습니다.")
+ toast.success(isReinspection ? "재실사 요청이 성공적으로 발송되었습니다." : "방문실사 요청이 성공적으로 발송되었습니다.")
} catch (error) {
- toast.error("방문실사 요청 발송 중 오류가 발생했습니다.")
+ toast.error(isReinspection ? "재실사 요청 발송 중 오류가 발생했습니다." : "방문실사 요청 발송 중 오류가 발생했습니다.")
console.error("방문실사 요청 오류:", error)
} finally {
setIsPending(false)
@@ -294,9 +318,12 @@ export function SiteVisitDialog({ <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
- <DialogTitle>방문실사 요청 생성</DialogTitle>
+ <DialogTitle>{isReinspection ? "재실사 요청 생성" : "방문실사 요청 생성"}</DialogTitle>
<DialogDescription>
- 협력업체에 방문실사 요청을 생성하고, 협력업체가 입력할 정보 항목을 설정합니다.
+ {isReinspection
+ ? "협력업체에 재실사 요청을 생성하고, 협력업체가 입력할 정보 항목을 설정합니다."
+ : "협력업체에 방문실사 요청을 생성하고, 협력업체가 입력할 정보 항목을 설정합니다."
+ }
</DialogDescription>
</DialogHeader>
@@ -710,7 +737,7 @@ export function SiteVisitDialog({ 취소
</Button>
<Button type="submit" disabled={isPending}>
- {isPending ? "처리 중..." : "방문실사 요청 생성"}
+ {isPending ? "처리 중..." : (isReinspection ? "재실사 요청 생성" : "방문실사 요청 생성")}
</Button>
</DialogFooter>
</form>
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 b4d7d038..3e10177d 100644 --- a/lib/pq/pq-review-table-new/vendors-table-columns.tsx +++ b/lib/pq/pq-review-table-new/vendors-table-columns.tsx @@ -75,6 +75,7 @@ export interface PQSubmission { qmManagerEmail: string | null // QM 담당자 이메일
investigationAddress: string | null
investigationMethod: string | null
+ hasSupplementRequested: boolean
scheduledStartAt: Date | null
scheduledEndAt: Date | null
requestedAt: Date | null
@@ -100,24 +101,6 @@ interface GetColumnsProps { router: NextRouter;
}
-// 상태에 따른 Badge 변형 결정 함수
-function getStatusBadge(status: string) {
- switch (status) {
- case "REQUESTED":
- return <Badge variant="outline">요청됨</Badge>
- case "IN_PROGRESS":
- return <Badge variant="secondary">진행 중</Badge>
- case "SUBMITTED":
- return <Badge>제출됨</Badge>
- case "APPROVED":
- return <Badge variant="success">승인됨</Badge>
- case "REJECTED":
- return <Badge variant="destructive">거부됨</Badge>
- default:
- return <Badge variant="outline">{status}</Badge>
- }
-}
-
/**
* tanstack table 컬럼 정의
*/
@@ -285,15 +268,15 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC const combinedStatus = getCombinedStatus(row.original);
return value.includes(combinedStatus.status);
},
- enableSorting: true,
+ enableSorting: false,
enableHiding: true,
excelHeader: "진행현황",
};
// PQ 상태와 실사 상태를 결합하는 헬퍼 함수
function getCombinedStatus(submission: PQSubmission) {
- // PQ가 승인되지 않은 경우, PQ 상태를 우선 표시
- if (submission.status !== "APPROVED") {
+ // PQ가 QM 승인되지 않은 경우, PQ 상태를 우선 표시
+ if (submission.status !== "QM_APPROVED") {
switch (submission.status) {
case "REQUESTED":
return { status: "PQ_REQUESTED", label: "PQ 요청됨", variant: "outline" as const };
@@ -301,22 +284,30 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC return { status: "PQ_IN_PROGRESS", label: "PQ 진행 중", variant: "secondary" as const };
case "SUBMITTED":
return { status: "PQ_SUBMITTED", label: "PQ 제출됨", variant: "default" as const };
+ case "APPROVED":
+ return { status: "PQ_APPROVED", label: "PQ 승인됨", variant: "success" as const };
case "REJECTED":
return { status: "PQ_REJECTED", label: "PQ 거부됨", variant: "destructive" as const };
+ case "QM_REVIEWING":
+ return { status: "PQ_QM_REVIEWING", label: "QM 검토 중", variant: "secondary" as const };
+ case "QM_REJECTED":
+ return { status: "PQ_QM_REJECTED", label: "QM 거부됨", variant: "destructive" as const };
default:
return { status: submission.status, label: submission.status, variant: "outline" as const };
}
}
- // PQ가 승인되었지만 실사가 없는 경우
+ // PQ가 QM 승인되었지만 실사가 없는 경우
if (!submission.investigation) {
- return { status: "PQ_APPROVED", label: "PQ 승인됨", variant: "success" as const };
+ return { status: "PQ_QM_APPROVED", label: "PQ 승인됨", variant: "success" as const };
}
// PQ가 승인되고 실사가 있는 경우
switch (submission.investigation.investigationStatus) {
case "PLANNED":
return { status: "INVESTIGATION_PLANNED", label: "실사 계획됨", variant: "outline" as const };
+ case "QM_REVIEW_CONFIRMED":
+ return { status: "INVESTIGATION_QM_REVIEW_CONFIRMED", label: "QM 검토 완료", variant: "outline" as const };
case "IN_PROGRESS":
return { status: "INVESTIGATION_IN_PROGRESS", label: "실사 진행 중", variant: "secondary" as const };
case "COMPLETED":
@@ -343,6 +334,10 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC case "SUPPLEMENT_REQUIRED":
return { status: "INVESTIGATION_SUPPLEMENT_REQUIRED", label: "실사 보완 요구됨", variant: "secondary" as const };
case "RESULT_SENT":
+ // 보완을 통해 최종 합격/불합격한 경우
+ if (submission.investigation.hasSupplementRequested) {
+ return { status: "INVESTIGATION_RESULT_SENT_SUPPLEMENT", label: "실사 결과 발송(보완)", variant: "success" as const };
+ }
return { status: "INVESTIGATION_RESULT_SENT", label: "실사 결과 발송", variant: "success" as const };
default:
return {
@@ -761,7 +756,7 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedC }}
>
<Edit className="mr-2 h-4 w-4" />
- 실사 정보 수정
+ 구매 자체 평가
</DropdownMenuItem>
)}
diff --git a/lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx b/lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx index ea6b6189..98b1cc76 100644 --- a/lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx +++ b/lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx @@ -15,6 +15,7 @@ import { getFactoryLocationAnswer,
getQMManagers
} from "@/lib/pq/service"
+import { SiteVisitDialog } from "./site-visit-dialog"
import { RequestInvestigationDialog } from "./request-investigation-dialog"
import { CancelInvestigationDialog, ReRequestInvestigationDialog } from "./cancel-investigation-dialog"
import { SendResultsDialog } from "./send-results-dialog"
@@ -49,6 +50,7 @@ export function VendorsTableToolbarActions({ table }: VendorsTableToolbarActions const [isCancelDialogOpen, setIsCancelDialogOpen] = React.useState(false)
const [isSendResultsDialogOpen, setIsSendResultsDialogOpen] = React.useState(false)
const [isReRequestDialogOpen, setIsReRequestDialogOpen] = React.useState(false)
+ const [isReinspectionDialogOpen, setIsReinspectionDialogOpen] = React.useState(false)
const [isApprovalDialogOpen, setIsApprovalDialogOpen] = React.useState(false)
const [isReRequestApprovalDialogOpen, setIsReRequestApprovalDialogOpen] = React.useState(false)
@@ -441,6 +443,53 @@ const handleOpenRequestDialog = async () => { }
}
+ // 재실사 요청 처리
+ const handleRequestReinspection = async (data: {
+ qmManagerId: number,
+ forecastedAt: Date,
+ investigationAddress: string,
+ investigationNotes?: string
+ }) => {
+ try {
+ // 보완-재실사 대상 실사만 필터링
+ const supplementReinspectInvestigations = selectedRows.filter(row =>
+ row.original.investigation &&
+ row.original.investigation.evaluationResult === "SUPPLEMENT_REINSPECT"
+ );
+
+ if (supplementReinspectInvestigations.length === 0) {
+ toast.error("보완-재실사 대상 실사가 없습니다.");
+ return;
+ }
+
+ // 첫 번째 대상 실사로 재실사 요청 생성
+ const targetInvestigation = supplementReinspectInvestigations[0].original.investigation!;
+ const { requestSupplementReinspectionAction } = await import('@/lib/vendor-investigation/service');
+
+ const result = await requestSupplementReinspectionAction({
+ investigationId: targetInvestigation.id,
+ siteVisitData: {
+ inspectionDuration: 1.0, // 기본 1일
+ requestedStartDate: data.forecastedAt,
+ requestedEndDate: new Date(data.forecastedAt.getTime() + 24 * 60 * 60 * 1000), // 1일 후
+ shiAttendees: {},
+ vendorRequests: {},
+ additionalRequests: data.investigationNotes || "보완을 위한 재실사 요청입니다.",
+ }
+ });
+
+ if (result.success) {
+ toast.success("재실사 요청이 생성되었습니다.");
+ window.location.reload();
+ } else {
+ toast.error(result.error || "재실사 요청 생성 중 오류가 발생했습니다.");
+ }
+ } catch (error) {
+ console.error("재실사 요청 오류:", error);
+ toast.error("재실사 요청 중 오류가 발생했습니다.");
+ }
+ };
+
// 실사 결과 발송 처리
const handleSendInvestigationResults = async (data: { purchaseComment?: string }) => {
try {
@@ -505,8 +554,14 @@ const handleOpenRequestDialog = async () => { row.original.investigation.investigationStatus === "CANCELED"
).length
+ // 재실사 요청 대상 수 확인 (보완-재실사 결과만)
+ const reinspectInvestigationsCount = selectedRows.filter(row =>
+ row.original.investigation &&
+ row.original.investigation.evaluationResult === "SUPPLEMENT_REINSPECT"
+ ).length
+
// 미실사 PQ가 선택되었는지 확인
- const hasNonInspectionPQ = selectedRows.some(row =>
+ const hasNonInspectionPQ = selectedRows.some(row =>
row.original.type === "NON_INSPECTION"
)
@@ -651,6 +706,22 @@ const handleOpenRequestDialog = async () => { <span className="hidden sm:inline">실사 재의뢰</span>
</Button>
+ {/* 재실사 요청 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setIsReinspectionDialogOpen(true)}
+ disabled={
+ isLoading ||
+ selectedRows.length === 0 ||
+ reinspectInvestigationsCount === 0
+ }
+ className="gap-2"
+ >
+ <RefreshCw className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">재방문 실사 요청</span>
+ </Button>
+
{/* 실사 결과 발송 버튼 */}
<Button
variant="outline"
@@ -727,6 +798,24 @@ const handleOpenRequestDialog = async () => { auditResults={auditResults}
/>
+ {/* 재방문실사 요청 Dialog */}
+ <SiteVisitDialog
+ isOpen={isReinspectionDialogOpen}
+ onClose={() => setIsReinspectionDialogOpen(false)}
+ onSubmit={handleRequestReinspection}
+ investigation={{
+ id: 0, // 재실사용으로 0으로 설정 (기존 데이터 로드 안함)
+ investigationMethod: "SITE_VISIT_EVAL",
+ investigationAddress: "",
+ vendorName: "재실사 대상",
+ vendorCode: "N/A",
+ projectName: "",
+ projectCode: "",
+ pqItems: null
+ }}
+ isReinspection={true}
+ />
+
{/* 결재 미리보기 Dialog - 실사 의뢰 */}
{session?.user && investigationFormData && (
<ApprovalPreviewDialog
diff --git a/lib/pq/pq-review-table-new/vendors-table.tsx b/lib/pq/pq-review-table-new/vendors-table.tsx index f8f9928e..e55da8c5 100644 --- a/lib/pq/pq-review-table-new/vendors-table.tsx +++ b/lib/pq/pq-review-table-new/vendors-table.tsx @@ -310,7 +310,10 @@ export function PQSubmissionsTable({ promises, className }: PQSubmissionsTablePr enableAdvancedFilter: true,
enableRowSelection: true,
maxSelections: 1,
- initialState,
+ initialState: {
+ sorting: [{ id: "updatedAt", desc: true }],
+ columnPinning: { right: ["actions"] },
+ },
getRowId: (originalRow) => String(originalRow.id),
shallow: false,
clearOnDefault: true,
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
|
