diff options
Diffstat (limited to 'lib/tbe-last/table/evaluation-dialog.tsx')
| -rw-r--r-- | lib/tbe-last/table/evaluation-dialog.tsx | 432 |
1 files changed, 432 insertions, 0 deletions
diff --git a/lib/tbe-last/table/evaluation-dialog.tsx b/lib/tbe-last/table/evaluation-dialog.tsx new file mode 100644 index 00000000..ac1d923b --- /dev/null +++ b/lib/tbe-last/table/evaluation-dialog.tsx @@ -0,0 +1,432 @@ +// lib/tbe-last/table/dialogs/evaluation-dialog.tsx + +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Textarea } from "@/components/ui/textarea" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Checkbox } from "@/components/ui/checkbox" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { TbeLastView } from "@/db/schema" +import { toast } from "sonner" +import { updateTbeEvaluation ,getTbeVendorDocuments} from "../service" +import { + FileText, + CheckCircle, + XCircle, + AlertCircle, + Clock, + Loader2, + Info +} from "lucide-react" + +// 폼 스키마 +const evaluationSchema = z.object({ + evaluationResult: z.enum(["Acceptable", "Acceptable with Comment", "Not Acceptable"], { + required_error: "평가 결과를 선택해주세요", + }), + conditionalRequirements: z.string().optional(), + conditionsFulfilled: z.boolean().default(false), + overallRemarks: z.string().optional(), +}) + +type EvaluationFormValues = z.infer<typeof evaluationSchema> + +interface EvaluationDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + selectedSession: TbeLastView | null + onSuccess?: () => void +} + +export function EvaluationDialog({ + open, + onOpenChange, + selectedSession, + onSuccess +}: EvaluationDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [isLoadingDocs, setIsLoadingDocs] = React.useState(false) + const [vendorDocuments, setVendorDocuments] = React.useState<any[]>([]) + + const form = useForm<EvaluationFormValues>({ + resolver: zodResolver(evaluationSchema), + defaultValues: { + evaluationResult: undefined, + conditionalRequirements: "", + conditionsFulfilled: false, + overallRemarks: "", + }, + }) + + const watchEvaluationResult = form.watch("evaluationResult") + const isFormValid = form.formState.isValid + + // 벤더 문서 리뷰 상태 가져오기 + React.useEffect(() => { + if (open && selectedSession?.tbeSessionId) { + fetchVendorDocuments() + + // 기존 평가 데이터가 있으면 폼에 설정 + if (selectedSession.evaluationResult) { + form.reset({ + evaluationResult: selectedSession.evaluationResult as any, + conditionalRequirements: selectedSession.conditionalRequirements || "", + conditionsFulfilled: selectedSession.conditionsFulfilled || false, + overallRemarks: selectedSession.overallRemarks || "", + }) + } else { + // 기존 평가 데이터가 없으면 초기화 + form.reset({ + evaluationResult: undefined, + conditionalRequirements: "", + conditionsFulfilled: false, + overallRemarks: "", + }) + } + } else if (!open) { + // 다이얼로그가 닫힐 때 폼 리셋 + form.reset({ + evaluationResult: undefined, + conditionalRequirements: "", + conditionsFulfilled: false, + overallRemarks: "", + }) + setVendorDocuments([]) + } + }, [open, selectedSession]) + + const fetchVendorDocuments = async () => { + if (!selectedSession?.tbeSessionId) return + + setIsLoadingDocs(true) + try { + // 서버 액션 호출 + const result = await getTbeVendorDocuments(selectedSession.tbeSessionId) + + if (result.success) { + setVendorDocuments(result.documents || []) + } else { + console.error("Failed to fetch vendor documents:", result.error) + toast.error(result.error || "벤더 문서 정보를 불러오는데 실패했습니다") + } + } catch (error) { + console.error("Failed to fetch vendor documents:", error) + toast.error("벤더 문서 정보를 불러오는데 실패했습니다") + } finally { + setIsLoadingDocs(false) + } + } + + const getReviewStatusIcon = (status: string) => { + switch (status) { + case "승인": + return <CheckCircle className="h-4 w-4 text-green-600" /> + case "반려": + return <XCircle className="h-4 w-4 text-red-600" /> + case "재검토필요": + return <AlertCircle className="h-4 w-4 text-yellow-600" /> + case "검토완료": + return <CheckCircle className="h-4 w-4 text-blue-600" /> + case "검토중": + return <Clock className="h-4 w-4 text-orange-600" /> + default: + return <Clock className="h-4 w-4 text-gray-400" /> + } + } + + const getReviewStatusVariant = (status: string): any => { + switch (status) { + case "승인": + return "default" + case "반려": + return "destructive" + case "재검토필요": + return "secondary" + case "검토완료": + return "outline" + default: + return "outline" + } + } + + const onSubmit = async (values: EvaluationFormValues) => { + if (!selectedSession?.tbeSessionId) return + + // 벤더 문서가 없는 경우 경고 + if (vendorDocuments.length === 0 && !isLoadingDocs) { + const confirmed = window.confirm( + "검토된 벤더 문서가 없습니다. 그래도 평가를 진행하시겠습니까?" + ) + if (!confirmed) return + } + + setIsLoading(true) + try { + // 서버 액션 호출 + const result = await updateTbeEvaluation(selectedSession.tbeSessionId, { + ...values, + status: "완료", // 평가 완료 시 상태 업데이트 + }) + + if (result.success) { + toast.success("평가가 성공적으로 저장되었습니다") + form.reset() + onOpenChange(false) + onSuccess?.() + } else { + toast.error(result.error || "평가 저장에 실패했습니다") + } + } catch (error) { + console.error("Failed to save evaluation:", error) + toast.error("평가 저장 중 오류가 발생했습니다") + } finally { + setIsLoading(false) + } + } + + const allDocumentsApproved = vendorDocuments.length > 0 && + vendorDocuments.every((doc: any) => doc.reviewStatus === "승인" || doc.reviewStatus === "검토완료") + + const hasRejectedDocuments = vendorDocuments.some((doc: any) => doc.reviewStatus === "반려") + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-3xl max-h-[90vh]"> + <DialogHeader> + <DialogTitle>TBE 결과 입력</DialogTitle> + <DialogDescription> + {selectedSession?.sessionCode} - {selectedSession?.vendorName} + </DialogDescription> + </DialogHeader> + + <div className="overflow-y-auto max-h-[calc(90vh-200px)] pr-4"> + <div className="space-y-6"> + {/* 벤더 문서 검토 현황 */} + <div className="space-y-3"> + <h3 className="text-sm font-semibold">벤더 문서 검토 현황</h3> + + {isLoadingDocs ? ( + <div className="flex items-center justify-center py-4"> + <Loader2 className="h-4 w-4 animate-spin mr-2" /> + <span className="text-sm text-muted-foreground">문서 정보 로딩 중...</span> + </div> + ) : vendorDocuments.length === 0 ? ( + <Alert> + <Info className="h-4 w-4" /> + <AlertDescription> + 검토할 벤더 문서가 없습니다. + </AlertDescription> + </Alert> + ) : ( + <div className="space-y-2"> + {vendorDocuments.map((doc: any) => ( + <div key={doc.id} className="flex items-center justify-between p-3 border rounded-lg"> + <div className="flex items-center gap-3"> + <FileText className="h-4 w-4 text-muted-foreground" /> + <div> + <p className="text-sm font-medium">{doc.documentName}</p> + <p className="text-xs text-muted-foreground">{doc.documentType}</p> + </div> + </div> + <div className="flex items-center gap-2"> + {getReviewStatusIcon(doc.reviewStatus)} + <Badge variant={getReviewStatusVariant(doc.reviewStatus)}> + {doc.reviewStatus} + </Badge> + </div> + </div> + ))} + + {/* 문서 검토 상태 요약 */} + <div className="mt-3 p-3 bg-muted rounded-lg"> + <div className="flex items-center justify-between text-sm"> + <span>전체 문서: {vendorDocuments.length}개</span> + <div className="flex items-center gap-4"> + <span className="text-green-600"> + 승인: {vendorDocuments.filter(d => d.reviewStatus === "승인").length} + </span> + <span className="text-red-600"> + 반려: {vendorDocuments.filter(d => d.reviewStatus === "반려").length} + </span> + <span className="text-gray-600"> + 미검토: {vendorDocuments.filter(d => d.reviewStatus === "미검토").length} + </span> + </div> + </div> + + {hasRejectedDocuments && ( + <Alert className="mt-2" variant="destructive"> + <AlertCircle className="h-4 w-4" /> + <AlertDescription> + 반려된 문서가 있습니다. 평가 결과를 "Not Acceptable"로 설정하는 것을 권장합니다. + </AlertDescription> + </Alert> + )} + + {!allDocumentsApproved && !hasRejectedDocuments && ( + <Alert className="mt-2"> + <Info className="h-4 w-4" /> + <AlertDescription> + 모든 문서 검토가 완료되지 않았습니다. + </AlertDescription> + </Alert> + )} + </div> + </div> + )} + </div> + + {/* 평가 폼 */} + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + <FormField + control={form.control} + name="evaluationResult" + render={({ field }) => ( + <FormItem> + <FormLabel> + 평가 결과 <span className="text-red-500">*</span> + </FormLabel> + <Select onValueChange={field.onChange} defaultValue={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue placeholder="평가 결과를 선택하세요" /> + </SelectTrigger> + </FormControl> + <SelectContent> + <SelectItem value="Acceptable">Acceptable</SelectItem> + <SelectItem value="Acceptable with Comment">Acceptable with Comment</SelectItem> + <SelectItem value="Not Acceptable">Not Acceptable</SelectItem> + </SelectContent> + </Select> + <FormDescription> + 최종 평가 결과를 선택합니다. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* 조건부 승인 필드 */} + {watchEvaluationResult === "Acceptable with Comment" && ( + <> + <FormField + control={form.control} + name="conditionalRequirements" + render={({ field }) => ( + <FormItem> + <FormLabel>조건부 요구사항</FormLabel> + <FormControl> + <Textarea + placeholder="조건부 승인에 필요한 요구사항을 입력하세요..." + className="min-h-[100px]" + {...field} + /> + </FormControl> + <FormDescription> + 벤더가 충족해야 할 조건을 명확히 기술합니다. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="conditionsFulfilled" + render={({ field }) => ( + <FormItem className="flex flex-row items-start space-x-3 space-y-0"> + <FormControl> + <Checkbox + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + <div className="space-y-1 leading-none"> + <FormLabel> + 조건 충족 확인 + </FormLabel> + <FormDescription> + 벤더가 요구 조건을 모두 충족했는지 확인합니다. + </FormDescription> + </div> + </FormItem> + )} + /> + </> + )} + + {/* 평가 요약 - 종합 의견만 */} + <FormField + control={form.control} + name="overallRemarks" + render={({ field }) => ( + <FormItem> + <FormLabel>종합 의견</FormLabel> + <FormControl> + <Textarea + placeholder="종합적인 평가 의견을 입력하세요..." + className="min-h-[100px]" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </form> + </Form> + </div> + </div> + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isLoading} + > + 취소 + </Button> + <Button + type="submit" + onClick={form.handleSubmit(onSubmit)} + disabled={isLoading || !isFormValid} + > + {isLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />} + 평가 저장 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
