"use client" import * as React from "react" import { useRouter } from "next/navigation" import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Textarea } from "@/components/ui/textarea" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { useToast } from "@/hooks/use-toast" import { CheckCircle, AlertCircle, Paperclip, Square, Download } from "lucide-react" import { PQGroupData } from "@/lib/pq/service" import { approvePQAction, rejectPQAction, updateSHICommentAction, approveQMReviewAction, rejectQMReviewAction, requestPqSupplementAction, approveSafetyPQAction, rejectSafetyPQAction } from "@/lib/pq/service" import { FileList, FileListHeader, FileListInfo, FileListItem, FileListName, FileListDescription, FileListAction, FileListIcon } from "@/components/ui/file-list" // import * as ExcelJS from 'exceljs'; // import { saveAs } from "file-saver"; // PQ 제출 정보 타입 interface PQSubmission { id: number vendorId: number vendorName: string | null vendorCode: string | null type: string status: string projectId: number | null projectName: string | null projectCode: string | null submittedAt: Date | null approvedAt: Date | null rejectedAt: Date | null rejectReason: string | null } interface PQReviewWrapperProps { pqData: PQGroupData[] vendorId: number pqSubmission: PQSubmission vendorInfo?: any // 협력업체 정보 (선택사항) vendorCountry?: string | null } export function PQReviewWrapper({ pqData, vendorId, pqSubmission, vendorInfo, vendorCountry, }: PQReviewWrapperProps) { const router = useRouter() const { toast } = useToast() const [isSafetyApproving, setIsSafetyApproving] = React.useState(false) const [isSafetyRejecting, setIsSafetyRejecting] = React.useState(false) const [isApproving, setIsApproving] = React.useState(false) const [isRejecting, setIsRejecting] = React.useState(false) const [isQMApproving, setIsQMApproving] = React.useState(false) const [isQMRejecting, setIsQMRejecting] = React.useState(false) const [showSafetyApproveDialog, setShowSafetyApproveDialog] = React.useState(false) const [showSafetyRejectDialog, setShowSafetyRejectDialog] = React.useState(false) const [showApproveDialog, setShowApproveDialog] = React.useState(false) const [showRejectDialog, setShowRejectDialog] = React.useState(false) const [showQMApproveDialog, setShowQMApproveDialog] = React.useState(false) const [showQMRejectDialog, setShowQMRejectDialog] = React.useState(false) const [showSupplementDialog, setShowSupplementDialog] = React.useState(false) const [safetyRejectReason, setSafetyRejectReason] = React.useState("") const [rejectReason, setRejectReason] = React.useState("") const [qmRejectReason, setQmRejectReason] = React.useState("") const [supplementComment, setSupplementComment] = React.useState("") const [shiComments, setShiComments] = React.useState>({}) const [isUpdatingComment, setIsUpdatingComment] = React.useState(null) const [isSendingSupplement, setIsSendingSupplement] = React.useState(false) // 코드 순서로 정렬하는 함수 (1-1-1, 1-1-2, 1-2-1 순서) const sortByCode = (items: any[]) => { return [...items].sort((a, b) => { const parseCode = (code: string) => { return code.split('-').map(part => parseInt(part, 10)) } const aCode = parseCode(a.code) const bCode = parseCode(b.code) for (let i = 0; i < Math.max(aCode.length, bCode.length); i++) { const aPart = aCode[i] || 0 const bPart = bCode[i] || 0 if (aPart !== bPart) { return aPart - bPart } } return 0 }) } // 벤더 내자/외자 판별 (국가 코드 기반) const isDomesticVendor = React.useMemo(() => { if (!vendorCountry) return null; // 정보 없으면 필터 미적용 return vendorCountry === "KR" || vendorCountry === "한국"; }, [vendorCountry]); // 벤더 유형에 따라 PQ 항목 필터링 const filteredData: PQGroupData[] = React.useMemo(() => { if (isDomesticVendor === null) return pqData; const filterItemByType = (item: any) => { const itemType = item.type || "내외자"; if (itemType === "내외자") return true; if (itemType === "내자") return isDomesticVendor === true; if (itemType === "외자") return isDomesticVendor === false; return true; }; return pqData .map((group) => ({ ...group, items: group.items.filter(filterItemByType), })) .filter((group) => group.items.length > 0); }, [pqData, isDomesticVendor]); // 기존 SHI 코멘트를 로컬 상태에 초기화 React.useEffect(() => { const initialComments: Record = {} pqData.forEach(group => { group.items.forEach(item => { if (item.answerId && item.shiComment) { initialComments[item.answerId] = item.shiComment } }) }) setShiComments(initialComments) }, [pqData]) // 안전 PQ 승인 처리 const handleSafetyApprove = async () => { try { setIsSafetyApproving(true) const result = await approveSafetyPQAction({ pqSubmissionId: pqSubmission.id, vendorId: vendorId, }) if (result.ok) { toast({ title: "안전 PQ 승인 완료", description: "안전 검토가 승인되었습니다.", }) router.refresh() } else { toast({ title: "안전 승인 실패", description: result.error || "안전 PQ 승인 중 오류가 발생했습니다.", variant: "destructive", }) } } catch (error) { console.error("안전 PQ 승인 오류:", error) toast({ title: "안전 승인 실패", description: "안전 PQ 승인 중 오류가 발생했습니다.", variant: "destructive", }) } finally { setIsSafetyApproving(false) setShowSafetyApproveDialog(false) } } // 안전 PQ 거절 처리 const handleSafetyReject = async () => { if (!safetyRejectReason.trim()) { toast({ title: "거절 사유 필요", description: "안전 거절 사유를 입력해주세요.", variant: "destructive", }) return } try { setIsSafetyRejecting(true) const result = await rejectSafetyPQAction({ pqSubmissionId: pqSubmission.id, vendorId: vendorId, rejectReason: safetyRejectReason, }) if (result.ok) { toast({ title: "안전 PQ 거절 완료", description: "안전 검토에서 거절되었습니다.", }) router.refresh() } else { toast({ title: "안전 거절 실패", description: result.error || "안전 PQ 거절 중 오류가 발생했습니다.", variant: "destructive", }) } } catch (error) { console.error("안전 PQ 거절 오류:", error) toast({ title: "안전 거절 실패", description: "안전 PQ 거절 중 오류가 발생했습니다.", variant: "destructive", }) } finally { setSafetyRejectReason("") setIsSafetyRejecting(false) setShowSafetyRejectDialog(false) } } // PQ 승인 처리 const handleApprove = async () => { try { setIsApproving(true) const result = await approvePQAction({ pqSubmissionId: pqSubmission.id, vendorId: vendorId }) if (result.ok) { toast({ title: "PQ 승인 완료", description: "PQ가 성공적으로 승인되었습니다.", }) // 페이지 새로고침 router.push(`/evcp/pq_new/`) } else { toast({ title: "승인 실패", description: result.error || "PQ 승인 중 오류가 발생했습니다.", variant: "destructive" }) } } catch (error) { console.error("PQ 승인 오류:", error) toast({ title: "승인 실패", description: "PQ 승인 중 오류가 발생했습니다.", variant: "destructive" }) } finally { setIsApproving(false) setShowApproveDialog(false) } } // QM 승인 처리 const handleQMApprove = async () => { try { setIsQMApproving(true) const result = await approveQMReviewAction({ pqSubmissionId: pqSubmission.id, vendorId: vendorId }) if (result.ok) { toast({ title: "QM 승인 완료", description: "PQ가 최종 승인되어 실사 프로세스가 시작됩니다.", }) // 페이지 새로고침 router.refresh() } else { toast({ title: "QM 승인 실패", description: result.error || "QM 승인 중 오류가 발생했습니다.", variant: "destructive" }) } } catch (error) { console.error("QM 승인 오류:", error) toast({ title: "QM 승인 실패", description: "QM 승인 중 오류가 발생했습니다.", variant: "destructive" }) } finally { setIsQMApproving(false) setShowQMApproveDialog(false) } } // 보완요청 처리 const handleRequestSupplement = async () => { if (!supplementComment.trim()) { toast({ title: "보완요청 내용 필요", description: "보완요청 사유를 입력해주세요.", variant: "destructive" }) return } try { setIsSendingSupplement(true) const result = await requestPqSupplementAction({ pqSubmissionId: pqSubmission.id, vendorId, comment: supplementComment, }) if (result.ok) { toast({ title: "보완요청 전송", description: "보완요청 메일을 발송했습니다." }) setShowSupplementDialog(false) setSupplementComment("") router.refresh() } else { toast({ title: "전송 실패", description: result.error || "보완요청 전송 중 오류가 발생했습니다.", variant: "destructive" }) } } catch (e) { console.error(e) toast({ title: "전송 실패", description: "보완요청 전송 중 오류가 발생했습니다.", variant: "destructive" }) } finally { setIsSendingSupplement(false) } } // QM 거절 처리 const handleQMReject = async () => { if (!qmRejectReason.trim()) { toast({ title: "거절 사유 필요", description: "거절 사유를 입력해주세요.", variant: "destructive" }) return } try { setIsQMRejecting(true) const result = await rejectQMReviewAction({ pqSubmissionId: pqSubmission.id, vendorId: vendorId, rejectReason: qmRejectReason }) if (result.ok) { toast({ title: "QM 거절 완료", description: "PQ가 QM에 의해 거절되었습니다.", }) // 페이지 새로고침 router.refresh() } else { toast({ title: "QM 거절 실패", description: result.error || "QM 거절 중 오류가 발생했습니다.", variant: "destructive" }) } } catch (error) { console.error("QM 거절 오류:", error) toast({ title: "QM 거절 실패", description: "QM 거절 중 오류가 발생했습니다.", variant: "destructive" }) } finally { setIsQMRejecting(false) setShowQMRejectDialog(false) } } // SHI 코멘트 업데이트 처리 const handleSHICommentUpdate = async (answerId: number) => { const comment = shiComments[answerId] || "" try { setIsUpdatingComment(answerId) const result = await updateSHICommentAction({ answerId, shiComment: comment, }) if (result.ok) { toast({ title: "SHI 코멘트 저장 완료", description: "SHI 코멘트가 저장되었습니다.", }) // 페이지 새로고침 router.refresh() } else { toast({ title: "저장 실패", description: result.error || "SHI 코멘트 저장 중 오류가 발생했습니다.", variant: "destructive" }) } } catch (error) { console.error("SHI 코멘트 저장 오류:", error) toast({ title: "저장 실패", description: "SHI 코멘트 저장 중 오류가 발생했습니다.", variant: "destructive" }) } finally { setIsUpdatingComment(null) } } // PQ 거부 처리 const handleReject = async () => { if (!rejectReason.trim()) { toast({ title: "거부 사유 필요", description: "거부 사유를 입력해주세요.", variant: "destructive" }) return } try { setIsRejecting(true) const result = await rejectPQAction({ pqSubmissionId: pqSubmission.id, vendorId: vendorId, rejectReason: rejectReason }) if (result.ok) { toast({ title: "PQ 거부 완료", description: "PQ가 거부되었습니다.", }) // 페이지 리다이렉트 router.push(`/evcp/pq_new/`) } else { toast({ title: "거부 실패", description: result.error || "PQ 거부 중 오류가 발생했습니다.", variant: "destructive" }) } } catch (error) { console.error("PQ 거부 오류:", error) toast({ title: "거부 실패", description: "PQ 거부 중 오류가 발생했습니다.", variant: "destructive" }) } finally { setIsRejecting(false) setShowRejectDialog(false) } } return (
{filteredData.length === 0 && (
표시할 PQ 항목이 없습니다. (벤더 내/외자 구분 필터 적용)
)} {/* 그룹별 PQ 항목 표시 */} {filteredData.map((group) => (

{group.groupName}

{sortByCode(group.items).map((item) => (
{item.code} - {item.checkPoint} {item.description && ( {item.description} )} {item.remarks && (

Remark:

{item.remarks}

)}
{/* 항목 상태 표시 */} {!!item.answer || item.attachments.length > 0 ? ( 답변 있음 ) : ( 답변 없음 )}
{item.criteriaAttachments && item.criteriaAttachments.length > 0 && (

기준 첨부파일

{item.criteriaAttachments.map((file) => ( {file.fileName} {file.fileSize && ( {file.fileSize} bytes )} { try { const { downloadFile } = await import('@/lib/file-download') await downloadFile(file.filePath, file.fileName, { showToast: true }) } catch (error) { toast({ title: "다운로드 실패", description: "파일 다운로드 중 오류가 발생했습니다.", variant: "destructive" }) } }} > Download ))}
)} {/* 프로젝트별 추가 정보 */} {pqSubmission.projectId && item.contractInfo && (

계약 정보

{item.contractInfo}
)} {pqSubmission.projectId && item.additionalRequirement && (

추가 요구사항

{item.additionalRequirement}
)} {/* 벤더 답변 - 입력 형식에 따라 다르게 표시 */}

벤더 답변 {item.inputFormat && ( {item.inputFormat === "TEXT" && "텍스트"} {item.inputFormat === "EMAIL" && "이메일"} {item.inputFormat === "PHONE" && "전화번호"} {item.inputFormat === "FAX" && "팩스번호"} {item.inputFormat === "NUMBER" && "숫자"} {item.inputFormat === "NUMBER_WITH_UNIT" && "숫자+단위"} {item.inputFormat === "FILE" && "파일"} {item.inputFormat === "TEXT_FILE" && "텍스트+파일"} )}

{(() => { const inputFormat = item.inputFormat || "TEXT"; switch (inputFormat) { case "EMAIL": return (
이메일 주소:
{item.answer || 답변 없음}
); case "PHONE": return (
전화번호:
{item.answer || 답변 없음}
); case "FAX": return (
팩스번호:
{item.answer || 답변 없음}
); case "NUMBER": return (
숫자 값:
{item.answer || 답변 없음}
); case "NUMBER_WITH_UNIT": const numberWithUnit = item.answer || ""; const [number, unit] = numberWithUnit.split(' '); return (
숫자+단위:
{number && ( {number} )} {unit && ( {unit} )} {!numberWithUnit && ( 답변 없음 )}
); case "FILE": return (
파일 업로드 항목:
{item.attachments.length > 0 ? "파일이 업로드되었습니다." : "파일이 업로드되지 않았습니다."}
); case "TEXT_FILE": return (
텍스트 답변:
{item.answer || 텍스트 답변 없음}
파일 업로드:
{item.attachments.length > 0 ? "파일이 업로드되었습니다." : "파일이 업로드되지 않았습니다."}
); default: // TEXT return (
{item.answer || 답변 없음}
); } })()}
{/* SHI 코멘트 필드 (편집 가능) */}

SHI 코멘트