summaryrefslogtreecommitdiff
path: root/lib/evaluation-target-list/table/evaluation-targets-columns.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/evaluation-target-list/table/evaluation-targets-columns.tsx')
-rw-r--r--lib/evaluation-target-list/table/evaluation-targets-columns.tsx345
1 files changed, 345 insertions, 0 deletions
diff --git a/lib/evaluation-target-list/table/evaluation-targets-columns.tsx b/lib/evaluation-target-list/table/evaluation-targets-columns.tsx
new file mode 100644
index 00000000..b1e19434
--- /dev/null
+++ b/lib/evaluation-target-list/table/evaluation-targets-columns.tsx
@@ -0,0 +1,345 @@
+"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 } from "lucide-react";
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header";
+import { EvaluationTargetWithDepartments } from "@/db/schema";
+
+// 상태별 색상 매핑
+const getStatusBadgeVariant = (status: string) => {
+ switch (status) {
+ case "PENDING":
+ return "secondary";
+ case "CONFIRMED":
+ return "default";
+ case "EXCLUDED":
+ return "destructive";
+ default:
+ return "outline";
+ }
+};
+
+// 의견 일치 여부 배지
+const getConsensusBadge = (consensusStatus: boolean | null) => {
+ if (consensusStatus === null) {
+ return <Badge variant="outline">검토 중</Badge>;
+ }
+ if (consensusStatus === true) {
+ return <Badge variant="default" className="bg-green-600">의견 일치</Badge>;
+ }
+ return <Badge variant="destructive">의견 불일치</Badge>;
+};
+
+// 구분 배지
+const getDivisionBadge = (division: string) => {
+ return (
+ <Badge variant={division === "OCEAN" ? "default" : "secondary"}>
+ {division === "OCEAN" ? "해양" : "조선"}
+ </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>
+ );
+};
+
+export function getEvaluationTargetsColumns(): ColumnDef<EvaluationTargetWithDepartments>[] {
+ 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: "evaluationYear",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가년도" />,
+ cell: ({ row }) => <span className="font-medium">{row.getValue("evaluationYear")}</span>,
+ size: 100,
+ },
+
+ // ░░░ 구분 ░░░
+ {
+ accessorKey: "division",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="구분" />,
+ cell: ({ row }) => getDivisionBadge(row.getValue("division")),
+ size: 80,
+ },
+
+ // ░░░ 벤더 코드 ░░░
+ {
+ accessorKey: "vendorCode",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더 코드" />,
+ cell: ({ row }) => (
+ <span className="font-mono text-sm">{row.getValue("vendorCode")}</span>
+ ),
+ size: 120,
+ },
+
+ // ░░░ 벤더명 ░░░
+ {
+ accessorKey: "vendorName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더명" />,
+ cell: ({ row }) => (
+ <div className="truncate max-w-[200px]" title={row.getValue<string>("vendorName")!}>
+ {row.getValue("vendorName") as string}
+ </div>
+ ),
+ size: 200,
+ },
+
+ // ░░░ 내외자 ░░░
+ {
+ accessorKey: "domesticForeign",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="내외자" />,
+ cell: ({ row }) => getDomesticForeignBadge(row.getValue("domesticForeign")),
+ size: 80,
+ },
+
+ // ░░░ 자재구분 ░░░
+ {
+ accessorKey: "materialType",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="자재구분" />,
+ cell: ({ row }) => getMaterialTypeBadge(row.getValue("materialType")),
+ size: 120,
+ },
+
+ // ░░░ 상태 ░░░
+ {
+ accessorKey: "status",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="상태" />,
+ cell: ({ row }) => {
+ const status = row.getValue<string>("status");
+ const statusMap = {
+ PENDING: "검토 중",
+ CONFIRMED: "확정",
+ EXCLUDED: "제외"
+ };
+ return (
+ <Badge variant={getStatusBadgeVariant(status)}>
+ {statusMap[status] || status}
+ </Badge>
+ );
+ },
+ size: 100,
+ },
+
+ // ░░░ 의견 일치 여부 ░░░
+ {
+ accessorKey: "consensusStatus",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="의견 일치" />,
+ cell: ({ row }) => getConsensusBadge(row.getValue("consensusStatus")),
+ size: 100,
+ },
+
+ // ░░░ 담당자 현황 ░░░
+ {
+ id: "reviewers",
+ header: "담당자 현황",
+ cell: ({ row }) => {
+ const reviewers = row.original.reviewers || [];
+ const totalReviewers = reviewers.length;
+ const completedReviews = reviewers.filter(r => r.review?.isApproved !== null).length;
+ const approvedReviews = reviewers.filter(r => r.review?.isApproved === true).length;
+
+ return (
+ <div className="flex items-center gap-2">
+ <div className="text-xs">
+ <span className="text-green-600 font-medium">{approvedReviews}</span>
+ <span className="text-muted-foreground">/{completedReviews}</span>
+ <span className="text-muted-foreground">/{totalReviewers}</span>
+ </div>
+ {totalReviewers > 0 && (
+ <div className="flex gap-1">
+ {reviewers.slice(0, 3).map((reviewer, idx) => (
+ <div
+ key={idx}
+ className={`w-2 h-2 rounded-full ${
+ reviewer.review?.isApproved === true
+ ? "bg-green-500"
+ : reviewer.review?.isApproved === false
+ ? "bg-red-500"
+ : "bg-gray-300"
+ }`}
+ title={`${reviewer.departmentCode}: ${
+ reviewer.review?.isApproved === true
+ ? "승인"
+ : reviewer.review?.isApproved === false
+ ? "거부"
+ : "대기중"
+ }`}
+ />
+ ))}
+ {totalReviewers > 3 && (
+ <span className="text-xs text-muted-foreground">+{totalReviewers - 3}</span>
+ )}
+ </div>
+ )}
+ </div>
+ );
+ },
+ size: 120,
+ enableSorting: false,
+ },
+
+ // ░░░ 관리자 의견 ░░░
+ {
+ accessorKey: "adminComment",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="관리자 의견" />,
+ cell: ({ row }) => {
+ const comment = row.getValue<string>("adminComment");
+ return comment ? (
+ <div className="truncate max-w-[150px]" title={comment}>
+ {comment}
+ </div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 150,
+ },
+
+ // ░░░ 종합 의견 ░░░
+ {
+ accessorKey: "consolidatedComment",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="종합 의견" />,
+ cell: ({ row }) => {
+ const comment = row.getValue<string>("consolidatedComment");
+ return comment ? (
+ <div className="truncate max-w-[150px]" title={comment}>
+ {comment}
+ </div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 150,
+ },
+
+ // ░░░ 확정일 ░░░
+ {
+ accessorKey: "confirmedAt",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="확정일" />,
+ cell: ({ row }) => {
+ const confirmedAt = row.getValue<Date>("confirmedAt");
+ return confirmedAt ? (
+ <span className="text-sm">
+ {new Intl.DateTimeFormat("ko-KR", {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ }).format(new Date(confirmedAt))}
+ </span>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ );
+ },
+ size: 100,
+ },
+
+ // ░░░ Actions ░░░
+ {
+ id: "actions",
+ enableHiding: false,
+ size: 120,
+ minSize: 120,
+ cell: ({ row }) => {
+ const record = row.original;
+ const [openDetail, setOpenDetail] = React.useState(false);
+ const [openEdit, setOpenEdit] = React.useState(false);
+ const [openRequest, setOpenRequest] = React.useState(false);
+
+ return (
+ <div className="flex items-center gap-1">
+ <Button
+ variant="ghost"
+ size="icon"
+ className="size-8"
+ onClick={() => setOpenDetail(true)}
+ aria-label="상세보기"
+ title="상세보기"
+ >
+ <Eye className="size-4" />
+ </Button>
+
+ <Button
+ variant="ghost"
+ size="icon"
+ className="size-8"
+ onClick={() => setOpenEdit(true)}
+ aria-label="수정"
+ title="수정"
+ >
+ <Pencil className="size-4" />
+ </Button>
+
+ <Button
+ variant="ghost"
+ size="icon"
+ className="size-8"
+ onClick={() => setOpenRequest(true)}
+ aria-label="의견요청"
+ title="의견요청"
+ >
+ <MessageSquare className="size-4" />
+ </Button>
+
+ {/* TODO: 실제 다이얼로그 컴포넌트들로 교체 */}
+ {openDetail && (
+ <div onClick={() => setOpenDetail(false)}>
+ {/* <EvaluationTargetDetailDialog /> */}
+ </div>
+ )}
+ {openEdit && (
+ <div onClick={() => setOpenEdit(false)}>
+ {/* <EditEvaluationTargetDialog /> */}
+ </div>
+ )}
+ {openRequest && (
+ <div onClick={() => setOpenRequest(false)}>
+ {/* <RequestReviewDialog /> */}
+ </div>
+ )}
+ </div>
+ );
+ },
+ },
+ ];
+} \ No newline at end of file