diff options
Diffstat (limited to 'components/pq-input/pq-review-wrapper.tsx')
| -rw-r--r-- | components/pq-input/pq-review-wrapper.tsx | 404 |
1 files changed, 263 insertions, 141 deletions
diff --git a/components/pq-input/pq-review-wrapper.tsx b/components/pq-input/pq-review-wrapper.tsx index 1e172744..ac9629cb 100644 --- a/components/pq-input/pq-review-wrapper.tsx +++ b/components/pq-input/pq-review-wrapper.tsx @@ -21,9 +21,10 @@ import { DialogTitle } from "@/components/ui/dialog" import { useToast } from "@/hooks/use-toast" -import { CheckCircle, AlertCircle, Paperclip, Square } from "lucide-react" +import { CheckCircle, AlertCircle, Paperclip, Square, Download } from "lucide-react" import { PQGroupData } from "@/lib/pq/service" -import { approvePQAction, rejectPQAction, updateSHICommentAction, approveQMReviewAction, rejectQMReviewAction, requestPqSupplementAction } 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"; @@ -49,25 +50,32 @@ interface PQReviewWrapperProps { vendorId: number pqSubmission: PQSubmission vendorInfo?: any // 협력업체 정보 (선택사항) + vendorCountry?: string | null } export function PQReviewWrapper({ pqData, vendorId, pqSubmission, - vendorInfo + 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("") @@ -96,6 +104,32 @@ export function PQReviewWrapper({ 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 코멘트를 로컬 상태에 초기화 @@ -111,6 +145,87 @@ export function PQReviewWrapper({ 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 { @@ -298,141 +413,6 @@ export function PQReviewWrapper({ } } - // // Excel export 처리 - // const handleExportToExcel = async () => { - // try { - // setIsExporting(true) - - // // 워크북 생성 - // const workbook = new ExcelJS.Workbook() - // workbook.creator = 'PQ Management System' - // workbook.created = new Date() - - // // 메인 시트 생성 - // const worksheet = workbook.addWorksheet("PQ 항목") - - // // 헤더 정의 - // const headers = [ - // "그룹명", - // "코드", - // "체크포인트", - // "설명", - // "입력형식", - // "필수여부", - // "벤더답변", - // "SHI 코멘트", - // "벤더 답변", - // ] - - // // 헤더 추가 - // worksheet.addRow(headers) - - // // 헤더 스타일 적용 - // const headerRow = worksheet.getRow(1) - // headerRow.font = { bold: true } - // headerRow.fill = { - // type: 'pattern', - // pattern: 'solid', - // fgColor: { argb: 'FFE0E0E0' } - // } - // headerRow.alignment = { vertical: 'middle', horizontal: 'center' } - - // // 컬럼 너비 설정 - // worksheet.columns = [ - // { header: "그룹명", key: "groupName", width: 15 }, - // { header: "코드", key: "code", width: 12 }, - // { header: "체크포인트", key: "checkPoint", width: 30 }, - // { header: "설명", key: "description", width: 40 }, - // { header: "입력형식", key: "inputFormat", width: 12 }, - - // { header: "벤더답변", key: "answer", width: 30 }, - // { header: "SHI 코멘트", key: "shiComment", width: 30 }, - // { header: "벤더 답변", key: "vendorReply", width: 30 }, - // ] - - // // 데이터 추가 - // pqData.forEach(group => { - // group.items.forEach(item => { - // const rowData = [ - // group.groupName, - // item.code, - // item.checkPoint, - // item.description || "", - // item.inputFormat || "", - - // item.answer || "", - // item.shiComment || "", - // item.vendorReply || "", - // ] - // worksheet.addRow(rowData) - // }) - // }) - - // // 전체 셀에 테두리 추가 - // worksheet.eachRow((row, rowNumber) => { - // row.eachCell((cell) => { - // cell.border = { - // top: { style: 'thin' }, - // left: { style: 'thin' }, - // bottom: { style: 'thin' }, - // right: { style: 'thin' } - // } - // // 긴 텍스트는 자동 줄바꿈 - // cell.alignment = { - // vertical: 'top', - // horizontal: 'left', - // wrapText: true - // } - // }) - // }) - - // // 정보 시트 생성 - // const infoSheet = workbook.addWorksheet("정보") - // infoSheet.addRow(["벤더명", pqSubmission.vendorName]) - // if (pqSubmission.projectName) { - // infoSheet.addRow(["프로젝트명", pqSubmission.projectName]) - // } - // infoSheet.addRow(["생성일", new Date().toLocaleDateString('ko-KR')]) - // infoSheet.addRow(["총 항목 수", pqData.reduce((total, group) => total + group.items.length, 0)]) - - // // 정보 시트 스타일링 - // infoSheet.columns = [ - // { header: "항목", key: "item", width: 20 }, - // { header: "값", key: "value", width: 40 } - // ] - - // const infoHeaderRow = infoSheet.getRow(1) - // infoHeaderRow.font = { bold: true } - // infoHeaderRow.fill = { - // type: 'pattern', - // pattern: 'solid', - // fgColor: { argb: 'FFE6F3FF' } - // } - - // // 파일명 생성 - // const defaultFilename = pqSubmission.projectName - // ? `${pqSubmission.vendorName}_${pqSubmission.projectName}_PQ_${new Date().toISOString().slice(0, 10)}` - // : `${pqSubmission.vendorName}_PQ_${new Date().toISOString().slice(0, 10)}` - // const finalFilename = defaultFilename - - // // 파일 다운로드 - // const buffer = await workbook.xlsx.writeBuffer() - // const blob = new Blob([buffer], { - // type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - // }) - // saveAs(blob, `${finalFilename}.xlsx`) - // } catch (error) { - // console.error("Excel export 오류:", error) - // toast({ - // title: "내보내기 실패", - // description: "Excel 내보내기 중 오류가 발생했습니다.", - // variant: "destructive" - // }) - // } finally { - // setIsExporting(false) - // } - // } - // PQ 거부 처리 const handleReject = async () => { if (!rejectReason.trim()) { @@ -482,8 +462,14 @@ export function PQReviewWrapper({ return ( <div className="space-y-6"> + {filteredData.length === 0 && ( + <div className="rounded-md border border-dashed p-4 text-sm text-muted-foreground"> + 표시할 PQ 항목이 없습니다. (벤더 내/외자 구분 필터 적용) + </div> + )} + {/* 그룹별 PQ 항목 표시 */} - {pqData.map((group) => ( + {filteredData.map((group) => ( <div key={group.groupName} className="space-y-4"> <h3 className="text-lg font-medium">{group.groupName}</h3> @@ -530,6 +516,43 @@ export function PQReviewWrapper({ </div> </CardHeader> <CardContent className="space-y-4"> + {item.criteriaAttachments && item.criteriaAttachments.length > 0 && ( + <div className="space-y-2"> + <p className="text-sm font-medium">기준 첨부파일</p> + <FileList> + {item.criteriaAttachments.map((file) => ( + <FileListItem key={file.attachId}> + <FileListHeader> + <FileListIcon /> + <FileListInfo> + <FileListName>{file.fileName}</FileListName> + {file.fileSize && ( + <FileListDescription>{file.fileSize} bytes</FileListDescription> + )} + </FileListInfo> + <FileListAction + onClick={async () => { + try { + const { downloadFile } = await import('@/lib/file-download') + await downloadFile(file.filePath, file.fileName, { showToast: true }) + } catch (error) { + toast({ + title: "다운로드 실패", + description: "파일 다운로드 중 오류가 발생했습니다.", + variant: "destructive" + }) + } + }} + > + <Download className="h-4 w-4" /> + <span className="sr-only">Download</span> + </FileListAction> + </FileListHeader> + </FileListItem> + ))} + </FileList> + </div> + )} {/* 프로젝트별 추가 정보 */} {pqSubmission.projectId && item.contractInfo && ( <div className="space-y-1"> @@ -767,15 +790,42 @@ export function PQReviewWrapper({ {/* 검토 버튼 - 상태에 따라 다른 버튼 표시 */} <div className="fixed bottom-4 right-4 bg-background p-4 rounded-lg shadow-md border"> <div className="flex gap-2"> - {/* SUBMITTED 상태: 구매 담당자 승인/거절 */} + {/* SUBMITTED 상태: 안전 담당자 승인/거절 */} {pqSubmission.status === "SUBMITTED" && ( <> <Button variant="outline" + onClick={() => setShowSafetyRejectDialog(true)} + disabled={isSafetyRejecting} + > + {isSafetyRejecting ? "안전 거절 중..." : "안전 거절"} + </Button> + <Button + variant="secondary" + onClick={() => setShowSupplementDialog(true)} + disabled={isSendingSupplement} + > + 보완요청 + </Button> + <Button + variant="default" + onClick={() => setShowSafetyApproveDialog(true)} + disabled={isSafetyApproving} + > + {isSafetyApproving ? "안전 승인 중..." : "안전 승인"} + </Button> + </> + )} + + {/* SAFETY_APPROVED 상태: 구매 담당자 승인/거절 */} + {pqSubmission.status === "SAFETY_APPROVED" && ( + <> + <Button + variant="outline" onClick={() => setShowRejectDialog(true)} disabled={isRejecting} > - {isRejecting ? "거부 중..." : "거부"} + {isRejecting ? "거부 중..." : "구매 거부"} </Button> <Button variant="secondary" @@ -844,10 +894,82 @@ export function PQReviewWrapper({ <span>거절됨</span> </div> )} + + {/* SAFETY_REJECTED 상태: 안전 거절 표시 */} + {pqSubmission.status === "SAFETY_REJECTED" && ( + <div className="flex items-center gap-2 text-red-600"> + <AlertCircle className="h-4 w-4" /> + <span>안전 거절됨</span> + </div> + )} </div> </div> + {/* 안전 승인 확인 다이얼로그 */} + <Dialog open={showSafetyApproveDialog} onOpenChange={setShowSafetyApproveDialog}> + <DialogContent> + <DialogHeader> + <DialogTitle>안전 PQ 승인 확인</DialogTitle> + <DialogDescription> + {pqSubmission.vendorName || "알 수 없는 업체"}의 { + pqSubmission.type === "GENERAL" ? "일반" : + pqSubmission.type === "PROJECT" ? "프로젝트" : + pqSubmission.type === "NON_INSPECTION" ? "미실사" : "일반" + } PQ를 안전 검토에서 승인하시겠습니까? + {pqSubmission.projectId && ( + <span> 프로젝트: {pqSubmission.projectName}</span> + )} + </DialogDescription> + </DialogHeader> + <DialogFooter> + <Button variant="outline" onClick={() => setShowSafetyApproveDialog(false)}> + 취소 + </Button> + <Button onClick={handleSafetyApprove} disabled={isSafetyApproving}> + {isSafetyApproving ? "안전 승인 중..." : "안전 승인"} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + + {/* 안전 거부 확인 다이얼로그 */} + <Dialog open={showSafetyRejectDialog} onOpenChange={setShowSafetyRejectDialog}> + <DialogContent> + <DialogHeader> + <DialogTitle>안전 PQ 거부</DialogTitle> + <DialogDescription> + {pqSubmission.vendorName || "알 수 없는 업체"}의 { + pqSubmission.type === "GENERAL" ? "일반" : + pqSubmission.type === "PROJECT" ? "프로젝트" : + pqSubmission.type === "NON_INSPECTION" ? "미실사" : "일반" + } PQ를 안전 검토에서 거부하는 이유를 입력해주세요. + {pqSubmission.projectId && ( + <span> 프로젝트: {pqSubmission.projectName}</span> + )} + </DialogDescription> + </DialogHeader> + <Textarea + value={safetyRejectReason} + onChange={(e) => setSafetyRejectReason(e.target.value)} + placeholder="안전 거부 사유를 입력하세요" + className="min-h-24" + /> + <DialogFooter> + <Button variant="outline" onClick={() => setShowSafetyRejectDialog(false)}> + 취소 + </Button> + <Button + variant="destructive" + onClick={handleSafetyReject} + disabled={isSafetyRejecting || !safetyRejectReason.trim()} + > + {isSafetyRejecting ? "안전 거절 중..." : "안전 거절"} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + {/* 승인 확인 다이얼로그 */} <Dialog open={showApproveDialog} onOpenChange={setShowApproveDialog}> <DialogContent> |
