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/update-evaluation-target.tsx | 760 +++++++++++++++++++++ 1 file changed, 760 insertions(+) create mode 100644 lib/evaluation-target-list/table/update-evaluation-target.tsx (limited to 'lib/evaluation-target-list/table/update-evaluation-target.tsx') diff --git a/lib/evaluation-target-list/table/update-evaluation-target.tsx b/lib/evaluation-target-list/table/update-evaluation-target.tsx new file mode 100644 index 00000000..0d56addb --- /dev/null +++ b/lib/evaluation-target-list/table/update-evaluation-target.tsx @@ -0,0 +1,760 @@ +"use client" + +import * as React from "react" +import { useRouter } from "next/navigation" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" +import { Check, ChevronsUpDown, Loader2, X } from "lucide-react" +import { toast } from "sonner" +import { useSession } from "next-auth/react" + +import { Button } from "@/components/ui/button" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { cn } from "@/lib/utils" + +import { + updateEvaluationTarget, + getAvailableReviewers, + getDepartmentInfo, + type UpdateEvaluationTargetInput, +} from "../service" +import { EvaluationTargetWithDepartments } from "@/db/schema" + +// 편집 가능한 필드들에 대한 스키마 +const editEvaluationTargetSchema = z.object({ + adminComment: z.string().optional(), + consolidatedComment: z.string().optional(), + ldClaimCount: z.number().min(0).optional(), + ldClaimAmount: z.number().min(0).optional(), + ldClaimCurrency: z.enum(["KRW", "USD", "EUR", "JPY"]).optional(), + consensusStatus: z.boolean().nullable().optional(), + orderIsApproved: z.boolean().nullable().optional(), + procurementIsApproved: z.boolean().nullable().optional(), + qualityIsApproved: z.boolean().nullable().optional(), + designIsApproved: z.boolean().nullable().optional(), + csIsApproved: z.boolean().nullable().optional(), + // 담당자 정보 수정 + orderReviewerEmail: z.string().optional(), + procurementReviewerEmail: z.string().optional(), + qualityReviewerEmail: z.string().optional(), + designReviewerEmail: z.string().optional(), + csReviewerEmail: z.string().optional(), +}) + +type EditEvaluationTargetFormValues = z.infer + +interface EditEvaluationTargetSheetProps { + open: boolean + onOpenChange: (open: boolean) => void + evaluationTarget: EvaluationTargetWithDepartments | null +} + +// 권한 타입 정의 +type PermissionLevel = "none" | "department" | "admin" + +interface UserPermissions { + level: PermissionLevel + editableApprovals: string[] // 편집 가능한 approval 필드들 +} + +export function EditEvaluationTargetSheet({ + open, + onOpenChange, + evaluationTarget, +}: EditEvaluationTargetSheetProps) { + const router = useRouter() + const [isSubmitting, setIsSubmitting] = React.useState(false) + const { data: session } = useSession() + + // 담당자 관련 상태 + const [reviewers, setReviewers] = React.useState>([]) + const [isLoadingReviewers, setIsLoadingReviewers] = React.useState(false) + + // 각 부서별 담당자 선택 상태 + const [reviewerSearches, setReviewerSearches] = React.useState>({}) + const [reviewerOpens, setReviewerOpens] = React.useState>({}) + + // 부서 정보 상태 + const [departments, setDepartments] = React.useState>([]) + + // 사용자 권한 계산 + const userPermissions = React.useMemo((): UserPermissions => { + if (!session?.user || !evaluationTarget) { + return { level: "none", editableApprovals: [] } + } + + const userEmail = session.user.email + const userRole = session.user.role + + // 평가관리자는 모든 권한 + if (userRole === "평가관리자") { + return { + level: "admin", + editableApprovals: [ + "orderIsApproved", + "procurementIsApproved", + "qualityIsApproved", + "designIsApproved", + "csIsApproved" + ] + } + } + + // 부서별 담당자 권한 확인 + const editableApprovals: string[] = [] + + if (evaluationTarget.orderReviewerEmail === userEmail) { + editableApprovals.push("orderIsApproved") + } + if (evaluationTarget.procurementReviewerEmail === userEmail) { + editableApprovals.push("procurementIsApproved") + } + if (evaluationTarget.qualityReviewerEmail === userEmail) { + editableApprovals.push("qualityIsApproved") + } + if (evaluationTarget.designReviewerEmail === userEmail) { + editableApprovals.push("designIsApproved") + } + if (evaluationTarget.csReviewerEmail === userEmail) { + editableApprovals.push("csIsApproved") + } + + return { + level: editableApprovals.length > 0 ? "department" : "none", + editableApprovals + } + }, [session, evaluationTarget]) + + const form = useForm({ + resolver: zodResolver(editEvaluationTargetSchema), + defaultValues: { + adminComment: "", + consolidatedComment: "", + ldClaimCount: 0, + ldClaimAmount: 0, + ldClaimCurrency: "KRW", + consensusStatus: null, + orderIsApproved: null, + procurementIsApproved: null, + qualityIsApproved: null, + designIsApproved: null, + csIsApproved: null, + orderReviewerEmail: "", + procurementReviewerEmail: "", + qualityReviewerEmail: "", + designReviewerEmail: "", + csReviewerEmail: "", + }, + }) + + // 부서 정보 로드 + const loadDepartments = React.useCallback(async () => { + try { + const departmentList = await getDepartmentInfo() + setDepartments(departmentList) + } catch (error) { + console.error("Error loading departments:", error) + toast.error("부서 정보를 불러오는데 실패했습니다.") + } + }, []) + + // 담당자 목록 로드 + const loadReviewers = React.useCallback(async () => { + setIsLoadingReviewers(true) + try { + const reviewerList = await getAvailableReviewers() + setReviewers(reviewerList) + } catch (error) { + console.error("Error loading reviewers:", error) + toast.error("담당자 목록을 불러오는데 실패했습니다.") + } finally { + setIsLoadingReviewers(false) + } + }, []) + + // 시트가 열릴 때 데이터 로드 및 폼 초기화 + React.useEffect(() => { + if (open && evaluationTarget) { + loadDepartments() + loadReviewers() + + // 폼에 기존 데이터 설정 + form.reset({ + adminComment: evaluationTarget.adminComment || "", + consolidatedComment: evaluationTarget.consolidatedComment || "", + ldClaimCount: evaluationTarget.ldClaimCount || 0, + ldClaimAmount: parseFloat(evaluationTarget.ldClaimAmount || "0"), + ldClaimCurrency: evaluationTarget.ldClaimCurrency || "KRW", + consensusStatus: evaluationTarget.consensusStatus, + orderIsApproved: evaluationTarget.orderIsApproved, + procurementIsApproved: evaluationTarget.procurementIsApproved, + qualityIsApproved: evaluationTarget.qualityIsApproved, + designIsApproved: evaluationTarget.designIsApproved, + csIsApproved: evaluationTarget.csIsApproved, + orderReviewerEmail: evaluationTarget.orderReviewerEmail || "", + procurementReviewerEmail: evaluationTarget.procurementReviewerEmail || "", + qualityReviewerEmail: evaluationTarget.qualityReviewerEmail || "", + designReviewerEmail: evaluationTarget.designReviewerEmail || "", + csReviewerEmail: evaluationTarget.csReviewerEmail || "", + }) + } + }, [open, evaluationTarget, form]) + + // 폼 제출 + async function onSubmit(data: EditEvaluationTargetFormValues) { + if (!evaluationTarget) return + + setIsSubmitting(true) + try { + const input: UpdateEvaluationTargetInput = { + id: evaluationTarget.id, + ...data, + } + + console.log("Updating evaluation target:", input) + + const result = await updateEvaluationTarget(input) + + if (result.success) { + toast.success(result.message || "평가 대상이 성공적으로 수정되었습니다.") + onOpenChange(false) + router.refresh() + } else { + toast.error(result.error || "평가 대상 수정에 실패했습니다.") + } + } catch (error) { + console.error("Error updating evaluation target:", error) + toast.error("평가 대상 수정 중 오류가 발생했습니다.") + } finally { + setIsSubmitting(false) + } + } + + // 시트 닫기 핸들러 + const handleOpenChange = (open: boolean) => { + onOpenChange(open) + if (!open) { + form.reset() + setReviewerSearches({}) + setReviewerOpens({}) + } + } + + // 담당자 검색 필터링 + const getFilteredReviewers = (search: string) => { + if (!search) return reviewers + return reviewers.filter(reviewer => + reviewer.name.toLowerCase().includes(search.toLowerCase()) || + reviewer.email.toLowerCase().includes(search.toLowerCase()) + ) + } + + // 필드 편집 권한 확인 + const canEditField = (fieldName: string): boolean => { + if (userPermissions.level === "admin") return true + if (userPermissions.level === "none") return false + + // 부서 담당자는 자신의 approval만 편집 가능 + return userPermissions.editableApprovals.includes(fieldName) + } + + // 관리자 전용 필드 확인 + const canEditAdminFields = (): boolean => { + return userPermissions.level === "admin" + } + + if (!evaluationTarget) { + return null + } + + // 권한이 없는 경우 + if (userPermissions.level === "none") { + return ( + + + + 평가 대상 수정 + + 권한이 없어 수정할 수 없습니다. + + +
+

