diff options
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.tsx | 384 |
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 |
