summaryrefslogtreecommitdiff
path: root/lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-07 01:44:45 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-07 01:44:45 +0000
commit90f79a7a691943a496f67f01c1e493256070e4de (patch)
tree37275fde3ae08c2bca384fbbc8eb378de7e39230 /lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx
parentfbb3b7f05737f9571b04b0a8f4f15c0928de8545 (diff)
(대표님) 변경사항 20250707 10시 43분 - unstaged 변경사항 추가
Diffstat (limited to 'lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx')
-rw-r--r--lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx305
1 files changed, 305 insertions, 0 deletions
diff --git a/lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx b/lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx
new file mode 100644
index 00000000..7d6ca45d
--- /dev/null
+++ b/lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx
@@ -0,0 +1,305 @@
+"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: "S", label: "S등급 (90점 이상)" },
+ { value: "A", label: "A등급 (80-89점)" },
+ { value: "B", label: "B등급 (70-79점)" },
+ { value: "C", label: "C등급 (60-69점)" },
+ { value: "D", label: "D등급 (60점 미만)" },
+] as const
+
+// 점수에 따른 등급 계산
+const calculateGrade = (score: number): string => {
+ if (score >= 90) return "S"
+ if (score >= 80) return "A"
+ if (score >= 70) return "B"
+ if (score >= 60) return "C"
+ return "D"
+}
+
+// 개별 평가 스키마
+const evaluationItemSchema = z.object({
+ id: z.number(),
+ vendorName: z.string(),
+ vendorCode: z.string(),
+ evaluationScore: z.number().nullable(),
+ finalScore: z.number()
+ .min(0, "점수는 0 이상이어야 합니다"),
+ // .max(100, "점수는 100 이하여야 합니다"),
+ finalGrade: z.enum(["S", "A", "B", "C", "D"]),
+})
+
+// 전체 폼 스키마
+const finalizeEvaluationSchema = z.object({
+ evaluations: z.array(evaluationItemSchema).min(1, "확정할 평가가 없습니다"),
+})
+
+type FinalizeEvaluationFormData = z.infer<typeof finalizeEvaluationSchema>
+
+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<FinalizeEvaluationFormData>({
+ 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 => ({
+ id: evaluation.id,
+ vendorName: evaluation.vendorName || "",
+ vendorCode: evaluation.vendorCode || "",
+ evaluationScore: evaluation.evaluationScore || null,
+ finalScore: Number(evaluation.evaluationScore || 0),
+ finalGrade: calculateGrade(Number(evaluation.evaluationScore || 0)),
+ }))
+
+ form.reset({ evaluations: formData })
+ }
+ }, [evaluations, form])
+
+ // 점수 변경 시 등급 자동 계산
+ const handleScoreChange = (index: number, score: number) => {
+ const currentEvaluation = form.getValues(`evaluations.${index}`)
+ const newGrade = calculateGrade(score)
+
+ update(index, {
+ ...currentEvaluation,
+ finalScore: score,
+ finalGrade: newGrade,
+ })
+ }
+
+ // 폼 제출
+ 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 (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <CheckCircle2 className="h-5 w-5 text-purple-600" />
+ 평가 확정
+ </DialogTitle>
+ <DialogDescription>
+ 검토가 완료된 평가의 최종 점수와 등급을 확정합니다.
+ 확정 후에는 수정이 제한됩니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <Alert>
+ <AlertCircle className="h-4 w-4" />
+ <AlertDescription>
+ 확정할 평가: <strong>{evaluations.length}건</strong>
+ <br />
+ 평가 점수는 리뷰어들의 평가를 바탕으로 계산된 값을 기본으로 하며, 필요시 조정 가능합니다.
+ </AlertDescription>
+ </Alert>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
+ <div className="rounded-md border">
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead className="w-[200px]">협력업체</TableHead>
+ <TableHead className="w-[100px]">평가점수</TableHead>
+ <TableHead className="w-[120px]">최종점수</TableHead>
+ <TableHead className="w-[120px]">최종등급</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {fields.map((field, index) => (
+ <TableRow key={field.id}>
+ <TableCell>
+ <div className="space-y-1">
+ <div className="font-medium">
+ {form.watch(`evaluations.${index}.vendorName`)}
+ </div>
+ <div className="text-sm text-muted-foreground">
+ {form.watch(`evaluations.${index}.vendorCode`)}
+ </div>
+ </div>
+ </TableCell>
+
+ <TableCell>
+ <div className="text-center">
+ {form.watch(`evaluations.${index}.evaluationScore`) !== null ? (
+ <Badge variant="outline" className="font-mono">
+ {Number(form.watch(`evaluations.${index}.evaluationScore`)).toFixed(1)}점
+ </Badge>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ )}
+ </div>
+ </TableCell>
+
+ <TableCell>
+ <FormField
+ control={form.control}
+ name={`evaluations.${index}.finalScore`}
+ render={({ field }) => (
+ <FormItem>
+ <FormControl>
+ <Input
+ type="number"
+ min="0"
+ max="100"
+ step="0.1"
+ {...field}
+ onChange={(e) => {
+ const value = parseFloat(e.target.value)
+ field.onChange(value)
+ if (!isNaN(value)) {
+ handleScoreChange(index, value)
+ }
+ }}
+ className="text-center font-mono"
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </TableCell>
+
+ <TableCell>
+ <FormField
+ control={form.control}
+ name={`evaluations.${index}.finalGrade`}
+ render={({ field }) => (
+ <FormItem>
+ <FormControl>
+ <Select value={field.value} onValueChange={field.onChange}>
+ <SelectTrigger>
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ {GRADE_OPTIONS.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ </div>
+
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => onOpenChange(false)}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button
+ type="submit"
+ disabled={isLoading}
+ className="bg-purple-600 hover:bg-purple-700"
+ >
+ {isLoading ? "확정 중..." : `평가 확정 (${fields.length}건)`}
+ </Button>
+ </DialogFooter>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file