summaryrefslogtreecommitdiff
path: root/lib/pq
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pq')
-rw-r--r--lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx1819
1 files changed, 903 insertions, 916 deletions
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 f93959a6..4584e772 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
@@ -1,917 +1,904 @@
-"use client"
-
-import * as React from "react"
-import { type Table } from "@tanstack/react-table"
-import { Download, ClipboardCheck, X, Send, RefreshCw } from "lucide-react"
-import { toast } from "sonner"
-import { useSession } from "next-auth/react"
-
-import { exportTableToExcel } from "@/lib/export"
-import { Button } from "@/components/ui/button"
-import { PQSubmission } from "./vendors-table-columns"
-import {
- cancelInvestigationAction,
- sendInvestigationResultsAction,
- getFactoryLocationAnswer,
- getQMManagers
-} from "@/lib/pq/service"
-import { SiteVisitDialog } from "./site-visit-dialog"
-import type { SiteVisitRequestFormValues } from "./site-visit-dialog"
-import { RequestInvestigationDialog } from "./request-investigation-dialog"
-import { CancelInvestigationDialog, ReRequestInvestigationDialog } from "./cancel-investigation-dialog"
-import { SendResultsDialog } from "./send-results-dialog"
-import { ApprovalPreviewDialog } from "@/components/approval/ApprovalPreviewDialog"
-import {
- requestPQInvestigationWithApproval,
- reRequestPQInvestigationWithApproval
-} from "@/lib/vendor-investigation/approval-actions"
-import type { ApprovalLineItem } from "@/components/knox/approval/ApprovalLineSelector"
-import { debugLog, debugError, debugSuccess } from "@/lib/debug-utils"
-
-interface VendorsTableToolbarActionsProps {
- table: Table<PQSubmission>
-}
-
-interface InvestigationInitialData {
- investigationMethod?: "PURCHASE_SELF_EVAL" | "DOCUMENT_EVAL" | "PRODUCT_INSPECTION" | "SITE_VISIT_EVAL";
- qmManagerId?: number;
- forecastedAt?: Date;
- createdAt?: Date;
- investigationAddress?: string;
- investigationNotes?: string;
-}
-
-export function VendorsTableToolbarActions({ table }: VendorsTableToolbarActionsProps) {
- const selectedRows = table.getFilteredSelectedRowModel().rows
- const [isLoading, setIsLoading] = React.useState(false)
- const { data: session } = useSession()
-
- // Dialog 상태 관리
- const [isRequestDialogOpen, setIsRequestDialogOpen] = React.useState(false)
- 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)
-
- // 초기 데이터 상태
- const [dialogInitialData, setDialogInitialData] = React.useState<InvestigationInitialData | undefined>(undefined)
-
- // 실사 의뢰 임시 데이터 (결재 다이얼로그로 전달)
- const [investigationFormData, setInvestigationFormData] = React.useState<{
- qmManagerId: number;
- qmManagerName: string;
- qmManagerEmail?: string;
- forecastedAt: Date;
- investigationAddress: string;
- investigationNotes?: string;
- } | null>(null)
-
- // 실사 재의뢰 임시 데이터
- const [reRequestData, setReRequestData] = React.useState<{
- investigationIds: number[];
- vendorNames: string;
- } | null>(null)
-
- // 결재 템플릿 변수
- const [approvalVariables, setApprovalVariables] = React.useState<Record<string, string>>({})
- const [reRequestApprovalVariables, setReRequestApprovalVariables] = React.useState<Record<string, string>>({})
-
- // 실사 의뢰 대화상자 열기 핸들러
-// 실사 의뢰 대화상자 열기 핸들러
-const handleOpenRequestDialog = async () => {
- setIsLoading(true);
- const initialData: InvestigationInitialData = {};
-
- try {
- // 선택된 행이 정확히 1개인 경우에만 초기값 설정
- if (selectedRows.length === 1) {
- const row = selectedRows[0].original;
-
- // 승인된 PQ이고 아직 실사가 없는 경우
- if (row.status === "APPROVED" && !row.investigation) {
- // Factory Location 정보 가져오기
- const locationResponse = await getFactoryLocationAnswer(
- row.vendorId,
- row.projectId
- );
-
- // 기본 주소 설정 - Factory Location 응답 또는 fallback
- let defaultAddress = "";
- if (locationResponse.success && locationResponse.factoryLocation) {
- defaultAddress = locationResponse.factoryLocation;
- } else {
- // Factory Location을 찾지 못한 경우 fallback
- defaultAddress = row.taxId ?
- `${row.vendorName} 사업장 (${row.taxId})` :
- `${row.vendorName} 사업장`;
- }
-
- // 이미 같은 회사에 대한 다른 실사가 있는지 확인
- const existingInvestigations = table.getFilteredRowModel().rows
- .map(r => r.original)
- .filter(r =>
- r.vendorId === row.vendorId &&
- r.investigation !== null
- );
-
- // 같은 업체의 이전 실사 기록이 있다면 참고하되, 주소는 Factory Location 사용
- if (existingInvestigations.length > 0) {
- // 날짜 기준으로 정렬하여 가장 최근 것을 가져옴
- const latestInvestigation = existingInvestigations.sort((a, b) => {
- const dateA = a.investigation?.createdAt || new Date(0);
- const dateB = b.investigation?.createdAt || new Date(0);
- return (dateB as Date).getTime() - (dateA as Date).getTime();
- })[0].investigation;
-
- if (latestInvestigation) {
- initialData.investigationMethod = latestInvestigation.investigationMethod || undefined;
- initialData.qmManagerId = latestInvestigation.qmManagerId || undefined;
- initialData.investigationAddress = defaultAddress; // Factory Location 사용
-
- // 날짜는 미래로 설정
- const futureDate = new Date();
- futureDate.setDate(futureDate.getDate() + 14); // 기본값으로 2주 후
- initialData.forecastedAt = futureDate;
- }
- } else {
- // 기본값 설정
- initialData.investigationMethod = undefined;
- const futureDate = new Date();
- futureDate.setDate(futureDate.getDate() + 14); // 기본값으로 2주 후
- initialData.forecastedAt = futureDate;
- initialData.investigationAddress = defaultAddress; // Factory Location 사용
- }
- }
- // 실사가 이미 있고 수정하는 경우
- // else if (row.investigation) {
- // initialData.investigationMethod = row.investigation.investigationMethod || undefined;
- // initialData.qmManagerId = row.investigation.qmManagerId !== null ?
- // row.investigation.qmManagerId : undefined;
- // initialData.forecastedAt = row.investigation.forecastedAt || new Date();
- // initialData.investigationAddress = row.investigation.investigationAddress || "";
- // initialData.investigationNotes = row.investigation.investigationNotes || "";
- // }
- }
- } catch (error) {
- console.error("초기 데이터 로드 중 오류:", error);
- toast.error("초기 데이터 로드 중 오류가 발생했습니다.");
- } finally {
- setIsLoading(false);
-
- // 초기 데이터 설정 및 대화상자 열기
- setDialogInitialData(Object.keys(initialData).length > 0 ? initialData : undefined);
- setIsRequestDialogOpen(true);
- }
-};
- // 실사 의뢰 요청 처리 - Step 1: RequestInvestigationDialog에서 정보 입력 후
- const handleRequestInvestigation = async (formData: {
- qmManagerId: number,
- forecastedAt: Date,
- investigationAddress: string,
- investigationNotes?: string
- }) => {
- try {
- // 승인된 PQ 제출만 필터링 (미실사 PQ 제외)
- const approvedPQs = selectedRows.filter(row =>
- row.original.status === "APPROVED" &&
- !row.original.investigation &&
- row.original.type !== "NON_INSPECTION"
- )
-
- if (approvedPQs.length === 0) {
- if (hasNonInspectionPQ) {
- toast.error("미실사 PQ는 실사 의뢰할 수 없습니다. 미실사 PQ를 제외하고 선택해주세요.")
- } else {
- toast.error("실사를 의뢰할 수 있는 업체가 없습니다. 승인된 PQ 제출만 실사 의뢰가 가능합니다.")
- }
- return
- }
-
- // QM 담당자 이름 및 이메일 조회
- const qmManagersResult = await getQMManagers()
- const qmManager = qmManagersResult.success
- ? qmManagersResult.data.find(m => m.id === formData.qmManagerId)
- : null
- const qmManagerName = qmManager?.name || `QM담당자 #${formData.qmManagerId}`
- const qmManagerEmail = qmManager?.email || undefined
-
- // 협력사 이름 목록 생성
- const vendorNames = approvedPQs
- .map(row => row.original.vendorName)
- .join(', ')
-
- // 실사 폼 데이터 저장 (이메일 추가)
- setInvestigationFormData({
- qmManagerId: formData.qmManagerId,
- qmManagerName,
- qmManagerEmail,
- forecastedAt: formData.forecastedAt,
- investigationAddress: formData.investigationAddress,
- investigationNotes: formData.investigationNotes,
- })
-
- // 결재 템플릿 변수 생성
- const requestedAt = new Date()
- const { mapPQInvestigationToTemplateVariables } = await import('@/lib/vendor-investigation/handlers')
- const variables = await mapPQInvestigationToTemplateVariables({
- vendorNames,
- qmManagerName,
- qmManagerEmail,
- forecastedAt: formData.forecastedAt,
- investigationAddress: formData.investigationAddress,
- investigationNotes: formData.investigationNotes,
- requestedAt,
- })
-
- setApprovalVariables(variables)
-
- // RequestInvestigationDialog 닫고 ApprovalPreviewDialog 열기
- setIsRequestDialogOpen(false)
- setIsApprovalDialogOpen(true)
- } catch (error) {
- console.error("결재 준비 중 오류 발생:", error)
- toast.error("결재 준비 중 오류가 발생했습니다.")
- }
- }
-
- // 실사 의뢰 결재 요청 처리 - Step 2: ApprovalPreviewDialog에서 결재선 선택 후
- const handleApprovalSubmit = async (approvers: ApprovalLineItem[]) => {
- debugLog('[InvestigationApproval] 실사 의뢰 결재 요청 시작', {
- approversCount: approvers.length,
- hasSession: !!session?.user,
- hasFormData: !!investigationFormData,
- });
-
- if (!session?.user || !investigationFormData) {
- debugError('[InvestigationApproval] 세션 또는 폼 데이터 없음');
- throw new Error('세션 정보가 없습니다.');
- }
-
- // 승인된 PQ 제출만 필터링
- const approvedPQs = selectedRows.filter(row =>
- row.original.status === "APPROVED" &&
- !row.original.investigation &&
- row.original.type !== "NON_INSPECTION"
- )
-
- debugLog('[InvestigationApproval] 승인된 PQ 건수', {
- count: approvedPQs.length,
- });
-
- // 협력사 이름 목록
- const vendorNames = approvedPQs
- .map(row => row.original.vendorName)
- .join(', ')
-
- // 결재선에서 EP ID 추출 (상신자 제외)
- const approverEpIds = approvers
- .filter((line) => line.seq !== "0" && line.epId)
- .map((line) => line.epId!)
-
- debugLog('[InvestigationApproval] 결재선 추출 완료', {
- approverEpIds,
- });
-
- // 결재 워크플로우 시작
- const result = await requestPQInvestigationWithApproval({
- pqSubmissionIds: approvedPQs.map(row => row.original.id),
- vendorNames,
- qmManagerId: investigationFormData.qmManagerId,
- qmManagerName: investigationFormData.qmManagerName,
- qmManagerEmail: investigationFormData.qmManagerEmail,
- forecastedAt: investigationFormData.forecastedAt,
- investigationAddress: investigationFormData.investigationAddress,
- investigationNotes: investigationFormData.investigationNotes,
- currentUser: {
- id: Number(session.user.id),
- epId: session.user.epId || null,
- email: session.user.email || undefined,
- },
- approvers: approverEpIds,
- })
-
- debugSuccess('[InvestigationApproval] 결재 요청 성공', {
- approvalId: result.approvalId,
- pendingActionId: result.pendingActionId,
- });
-
- if (result.status === 'pending_approval') {
- // 성공 시에만 상태 초기화 및 페이지 리로드
- setInvestigationFormData(null)
- setDialogInitialData(undefined)
- window.location.reload()
- }
- }
-
- const handleCloseRequestDialog = () => {
- setIsRequestDialogOpen(false);
- setDialogInitialData(undefined);
- };
-
-
- // 실사 의뢰 취소 처리
- const handleCancelInvestigation = async () => {
- setIsLoading(true)
- try {
- // 실사가 계획됨 상태인 PQ만 필터링
- const plannedInvestigations = selectedRows.filter(row =>
- row.original.investigation &&
- row.original.investigation.investigationStatus === "PLANNED"
- )
-
- if (plannedInvestigations.length === 0) {
- toast.error("취소할 수 있는 실사 의뢰가 없습니다. 계획 상태의 실사만 취소할 수 있습니다.")
- return
- }
-
- // 서버 액션 호출
- const result = await cancelInvestigationAction(
- plannedInvestigations.map(row => row.original.investigation!.id)
- )
-
- if (result.success) {
- toast.success(`${result.count}개 업체에 대한 실사 의뢰가 취소되었습니다.`)
- window.location.reload()
- } else {
- toast.error(result.error || "실사 취소 처리 중 오류가 발생했습니다.")
- }
- } catch (error) {
- console.error("실사 의뢰 취소 중 오류 발생:", error)
- toast.error("실사 의뢰 취소 중 오류가 발생했습니다.")
- } finally {
- setIsLoading(false)
- setIsCancelDialogOpen(false)
- }
- }
-
- // 실사 재의뢰 처리 - Step 1: 확인 다이얼로그에서 확인 후
- const handleReRequestInvestigation = async (reason?: string) => {
- try {
- // 취소된 실사만 필터링
- const canceledInvestigations = selectedRows.filter(row =>
- row.original.investigation &&
- row.original.investigation.investigationStatus === "CANCELED"
- )
-
- if (canceledInvestigations.length === 0) {
- toast.error("재의뢰할 수 있는 실사가 없습니다. 취소 상태의 실사만 재의뢰할 수 있습니다.")
- return
- }
-
- // 협력사 이름 목록 생성
- const vendorNames = canceledInvestigations
- .map(row => row.original.vendorName)
- .join(', ')
-
- // 재의뢰 데이터 저장
- const investigationIds = canceledInvestigations.map(row => row.original.investigation!.id)
- setReRequestData({
- investigationIds,
- vendorNames,
- })
-
- // 결재 템플릿 변수 생성
- const reRequestedAt = new Date()
- const { mapPQReRequestToTemplateVariables } = await import('@/lib/vendor-investigation/handlers')
- const variables = await mapPQReRequestToTemplateVariables({
- vendorNames,
- investigationCount: investigationIds.length,
- reRequestedAt,
- reason,
- })
-
- setReRequestApprovalVariables(variables)
-
- // ReRequestInvestigationDialog 닫고 ApprovalPreviewDialog 열기
- setIsReRequestDialogOpen(false)
- setIsReRequestApprovalDialogOpen(true)
- } catch (error) {
- console.error("재의뢰 결재 준비 중 오류 발생:", error)
- toast.error("재의뢰 결재 준비 중 오류가 발생했습니다.")
- }
- }
-
- // 실사 재의뢰 결재 요청 처리 - Step 2: ApprovalPreviewDialog에서 결재선 선택 후
- const handleReRequestApprovalSubmit = async (approvers: ApprovalLineItem[]) => {
- debugLog('[ReRequestApproval] 실사 재의뢰 결재 요청 시작', {
- approversCount: approvers.length,
- hasSession: !!session?.user,
- hasReRequestData: !!reRequestData,
- });
-
- if (!session?.user || !reRequestData) {
- debugError('[ReRequestApproval] 세션 또는 재의뢰 데이터 없음');
- throw new Error('세션 정보가 없습니다.');
- }
-
- debugLog('[ReRequestApproval] 재의뢰 대상', {
- investigationIds: reRequestData.investigationIds,
- vendorNames: reRequestData.vendorNames,
- });
-
- // 결재선에서 EP ID 추출 (상신자 제외)
- const approverEpIds = approvers
- .filter((line) => line.seq !== "0" && line.epId)
- .map((line) => line.epId!)
-
- debugLog('[ReRequestApproval] 결재선 추출 완료', {
- approverEpIds,
- });
-
- // 결재 워크플로우 시작
- const result = await reRequestPQInvestigationWithApproval({
- investigationIds: reRequestData.investigationIds,
- vendorNames: reRequestData.vendorNames,
- currentUser: {
- id: Number(session.user.id),
- epId: session.user.epId || null,
- email: session.user.email || undefined,
- },
- approvers: approverEpIds,
- })
-
- debugSuccess('[ReRequestApproval] 재의뢰 결재 요청 성공', {
- approvalId: result.approvalId,
- pendingActionId: result.pendingActionId,
- });
-
- if (result.status === 'pending_approval') {
- // 성공 시에만 상태 초기화 및 페이지 리로드
- setReRequestData(null)
- window.location.reload()
- }
- }
-
- // 재실사 요청 처리
- const handleRequestReinspection = async (
- data: SiteVisitRequestFormValues,
- attachments?: File[]
- ) => {
- try {
- // 보완-재실사 대상 실사만 필터링
- const supplementReinspectInvestigations = selectedRows.filter(row =>
- row.original.investigation &&
- row.original.investigation.evaluationResult === "SUPPLEMENT_REINSPECT"
- );
-
- if (supplementReinspectInvestigations.length === 0) {
- toast.error("보완-재실사 대상 실사가 없습니다.");
- return;
- }
-
- // 첫 번째 대상 실사로 재실사 요청 생성
- const targetRow = supplementReinspectInvestigations[0].original;
- const targetInvestigation = targetRow.investigation!;
- const { requestSupplementReinspectionAction } = await import('@/lib/vendor-investigation/service');
-
- // SiteVisitRequestFormValues를 requestSupplementReinspectionAction 형식으로 변환
- // shiAttendees는 그대로 전달 (새로운 형식: {checked, attendees})
- const result = await requestSupplementReinspectionAction({
- investigationId: targetInvestigation.id,
- siteVisitData: {
- inspectionDuration: data.inspectionDuration,
- requestedStartDate: data.requestedStartDate,
- requestedEndDate: data.requestedEndDate,
- shiAttendees: data.shiAttendees || {},
- vendorRequests: data.vendorRequests || {},
- additionalRequests: data.additionalRequests || "",
- },
- });
-
- if (result.success) {
- toast.success("재실사 요청이 생성되었습니다.");
- setIsReinspectionDialogOpen(false);
- window.location.reload();
- } else {
- toast.error(result.error || "재실사 요청 생성 중 오류가 발생했습니다.");
- }
- } catch (error) {
- console.error("재실사 요청 오류:", error);
- toast.error("재실사 요청 중 오류가 발생했습니다.");
- }
- };
-
- // 실사 결과 발송 처리
- const handleSendInvestigationResults = async (data: { purchaseComment?: string }) => {
- try {
- setIsLoading(true)
-
- // 완료된 실사 중 승인된 결과 또는 보완된 결과만 필터링
- const approvedInvestigations = selectedRows.filter(row => {
- const investigation = row.original.investigation
- return investigation &&
- (investigation.investigationStatus === "COMPLETED" ||
- investigation.investigationStatus === "SUPPLEMENT_REQUIRED" ||
- investigation.evaluationResult === "REJECTED")
-
- })
-
- if (approvedInvestigations.length === 0) {
- toast.error("발송할 실사 결과가 없습니다. 완료되고 승인된 실사만 결과를 발송할 수 있습니다.")
- return
- }
-
- // 서버 액션 호출
- const result = await sendInvestigationResultsAction({
- investigationIds: approvedInvestigations.map(row => row.original.investigation!.id),
- purchaseComment: data.purchaseComment,
- })
-
- if (result.success) {
- toast.success(result.message || `${result.data?.successCount || 0}개 업체에 대한 실사 결과가 발송되었습니다.`)
- window.location.reload()
- } else {
- toast.error(result.error || "실사 결과 발송 처리 중 오류가 발생했습니다.")
- }
- } catch (error) {
- console.error("실사 결과 발송 중 오류 발생:", error)
- toast.error("실사 결과 발송 중 오류가 발생했습니다.")
- } finally {
- setIsLoading(false)
- setIsSendResultsDialogOpen(false)
- }
- }
-
- // 승인된 업체 수 확인 (미실사 PQ 제외)
- const approvedPQsCount = selectedRows.filter(row =>
- row.original.status === "APPROVED" &&
- !row.original.investigation &&
- row.original.type !== "NON_INSPECTION"
- ).length
-
- // 계획 상태 실사 수 확인
- const plannedInvestigationsCount = selectedRows.filter(row =>
- row.original.investigation &&
- row.original.investigation.investigationStatus === "PLANNED"
- ).length
-
- // 완료된 실사 수 확인 (승인된 결과만)
- const completedInvestigationsCount = selectedRows.filter(row =>
- row.original.investigation &&
- row.original.investigation.investigationStatus === "COMPLETED" &&
- row.original.investigation.evaluationResult === "APPROVED"
- ).length
-
- // 취소된 실사 수 확인
- const canceledInvestigationsCount = selectedRows.filter(row =>
- row.original.investigation &&
- row.original.investigation.investigationStatus === "CANCELED"
- ).length
-
- // 재실사 요청 대상 수 확인 (보완-재실사 결과만)
- const reinspectInvestigationsCount = selectedRows.filter(row =>
- row.original.investigation &&
- row.original.investigation.evaluationResult === "SUPPLEMENT_REINSPECT"
- ).length
-
- // 재실사 요청 가능 여부 확인 (방문실사평가 또는 제품검사평가만 가능)
- const canRequestReinspection = selectedRows.some(row => {
- const investigation = row.original.investigation
- if (!investigation) return false
- if (investigation.evaluationResult !== "SUPPLEMENT_REINSPECT") return false
- const method = investigation.investigationMethod
- // 서류평가 또는 구매자체평가는 재방문실사 불가
- return method === "SITE_VISIT_EVAL" || method === "PRODUCT_INSPECTION"
- })
-
- // 미실사 PQ가 선택되었는지 확인
- const hasNonInspectionPQ = selectedRows.some(row =>
- row.original.type === "NON_INSPECTION"
- )
-
- // 실사 방법 라벨 변환 함수
- const getInvestigationMethodLabel = (method: string): string => {
- switch (method) {
- case "PURCHASE_SELF_EVAL":
- return "구매자체평가"
- case "DOCUMENT_EVAL":
- return "서류평가"
- case "PRODUCT_INSPECTION":
- return "제품검사평가"
- case "SITE_VISIT_EVAL":
- return "방문실사평가"
- default:
- return method
- }
- }
-
- // 실사 결과 발송용 데이터 준비
- const auditResults = selectedRows
- .filter(row =>
- row.original.investigation &&
- (row.original.investigation.investigationStatus === "COMPLETED" || row.original.investigation.investigationStatus === "SUPPLEMENT_REQUIRED") && (
- (row.original.investigation.evaluationResult === "APPROVED" ||
- row.original.investigation.evaluationResult === "SUPPLEMENT" ||
- row.original.investigation.evaluationResult === "SUPPLEMENT_REINSPECT" ||
- row.original.investigation.evaluationResult === "SUPPLEMENT_DOCUMENT"))
- )
- .map(row => {
- const investigation = row.original.investigation!
- const pqSubmission = row.original
-
- // pqItems를 상세하게 포맷팅 (itemCode-itemName 형태로 모든 항목 표시)
- const formatAuditItem = (pqItems: any): string => {
- if (!pqItems) return pqSubmission.projectName || "N/A";
-
- try {
- // 이미 파싱된 객체 배열인 경우
- if (Array.isArray(pqItems)) {
- return pqItems.map(item => {
- if (typeof item === 'string') return item;
- if (typeof item === 'object') {
- const code = item.itemCode || item.code || "";
- const name = item.itemName || item.name || "";
- if (code && name) return `${code}-${name}`;
- return name || code || String(item);
- }
- return String(item);
- }).join(', ');
- }
-
- // JSON 문자열인 경우
- if (typeof pqItems === 'string') {
- try {
- const parsed = JSON.parse(pqItems);
- if (Array.isArray(parsed)) {
- return parsed.map(item => {
- if (typeof item === 'string') return item;
- if (typeof item === 'object') {
- const code = item.itemCode || item.code || "";
- const name = item.itemName || item.name || "";
- if (code && name) return `${code}-${name}`;
- return name || code || String(item);
- }
- return String(item);
- }).join(', ');
- }
- return String(parsed);
- } catch {
- return String(pqItems);
- }
- }
-
- // 기타 경우
- return String(pqItems);
- } catch {
- return pqSubmission.projectName || "N/A";
- }
- };
-
- return {
- id: investigation.id,
- vendorCode: row.original.vendorCode || "N/A",
- vendorName: row.original.vendorName || "N/A",
- vendorEmail: row.original.email || "N/A",
- vendorContactPerson: (row.original as any).representativeName || row.original.vendorName || "N/A",
- pqNumber: pqSubmission.pqNumber || "N/A",
- auditItem: formatAuditItem(pqSubmission.pqItems),
- auditFactoryAddress: investigation.investigationAddress || "N/A",
- auditMethod: getInvestigationMethodLabel(investigation.investigationMethod || ""),
- auditResult: investigation.evaluationResult === "APPROVED" ? "Pass(승인)" :
- investigation.evaluationResult === "SUPPLEMENT" ? "Pass(조건부승인)" :
- investigation.evaluationResult === "REJECTED" ? "Fail(미승인)" : "N/A",
- additionalNotes: investigation.investigationNotes || undefined,
- investigationNotes: investigation.investigationNotes || undefined,
- }
- })
-
- return (
- <>
- <div className="flex items-center gap-2">
- {/* 실사 의뢰 버튼 */}
- <Button
- variant="outline"
- size="sm"
- onClick={handleOpenRequestDialog} // 여기를 수정: 새로운 핸들러 함수 사용
- disabled={isLoading || selectedRows.length === 0 || hasNonInspectionPQ}
- className="gap-2"
- title={hasNonInspectionPQ ? "미실사 PQ는 실사 의뢰할 수 없습니다." : undefined}
- >
- <ClipboardCheck className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">실사 의뢰</span>
- </Button>
-
- {/* 실사 의뢰 취소 버튼 */}
- <Button
- variant="outline"
- size="sm"
- onClick={() => setIsCancelDialogOpen(true)}
- disabled={
- isLoading ||
- selectedRows.length === 0 ||
- !selectedRows.every(row => row.original.investigation?.investigationStatus === "PLANNED")
- }
- className="gap-2"
- >
- <X className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">실사 취소</span>
- </Button>
-
- {/* 실사 재의뢰 버튼 */}
- <Button
- variant="outline"
- size="sm"
- onClick={() => setIsReRequestDialogOpen(true)}
- disabled={
- isLoading ||
- selectedRows.length === 0 ||
- !selectedRows.every(row => row.original.investigation?.investigationStatus === "CANCELED")
- }
- className="gap-2"
- >
- <RefreshCw className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">실사 재의뢰</span>
- </Button>
-
- {/* 재실사 요청 버튼 */}
- <Button
- variant="outline"
- size="sm"
- onClick={() => setIsReinspectionDialogOpen(true)}
- disabled={
- isLoading ||
- selectedRows.length === 0 ||
- reinspectInvestigationsCount === 0 ||
- !canRequestReinspection
- }
- className="gap-2"
- title={
- !canRequestReinspection && reinspectInvestigationsCount > 0
- ? "재방문 실사 요청은 방문실사평가 또는 제품검사평가에만 가능합니다."
- : undefined
- }
- >
- <RefreshCw className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">재방문 실사 요청</span>
- </Button>
-
- {/* 실사 결과 발송 버튼 */}
- <Button
- variant="outline"
- size="sm"
- onClick={() => setIsSendResultsDialogOpen(true)}
- disabled={
- isLoading ||
- selectedRows.length === 0 ||
- !selectedRows.every(row => {
- const investigation = row.original.investigation;
- if (!investigation) return false;
-
- // 실사 완료 상태이거나 평가 결과가 있는 경우에만 활성화, 실사결과발송 상태가 아닌 경우에만 활성화
- return investigation.investigationStatus === "COMPLETED" ||
- investigation.investigationStatus === "SUPPLEMENT_REQUIRED" ||
- investigation.evaluationResult === "REJECTED"
- })
- }
- className="gap-2"
- >
- <Send className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">결과 발송</span>
- </Button>
-
- {/** Export 버튼 */}
- <Button
- variant="outline"
- size="sm"
- onClick={() =>
- exportTableToExcel(table, {
- filename: "vendors-pq-submissions",
- excludeColumns: ["select", "actions"],
- })
- }
- className="gap-2"
- >
- <Download className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">Export</span>
- </Button>
- </div>
-
- {/* 실사 의뢰 Dialog */}
- <RequestInvestigationDialog
- isOpen={isRequestDialogOpen}
- onClose={handleCloseRequestDialog} // 새로운 핸들러로 변경
- onSubmit={handleRequestInvestigation}
- selectedCount={approvedPQsCount}
- initialData={dialogInitialData} // 초기 데이터 전달
- />
-
-
- {/* 실사 취소 Dialog */}
- <CancelInvestigationDialog
- isOpen={isCancelDialogOpen}
- onClose={() => setIsCancelDialogOpen(false)}
- onConfirm={handleCancelInvestigation}
- selectedCount={plannedInvestigationsCount}
- />
-
- {/* 실사 재의뢰 Dialog */}
- <ReRequestInvestigationDialog
- isOpen={isReRequestDialogOpen}
- onClose={() => setIsReRequestDialogOpen(false)}
- onConfirm={handleReRequestInvestigation}
- selectedCount={canceledInvestigationsCount}
- />
-
- {/* 결과 발송 Dialog */}
- <SendResultsDialog
- isOpen={isSendResultsDialogOpen}
- onClose={() => setIsSendResultsDialogOpen(false)}
- onConfirm={handleSendInvestigationResults}
- selectedCount={completedInvestigationsCount}
- auditResults={auditResults}
- />
-
- {/* 재방문실사 요청 Dialog */}
- {(() => {
- // 보완-재실사 대상 실사 찾기
- const supplementReinspectInvestigations = selectedRows.filter(row =>
- row.original.investigation &&
- row.original.investigation.evaluationResult === "SUPPLEMENT_REINSPECT"
- );
-
- if (supplementReinspectInvestigations.length === 0) {
- return null;
- }
-
- const targetRow = supplementReinspectInvestigations[0].original;
- const targetInvestigation = targetRow.investigation!;
-
- return (
- <SiteVisitDialog
- isOpen={isReinspectionDialogOpen}
- onClose={() => setIsReinspectionDialogOpen(false)}
- onSubmit={handleRequestReinspection}
- investigation={{
- id: targetInvestigation.id,
- investigationMethod: targetInvestigation.investigationMethod || undefined,
- investigationAddress: targetInvestigation.investigationAddress || undefined,
- investigationNotes: targetInvestigation.investigationNotes || undefined,
- vendorName: targetRow.vendorName,
- vendorCode: targetRow.vendorCode,
- projectName: targetRow.projectName || undefined,
- projectCode: targetRow.projectCode || undefined,
- pqItems: targetRow.pqItems || null,
- }}
- isReinspection={true}
- />
- );
- })()}
-
- {/* 결재 미리보기 Dialog - 실사 의뢰 */}
- {session?.user && investigationFormData && (
- <ApprovalPreviewDialog
- open={isApprovalDialogOpen}
- onOpenChange={(open) => {
- setIsApprovalDialogOpen(open)
- if (!open) {
- // 다이얼로그가 닫히면 실사 폼 데이터도 초기화
- setInvestigationFormData(null)
- }
- }}
- templateName="Vendor 실사의뢰"
- variables={approvalVariables}
- title={`Vendor 실사의뢰 - ${selectedRows.filter(row =>
- row.original.status === "APPROVED" &&
- !row.original.investigation &&
- row.original.type !== "NON_INSPECTION"
- ).map(row => row.original.vendorName).join(', ')}`}
- description={`${approvedPQsCount}개 업체에 대한 실사 의뢰`}
- currentUser={{
- id: Number(session.user.id),
- epId: session.user.epId || null,
- name: session.user.name || null,
- email: session.user.email || '',
- }}
- onSubmit={handleApprovalSubmit}
- />
- )}
-
- {/* 결재 미리보기 Dialog - 실사 재의뢰 */}
- {session?.user && reRequestData && (
- <ApprovalPreviewDialog
- open={isReRequestApprovalDialogOpen}
- onOpenChange={(open) => {
- setIsReRequestApprovalDialogOpen(open)
- if (!open) {
- // 다이얼로그가 닫히면 재의뢰 데이터도 초기화
- setReRequestData(null)
- }
- }}
- templateName="Vendor 실사 재의뢰"
- variables={reRequestApprovalVariables}
- title={`Vendor 실사 재의뢰 - ${reRequestData.vendorNames}`}
- description={`${reRequestData.investigationIds.length}개 업체에 대한 실사 재의뢰`}
- currentUser={{
- id: Number(session.user.id),
- epId: session.user.epId || null,
- name: session.user.name || null,
- email: session.user.email || '',
- }}
- onSubmit={handleReRequestApprovalSubmit}
- />
- )}
- </>
- )
+"use client"
+
+import * as React from "react"
+import { type Table } from "@tanstack/react-table"
+import { Download, ClipboardCheck, X, Send, RefreshCw } from "lucide-react"
+import { toast } from "sonner"
+import { useSession } from "next-auth/react"
+
+import { exportTableToExcel } from "@/lib/export"
+import { Button } from "@/components/ui/button"
+import { PQSubmission } from "./vendors-table-columns"
+import {
+ cancelInvestigationAction,
+ sendInvestigationResultsAction,
+ getFactoryLocationAnswer,
+ getQMManagers
+} from "@/lib/pq/service"
+import { SiteVisitDialog } from "./site-visit-dialog"
+import type { SiteVisitRequestFormValues } from "./site-visit-dialog"
+import { RequestInvestigationDialog } from "./request-investigation-dialog"
+import { CancelInvestigationDialog, ReRequestInvestigationDialog } from "./cancel-investigation-dialog"
+import { SendResultsDialog } from "./send-results-dialog"
+import { ApprovalPreviewDialog } from "@/lib/approval/approval-preview-dialog"
+import {
+ requestPQInvestigationWithApproval,
+ reRequestPQInvestigationWithApproval
+} from "@/lib/vendor-investigation/approval-actions"
+import { debugLog, debugError, debugSuccess } from "@/lib/debug-utils"
+
+interface VendorsTableToolbarActionsProps {
+ table: Table<PQSubmission>
+}
+
+interface InvestigationInitialData {
+ investigationMethod?: "PURCHASE_SELF_EVAL" | "DOCUMENT_EVAL" | "PRODUCT_INSPECTION" | "SITE_VISIT_EVAL";
+ qmManagerId?: number;
+ forecastedAt?: Date;
+ createdAt?: Date;
+ investigationAddress?: string;
+ investigationNotes?: string;
+}
+
+export function VendorsTableToolbarActions({ table }: VendorsTableToolbarActionsProps) {
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+ const [isLoading, setIsLoading] = React.useState(false)
+ const { data: session } = useSession()
+
+ // Dialog 상태 관리
+ const [isRequestDialogOpen, setIsRequestDialogOpen] = React.useState(false)
+ 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)
+
+ // 초기 데이터 상태
+ const [dialogInitialData, setDialogInitialData] = React.useState<InvestigationInitialData | undefined>(undefined)
+
+ // 실사 의뢰 임시 데이터 (결재 다이얼로그로 전달)
+ const [investigationFormData, setInvestigationFormData] = React.useState<{
+ qmManagerId: number;
+ qmManagerName: string;
+ qmManagerEmail?: string;
+ forecastedAt: Date;
+ investigationAddress: string;
+ investigationNotes?: string;
+ } | null>(null)
+
+ // 실사 재의뢰 임시 데이터
+ const [reRequestData, setReRequestData] = React.useState<{
+ investigationIds: number[];
+ vendorNames: string;
+ } | null>(null)
+
+ // 결재 템플릿 변수
+ const [approvalVariables, setApprovalVariables] = React.useState<Record<string, string>>({})
+ const [reRequestApprovalVariables, setReRequestApprovalVariables] = React.useState<Record<string, string>>({})
+
+ // 실사 의뢰 대화상자 열기 핸들러
+// 실사 의뢰 대화상자 열기 핸들러
+const handleOpenRequestDialog = async () => {
+ setIsLoading(true);
+ const initialData: InvestigationInitialData = {};
+
+ try {
+ // 선택된 행이 정확히 1개인 경우에만 초기값 설정
+ if (selectedRows.length === 1) {
+ const row = selectedRows[0].original;
+
+ // 승인된 PQ이고 아직 실사가 없는 경우
+ if (row.status === "APPROVED" && !row.investigation) {
+ // Factory Location 정보 가져오기
+ const locationResponse = await getFactoryLocationAnswer(
+ row.vendorId,
+ row.projectId
+ );
+
+ // 기본 주소 설정 - Factory Location 응답 또는 fallback
+ let defaultAddress = "";
+ if (locationResponse.success && locationResponse.factoryLocation) {
+ defaultAddress = locationResponse.factoryLocation;
+ } else {
+ // Factory Location을 찾지 못한 경우 fallback
+ defaultAddress = row.taxId ?
+ `${row.vendorName} 사업장 (${row.taxId})` :
+ `${row.vendorName} 사업장`;
+ }
+
+ // 이미 같은 회사에 대한 다른 실사가 있는지 확인
+ const existingInvestigations = table.getFilteredRowModel().rows
+ .map(r => r.original)
+ .filter(r =>
+ r.vendorId === row.vendorId &&
+ r.investigation !== null
+ );
+
+ // 같은 업체의 이전 실사 기록이 있다면 참고하되, 주소는 Factory Location 사용
+ if (existingInvestigations.length > 0) {
+ // 날짜 기준으로 정렬하여 가장 최근 것을 가져옴
+ const latestInvestigation = existingInvestigations.sort((a, b) => {
+ const dateA = a.investigation?.createdAt || new Date(0);
+ const dateB = b.investigation?.createdAt || new Date(0);
+ return (dateB as Date).getTime() - (dateA as Date).getTime();
+ })[0].investigation;
+
+ if (latestInvestigation) {
+ initialData.investigationMethod = latestInvestigation.investigationMethod || undefined;
+ initialData.qmManagerId = latestInvestigation.qmManagerId || undefined;
+ initialData.investigationAddress = defaultAddress; // Factory Location 사용
+
+ // 날짜는 미래로 설정
+ const futureDate = new Date();
+ futureDate.setDate(futureDate.getDate() + 14); // 기본값으로 2주 후
+ initialData.forecastedAt = futureDate;
+ }
+ } else {
+ // 기본값 설정
+ initialData.investigationMethod = undefined;
+ const futureDate = new Date();
+ futureDate.setDate(futureDate.getDate() + 14); // 기본값으로 2주 후
+ initialData.forecastedAt = futureDate;
+ initialData.investigationAddress = defaultAddress; // Factory Location 사용
+ }
+ }
+ // 실사가 이미 있고 수정하는 경우
+ // else if (row.investigation) {
+ // initialData.investigationMethod = row.investigation.investigationMethod || undefined;
+ // initialData.qmManagerId = row.investigation.qmManagerId !== null ?
+ // row.investigation.qmManagerId : undefined;
+ // initialData.forecastedAt = row.investigation.forecastedAt || new Date();
+ // initialData.investigationAddress = row.investigation.investigationAddress || "";
+ // initialData.investigationNotes = row.investigation.investigationNotes || "";
+ // }
+ }
+ } catch (error) {
+ console.error("초기 데이터 로드 중 오류:", error);
+ toast.error("초기 데이터 로드 중 오류가 발생했습니다.");
+ } finally {
+ setIsLoading(false);
+
+ // 초기 데이터 설정 및 대화상자 열기
+ setDialogInitialData(Object.keys(initialData).length > 0 ? initialData : undefined);
+ setIsRequestDialogOpen(true);
+ }
+};
+ // 실사 의뢰 요청 처리 - Step 1: RequestInvestigationDialog에서 정보 입력 후
+ const handleRequestInvestigation = async (formData: {
+ qmManagerId: number,
+ forecastedAt: Date,
+ investigationAddress: string,
+ investigationNotes?: string
+ }) => {
+ try {
+ // 승인된 PQ 제출만 필터링 (미실사 PQ 제외)
+ const approvedPQs = selectedRows.filter(row =>
+ row.original.status === "APPROVED" &&
+ !row.original.investigation &&
+ row.original.type !== "NON_INSPECTION"
+ )
+
+ if (approvedPQs.length === 0) {
+ if (hasNonInspectionPQ) {
+ toast.error("미실사 PQ는 실사 의뢰할 수 없습니다. 미실사 PQ를 제외하고 선택해주세요.")
+ } else {
+ toast.error("실사를 의뢰할 수 있는 업체가 없습니다. 승인된 PQ 제출만 실사 의뢰가 가능합니다.")
+ }
+ return
+ }
+
+ // QM 담당자 이름 및 이메일 조회
+ const qmManagersResult = await getQMManagers()
+ const qmManager = qmManagersResult.success
+ ? qmManagersResult.data.find(m => m.id === formData.qmManagerId)
+ : null
+ const qmManagerName = qmManager?.name || `QM담당자 #${formData.qmManagerId}`
+ const qmManagerEmail = qmManager?.email || undefined
+
+ // 협력사 이름 목록 생성
+ const vendorNames = approvedPQs
+ .map(row => row.original.vendorName)
+ .join(', ')
+
+ // 실사 폼 데이터 저장 (이메일 추가)
+ setInvestigationFormData({
+ qmManagerId: formData.qmManagerId,
+ qmManagerName,
+ qmManagerEmail,
+ forecastedAt: formData.forecastedAt,
+ investigationAddress: formData.investigationAddress,
+ investigationNotes: formData.investigationNotes,
+ })
+
+ // 결재 템플릿 변수 생성
+ const requestedAt = new Date()
+ const { mapPQInvestigationToTemplateVariables } = await import('@/lib/vendor-investigation/handlers')
+ const variables = await mapPQInvestigationToTemplateVariables({
+ vendorNames,
+ qmManagerName,
+ qmManagerEmail,
+ forecastedAt: formData.forecastedAt,
+ investigationAddress: formData.investigationAddress,
+ investigationNotes: formData.investigationNotes,
+ requestedAt,
+ })
+
+ setApprovalVariables(variables)
+
+ // RequestInvestigationDialog 닫고 ApprovalPreviewDialog 열기
+ setIsRequestDialogOpen(false)
+ setIsApprovalDialogOpen(true)
+ } catch (error) {
+ console.error("결재 준비 중 오류 발생:", error)
+ toast.error("결재 준비 중 오류가 발생했습니다.")
+ }
+ }
+
+ // 실사 의뢰 결재 요청 처리 - Step 2: ApprovalPreviewDialog에서 결재선 선택 후
+ const handleApprovalSubmit = async ({ approvers, title, attachments }: { approvers: string[], title: string, attachments?: File[] }) => {
+ debugLog('[InvestigationApproval] 실사 의뢰 결재 요청 시작', {
+ approversCount: approvers.length,
+ hasSession: !!session?.user,
+ hasFormData: !!investigationFormData,
+ });
+
+ if (!session?.user || !investigationFormData) {
+ debugError('[InvestigationApproval] 세션 또는 폼 데이터 없음');
+ throw new Error('세션 정보가 없습니다.');
+ }
+
+ // 승인된 PQ 제출만 필터링
+ const approvedPQs = selectedRows.filter(row =>
+ row.original.status === "APPROVED" &&
+ !row.original.investigation &&
+ row.original.type !== "NON_INSPECTION"
+ )
+
+ debugLog('[InvestigationApproval] 승인된 PQ 건수', {
+ count: approvedPQs.length,
+ });
+
+ // 협력사 이름 목록
+ const vendorNames = approvedPQs
+ .map(row => row.original.vendorName)
+ .join(', ')
+
+ debugLog('[InvestigationApproval] 결재선 추출 완료', {
+ approverEpIds: approvers,
+ });
+
+ // 결재 워크플로우 시작 (approvers는 이미 EP ID 배열)
+ const result = await requestPQInvestigationWithApproval({
+ pqSubmissionIds: approvedPQs.map(row => row.original.id),
+ vendorNames,
+ qmManagerId: investigationFormData.qmManagerId,
+ qmManagerName: investigationFormData.qmManagerName,
+ qmManagerEmail: investigationFormData.qmManagerEmail,
+ forecastedAt: investigationFormData.forecastedAt,
+ investigationAddress: investigationFormData.investigationAddress,
+ investigationNotes: investigationFormData.investigationNotes,
+ currentUser: {
+ id: Number(session.user.id),
+ epId: session.user.epId || null,
+ email: session.user.email || undefined,
+ },
+ approvers: approvers,
+ })
+
+ debugSuccess('[InvestigationApproval] 결재 요청 성공', {
+ approvalId: result.approvalId,
+ pendingActionId: result.pendingActionId,
+ });
+
+ if (result.status === 'pending_approval') {
+ // 성공 시에만 상태 초기화 및 페이지 리로드
+ setInvestigationFormData(null)
+ setDialogInitialData(undefined)
+ window.location.reload()
+ }
+ }
+
+ const handleCloseRequestDialog = () => {
+ setIsRequestDialogOpen(false);
+ setDialogInitialData(undefined);
+ };
+
+
+ // 실사 의뢰 취소 처리
+ const handleCancelInvestigation = async () => {
+ setIsLoading(true)
+ try {
+ // 실사가 계획됨 상태인 PQ만 필터링
+ const plannedInvestigations = selectedRows.filter(row =>
+ row.original.investigation &&
+ row.original.investigation.investigationStatus === "PLANNED"
+ )
+
+ if (plannedInvestigations.length === 0) {
+ toast.error("취소할 수 있는 실사 의뢰가 없습니다. 계획 상태의 실사만 취소할 수 있습니다.")
+ return
+ }
+
+ // 서버 액션 호출
+ const result = await cancelInvestigationAction(
+ plannedInvestigations.map(row => row.original.investigation!.id)
+ )
+
+ if (result.success) {
+ toast.success(`${result.count}개 업체에 대한 실사 의뢰가 취소되었습니다.`)
+ window.location.reload()
+ } else {
+ toast.error(result.error || "실사 취소 처리 중 오류가 발생했습니다.")
+ }
+ } catch (error) {
+ console.error("실사 의뢰 취소 중 오류 발생:", error)
+ toast.error("실사 의뢰 취소 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ setIsCancelDialogOpen(false)
+ }
+ }
+
+ // 실사 재의뢰 처리 - Step 1: 확인 다이얼로그에서 확인 후
+ const handleReRequestInvestigation = async (reason?: string) => {
+ try {
+ // 취소된 실사만 필터링
+ const canceledInvestigations = selectedRows.filter(row =>
+ row.original.investigation &&
+ row.original.investigation.investigationStatus === "CANCELED"
+ )
+
+ if (canceledInvestigations.length === 0) {
+ toast.error("재의뢰할 수 있는 실사가 없습니다. 취소 상태의 실사만 재의뢰할 수 있습니다.")
+ return
+ }
+
+ // 협력사 이름 목록 생성
+ const vendorNames = canceledInvestigations
+ .map(row => row.original.vendorName)
+ .join(', ')
+
+ // 재의뢰 데이터 저장
+ const investigationIds = canceledInvestigations.map(row => row.original.investigation!.id)
+ setReRequestData({
+ investigationIds,
+ vendorNames,
+ })
+
+ // 결재 템플릿 변수 생성
+ const reRequestedAt = new Date()
+ const { mapPQReRequestToTemplateVariables } = await import('@/lib/vendor-investigation/handlers')
+ const variables = await mapPQReRequestToTemplateVariables({
+ vendorNames,
+ investigationCount: investigationIds.length,
+ reRequestedAt,
+ reason,
+ })
+
+ setReRequestApprovalVariables(variables)
+
+ // ReRequestInvestigationDialog 닫고 ApprovalPreviewDialog 열기
+ setIsReRequestDialogOpen(false)
+ setIsReRequestApprovalDialogOpen(true)
+ } catch (error) {
+ console.error("재의뢰 결재 준비 중 오류 발생:", error)
+ toast.error("재의뢰 결재 준비 중 오류가 발생했습니다.")
+ }
+ }
+
+ // 실사 재의뢰 결재 요청 처리 - Step 2: ApprovalPreviewDialog에서 결재선 선택 후
+ const handleReRequestApprovalSubmit = async ({ approvers, title, attachments }: { approvers: string[], title: string, attachments?: File[] }) => {
+ debugLog('[ReRequestApproval] 실사 재의뢰 결재 요청 시작', {
+ approversCount: approvers.length,
+ hasSession: !!session?.user,
+ hasReRequestData: !!reRequestData,
+ });
+
+ if (!session?.user || !reRequestData) {
+ debugError('[ReRequestApproval] 세션 또는 재의뢰 데이터 없음');
+ throw new Error('세션 정보가 없습니다.');
+ }
+
+ debugLog('[ReRequestApproval] 재의뢰 대상', {
+ investigationIds: reRequestData.investigationIds,
+ vendorNames: reRequestData.vendorNames,
+ });
+
+ debugLog('[ReRequestApproval] 결재선 추출 완료', {
+ approverEpIds: approvers,
+ });
+
+ // 결재 워크플로우 시작 (approvers는 이미 EP ID 배열)
+ const result = await reRequestPQInvestigationWithApproval({
+ investigationIds: reRequestData.investigationIds,
+ vendorNames: reRequestData.vendorNames,
+ currentUser: {
+ id: Number(session.user.id),
+ epId: session.user.epId || null,
+ email: session.user.email || undefined,
+ },
+ approvers: approvers,
+ })
+
+ debugSuccess('[ReRequestApproval] 재의뢰 결재 요청 성공', {
+ approvalId: result.approvalId,
+ pendingActionId: result.pendingActionId,
+ });
+
+ if (result.status === 'pending_approval') {
+ // 성공 시에만 상태 초기화 및 페이지 리로드
+ setReRequestData(null)
+ window.location.reload()
+ }
+ }
+
+ // 재실사 요청 처리
+ const handleRequestReinspection = async (
+ data: SiteVisitRequestFormValues,
+ attachments?: File[]
+ ) => {
+ try {
+ // 보완-재실사 대상 실사만 필터링
+ const supplementReinspectInvestigations = selectedRows.filter(row =>
+ row.original.investigation &&
+ row.original.investigation.evaluationResult === "SUPPLEMENT_REINSPECT"
+ );
+
+ if (supplementReinspectInvestigations.length === 0) {
+ toast.error("보완-재실사 대상 실사가 없습니다.");
+ return;
+ }
+
+ // 첫 번째 대상 실사로 재실사 요청 생성
+ const targetRow = supplementReinspectInvestigations[0].original;
+ const targetInvestigation = targetRow.investigation!;
+ const { requestSupplementReinspectionAction } = await import('@/lib/vendor-investigation/service');
+
+ // SiteVisitRequestFormValues를 requestSupplementReinspectionAction 형식으로 변환
+ // shiAttendees는 그대로 전달 (새로운 형식: {checked, attendees})
+ const result = await requestSupplementReinspectionAction({
+ investigationId: targetInvestigation.id,
+ siteVisitData: {
+ inspectionDuration: data.inspectionDuration,
+ requestedStartDate: data.requestedStartDate,
+ requestedEndDate: data.requestedEndDate,
+ shiAttendees: data.shiAttendees || {},
+ vendorRequests: data.vendorRequests || {},
+ additionalRequests: data.additionalRequests || "",
+ },
+ });
+
+ if (result.success) {
+ toast.success("재실사 요청이 생성되었습니다.");
+ setIsReinspectionDialogOpen(false);
+ window.location.reload();
+ } else {
+ toast.error(result.error || "재실사 요청 생성 중 오류가 발생했습니다.");
+ }
+ } catch (error) {
+ console.error("재실사 요청 오류:", error);
+ toast.error("재실사 요청 중 오류가 발생했습니다.");
+ }
+ };
+
+ // 실사 결과 발송 처리
+ const handleSendInvestigationResults = async (data: { purchaseComment?: string }) => {
+ try {
+ setIsLoading(true)
+
+ // 완료된 실사 중 승인된 결과 또는 보완된 결과만 필터링
+ const approvedInvestigations = selectedRows.filter(row => {
+ const investigation = row.original.investigation
+ return investigation &&
+ (investigation.investigationStatus === "COMPLETED" ||
+ investigation.investigationStatus === "SUPPLEMENT_REQUIRED" ||
+ investigation.evaluationResult === "REJECTED")
+
+ })
+
+ if (approvedInvestigations.length === 0) {
+ toast.error("발송할 실사 결과가 없습니다. 완료되고 승인된 실사만 결과를 발송할 수 있습니다.")
+ return
+ }
+
+ // 서버 액션 호출
+ const result = await sendInvestigationResultsAction({
+ investigationIds: approvedInvestigations.map(row => row.original.investigation!.id),
+ purchaseComment: data.purchaseComment,
+ })
+
+ if (result.success) {
+ toast.success(result.message || `${result.data?.successCount || 0}개 업체에 대한 실사 결과가 발송되었습니다.`)
+ window.location.reload()
+ } else {
+ toast.error(result.error || "실사 결과 발송 처리 중 오류가 발생했습니다.")
+ }
+ } catch (error) {
+ console.error("실사 결과 발송 중 오류 발생:", error)
+ toast.error("실사 결과 발송 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ setIsSendResultsDialogOpen(false)
+ }
+ }
+
+ // 승인된 업체 수 확인 (미실사 PQ 제외)
+ const approvedPQsCount = selectedRows.filter(row =>
+ row.original.status === "APPROVED" &&
+ !row.original.investigation &&
+ row.original.type !== "NON_INSPECTION"
+ ).length
+
+ // 계획 상태 실사 수 확인
+ const plannedInvestigationsCount = selectedRows.filter(row =>
+ row.original.investigation &&
+ row.original.investigation.investigationStatus === "PLANNED"
+ ).length
+
+ // 완료된 실사 수 확인 (승인된 결과만)
+ const completedInvestigationsCount = selectedRows.filter(row =>
+ row.original.investigation &&
+ row.original.investigation.investigationStatus === "COMPLETED" &&
+ row.original.investigation.evaluationResult === "APPROVED"
+ ).length
+
+ // 취소된 실사 수 확인
+ const canceledInvestigationsCount = selectedRows.filter(row =>
+ row.original.investigation &&
+ row.original.investigation.investigationStatus === "CANCELED"
+ ).length
+
+ // 재실사 요청 대상 수 확인 (보완-재실사 결과만)
+ const reinspectInvestigationsCount = selectedRows.filter(row =>
+ row.original.investigation &&
+ row.original.investigation.evaluationResult === "SUPPLEMENT_REINSPECT"
+ ).length
+
+ // 재실사 요청 가능 여부 확인 (방문실사평가 또는 제품검사평가만 가능)
+ const canRequestReinspection = selectedRows.some(row => {
+ const investigation = row.original.investigation
+ if (!investigation) return false
+ if (investigation.evaluationResult !== "SUPPLEMENT_REINSPECT") return false
+ const method = investigation.investigationMethod
+ // 서류평가 또는 구매자체평가는 재방문실사 불가
+ return method === "SITE_VISIT_EVAL" || method === "PRODUCT_INSPECTION"
+ })
+
+ // 미실사 PQ가 선택되었는지 확인
+ const hasNonInspectionPQ = selectedRows.some(row =>
+ row.original.type === "NON_INSPECTION"
+ )
+
+ // 실사 방법 라벨 변환 함수
+ const getInvestigationMethodLabel = (method: string): string => {
+ switch (method) {
+ case "PURCHASE_SELF_EVAL":
+ return "구매자체평가"
+ case "DOCUMENT_EVAL":
+ return "서류평가"
+ case "PRODUCT_INSPECTION":
+ return "제품검사평가"
+ case "SITE_VISIT_EVAL":
+ return "방문실사평가"
+ default:
+ return method
+ }
+ }
+
+ // 실사 결과 발송용 데이터 준비
+ const auditResults = selectedRows
+ .filter(row =>
+ row.original.investigation &&
+ (row.original.investigation.investigationStatus === "COMPLETED" || row.original.investigation.investigationStatus === "SUPPLEMENT_REQUIRED") && (
+ (row.original.investigation.evaluationResult === "APPROVED" ||
+ row.original.investigation.evaluationResult === "SUPPLEMENT" ||
+ row.original.investigation.evaluationResult === "SUPPLEMENT_REINSPECT" ||
+ row.original.investigation.evaluationResult === "SUPPLEMENT_DOCUMENT"))
+ )
+ .map(row => {
+ const investigation = row.original.investigation!
+ const pqSubmission = row.original
+
+ // pqItems를 상세하게 포맷팅 (itemCode-itemName 형태로 모든 항목 표시)
+ const formatAuditItem = (pqItems: any): string => {
+ if (!pqItems) return pqSubmission.projectName || "N/A";
+
+ try {
+ // 이미 파싱된 객체 배열인 경우
+ if (Array.isArray(pqItems)) {
+ return pqItems.map(item => {
+ if (typeof item === 'string') return item;
+ if (typeof item === 'object') {
+ const code = item.itemCode || item.code || "";
+ const name = item.itemName || item.name || "";
+ if (code && name) return `${code}-${name}`;
+ return name || code || String(item);
+ }
+ return String(item);
+ }).join(', ');
+ }
+
+ // JSON 문자열인 경우
+ if (typeof pqItems === 'string') {
+ try {
+ const parsed = JSON.parse(pqItems);
+ if (Array.isArray(parsed)) {
+ return parsed.map(item => {
+ if (typeof item === 'string') return item;
+ if (typeof item === 'object') {
+ const code = item.itemCode || item.code || "";
+ const name = item.itemName || item.name || "";
+ if (code && name) return `${code}-${name}`;
+ return name || code || String(item);
+ }
+ return String(item);
+ }).join(', ');
+ }
+ return String(parsed);
+ } catch {
+ return String(pqItems);
+ }
+ }
+
+ // 기타 경우
+ return String(pqItems);
+ } catch {
+ return pqSubmission.projectName || "N/A";
+ }
+ };
+
+ return {
+ id: investigation.id,
+ vendorCode: row.original.vendorCode || "N/A",
+ vendorName: row.original.vendorName || "N/A",
+ vendorEmail: row.original.email || "N/A",
+ vendorContactPerson: (row.original as any).representativeName || row.original.vendorName || "N/A",
+ pqNumber: pqSubmission.pqNumber || "N/A",
+ auditItem: formatAuditItem(pqSubmission.pqItems),
+ auditFactoryAddress: investigation.investigationAddress || "N/A",
+ auditMethod: getInvestigationMethodLabel(investigation.investigationMethod || ""),
+ auditResult: investigation.evaluationResult === "APPROVED" ? "Pass(승인)" :
+ investigation.evaluationResult === "SUPPLEMENT" ? "Pass(조건부승인)" :
+ investigation.evaluationResult === "REJECTED" ? "Fail(미승인)" : "N/A",
+ additionalNotes: investigation.investigationNotes || undefined,
+ investigationNotes: investigation.investigationNotes || undefined,
+ }
+ })
+
+ return (
+ <>
+ <div className="flex items-center gap-2">
+ {/* 실사 의뢰 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleOpenRequestDialog} // 여기를 수정: 새로운 핸들러 함수 사용
+ disabled={isLoading || selectedRows.length === 0 || hasNonInspectionPQ}
+ className="gap-2"
+ title={hasNonInspectionPQ ? "미실사 PQ는 실사 의뢰할 수 없습니다." : undefined}
+ >
+ <ClipboardCheck className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">실사 의뢰</span>
+ </Button>
+
+ {/* 실사 의뢰 취소 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setIsCancelDialogOpen(true)}
+ disabled={
+ isLoading ||
+ selectedRows.length === 0 ||
+ !selectedRows.every(row => row.original.investigation?.investigationStatus === "PLANNED")
+ }
+ className="gap-2"
+ >
+ <X className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">실사 취소</span>
+ </Button>
+
+ {/* 실사 재의뢰 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setIsReRequestDialogOpen(true)}
+ disabled={
+ isLoading ||
+ selectedRows.length === 0 ||
+ !selectedRows.every(row => row.original.investigation?.investigationStatus === "CANCELED")
+ }
+ className="gap-2"
+ >
+ <RefreshCw className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">실사 재의뢰</span>
+ </Button>
+
+ {/* 재실사 요청 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setIsReinspectionDialogOpen(true)}
+ disabled={
+ isLoading ||
+ selectedRows.length === 0 ||
+ reinspectInvestigationsCount === 0 ||
+ !canRequestReinspection
+ }
+ className="gap-2"
+ title={
+ !canRequestReinspection && reinspectInvestigationsCount > 0
+ ? "재방문 실사 요청은 방문실사평가 또는 제품검사평가에만 가능합니다."
+ : undefined
+ }
+ >
+ <RefreshCw className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">재방문 실사 요청</span>
+ </Button>
+
+ {/* 실사 결과 발송 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setIsSendResultsDialogOpen(true)}
+ disabled={
+ isLoading ||
+ selectedRows.length === 0 ||
+ !selectedRows.every(row => {
+ const investigation = row.original.investigation;
+ if (!investigation) return false;
+
+ // 실사 완료 상태이거나 평가 결과가 있는 경우에만 활성화, 실사결과발송 상태가 아닌 경우에만 활성화
+ return investigation.investigationStatus === "COMPLETED" ||
+ investigation.investigationStatus === "SUPPLEMENT_REQUIRED" ||
+ investigation.evaluationResult === "REJECTED"
+ })
+ }
+ className="gap-2"
+ >
+ <Send className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">결과 발송</span>
+ </Button>
+
+ {/** Export 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() =>
+ exportTableToExcel(table, {
+ filename: "vendors-pq-submissions",
+ excludeColumns: ["select", "actions"],
+ })
+ }
+ className="gap-2"
+ >
+ <Download className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">Export</span>
+ </Button>
+ </div>
+
+ {/* 실사 의뢰 Dialog */}
+ <RequestInvestigationDialog
+ isOpen={isRequestDialogOpen}
+ onClose={handleCloseRequestDialog} // 새로운 핸들러로 변경
+ onSubmit={handleRequestInvestigation}
+ selectedCount={approvedPQsCount}
+ initialData={dialogInitialData} // 초기 데이터 전달
+ />
+
+
+ {/* 실사 취소 Dialog */}
+ <CancelInvestigationDialog
+ isOpen={isCancelDialogOpen}
+ onClose={() => setIsCancelDialogOpen(false)}
+ onConfirm={handleCancelInvestigation}
+ selectedCount={plannedInvestigationsCount}
+ />
+
+ {/* 실사 재의뢰 Dialog */}
+ <ReRequestInvestigationDialog
+ isOpen={isReRequestDialogOpen}
+ onClose={() => setIsReRequestDialogOpen(false)}
+ onConfirm={handleReRequestInvestigation}
+ selectedCount={canceledInvestigationsCount}
+ />
+
+ {/* 결과 발송 Dialog */}
+ <SendResultsDialog
+ isOpen={isSendResultsDialogOpen}
+ onClose={() => setIsSendResultsDialogOpen(false)}
+ onConfirm={handleSendInvestigationResults}
+ selectedCount={completedInvestigationsCount}
+ auditResults={auditResults}
+ />
+
+ {/* 재방문실사 요청 Dialog */}
+ {(() => {
+ // 보완-재실사 대상 실사 찾기
+ const supplementReinspectInvestigations = selectedRows.filter(row =>
+ row.original.investigation &&
+ row.original.investigation.evaluationResult === "SUPPLEMENT_REINSPECT"
+ );
+
+ if (supplementReinspectInvestigations.length === 0) {
+ return null;
+ }
+
+ const targetRow = supplementReinspectInvestigations[0].original;
+ const targetInvestigation = targetRow.investigation!;
+
+ return (
+ <SiteVisitDialog
+ isOpen={isReinspectionDialogOpen}
+ onClose={() => setIsReinspectionDialogOpen(false)}
+ onSubmit={handleRequestReinspection}
+ investigation={{
+ id: targetInvestigation.id,
+ investigationMethod: targetInvestigation.investigationMethod || undefined,
+ investigationAddress: targetInvestigation.investigationAddress || undefined,
+ investigationNotes: targetInvestigation.investigationNotes || undefined,
+ vendorName: targetRow.vendorName,
+ vendorCode: targetRow.vendorCode,
+ projectName: targetRow.projectName || undefined,
+ projectCode: targetRow.projectCode || undefined,
+ pqItems: targetRow.pqItems || null,
+ }}
+ isReinspection={true}
+ />
+ );
+ })()}
+
+ {/* 결재 미리보기 Dialog - 실사 의뢰 */}
+ {session?.user && session.user.epId && investigationFormData && (
+ <ApprovalPreviewDialog
+ open={isApprovalDialogOpen}
+ onOpenChange={(open) => {
+ setIsApprovalDialogOpen(open)
+ if (!open) {
+ // 다이얼로그가 닫히면 실사 폼 데이터도 초기화
+ setInvestigationFormData(null)
+ }
+ }}
+ templateName="Vendor 실사의뢰"
+ variables={approvalVariables}
+ title={`Vendor 실사의뢰 - ${selectedRows.filter(row =>
+ row.original.status === "APPROVED" &&
+ !row.original.investigation &&
+ row.original.type !== "NON_INSPECTION"
+ ).map(row => row.original.vendorName).join(', ')}`}
+ currentUser={{
+ id: Number(session.user.id),
+ epId: session.user.epId,
+ name: session.user.name || undefined,
+ email: session.user.email || undefined,
+ }}
+ onConfirm={handleApprovalSubmit}
+ />
+ )}
+
+ {/* 결재 미리보기 Dialog - 실사 재의뢰 */}
+ {session?.user && session.user.epId && reRequestData && (
+ <ApprovalPreviewDialog
+ open={isReRequestApprovalDialogOpen}
+ onOpenChange={(open) => {
+ setIsReRequestApprovalDialogOpen(open)
+ if (!open) {
+ // 다이얼로그가 닫히면 재의뢰 데이터도 초기화
+ setReRequestData(null)
+ }
+ }}
+ templateName="Vendor 실사 재의뢰"
+ variables={reRequestApprovalVariables}
+ title={`Vendor 실사 재의뢰 - ${reRequestData.vendorNames}`}
+ currentUser={{
+ id: Number(session.user.id),
+ epId: session.user.epId,
+ name: session.user.name || undefined,
+ email: session.user.email || undefined,
+ }}
+ onConfirm={handleReRequestApprovalSubmit}
+ />
+ )}
+ </>
+ )
} \ No newline at end of file