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 --- .../edit-investigation-dialog.tsx | 6 +- lib/pq/pq-review-table-new/site-visit-dialog.tsx | 135 ++++++++++++--------- .../pq-review-table-new/vendors-table-columns.tsx | 43 +++---- .../vendors-table-toolbar-actions.tsx | 91 +++++++++++++- lib/pq/pq-review-table-new/vendors-table.tsx | 5 +- 5 files changed, 197 insertions(+), 83 deletions(-) (limited to 'lib/pq/pq-review-table-new') 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({ - 실사 정보 수정 + 구매자체평가 실사 결과 수정 - 구매자체평가 실사 정보를 수정합니다. + 구매자체평가 실사 결과를 수정합니다. 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([]) @@ -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({ !open && onClose()}> - 방문실사 요청 생성 + {isReinspection ? "재실사 요청 생성" : "방문실사 요청 생성"} - 협력업체에 방문실사 요청을 생성하고, 협력업체가 입력할 정보 항목을 설정합니다. + {isReinspection + ? "협력업체에 재실사 요청을 생성하고, 협력업체가 입력할 정보 항목을 설정합니다." + : "협력업체에 방문실사 요청을 생성하고, 협력업체가 입력할 정보 항목을 설정합니다." + } @@ -710,7 +737,7 @@ export function SiteVisitDialog({ 취소 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 요청됨 - case "IN_PROGRESS": - return 진행 중 - case "SUBMITTED": - return 제출됨 - case "APPROVED": - return 승인됨 - case "REJECTED": - return 거부됨 - default: - return {status} - } -} - /** * 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 }} > - 실사 정보 수정 + 구매 자체 평가 )} 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 () => { 실사 재의뢰 + {/* 재실사 요청 버튼 */} + + {/* 실사 결과 발송 버튼 */}