+ 이 평가 대상을 수정할 권한이 없습니다. +

+
+
+
+ ) + } + + return ( + + + + 평가 대상 수정 + + 평가 대상 정보를 수정합니다. + {userPermissions.level === "department" && ( +
+ 부서 담당자 권한: 해당 부서의 평가 항목만 수정 가능합니다. +
+ )} + {userPermissions.level === "admin" && ( +
+ 평가관리자 권한: 모든 항목을 수정할 수 있습니다. +
+ )} +
+
+ +
+ + {/* 스크롤 가능한 컨텐츠 영역 */} +
+
+ {/* 기본 정보 (읽기 전용) */} + + + 기본 정보 + + +
+
+ 평가년도: {evaluationTarget.evaluationYear} +
+
+ 구분: {evaluationTarget.division === "PLANT" ? "해양" : "조선"} +
+
+ 벤더 코드: {evaluationTarget.vendorCode} +
+
+ 벤더명: {evaluationTarget.vendorName} +
+
+ 자재구분: {evaluationTarget.materialType} +
+
+ 상태: {evaluationTarget.status} +
+
+
+
+ + {/* L/D 클레임 정보 (관리자만) */} + {canEditAdminFields() && ( + + + L/D 클레임 정보 + + +
+ ( + + 클레임 건수 + + field.onChange(parseInt(e.target.value) || 0)} + /> + + + + )} + /> + + ( + + 클레임 금액 + + field.onChange(parseFloat(e.target.value) || 0)} + /> + + + + )} + /> + + ( + + 통화단위 + + + + )} + /> +
+
+
+ )} + + {/* 평가 상태 */} + + + 평가 상태 + + + + {/* 의견 일치 여부 (관리자만) */} + {canEditAdminFields() && ( + ( + + 의견 일치 여부 + + + + )} + /> + )} + + {/* 각 부서별 평가 */} +
+ {[ + { key: "orderIsApproved", label: "주문 부서 평가", email: evaluationTarget.orderReviewerEmail }, + { key: "procurementIsApproved", label: "조달 부서 평가", email: evaluationTarget.procurementReviewerEmail }, + { key: "qualityIsApproved", label: "품질 부서 평가", email: evaluationTarget.qualityReviewerEmail }, + { key: "designIsApproved", label: "설계 부서 평가", email: evaluationTarget.designReviewerEmail }, + { key: "csIsApproved", label: "CS 부서 평가", email: evaluationTarget.csReviewerEmail }, + ].map(({ key, label, email }) => ( + ( + + + {label} + {email && ( + + 담당자: {email} + + )} + + + {!canEditField(key) && ( +

+ 편집 권한이 없습니다. +

+ )} + +
+ )} + /> + ))} +
+
+
+ + {/* 의견 */} + + + 의견 + + + {/* 관리자 의견 (관리자만) */} + {canEditAdminFields() && ( + ( + + 관리자 의견 + +