diff options
Diffstat (limited to 'lib/evaluation-target-list/table/evaluation-targets-columns.tsx')
| -rw-r--r-- | lib/evaluation-target-list/table/evaluation-targets-columns.tsx | 345 |
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 |
