diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-07 01:44:45 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-07 01:44:45 +0000 |
| commit | 90f79a7a691943a496f67f01c1e493256070e4de (patch) | |
| tree | 37275fde3ae08c2bca384fbbc8eb378de7e39230 /lib/evaluation/table/evaluation-details-dialog.tsx | |
| parent | fbb3b7f05737f9571b04b0a8f4f15c0928de8545 (diff) | |
(대표님) 변경사항 20250707 10시 43분 - unstaged 변경사항 추가
Diffstat (limited to 'lib/evaluation/table/evaluation-details-dialog.tsx')
| -rw-r--r-- | lib/evaluation/table/evaluation-details-dialog.tsx | 366 |
1 files changed, 366 insertions, 0 deletions
diff --git a/lib/evaluation/table/evaluation-details-dialog.tsx b/lib/evaluation/table/evaluation-details-dialog.tsx new file mode 100644 index 00000000..df4ef016 --- /dev/null +++ b/lib/evaluation/table/evaluation-details-dialog.tsx @@ -0,0 +1,366 @@ +"use client" + +import * as React from "react" +import { + Eye, + Building2, + User, + Calendar, + CheckCircle2, + Clock, + MessageSquare, + Award, + FileText +} from "lucide-react" + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Separator } from "@/components/ui/separator" +import { Skeleton } from "@/components/ui/skeleton" +import { PeriodicEvaluationView } from "@/db/schema" +import { getEvaluationDetails, type EvaluationDetailData } from "../service" + +interface EvaluationDetailsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + evaluation: PeriodicEvaluationView | null +} + +// 카테고리별 색상 매핑 +const getCategoryBadgeVariant = (category: string) => { + switch (category) { + case "quality": + return "default" + case "delivery": + return "secondary" + case "price": + return "outline" + case "cooperation": + return "destructive" + default: + return "outline" + } +} + +// 카테고리명 매핑 +const CATEGORY_LABELS = { + "customer-service": "CS", + administrator: "관리자", + procurement: "구매", + design: "설계", + sourcing: "조달", + quality: "품질" +} as const + +const CATEGORY_LABELS2 = { + bonus: "가점항목", + delivery: "납기", + management: "경영현황", + penalty: "감점항목", + procurement: "구매", + quality: "품질" + } as const + +export function EvaluationDetailsDialog({ + open, + onOpenChange, + evaluation, +}: EvaluationDetailsDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [evaluationDetails, setEvaluationDetails] = React.useState<{ + evaluationInfo: any + reviewerDetails: EvaluationDetailData[] + } | null>(null) + + // 평가 상세 정보 로드 + React.useEffect(() => { + if (open && evaluation?.id) { + const loadEvaluationDetails = async () => { + try { + setIsLoading(true) + const details = await getEvaluationDetails(evaluation.id) + setEvaluationDetails(details) + } catch (error) { + console.error("Failed to load evaluation details:", error) + } finally { + setIsLoading(false) + } + } + + loadEvaluationDetails() + } + }, [open, evaluation?.id]) + + // 다이얼로그 닫을 때 데이터 리셋 + React.useEffect(() => { + if (!open) { + setEvaluationDetails(null) + } + }, [open]) + + if (!evaluation) return null + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-7xl max-h-[90vh] overflow-y-auto"> + <DialogHeader className="space-y-4"> + <DialogTitle className="flex items-center gap-2"> + <Eye className="h-5 w-5 text-blue-600" /> + 평가 상세 + </DialogTitle> + + {/* 평가 기본 정보 */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2 text-lg"> + <Building2 className="h-5 w-5" /> + 평가 정보 + </CardTitle> + </CardHeader> + <CardContent> + <div className="flex flex-wrap items-center gap-6 text-sm"> + {/* 협력업체 */} + <div className="flex items-center gap-2"> + <span className="text-muted-foreground">협력업체:</span> + <span className="font-medium">{evaluation.vendorName}</span> + <span className="text-muted-foreground">({evaluation.vendorCode})</span> + </div> + + {/* 평가년도 */} + <div className="flex items-center gap-2"> + <span className="text-muted-foreground">년도:</span> + <span className="font-medium">{evaluation.evaluationYear}년</span> + </div> + + {/* 구분 */} + <div className="flex items-center gap-2"> + <span className="text-muted-foreground">구분:</span> + <Badge variant="outline" className="text-xs"> + {evaluation.division === "PLANT" ? "해양" : "조선"} + </Badge> + </div> + + {/* 진행상태 */} + <div className="flex items-center gap-2"> + <span className="text-muted-foreground">상태:</span> + <Badge variant="secondary" className="text-xs">{evaluation.status}</Badge> + </div> + + {/* 평가점수/등급 */} + <div className="flex items-center gap-2"> + <span className="text-muted-foreground">평가점수/등급:</span> + {evaluation.evaluationScore ? ( + <div className="flex items-center gap-1"> + <span className="font-bold text-blue-600"> + {Number(evaluation.evaluationScore).toFixed(1)}점 + </span> + {evaluation.evaluationGrade && ( + <Badge variant="default" className="text-xs h-5"> + {evaluation.evaluationGrade} + </Badge> + )} + </div> + ) : ( + <span className="text-muted-foreground">-</span> + )} + </div> + + {/* 확정점수/등급 */} + <div className="flex items-center gap-2"> + <span className="text-muted-foreground">확정점수/등급:</span> + {evaluation.finalScore ? ( + <div className="flex items-center gap-1"> + <span className="font-bold text-green-600"> + {Number(evaluation.finalScore).toFixed(1)}점 + </span> + {evaluation.finalGrade && ( + <Badge variant="default" className="bg-green-600 text-xs h-5"> + {evaluation.finalGrade} + </Badge> + )} + </div> + ) : ( + <span className="text-muted-foreground">미확정</span> + )} + </div> + </div> + </CardContent> + </Card> + </DialogHeader> + + {isLoading ? ( + <div className="space-y-4"> + <Card> + <CardHeader> + <Skeleton className="h-6 w-48" /> + </CardHeader> + <CardContent> + <Skeleton className="h-64 w-full" /> + </CardContent> + </Card> + </div> + ) : evaluationDetails ? ( + <div className="space-y-6"> + {/* 통합 평가 테이블 */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <FileText className="h-5 w-5" /> + 평가 상세 내역 + </CardTitle> + </CardHeader> + <CardContent> + {evaluationDetails.reviewerDetails.some(r => r.evaluationItems.length > 0) ? ( + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-[120px]">담당자</TableHead> + {/* <TableHead className="w-[80px]">상태</TableHead> */} + <TableHead className="w-[100px]">평가부문</TableHead> + <TableHead className="w-[100px]">항목</TableHead> + <TableHead className="w-[150px]">구분</TableHead> + <TableHead className="w-[200px]">범위</TableHead> + <TableHead className="w-[200px]">선택옵션</TableHead> + <TableHead className="w-[80px]">점수</TableHead> + <TableHead className="min-w-[200px]">의견</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {evaluationDetails.reviewerDetails.map((reviewer) => + reviewer.evaluationItems.map((item, index) => ( + <TableRow key={`${reviewer.reviewerEvaluationId}-${item.criteriaId}-${index}`}> + <TableCell> + <div className="space-y-1"> + <div className="font-medium text-sm">{reviewer.departmentName}</div> + <div className="text-xs text-muted-foreground"> + {reviewer.reviewerName} + </div> + </div> + </TableCell> + {/* <TableCell> + {reviewer.isCompleted ? ( + <Badge variant="default" className="flex items-center gap-1"> + <CheckCircle2 className="h-3 w-3" /> + 완료 + </Badge> + ) : ( + <Badge variant="secondary" className="flex items-center gap-1"> + <Clock className="h-3 w-3" /> + 진행중 + </Badge> + )} + </TableCell> */} + <TableCell> + <Badge variant={getCategoryBadgeVariant(item.category)}> + {CATEGORY_LABELS[item.category as keyof typeof CATEGORY_LABELS] || item.category} + </Badge> + </TableCell> + <TableCell> + {CATEGORY_LABELS2[item.item as keyof typeof CATEGORY_LABELS2] || item.item} + </TableCell> + <TableCell className="font-medium"> + {item.classification} + </TableCell> + <TableCell className="text-sm"> + {item.range || "-"} + </TableCell> + <TableCell className="text-sm"> + {item.scoreType === "variable" ? ( + <Badge variant="outline">직접 입력</Badge> + ) : ( + item.selectedDetail || "-" + )} + </TableCell> + <TableCell> + {item.score !== null ? ( + <Badge variant="default" className="font-mono"> + {item.score.toFixed(1)} + </Badge> + ) : ( + <span className="text-muted-foreground">-</span> + )} + </TableCell> + <TableCell className="text-sm"> + {item.comment || ( + <span className="text-muted-foreground">의견 없음</span> + )} + </TableCell> + </TableRow> + )) + )} + </TableBody> + </Table> + ) : ( + <div className="text-center text-muted-foreground py-8"> + <FileText className="h-8 w-8 mx-auto mb-2" /> + <div>평가 항목이 없습니다</div> + </div> + )} + </CardContent> + </Card> + + {/* 리뷰어별 종합 의견 (있는 경우만) */} + {evaluationDetails.reviewerDetails.some(r => r.reviewerComment) && ( + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <MessageSquare className="h-5 w-5" /> + 종합 의견 + </CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + {evaluationDetails.reviewerDetails + .filter(reviewer => reviewer.reviewerComment) + .map((reviewer) => ( + <div key={reviewer.reviewerEvaluationId} className="space-y-2"> + <div className="flex items-center gap-2"> + <Badge variant="outline">{reviewer.departmentName}</Badge> + <span className="text-sm font-medium">{reviewer.reviewerName}</span> + </div> + <div className="bg-muted p-3 rounded-md text-sm"> + {reviewer.reviewerComment} + </div> + </div> + ))} + </CardContent> + </Card> + )} + + {evaluationDetails.reviewerDetails.length === 0 && ( + <Card> + <CardContent className="py-8"> + <div className="text-center text-muted-foreground"> + <User className="h-8 w-8 mx-auto mb-2" /> + <div>배정된 리뷰어가 없습니다</div> + </div> + </CardContent> + </Card> + )} + </div> + ) : ( + <Card> + <CardContent className="py-8"> + <div className="text-center text-muted-foreground"> + 평가 상세 정보를 불러올 수 없습니다 + </div> + </CardContent> + </Card> + )} + + <div className="flex justify-end pt-4"> + <Button variant="outline" onClick={() => onOpenChange(false)}> + 닫기 + </Button> + </div> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
