From aa86729f9a2ab95346a2851e3837de1c367aae17 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 20 Jun 2025 11:37:31 +0000 Subject: (대표님) 20250620 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/evaluation-target-action-dialogs.tsx | 384 +++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx (limited to 'lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx') diff --git a/lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx b/lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx new file mode 100644 index 00000000..47af419d --- /dev/null +++ b/lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx @@ -0,0 +1,384 @@ +// evaluation-target-action-dialogs.tsx +"use client" + +import * as React from "react" +import { Loader2, AlertTriangle, Check, X, MessageSquare } from "lucide-react" +import { toast } from "sonner" + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" +import { Badge } from "@/components/ui/badge" + +import { + confirmEvaluationTargets, + excludeEvaluationTargets, + requestEvaluationReview +} from "../service" +import { EvaluationTargetWithDepartments } from "@/db/schema" + +// ---------------------------------------------------------------- +// 확정 컨펌 다이얼로그 +// ---------------------------------------------------------------- +interface ConfirmTargetsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + targets: EvaluationTargetWithDepartments[] + onSuccess?: () => void +} + +export function ConfirmTargetsDialog({ + open, + onOpenChange, + targets, + onSuccess +}: ConfirmTargetsDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + + // 확정 가능한 대상들 (consensusStatus가 true인 것들) + const confirmableTargets = targets.filter( + t => t.status === "PENDING" && t.consensusStatus === true + ) + + const handleConfirm = async () => { + if (confirmableTargets.length === 0) return + + setIsLoading(true) + try { + const targetIds = confirmableTargets.map(t => t.id) + const result = await confirmEvaluationTargets(targetIds) + + if (result.success) { + toast.success(result.message) + onSuccess?.() + onOpenChange(false) + } else { + toast.error(result.error) + } + } catch (error) { + toast.error("확정 처리 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + return ( + + + + + + 평가 대상 확정 + + +
+

+ 선택된 {targets.length}개 항목 중{" "} + + {confirmableTargets.length}개 항목 + + 을 확정하시겠습니까? +

+ + {confirmableTargets.length !== targets.length && ( +
+

+ + 의견 일치 상태인 대기중 항목만 확정 가능합니다. + ({targets.length - confirmableTargets.length}개 항목 제외됨) +

+
+ )} + + {confirmableTargets.length > 0 && ( +
+
+ {confirmableTargets.slice(0, 5).map(target => ( +
+ + {target.vendorCode} + + {target.vendorName} +
+ ))} + {confirmableTargets.length > 5 && ( +

+ ...외 {confirmableTargets.length - 5}개 +

+ )} +
+
+ )} +
+
+
+ + 취소 + + {isLoading && } + 확정 ({confirmableTargets.length}) + + +
+
+ ) +} + +// ---------------------------------------------------------------- +// 제외 컨펌 다이얼로그 +// ---------------------------------------------------------------- +interface ExcludeTargetsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + targets: EvaluationTargetWithDepartments[] + onSuccess?: () => void +} + +export function ExcludeTargetsDialog({ + open, + onOpenChange, + targets, + onSuccess +}: ExcludeTargetsDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + + // 제외 가능한 대상들 (PENDING 상태인 것들) + const excludableTargets = targets.filter(t => t.status === "PENDING") + + const handleExclude = async () => { + if (excludableTargets.length === 0) return + + setIsLoading(true) + try { + const targetIds = excludableTargets.map(t => t.id) + const result = await excludeEvaluationTargets(targetIds) + + if (result.success) { + toast.success(result.message) + onSuccess?.() + onOpenChange(false) + } else { + toast.error(result.error) + } + } catch (error) { + toast.error("제외 처리 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + return ( + + + + + + 평가 대상 제외 + + +
+

+ 선택된 {targets.length}개 항목 중{" "} + + {excludableTargets.length}개 항목 + + 을 제외하시겠습니까? +

+ + {excludableTargets.length !== targets.length && ( +
+

+ + 대기중 상태인 항목만 제외 가능합니다. + ({targets.length - excludableTargets.length}개 항목 제외됨) +

+
+ )} + + {excludableTargets.length > 0 && ( +
+
+ {excludableTargets.slice(0, 5).map(target => ( +
+ + {target.vendorCode} + + {target.vendorName} +
+ ))} + {excludableTargets.length > 5 && ( +

+ ...외 {excludableTargets.length - 5}개 +

+ )} +
+
+ )} +
+
+
+ + 취소 + + {isLoading && } + 제외 ({excludableTargets.length}) + + +
+
+ ) +} + +// ---------------------------------------------------------------- +// 의견 요청 다이얼로그 +// ---------------------------------------------------------------- +interface RequestReviewDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + targets: EvaluationTargetWithDepartments[] + onSuccess?: () => void +} + +export function RequestReviewDialog({ + open, + onOpenChange, + targets, + onSuccess +}: RequestReviewDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [message, setMessage] = React.useState("") + + // 의견 요청 가능한 대상들 (PENDING 상태인 것들) + const reviewableTargets = targets.filter(t => t.status === "PENDING") + + // 담당자 이메일들 수집 + const reviewerEmails = React.useMemo(() => { + const emails = new Set() + reviewableTargets.forEach(target => { + if (target.orderReviewerEmail) emails.add(target.orderReviewerEmail) + if (target.procurementReviewerEmail) emails.add(target.procurementReviewerEmail) + if (target.qualityReviewerEmail) emails.add(target.qualityReviewerEmail) + if (target.designReviewerEmail) emails.add(target.designReviewerEmail) + if (target.csReviewerEmail) emails.add(target.csReviewerEmail) + }) + return Array.from(emails) + }, [reviewableTargets]) + + const handleRequestReview = async () => { + if (reviewableTargets.length === 0) return + + setIsLoading(true) + try { + const targetIds = reviewableTargets.map(t => t.id) + const result = await requestEvaluationReview(targetIds, message) + + if (result.success) { + toast.success(result.message) + onSuccess?.() + onOpenChange(false) + setMessage("") + } else { + toast.error(result.error) + } + } catch (error) { + toast.error("의견 요청 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + return ( + + + + + + 평가 의견 요청 + + + 선택된 평가 대상에 대한 의견을 담당자들에게 요청합니다. + + + +
+ {/* 요약 정보 */} +
+
+

+ 요청 대상: {reviewableTargets.length}개 평가 항목 +

+

+ 받는 사람: {reviewerEmails.length}명의 담당자 +

+
+
+ + {/* 메시지 입력 */} +
+ +