diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-20 11:37:31 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-20 11:37:31 +0000 |
| commit | aa86729f9a2ab95346a2851e3837de1c367aae17 (patch) | |
| tree | b601b18b6724f2fb449c7fa9ea50cbd652a8077d /lib/evaluation/table/evaluation-columns.tsx | |
| parent | 95bbe9c583ff841220da1267630e7b2025fc36dc (diff) | |
(대표님) 20250620 작업사항
Diffstat (limited to 'lib/evaluation/table/evaluation-columns.tsx')
| -rw-r--r-- | lib/evaluation/table/evaluation-columns.tsx | 441 |
1 files changed, 441 insertions, 0 deletions
diff --git a/lib/evaluation/table/evaluation-columns.tsx b/lib/evaluation/table/evaluation-columns.tsx new file mode 100644 index 00000000..0c207a53 --- /dev/null +++ b/lib/evaluation/table/evaluation-columns.tsx @@ -0,0 +1,441 @@ +// ================================================================ +// 1. PERIODIC EVALUATIONS COLUMNS +// ================================================================ + +"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 } from "lucide-react"; +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"; +import { PeriodicEvaluationView } from "@/db/schema"; + + + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<PeriodicEvaluationView> | null>>; +} + +// 상태별 색상 매핑 +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_SUBMISSION: "제출대기", + SUBMITTED: "제출완료", + IN_REVIEW: "검토중", + REVIEW_COMPLETED: "검토완료", + FINALIZED: "최종확정" + }; + return statusMap[status] || status; +}; + +// 등급별 색상 +const getGradeBadgeVariant = (grade: string | null) => { + 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 getDivisionBadge = (division: string) => { + return ( + <Badge variant={division === "PLANT" ? "default" : "secondary"}> + {division === "PLANT" ? "해양" : "조선"} + </Badge> + ); +}; + +// 자재구분 배지 +const getMaterialTypeBadge = (materialType: string) => { + const typeMap = { + EQUIPMENT: "기자재", + BULK: "벌크", + EQUIPMENT_BULK: "기자재/벌크" + }; + return <Badge variant="outline">{typeMap[materialType] || materialType}</Badge>; +}; + +// 내외자 배지 +const getDomesticForeignBadge = (domesticForeign: string) => { + return ( + <Badge variant={domesticForeign === "DOMESTIC" ? "default" : "secondary"}> + {domesticForeign === "DOMESTIC" ? "내자" : "외자"} + </Badge> + ); +}; + +// 진행률 배지 +const getProgressBadge = (completed: number, total: number) => { + if (total === 0) return <Badge variant="outline">-</Badge>; + + const percentage = Math.round((completed / total) * 100); + const variant = percentage === 100 ? "default" : percentage >= 50 ? "secondary" : "destructive"; + + return <Badge variant={variant}>{completed}/{total} ({percentage}%)</Badge>; +}; + +export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps): ColumnDef<PeriodicEvaluationWithRelations>[] { + return [ + // ═══════════════════════════════════════════════════════════════ + // 선택 및 기본 정보 + // ═══════════════════════════════════════════════════════════════ + + // Checkbox + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")} + onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)} + aria-label="select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(v) => row.toggleSelected(!!v)} + aria-label="select row" + className="translate-y-0.5" + /> + ), + size: 40, + enableSorting: false, + enableHiding: false, + }, + + // ░░░ 평가년도 ░░░ + { + accessorKey: "evaluationTarget.evaluationYear", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가년도" />, + cell: ({ row }) => <span className="font-medium">{row.original.evaluationTarget?.evaluationYear}</span>, + size: 100, + }, + + // ░░░ 평가기간 ░░░ + { + accessorKey: "evaluationPeriod", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가기간" />, + cell: ({ row }) => ( + <Badge variant="outline">{row.getValue("evaluationPeriod")}</Badge> + ), + size: 100, + }, + + // ░░░ 구분 ░░░ + { + accessorKey: "evaluationTarget.division", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="구분" />, + cell: ({ row }) => getDivisionBadge(row.original.evaluationTarget?.division || ""), + size: 80, + }, + + // ═══════════════════════════════════════════════════════════════ + // 협력업체 정보 + // ═══════════════════════════════════════════════════════════════ + { + header: "협력업체 정보", + columns: [ + { + accessorKey: "evaluationTarget.vendorCode", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더 코드" />, + cell: ({ row }) => ( + <span className="font-mono text-sm">{row.original.evaluationTarget?.vendorCode}</span> + ), + size: 120, + }, + + { + accessorKey: "evaluationTarget.vendorName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더명" />, + cell: ({ row }) => ( + <div className="truncate max-w-[200px]" title={row.original.evaluationTarget?.vendorName}> + {row.original.evaluationTarget?.vendorName} + </div> + ), + size: 200, + }, + + { + accessorKey: "evaluationTarget.domesticForeign", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="내외자" />, + cell: ({ row }) => getDomesticForeignBadge(row.original.evaluationTarget?.domesticForeign || ""), + size: 80, + }, + + { + accessorKey: "evaluationTarget.materialType", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="자재구분" />, + cell: ({ row }) => getMaterialTypeBadge(row.original.evaluationTarget?.materialType || ""), + size: 120, + }, + ] + }, + + // ═══════════════════════════════════════════════════════════════ + // 제출 현황 + // ═══════════════════════════════════════════════════════════════ + { + header: "제출 현황", + columns: [ + { + accessorKey: "documentsSubmitted", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="문서제출" />, + cell: ({ row }) => { + const submitted = row.getValue<boolean>("documentsSubmitted"); + return ( + <Badge variant={submitted ? "default" : "destructive"}> + {submitted ? "제출완료" : "미제출"} + </Badge> + ); + }, + size: 100, + }, + + { + accessorKey: "submissionDate", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="제출일" />, + cell: ({ row }) => { + const submissionDate = row.getValue<Date>("submissionDate"); + return submissionDate ? ( + <span className="text-sm"> + {new Intl.DateTimeFormat("ko-KR", { + month: "2-digit", + day: "2-digit", + }).format(new Date(submissionDate))} + </span> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 80, + }, + + { + accessorKey: "submissionDeadline", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="마감일" />, + cell: ({ row }) => { + const deadline = row.getValue<Date>("submissionDeadline"); + if (!deadline) return <span className="text-muted-foreground">-</span>; + + const now = new Date(); + const isOverdue = now > deadline; + + return ( + <span className={`text-sm ${isOverdue ? "text-red-600" : ""}`}> + {new Intl.DateTimeFormat("ko-KR", { + month: "2-digit", + day: "2-digit", + }).format(new Date(deadline))} + </span> + ); + }, + size: 80, + }, + ] + }, + + // ═══════════════════════════════════════════════════════════════ + // 평가 점수 + // ═══════════════════════════════════════════════════════════════ + { + header: "평가 점수", + columns: [ + { + accessorKey: "totalScore", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="총점" />, + cell: ({ row }) => { + const score = row.getValue<number>("totalScore"); + return score ? ( + <span className="font-medium">{score.toFixed(1)}</span> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 80, + }, + + { + accessorKey: "evaluationGrade", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="등급" />, + cell: ({ row }) => { + const grade = row.getValue<string>("evaluationGrade"); + return grade ? ( + <Badge variant={getGradeBadgeVariant(grade)}>{grade}</Badge> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 60, + }, + + { + accessorKey: "finalScore", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="최종점수" />, + cell: ({ row }) => { + const finalScore = row.getValue<number>("finalScore"); + return finalScore ? ( + <span className="font-bold text-green-600">{finalScore.toFixed(1)}</span> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 90, + }, + + { + accessorKey: "finalGrade", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="최종등급" />, + cell: ({ row }) => { + const finalGrade = row.getValue<string>("finalGrade"); + return finalGrade ? ( + <Badge variant={getGradeBadgeVariant(finalGrade)} className="bg-green-600"> + {finalGrade} + </Badge> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 90, + }, + ] + }, + + // ═══════════════════════════════════════════════════════════════ + // 진행 현황 + // ═══════════════════════════════════════════════════════════════ + { + header: "진행 현황", + columns: [ + { + accessorKey: "status", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="진행상태" />, + cell: ({ row }) => { + const status = row.getValue<string>("status"); + return ( + <Badge variant={getStatusBadgeVariant(status)}> + {getStatusLabel(status)} + </Badge> + ); + }, + size: 100, + }, + + { + id: "reviewProgress", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="리뷰진행" />, + cell: ({ row }) => { + const stats = row.original.reviewerStats; + if (!stats) return <span className="text-muted-foreground">-</span>; + + return getProgressBadge(stats.completedReviewers, stats.totalReviewers); + }, + size: 120, + }, + + { + accessorKey: "reviewCompletedAt", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="검토완료일" />, + cell: ({ row }) => { + const completedAt = row.getValue<Date>("reviewCompletedAt"); + return completedAt ? ( + <span className="text-sm"> + {new Intl.DateTimeFormat("ko-KR", { + month: "2-digit", + day: "2-digit", + }).format(new Date(completedAt))} + </span> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 100, + }, + + { + accessorKey: "finalizedAt", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="확정일" />, + cell: ({ row }) => { + const finalizedAt = row.getValue<Date>("finalizedAt"); + return finalizedAt ? ( + <span className="text-sm font-medium"> + {new Intl.DateTimeFormat("ko-KR", { + month: "2-digit", + day: "2-digit", + }).format(new Date(finalizedAt))} + </span> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 80, + }, + ] + }, + + // ░░░ Actions ░░░ + { + id: "actions", + enableHiding: false, + size: 40, + minSize: 40, + cell: ({ row }) => { + return ( + <div className="flex items-center gap-1"> + <Button + variant="ghost" + size="icon" + className="size-8" + onClick={() => setRowAction({ row, type: "view" })} + aria-label="상세보기" + title="상세보기" + > + <Eye className="size-4" /> + </Button> + + <Button + variant="ghost" + size="icon" + className="size-8" + onClick={() => setRowAction({ row, type: "update" })} + aria-label="수정" + title="수정" + > + <Pencil className="size-4" /> + </Button> + </div> + ); + }, + }, + ]; +}
\ No newline at end of file |
