// components/evaluation/evaluation-columns.tsx - 집계 모드 지원 업데이트 "use client"; import * as React from "react"; import { type ColumnDef } from "@tanstack/react-table"; import { Checkbox } from "@/components/ui/checkbox"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Pencil, Eye, MessageSquare, Check, X, Clock, FileText, Circle, Ellipsis, BarChart3, ChevronDown } from "lucide-react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"; import { PeriodicEvaluationView, PeriodicEvaluationAggregatedView } from "@/db/schema"; import { DataTableRowAction } from "@/types/table"; import { vendortypeMap } from "@/types/evaluation"; interface GetColumnsProps { setRowAction: React.Dispatch | null>>; viewMode?: "detailed" | "aggregated"; } // 집계 모드용 division 배지 const getDivisionBadgeWithAggregation = ( division: string, evaluationCount?: number, divisions?: string ) => { if (division === "BOTH") { return (
통합 {evaluationCount && evaluationCount > 1 && ( {evaluationCount}개

{divisions?.replace(',', ', ')} 평가 통합

)}
); } return ( {division === "PLANT" ? "해양" : "조선"} ); }; // 기존 함수들은 그대로 유지... const getStatusBadgeVariant = (status: string) => { switch (status) { case "PENDING_SUBMISSION": return "outline"; case "SUBMITTED": return "secondary"; case "IN_REVIEW": return "default"; case "REVIEW_COMPLETED": return "default"; case "FINALIZED": return "default"; default: return "outline"; } }; const getStatusLabel = (status: string) => { const statusMap = { PENDING: "대상확정", PENDING_SUBMISSION: "자료접수중", SUBMITTED: "제출완료", IN_REVIEW: "평가중", REVIEW_COMPLETED: "평가완료", FINALIZED: "결과확정" }; return statusMap[status] || status; }; // 부서별 상태 배지 함수 const getDepartmentStatusBadge = (status: string | null) => { if (!status) return (
-
); switch (status) { case "NOT_ASSIGNED": return (
미지정
); case "NOT_STARTED": return (
); case "IN_PROGRESS": return (
); case "COMPLETED": return (
); default: return (
-
); } }; // 등급별 색상 const getGradeBadgeVariant = (grade: string | null): "default" | "secondary" | "outline" | "destructive" => { if (!grade) return "outline"; switch (grade) { case "S": return "default"; case "A": return "secondary"; case "B": return "outline"; case "C": return "destructive"; case "D": return "destructive"; default: return "outline"; } }; // 자재구분 배지 const getMaterialTypeBadge = (materialType: string) => { return {vendortypeMap[materialType] || materialType}; }; // 내외자 배지 const getDomesticForeignBadge = (domesticForeign: string) => { return ( {domesticForeign === "DOMESTIC" ? "D" : "F"} ); }; export function getPeriodicEvaluationsColumns({ setRowAction, viewMode = "detailed" }: GetColumnsProps): ColumnDef[] { const baseColumns: ColumnDef[] = [ // ═══════════════════════════════════════════════════════════════ // 선택 및 기본 정보 // ═══════════════════════════════════════════════════════════════ // Checkbox { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!v)} aria-label="select all" className="translate-y-0.5" /> ), cell: ({ row }) => ( row.toggleSelected(!!v)} aria-label="select row" className="translate-y-0.5" /> ), size: 40, enableSorting: false, enableHiding: false, }, // ░░░ 평가년도 ░░░ { accessorKey: "evaluationYear", header: ({ column }) => , cell: ({ row }) => {row.original.evaluationYear}, size: 100, meta: { excelHeader: "평가년도", }, }, // ░░░ 구분 ░░░ - 집계 모드에 따라 다르게 렌더링 { accessorKey: "division", header: ({ column }) => , cell: ({ row }) => { const division =viewMode === "aggregated"?"BOTH": row.original.division || ""; if (viewMode === "aggregated") { const aggregatedRow = row.original as PeriodicEvaluationAggregatedView; return getDivisionBadgeWithAggregation( division, aggregatedRow.evaluationCount, aggregatedRow.divisions ); } return ( {division === "PLANT" ? "해양" : "조선"} ); }, size: viewMode === "aggregated" ? 120 : 80, meta: { excelHeader: "구분", }, }, { accessorKey: "status", header: ({ column }) => , cell: ({ row }) => getStatusLabel(row.original.status || ""), size: 80, meta: { excelHeader: "Status", }, }, // ═══════════════════════════════════════════════════════════════ // 협력업체 정보 // ═══════════════════════════════════════════════════════════════ { header: "협력업체 정보", columns: [ { accessorKey: "vendorCode", header: ({ column }) => , cell: ({ row }) => ( {row.original.vendorCode} ), size: 120, meta: { excelHeader: "벤더 코드", }, }, { accessorKey: "vendorName", header: ({ column }) => , cell: ({ row }) => (
{row.original.vendorName}
), size: 200, meta: { excelHeader: "벤더명", }, }, { accessorKey: "domesticForeign", header: ({ column }) => , cell: ({ row }) => getDomesticForeignBadge(row.original.domesticForeign || ""), size: 80, meta: { excelHeader: "내외자", }, }, { accessorKey: "materialType", header: ({ column }) => , cell: ({ row }) => getMaterialTypeBadge(row.original.materialType || ""), size: 120, meta: { excelHeader: "자재구분", }, }, ] }, // 집계 모드에서만 보이는 평가 개수 컬럼 ...(viewMode === "aggregated" ? [{ accessorKey: "evaluationCount", header: ({ column }) => (
), cell: ({ row }) => { const aggregatedRow = row.original as PeriodicEvaluationAggregatedView; const count = aggregatedRow.evaluationCount || 1; return (
1 ? "default" : "outline"} className="font-mono"> {count}개 {count > 1 && aggregatedRow.divisions && (
({aggregatedRow.divisions.replace(',', ', ')})

{aggregatedRow.divisions.replace(',', ', ')} 평가의 평균값

)}
); }, size: 100, meta: { excelHeader: "평가수", }, }] : []), { accessorKey: "finalScore", header: ({ column }) => , cell: ({ row }) => { const finalScore = row.getValue("finalScore"); const isAggregated = viewMode === "aggregated" && (row.original as PeriodicEvaluationAggregatedView).evaluationCount > 1; return finalScore ? (
{Number(finalScore).toFixed(1)} {isAggregated && ( 평균

여러 평가의 평균값

)}
) : ( - ); }, size: viewMode === "aggregated" ? 120 : 90, meta: { excelHeader: "확정점수", }, }, { accessorKey: "finalGrade", header: ({ column }) => , cell: ({ row }) => { const finalGrade = row.getValue("finalGrade"); const isAggregated = viewMode === "aggregated" && (row.original as PeriodicEvaluationAggregatedView).evaluationCount > 1; return finalGrade ? ( {finalGrade} ) : ( - ); }, size: 90, meta: { excelHeader: "확정등급", }, }, // ═══════════════════════════════════════════════════════════════ // 진행 현황 - 집계 모드에서는 최고 진행 상태를 보여줌 // ═══════════════════════════════════════════════════════════════ { header: "부서별 평가 현황", columns: [ { accessorKey: "orderEvalStatus", header: ({ column }) => , cell: ({ row }) => getDepartmentStatusBadge(row.getValue("orderEvalStatus")), size: 60, meta: { excelHeader: "발주", }, }, { accessorKey: "procurementEvalStatus", header: ({ column }) => , cell: ({ row }) => getDepartmentStatusBadge(row.getValue("procurementEvalStatus")), size: 70, meta: { excelHeader: "조달", }, }, { accessorKey: "qualityEvalStatus", header: ({ column }) => , cell: ({ row }) => getDepartmentStatusBadge(row.getValue("qualityEvalStatus")), size: 70, meta: { excelHeader: "품질", }, }, { accessorKey: "designEvalStatus", header: ({ column }) => , cell: ({ row }) => getDepartmentStatusBadge(row.getValue("designEvalStatus")), size: 70, meta: { excelHeader: "설계", }, }, { accessorKey: "csEvalStatus", header: ({ column }) => , cell: ({ row }) => getDepartmentStatusBadge(row.getValue("csEvalStatus")), size: 70, meta: { excelHeader: "CS", }, }, { accessorKey: "adminEvalStatus", header: ({ column }) => , cell: ({ row }) => getDepartmentStatusBadge(row.getValue("adminEvalStatus")), size: 120, meta: { excelHeader: "관리자", }, }, ] }, { header: "평가상세", enableHiding: true, size: 80, minSize: 80, cell: ({ row }) => { return (
setRowAction({ row, type: "view" })} className="flex items-center gap-2" > 평가 상세 setRowAction({ row, type: "vendor-submission" as any })} className="flex items-center gap-2" > 협력업체 제출 상세
); }, }, // ═══════════════════════════════════════════════════════════════ // 제출 현황 // ═══════════════════════════════════════════════════════════════ { header: "협력업체 제출 현황", columns: [ { accessorKey: "documentsSubmitted", header: ({ column }) => , cell: ({ row }) => { const submitted = row.getValue("documentsSubmitted"); const isAggregated = viewMode === "aggregated" && (row.original as PeriodicEvaluationAggregatedView).evaluationCount > 1; return (
{submitted ? "제출완료" : "미제출"} {isAggregated && ( 통합

모든 평가에서 제출 완료된 경우만 "제출완료"

)}
); }, size: viewMode === "aggregated" ? 140 : 120, meta: { excelHeader: "문서제출", }, }, { accessorKey: "submissionDate", header: ({ column }) => , cell: ({ row }) => { const submissionDate = row.getValue("submissionDate"); const isAggregated = viewMode === "aggregated" && (row.original as PeriodicEvaluationAggregatedView).evaluationCount > 1; return submissionDate ? (
{new Intl.DateTimeFormat("ko-KR", { month: "2-digit", day: "2-digit", }).format(new Date(submissionDate))} {isAggregated && ( 최신

가장 최근 제출일

)}
) : ( - ); }, size: viewMode === "aggregated" ? 110 : 80, meta: { excelHeader: "제출일", }, }, { accessorKey: "submissionDeadline", header: ({ column }) => , cell: ({ row }) => { const deadline = row.getValue("submissionDeadline"); if (!deadline) return -; const now = new Date(); const isOverdue = now > deadline; return ( {new Intl.DateTimeFormat("ko-KR", { month: "2-digit", day: "2-digit", }).format(new Date(deadline))} ); }, size: 80, meta: { excelHeader: "마감일", }, }, ] }, // ═══════════════════════════════════════════════════════════════ // 평가 점수 - 집계 모드에서는 평균임을 명시 // ═══════════════════════════════════════════════════════════════ { header: viewMode === "aggregated" ? "평가 점수 (평균)" : "평가 점수", columns: [ { accessorKey: "processScore", header: ({ column }) => , cell: ({ row }) => { const score = row.getValue("processScore"); const isAggregated = viewMode === "aggregated" && (row.original as PeriodicEvaluationAggregatedView).evaluationCount > 1; return score ? ( {Number(score).toFixed(1)} ) : ( - ); }, size: 80, meta: { excelHeader: "공정", }, }, { accessorKey: "priceScore", header: ({ column }) => , cell: ({ row }) => { const score = row.getValue("priceScore"); const isAggregated = viewMode === "aggregated" && (row.original as PeriodicEvaluationAggregatedView).evaluationCount > 1; return score ? ( {Number(score).toFixed(1)} ) : ( - ); }, size: 80, meta: { excelHeader: "가격", }, }, { accessorKey: "deliveryScore", header: ({ column }) => , cell: ({ row }) => { const score = row.getValue("deliveryScore"); const isAggregated = viewMode === "aggregated" && (row.original as PeriodicEvaluationAggregatedView).evaluationCount > 1; return score ? ( {Number(score).toFixed(1)} ) : ( - ); }, size: 80, meta: { excelHeader: "납기", }, }, { accessorKey: "selfEvaluationScore", header: ({ column }) => , cell: ({ row }) => { const score = row.getValue("selfEvaluationScore"); const isAggregated = viewMode === "aggregated" && (row.original as PeriodicEvaluationAggregatedView).evaluationCount > 1; return score ? ( {Number(score).toFixed(1)} ) : ( - ); }, size: 80, meta: { excelHeader: "자율평가", }, }, // ✅ 합계 - 4개 점수의 합으로 계산 { id: "totalScore", header: ({ column }) => , cell: ({ row }) => { const processScore = Number(row.getValue("processScore") || 0); const priceScore = Number(row.getValue("priceScore") || 0); const deliveryScore = Number(row.getValue("deliveryScore") || 0); const selfEvaluationScore = Number(row.getValue("selfEvaluationScore") || 0); const total = processScore + priceScore + deliveryScore + selfEvaluationScore; const isAggregated = viewMode === "aggregated" && (row.original as PeriodicEvaluationAggregatedView).evaluationCount > 1; return total > 0 ? ( {total.toFixed(1)} ) : ( - ); }, size: 80, meta: { excelHeader: "합계", }, }, { accessorKey: "participationBonus", header: ({ column }) => , cell: ({ row }) => { const score = row.getValue("participationBonus"); const isAggregated = viewMode === "aggregated" && (row.original as PeriodicEvaluationAggregatedView).evaluationCount > 1; return score ? ( +{Number(score).toFixed(1)} ) : ( - ); }, size: 100, meta: { excelHeader: "참여도(가점)", }, }, { accessorKey: "qualityDeduction", header: ({ column }) => , cell: ({ row }) => { const score = row.getValue("qualityDeduction"); const isAggregated = viewMode === "aggregated" && (row.original as PeriodicEvaluationAggregatedView).evaluationCount > 1; return score ? ( -{Number(score).toFixed(1)} ) : ( - ); }, size: 100, meta: { excelHeader: "품질(감점)", }, }, // ✅ 평가점수 컬럼 { id: "evaluationScore", header: ({ column }) => , cell: ({ row }) => { const processScore = Number(row.getValue("processScore") || 0); const priceScore = Number(row.getValue("priceScore") || 0); const deliveryScore = Number(row.getValue("deliveryScore") || 0); const selfEvaluationScore = Number(row.getValue("selfEvaluationScore") || 0); const participationBonus = Number(row.getValue("participationBonus") || 0); const qualityDeduction = Number(row.getValue("qualityDeduction") || 0); const totalScore = processScore + priceScore + deliveryScore + selfEvaluationScore; const evaluationScore = totalScore + participationBonus - qualityDeduction; const isAggregated = viewMode === "aggregated" && (row.original as PeriodicEvaluationAggregatedView).evaluationCount > 1; return totalScore > 0 ? ( {evaluationScore.toFixed(1)} ) : ( - ); }, size: 90, meta: { excelHeader: "평가점수", }, }, { id: "evaluationGrade", header: ({ column }) => , cell: ({ row }) => { // 확정된 등급이 있으면 우선 표시 const finalGrade = row.original.finalGrade; const isAggregated = viewMode === "aggregated" && (row.original as PeriodicEvaluationAggregatedView).evaluationCount > 1; if (finalGrade) { return ( {finalGrade}등급 ); } // 확정된 등급이 없으면 평가점수 기반으로 등급 계산 const processScore = Number(row.getValue("processScore") || 0); const priceScore = Number(row.getValue("priceScore") || 0); const deliveryScore = Number(row.getValue("deliveryScore") || 0); const selfEvaluationScore = Number(row.getValue("selfEvaluationScore") || 0); const participationBonus = Number(row.getValue("participationBonus") || 0); const qualityDeduction = Number(row.getValue("qualityDeduction") || 0); const totalScore = processScore + priceScore + deliveryScore + selfEvaluationScore; const evaluationScore = totalScore + participationBonus - qualityDeduction; // 점수 기반으로 등급 계산 // A: 95 이상, B: 90-95 미만, C: 60-90 미만, D: 60 미만 let calculatedGrade: string | null = null; if (evaluationScore > 0) { if (evaluationScore >= 95) { calculatedGrade = "A"; } else if (evaluationScore >= 90) { calculatedGrade = "B"; } else if (evaluationScore >= 60) { calculatedGrade = "C"; } else { calculatedGrade = "D"; } } return calculatedGrade ? ( {calculatedGrade}등급 ) : ( - ); }, minSize: 100, meta: { excelHeader: "평가등급", }, }, ] }, ]; return baseColumns; }