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