summaryrefslogtreecommitdiff
path: root/components/pq-input/pq-review-wrapper.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/pq-input/pq-review-wrapper.tsx')
-rw-r--r--components/pq-input/pq-review-wrapper.tsx330
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