diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-05-28 00:18:16 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-05-28 00:18:16 +0000 |
| commit | 748bb1720fd81e97a84c3e92f89d606e976b52e3 (patch) | |
| tree | a7f7f377035cd04912fe0541368884f976f4ee6d /components/pq-input/pq-review-wrapper.tsx | |
| parent | 9e280704988fdeffa05c1d8cbb731722f666c6af (diff) | |
(대표님) 컴포넌트 파트 커밋
Diffstat (limited to 'components/pq-input/pq-review-wrapper.tsx')
| -rw-r--r-- | components/pq-input/pq-review-wrapper.tsx | 330 |
1 files changed, 330 insertions, 0 deletions
diff --git a/components/pq-input/pq-review-wrapper.tsx b/components/pq-input/pq-review-wrapper.tsx new file mode 100644 index 00000000..216df422 --- /dev/null +++ b/components/pq-input/pq-review-wrapper.tsx @@ -0,0 +1,330 @@ +"use client" + +import * as React from "react" +import { useRouter } from "next/navigation" +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, + CardFooter +} 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, FileText, Paperclip } from "lucide-react" +import { PQGroupData } from "@/lib/pq/service" +import { approvePQAction, rejectPQAction } from "@/lib/pq/service" + +// PQ 제출 정보 타입 +interface PQSubmission { + id: number + vendorId: number + vendorName: string + vendorCode: string + 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 + canReview: boolean +} + +export function PQReviewWrapper({ + pqData, + vendorId, + pqSubmission, + canReview +}: PQReviewWrapperProps) { + const router = useRouter() + const { toast } = useToast() + const [isApproving, setIsApproving] = React.useState(false) + const [isRejecting, setIsRejecting] = React.useState(false) + const [showApproveDialog, setShowApproveDialog] = React.useState(false) + const [showRejectDialog, setShowRejectDialog] = React.useState(false) + const [rejectReason, setRejectReason] = React.useState("") + + // 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.refresh() + } 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) + } + } + + // 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.refresh() + } 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 ( + <div className="space-y-6"> + {/* 그룹별 PQ 항목 표시 */} + {pqData.map((group) => ( + <div key={group.groupName} className="space-y-4"> + <h3 className="text-lg font-medium">{group.groupName}</h3> + + <div className="grid grid-cols-1 gap-4"> + {group.items.map((item) => ( + <Card key={item.criteriaId}> + <CardHeader> + <div className="flex justify-between items-start"> + <div> + <CardTitle className="text-base"> + {item.code} - {item.checkPoint} + </CardTitle> + {item.description && ( + <CardDescription className="mt-1 whitespace-pre-wrap"> + {item.description} + </CardDescription> + )} + </div> + {/* 항목 상태 표시 */} + {!!item.answer || item.attachments.length > 0 ? ( + <Badge variant="outline" className="text-green-600 bg-green-50"> + <CheckCircle className="h-3 w-3 mr-1" /> + 답변 있음 + </Badge> + ) : ( + <Badge variant="outline" className="text-amber-600 bg-amber-50"> + <AlertCircle className="h-3 w-3 mr-1" /> + 답변 없음 + </Badge> + )} + </div> + </CardHeader> + <CardContent className="space-y-4"> + {/* 프로젝트별 추가 정보 */} + {pqSubmission.projectId && item.contractInfo && ( + <div className="space-y-1"> + <p className="text-sm font-medium">계약 정보</p> + <div className="rounded-md bg-muted/30 p-3 text-sm whitespace-pre-wrap"> + {item.contractInfo} + </div> + </div> + )} + + {pqSubmission.projectId && item.additionalRequirement && ( + <div className="space-y-1"> + <p className="text-sm font-medium">추가 요구사항</p> + <div className="rounded-md bg-muted/30 p-3 text-sm whitespace-pre-wrap"> + {item.additionalRequirement} + </div> + </div> + )} + + {/* 벤더 답변 */} + <div className="space-y-1"> + <p className="text-sm font-medium flex items-center gap-1"> + <FileText className="h-4 w-4" /> + 벤더 답변 + </p> + <div className="rounded-md border p-3 min-h-20 whitespace-pre-wrap"> + {item.answer || <span className="text-muted-foreground">답변 없음</span>} + </div> + </div> + + {/* 첨부 파일 */} + {item.attachments.length > 0 && ( + <div className="space-y-1"> + <p className="text-sm font-medium flex items-center gap-1"> + <Paperclip className="h-4 w-4" /> + 첨부 파일 ({item.attachments.length}) + </p> + <div className="rounded-md border p-3"> + <ul className="space-y-1"> + {item.attachments.map((attachment, idx) => ( + <li key={idx} className="flex items-center gap-2"> + <FileText className="h-4 w-4 text-muted-foreground" /> + <a + href={attachment.filePath} + target="_blank" + rel="noopener noreferrer" + className="text-sm text-blue-600 hover:underline" + > + {attachment.fileName} + </a> + </li> + ))} + </ul> + </div> + </div> + )} + </CardContent> + </Card> + ))} + </div> + </div> + ))} + + {/* 검토 버튼 */} + {canReview && ( + <div className="fixed bottom-4 right-4 bg-background p-4 rounded-lg shadow-md border"> + <div className="flex gap-2"> + <Button + variant="outline" + onClick={() => setShowRejectDialog(true)} + disabled={isRejecting} + > + {isRejecting ? "거부 중..." : "거부"} + </Button> + <Button + variant="default" + onClick={() => setShowApproveDialog(true)} + disabled={isApproving} + > + {isApproving ? "승인 중..." : "승인"} + </Button> + </div> + </div> + )} + + {/* 승인 확인 다이얼로그 */} + <Dialog open={showApproveDialog} onOpenChange={setShowApproveDialog}> + <DialogContent> + <DialogHeader> + <DialogTitle>PQ 승인 확인</DialogTitle> + <DialogDescription> + {pqSubmission.vendorName}의 {pqSubmission.type === "GENERAL" ? "일반" : "프로젝트"} PQ를 승인하시겠습니까? + {pqSubmission.projectId && ( + <span> 프로젝트: {pqSubmission.projectName}</span> + )} + </DialogDescription> + </DialogHeader> + <DialogFooter> + <Button variant="outline" onClick={() => setShowApproveDialog(false)}> + 취소 + </Button> + <Button onClick={handleApprove} disabled={isApproving}> + {isApproving ? "승인 중..." : "승인"} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + + {/* 거부 확인 다이얼로그 */} + <Dialog open={showRejectDialog} onOpenChange={setShowRejectDialog}> + <DialogContent> + <DialogHeader> + <DialogTitle>PQ 거부</DialogTitle> + <DialogDescription> + {pqSubmission.vendorName}의 {pqSubmission.type === "GENERAL" ? "일반" : "프로젝트"} PQ를 거부하는 이유를 입력해주세요. + {pqSubmission.projectId && ( + <span> 프로젝트: {pqSubmission.projectName}</span> + )} + </DialogDescription> + </DialogHeader> + <Textarea + value={rejectReason} + onChange={(e) => setRejectReason(e.target.value)} + placeholder="거부 사유를 입력하세요" + className="min-h-24" + /> + <DialogFooter> + <Button variant="outline" onClick={() => setShowRejectDialog(false)}> + 취소 + </Button> + <Button + variant="destructive" + onClick={handleReject} + disabled={isRejecting || !rejectReason.trim()} + > + {isRejecting ? "거부 중..." : "거부"} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + </div> + ) +}
\ No newline at end of file |
