"use client" import * as React from "react" import { zodResolver } from "@hookform/resolvers/zod" import { useForm, useFieldArray } from "react-hook-form" import * as z from "zod" import { toast } from "sonner" import { CheckCircle2, AlertCircle, Building2 } from "lucide-react" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" import { Alert, AlertDescription } from "@/components/ui/alert" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { PeriodicEvaluationView } from "@/db/schema" import { finalizeEvaluations } from "../service" // 등급 옵션 const GRADE_OPTIONS = [ { value: "A", label: "A등급 (95점 이상)" }, { value: "B", label: "B등급 (90-95점 미만)" }, { value: "C", label: "C등급 (60-90점 미만)" }, { value: "D", label: "D등급 (60점 미만)" }, ] as const // 점수에 따른 등급 계산 const calculateGrade = (score: number): "A" | "B" | "C" | "D" => { if (score >= 95) return "A" if (score >= 90) return "B" if (score >= 60) return "C" return "D" } // 등급에 따른 점수 계산 (등급 변경 시 점수 자동 조정) const calculateScoreFromGrade = (grade: "A" | "B" | "C" | "D"): number => { switch (grade) { case "A": return 95 // A등급 최소 점수 case "B": return 90 // B등급 최소 점수 case "C": return 60 // C등급 최소 점수 case "D": return 0 // D등급은 0점으로 설정 (또는 30점 중간값) default: return 0 } } // 평가점수 계산 (evaluation-columns.tsx와 동일한 로직) const calculateEvaluationScore = (evaluation: PeriodicEvaluationView): number => { const processScore = Number(evaluation.processScore || 0); const priceScore = Number(evaluation.priceScore || 0); const deliveryScore = Number(evaluation.deliveryScore || 0); const selfEvaluationScore = Number(evaluation.selfEvaluationScore || 0); const participationBonus = Number(evaluation.participationBonus || 0); const qualityDeduction = Number(evaluation.qualityDeduction || 0); const totalScore = processScore + priceScore + deliveryScore + selfEvaluationScore; const evaluationScore = totalScore + participationBonus - qualityDeduction; return evaluationScore; } // 개별 평가 스키마 const evaluationItemSchema = z.object({ id: z.number(), vendorName: z.string(), vendorCode: z.string(), evaluationScore: z.coerce.number().nullable(), finalScore: z.coerce.number() .min(0, "점수는 0 이상이어야 합니다") .max(100, "점수는 100 이하여야 합니다"), finalGrade: z.enum(["A", "B", "C", "D"]), }) // 전체 폼 스키마 const finalizeEvaluationSchema = z.object({ evaluations: z.array(evaluationItemSchema).min(1, "확정할 평가가 없습니다"), }) type FinalizeEvaluationFormData = z.infer interface FinalizeEvaluationDialogProps { open: boolean onOpenChange: (open: boolean) => void evaluations: PeriodicEvaluationView[] onSuccess?: () => void } export function FinalizeEvaluationDialog({ open, onOpenChange, evaluations, onSuccess, }: FinalizeEvaluationDialogProps) { const [isLoading, setIsLoading] = React.useState(false) const form = useForm({ resolver: zodResolver(finalizeEvaluationSchema), defaultValues: { evaluations: [], }, }) const { fields, update } = useFieldArray({ control: form.control, name: "evaluations", }) // evaluations가 변경될 때 폼 초기화 React.useEffect(() => { if (evaluations.length > 0) { const formData = evaluations.map(evaluation => { // 평가점수 계산 (참고용) const evaluationScore = calculateEvaluationScore(evaluation); // 최종점수가 있으면 우선 사용, 없으면 평가점수 사용 const finalScoreValue = evaluation.finalScore ? Number(evaluation.finalScore) : (evaluationScore > 0 ? evaluationScore : 0); // 최종등급이 있으면 우선 사용, 없으면 점수 기반으로 계산 const finalGradeValue = evaluation.finalGrade ? evaluation.finalGrade : calculateGrade(finalScoreValue); return { id: evaluation.id, vendorName: evaluation.vendorName || "", vendorCode: evaluation.vendorCode || "", evaluationScore: evaluationScore > 0 ? evaluationScore : null, finalScore: finalScoreValue, finalGrade: finalGradeValue, }; }); form.reset({ evaluations: formData }) } }, [evaluations, form]) // 점수 변경 시 등급 자동 계산 const handleScoreChange = (index: number, score: number) => { const newGrade = calculateGrade(score) // form.setValue를 사용하여 리렌더링 최소화 form.setValue(`evaluations.${index}.finalGrade`, newGrade, { shouldValidate: false }) } // 등급 변경 시 점수 자동 조정 const handleGradeChange = (index: number, grade: "A" | "B" | "C" | "D") => { const currentEvaluation = form.getValues(`evaluations.${index}`) const newScore = calculateScoreFromGrade(grade) update(index, { ...currentEvaluation, finalScore: newScore, finalGrade: grade, }) } // 폼 제출 const onSubmit = async (data: FinalizeEvaluationFormData) => { try { setIsLoading(true) const finalizeData = data.evaluations.map(evaluation => ({ id: evaluation.id, finalScore: evaluation.finalScore, finalGrade: evaluation.finalGrade, })) await finalizeEvaluations(finalizeData) toast.success("평가가 확정되었습니다", { description: `${data.evaluations.length}건의 평가가 최종 확정되었습니다.`, }) onSuccess?.() onOpenChange(false) } catch (error) { console.error("Failed to finalize evaluations:", error) toast.error("평가 확정 실패", { description: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.", }) } finally { setIsLoading(false) } } return ( 평가 확정 검토가 완료된 평가의 최종 점수와 등급을 확정합니다. 확정 후에는 수정이 제한됩니다. 확정할 평가: {evaluations.length}건
평가 점수는 리뷰어들의 평가를 바탕으로 계산된 값을 기본으로 하며, 필요시 조정 가능합니다.
협력업체 평가점수 최종점수 최종등급 {fields.map((field, index) => (
{form.watch(`evaluations.${index}.vendorName`)}
{form.watch(`evaluations.${index}.vendorCode`)}
{form.watch(`evaluations.${index}.evaluationScore`) !== null ? ( {Number(form.watch(`evaluations.${index}.evaluationScore`)).toFixed(1)}점 ) : ( - )}
( { const inputValue = e.target.value if (inputValue === "" || inputValue === "-") { field.onChange(0) } else { const numValue = Number(inputValue) if (!isNaN(numValue)) { // 입력 중에는 제한하지 않고, blur 시에만 제한 적용 field.onChange(numValue) } } }} onBlur={(e) => { const value = Number(e.target.value) const clampedValue = isNaN(value) ? 0 : Math.max(0, Math.min(100, value)) field.onChange(clampedValue) handleScoreChange(index, clampedValue) field.onBlur() }} className="text-center font-mono" /> )} /> ( )} />
))}
) }