summaryrefslogtreecommitdiff
path: root/lib/tbe-last/table/evaluation-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/tbe-last/table/evaluation-dialog.tsx')
-rw-r--r--lib/tbe-last/table/evaluation-dialog.tsx432
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