summaryrefslogtreecommitdiff
path: root/lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-20 11:37:31 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-20 11:37:31 +0000
commitaa86729f9a2ab95346a2851e3837de1c367aae17 (patch)
treeb601b18b6724f2fb449c7fa9ea50cbd652a8077d /lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx
parent95bbe9c583ff841220da1267630e7b2025fc36dc (diff)
(대표님) 20250620 작업사항
Diffstat (limited to 'lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx')
-rw-r--r--lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx384
1 files changed, 384 insertions, 0 deletions
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 (
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle className="flex items-center gap-2">
+ <Check className="h-5 w-5 text-green-600" />
+ 평가 대상 확정
+ </AlertDialogTitle>
+ <AlertDialogDescription asChild>
+ <div className="space-y-3">
+ <p>
+ 선택된 {targets.length}개 항목 중{" "}
+ <span className="font-semibold text-green-600">
+ {confirmableTargets.length}개 항목
+ </span>
+ 을 확정하시겠습니까?
+ </p>
+
+ {confirmableTargets.length !== targets.length && (
+ <div className="p-3 bg-yellow-50 rounded-lg border border-yellow-200">
+ <p className="text-sm text-yellow-800">
+ <AlertTriangle className="h-4 w-4 inline mr-1" />
+ 의견 일치 상태인 대기중 항목만 확정 가능합니다.
+ ({targets.length - confirmableTargets.length}개 항목 제외됨)
+ </p>
+ </div>
+ )}
+
+ {confirmableTargets.length > 0 && (
+ <div className="max-h-32 overflow-y-auto">
+ <div className="text-sm space-y-1">
+ {confirmableTargets.slice(0, 5).map(target => (
+ <div key={target.id} className="flex items-center gap-2">
+ <Badge variant="outline" className="text-xs">
+ {target.vendorCode}
+ </Badge>
+ <span className="text-xs">{target.vendorName}</span>
+ </div>
+ ))}
+ {confirmableTargets.length > 5 && (
+ <p className="text-xs text-muted-foreground">
+ ...외 {confirmableTargets.length - 5}개
+ </p>
+ )}
+ </div>
+ </div>
+ )}
+ </div>
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel>
+ <AlertDialogAction
+ onClick={handleConfirm}
+ disabled={isLoading || confirmableTargets.length === 0}
+ className="bg-green-600 hover:bg-green-700"
+ >
+ {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ 확정 ({confirmableTargets.length})
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ )
+}
+
+// ----------------------------------------------------------------
+// 제외 컨펌 다이얼로그
+// ----------------------------------------------------------------
+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 (
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle className="flex items-center gap-2">
+ <X className="h-5 w-5 text-red-600" />
+ 평가 대상 제외
+ </AlertDialogTitle>
+ <AlertDialogDescription asChild>
+ <div className="space-y-3">
+ <p>
+ 선택된 {targets.length}개 항목 중{" "}
+ <span className="font-semibold text-red-600">
+ {excludableTargets.length}개 항목
+ </span>
+ 을 제외하시겠습니까?
+ </p>
+
+ {excludableTargets.length !== targets.length && (
+ <div className="p-3 bg-yellow-50 rounded-lg border border-yellow-200">
+ <p className="text-sm text-yellow-800">
+ <AlertTriangle className="h-4 w-4 inline mr-1" />
+ 대기중 상태인 항목만 제외 가능합니다.
+ ({targets.length - excludableTargets.length}개 항목 제외됨)
+ </p>
+ </div>
+ )}
+
+ {excludableTargets.length > 0 && (
+ <div className="max-h-32 overflow-y-auto">
+ <div className="text-sm space-y-1">
+ {excludableTargets.slice(0, 5).map(target => (
+ <div key={target.id} className="flex items-center gap-2">
+ <Badge variant="outline" className="text-xs">
+ {target.vendorCode}
+ </Badge>
+ <span className="text-xs">{target.vendorName}</span>
+ </div>
+ ))}
+ {excludableTargets.length > 5 && (
+ <p className="text-xs text-muted-foreground">
+ ...외 {excludableTargets.length - 5}개
+ </p>
+ )}
+ </div>
+ </div>
+ )}
+ </div>
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel>
+ <AlertDialogAction
+ onClick={handleExclude}
+ disabled={isLoading || excludableTargets.length === 0}
+ className="bg-red-600 hover:bg-red-700"
+ >
+ {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ 제외 ({excludableTargets.length})
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ )
+}
+
+// ----------------------------------------------------------------
+// 의견 요청 다이얼로그
+// ----------------------------------------------------------------
+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<string>()
+ 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 (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="sm:max-w-md">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <MessageSquare className="h-5 w-5 text-blue-600" />
+ 평가 의견 요청
+ </DialogTitle>
+ <DialogDescription>
+ 선택된 평가 대상에 대한 의견을 담당자들에게 요청합니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ {/* 요약 정보 */}
+ <div className="p-3 bg-blue-50 rounded-lg border border-blue-200">
+ <div className="text-sm space-y-1">
+ <p>
+ <span className="font-medium">요청 대상:</span> {reviewableTargets.length}개 평가 항목
+ </p>
+ <p>
+ <span className="font-medium">받는 사람:</span> {reviewerEmails.length}명의 담당자
+ </p>
+ </div>
+ </div>
+
+ {/* 메시지 입력 */}
+ <div className="space-y-2">
+ <Label htmlFor="review-message">추가 메시지 (선택사항)</Label>
+ <Textarea
+ id="review-message"
+ placeholder="담당자들에게 전달할 추가 메시지를 입력하세요..."
+ value={message}
+ onChange={(e) => setMessage(e.target.value)}
+ rows={3}
+ />
+ </div>
+
+ {reviewableTargets.length !== targets.length && (
+ <div className="p-3 bg-yellow-50 rounded-lg border border-yellow-200">
+ <p className="text-sm text-yellow-800">
+ <AlertTriangle className="h-4 w-4 inline mr-1" />
+ 대기중 상태인 항목만 의견 요청 가능합니다.
+ ({targets.length - reviewableTargets.length}개 항목 제외됨)
+ </p>
+ </div>
+ )}
+ </div>
+
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={() => onOpenChange(false)}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button
+ onClick={handleRequestReview}
+ disabled={isLoading || reviewableTargets.length === 0}
+ >
+ {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ 의견 요청 발송
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file