diff options
Diffstat (limited to 'lib/pq')
| -rw-r--r-- | lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx | 1819 |
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 |
