diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-19 09:44:28 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-19 09:44:28 +0000 |
| commit | 95bbe9c583ff841220da1267630e7b2025fc36dc (patch) | |
| tree | 5e3d5bb3302530bbaa7f7abbe8c9cf8193ccbd4c /lib/vendor-evaluation-submit/table/evaluation-submissions-table-columns.tsx | |
| parent | 0eb030580b5cbe5f03d570c3c9d8c519bac3b783 (diff) | |
(대표님) 20250619 1844 KST 작업사항
Diffstat (limited to 'lib/vendor-evaluation-submit/table/evaluation-submissions-table-columns.tsx')
| -rw-r--r-- | lib/vendor-evaluation-submit/table/evaluation-submissions-table-columns.tsx | 641 |
1 files changed, 641 insertions, 0 deletions
diff --git a/lib/vendor-evaluation-submit/table/evaluation-submissions-table-columns.tsx b/lib/vendor-evaluation-submit/table/evaluation-submissions-table-columns.tsx new file mode 100644 index 00000000..869839cb --- /dev/null +++ b/lib/vendor-evaluation-submit/table/evaluation-submissions-table-columns.tsx @@ -0,0 +1,641 @@ +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { + Ellipsis, + InfoIcon, + PenToolIcon, + FileTextIcon, + ClipboardListIcon, + DownloadIcon, + CheckIcon, + XIcon, + ClockIcon, + Send +} from "lucide-react" + +import { formatDate, formatCurrency } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Badge } from "@/components/ui/badge" + +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { EvaluationSubmissionWithVendor } from "../service" + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<EvaluationSubmissionWithVendor> | null>> +} + +/** + * 제출 상태에 따른 배지 스타일 및 아이콘 + */ +const getStatusBadge = (status: string) => { + switch (status) { + case 'draft': + return { + variant: "secondary" as const, + icon: <ClockIcon className="h-3 w-3" />, + label: "임시저장" + } + case 'submitted': + return { + variant: "default" as const, + icon: <FileTextIcon className="h-3 w-3" />, + label: "제출완료" + } + case 'under_review': + return { + variant: "outline" as const, + icon: <ClipboardListIcon className="h-3 w-3" />, + label: "검토중" + } + case 'approved': + return { + variant: "default" as const, + icon: <CheckIcon className="h-3 w-3" />, + label: "승인", + className: "bg-green-100 text-green-800 border-green-200" + } + case 'rejected': + return { + variant: "destructive" as const, + icon: <XIcon className="h-3 w-3" />, + label: "반려" + } + default: + return { + variant: "secondary" as const, + icon: null, + label: status + } + } +} + +/** + * 평가 제출 테이블 컬럼 정의 + */ +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<EvaluationSubmissionWithVendor>[] { + + // ---------------------------------------------------------------- + // 1) select 컬럼 (체크박스) + // ---------------------------------------------------------------- + const selectColumn: ColumnDef<EvaluationSubmissionWithVendor> = { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + enableSorting: false, + enableHiding: false, + size: 40, + } + + // ---------------------------------------------------------------- + // 2) 기본 정보 컬럼들 + // ---------------------------------------------------------------- + const basicColumns: ColumnDef<EvaluationSubmissionWithVendor>[] = [ + // { + // accessorKey: "submissionId", + // header: ({ column }) => ( + // <DataTableColumnHeaderSimple column={column} title="제출 ID" /> + // ), + // cell: ({ row }) => ( + // <div className="font-mono text-sm"> + // {row.getValue("submissionId")} + // </div> + // ), + // enableSorting: true, + // enableHiding: true, + // size: 400, + // minSize: 400, + // }, + + { + id: "vendorInfo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="협력업체" /> + ), + cell: ({ row }) => { + const vendor = row.original.vendor; + return ( + <div className="space-y-1"> + <div className="font-medium">{vendor.vendorName}</div> + <div className="text-sm text-muted-foreground"> + {vendor.vendorCode} • {vendor.countryCode} + </div> + </div> + ); + }, + enableSorting: false, + size: 200, + }, + + { + accessorKey: "evaluationYear", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="평가연도" /> + ), + cell: ({ row }) => ( + <Badge variant="outline"> + {row.getValue("evaluationYear")}년 + </Badge> + ), + size: 60, + }, + + { + accessorKey: "evaluationRound", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="평가회차" /> + ), + cell: ({ row }) => { + const round = row.getValue("evaluationRound") as string; + return round ? ( + <Badge variant="secondary">{round}</Badge> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 60, + }, + ] + + // ---------------------------------------------------------------- + // 3) 상태 정보 컬럼들 + // ---------------------------------------------------------------- + const statusColumns: ColumnDef<EvaluationSubmissionWithVendor>[] = [ + { + accessorKey: "submissionStatus", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="제출상태" /> + ), + cell: ({ row }) => { + const status = row.getValue("submissionStatus") as string; + const badgeInfo = getStatusBadge(status); + + return ( + <Badge + variant={badgeInfo.variant} + className={`flex items-center gap-1 ${badgeInfo.className || ''}`} + > + {badgeInfo.icon} + {badgeInfo.label} + </Badge> + ); + }, + size: 120, + }, + + { + accessorKey: "submittedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="제출일시" /> + ), + cell: ({ row }) => { + const date = row.getValue("submittedAt") as Date; + return date ? formatDate(date) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 140, + }, + + { + id: "reviewInfo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="검토정보" /> + ), + cell: ({ row }) => { + const reviewedAt = row.original.reviewedAt; + const reviewedBy = row.original.reviewedBy; + + if (!reviewedAt) { + return <span className="text-muted-foreground">미검토</span>; + } + + return ( + <div className="space-y-1"> + <div className="text-sm">{formatDate(reviewedAt)}</div> + {reviewedBy && ( + <div className="text-xs text-muted-foreground">{reviewedBy}</div> + )} + </div> + ); + }, + enableSorting: false, + size: 140, + }, + ] + + // ---------------------------------------------------------------- + // 4) 점수 및 통계 컬럼들 + // ---------------------------------------------------------------- + const scoreColumns: ColumnDef<EvaluationSubmissionWithVendor>[] = [ + { + id: "generalProgress", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="일반평가" /> + ), + cell: ({ row }) => { + const totalItems = row.original.totalGeneralItems || 0; + const completedItems = row.original.completedGeneralItems || 0; + const completionRate = totalItems > 0 ? (completedItems / totalItems) * 100 : 0; + + return ( + <div className="text-center space-y-1"> + {/* ❌ 점수 표시 제거 */} + <div className="font-medium"> + {completionRate === 100 ? "완료" : "진행중"} + </div> + <div className="flex items-center gap-1"> + <Badge variant="outline" className="text-xs"> + {completedItems}/{totalItems}개 + </Badge> + {completionRate > 0 && ( + <span className="text-xs text-muted-foreground"> + ({completionRate.toFixed(0)}%) + </span> + )} + </div> + {/* 📊 진행률 바 */} + <div className="w-full bg-gray-200 rounded-full h-1"> + <div + className={`h-1 rounded-full transition-all duration-300 ${ + completionRate === 100 + ? 'bg-green-500' + : completionRate >= 50 + ? 'bg-blue-500' + : 'bg-yellow-500' + }`} + style={{ width: `${completionRate}%` }} + /> + </div> + </div> + ); + }, + enableSorting: false, + size: 120, + }, + + { + id: "esgScore", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="ESG평가" /> + ), + cell: ({ row }) => { + const averageScore = row.original.averageEsgScore; + const totalItems = row.original.totalEsgItems || 0; + const completedItems = row.original.completedEsgItems || 0; + const completionRate = totalItems > 0 ? (completedItems / totalItems) * 100 : 0; + const isKorean = row.original.vendor.countryCode === 'KR'; + + if (!isKorean) { + return ( + <div className="text-center text-muted-foreground"> + <Badge variant="outline">해당없음</Badge> + </div> + ); + } + + return ( + <div className="text-center space-y-1"> + {/* ✅ ESG는 평균점수 표시 */} + <div className="font-medium"> + {averageScore ? ( + <span className="text-blue-600"> + 평균 {parseFloat(averageScore.toString()).toFixed(1)}점 + </span> + ) : ( + <span className="text-muted-foreground">미완료</span> + )} + </div> + <div className="flex items-center gap-1"> + <Badge variant="outline" className="text-xs"> + {completedItems}/{totalItems}개 + </Badge> + {completionRate > 0 && ( + <span className="text-xs text-muted-foreground"> + ({completionRate.toFixed(0)}%) + </span> + )} + </div> + {/* 📊 진행률 바 */} + <div className="w-full bg-gray-200 rounded-full h-1"> + <div + className={`h-1 rounded-full transition-all duration-300 ${ + completionRate === 100 + ? 'bg-green-500' + : completionRate >= 50 + ? 'bg-blue-500' + : 'bg-yellow-500' + }`} + style={{ width: `${completionRate}%` }} + /> + </div> + </div> + ); + }, + enableSorting: false, + size: 140, + }, + + { + id: "overallProgress", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="전체 진행률" /> + ), + cell: ({ row }) => { + const totalGeneral = row.original.totalGeneralItems || 0; + const completedGeneral = row.original.completedGeneralItems || 0; + const totalEsg = row.original.totalEsgItems || 0; + const completedEsg = row.original.completedEsgItems || 0; + const isKorean = row.original.vendor.countryCode === 'KR'; + + const totalItems = totalGeneral + (isKorean ? totalEsg : 0); + const completedItems = completedGeneral + (isKorean ? completedEsg : 0); + const completionRate = totalItems > 0 ? (completedItems / totalItems) * 100 : 0; + + return ( + <div className="text-center space-y-2"> + <div className="w-full bg-gray-200 rounded-full h-2"> + <div + className={`h-2 rounded-full transition-all duration-300 ${ + completionRate === 100 + ? 'bg-green-500' + : completionRate >= 50 + ? 'bg-blue-500' + : 'bg-yellow-500' + }`} + style={{ width: `${completionRate}%` }} + /> + </div> + <div className="text-xs space-y-1"> + <div className="font-medium"> + {completionRate.toFixed(0)}% 완료 + </div> + <div className="text-muted-foreground"> + {completedItems}/{totalItems}개 항목 + </div> + </div> + </div> + ); + }, + enableSorting: false, + size: 120, + }, + + { + id: "attachments", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="첨부파일" /> + ), + cell: ({ row }) => { + const count = row.original._count.attachments; + + return ( + <div className="text-center"> + <Badge variant="outline"> + {count}개 파일 + </Badge> + </div> + ); + }, + enableSorting: false, + size: 100, + }, + ] + + + // ---------------------------------------------------------------- + // 5) 메타데이터 컬럼들 + // ---------------------------------------------------------------- + const metaColumns: ColumnDef<EvaluationSubmissionWithVendor>[] = [ + { + accessorKey: "createdAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="생성일" /> + ), + cell: ({ row }) => { + const date = row.getValue("createdAt") as Date; + return formatDate(date); + }, + size: 140, + }, + { + accessorKey: "updatedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="수정일" /> + ), + cell: ({ row }) => { + const date = row.getValue("updatedAt") as Date; + return formatDate(date); + }, + size: 140, + }, + ] + + // ---------------------------------------------------------------- + // 6) actions 컬럼 (드롭다운 메뉴) + // ---------------------------------------------------------------- + const actionsColumn: ColumnDef<EvaluationSubmissionWithVendor> = { + id: "actions", + header: "작업", + enableHiding: false, + cell: function Cell({ row }) { + const status = row.original.submissionStatus; + const isKorean = row.original.vendor.countryCode === 'KR'; + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" size="icon"> + <Ellipsis className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "general_evaluation" })} + > + <FileTextIcon className="mr-2 h-4 w-4" /> + 일반평가 작성 + </DropdownMenuItem> + + {isKorean && ( + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "esg_evaluation" })} + > + <ClipboardListIcon className="mr-2 h-4 w-4" /> + ESG평가 작성 + </DropdownMenuItem> + )} + + <DropdownMenuSeparator /> + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "submit" })} + > + <Send className="mr-2 h-4 w-4" /> + 제출 + </DropdownMenuItem> + + </DropdownMenuContent> + </DropdownMenu> + ) + }, + size: 80, + } + + // ---------------------------------------------------------------- + // 7) 최종 컬럼 배열 (그룹화 버전) + // ---------------------------------------------------------------- + return [ + selectColumn, + { + id: "basicInfo", + header: "기본 정보", + columns: basicColumns, + }, + { + id: "statusInfo", + header: "상태 정보", + columns: statusColumns, + }, + { + id: "scoreInfo", + header: "점수 및 통계", + columns: scoreColumns, + }, + { + id: "metadata", + header: "메타데이터", + columns: metaColumns, + }, + actionsColumn, + ] +} + +// ---------------------------------------------------------------- +// 8) 컬럼 설정 (필터링용) +// ---------------------------------------------------------------- +export const evaluationSubmissionsColumnsConfig = [ + { + id: "submissionId", + label: "제출 ID", + group: "기본 정보", + type: "text", + excelHeader: "Submission ID", + }, + { + id: "vendorName", + label: "협력업체명", + group: "기본 정보", + type: "text", + excelHeader: "Vendor Name", + }, + { + id: "vendorCode", + label: "협력업체 코드", + group: "기본 정보", + type: "text", + excelHeader: "Vendor Code", + }, + { + id: "evaluationYear", + label: "평가연도", + group: "기본 정보", + type: "number", + excelHeader: "Evaluation Year", + }, + { + id: "evaluationRound", + label: "평가회차", + group: "기본 정보", + type: "text", + excelHeader: "Evaluation Round", + }, + { + id: "submissionStatus", + label: "제출상태", + group: "상태 정보", + type: "select", + options: [ + { label: "임시저장", value: "draft" }, + { label: "제출완료", value: "submitted" }, + { label: "검토중", value: "under_review" }, + { label: "승인", value: "approved" }, + { label: "반려", value: "rejected" }, + ], + excelHeader: "Submission Status", + }, + { + id: "submittedAt", + label: "제출일시", + group: "상태 정보", + type: "date", + excelHeader: "Submitted At", + }, + { + id: "reviewedAt", + label: "검토일시", + group: "상태 정보", + type: "date", + excelHeader: "Reviewed At", + }, + { + id: "totalGeneralScore", + label: "일반평가 점수", + group: "점수 정보", + type: "number", + excelHeader: "Total General Score", + }, + { + id: "totalEsgScore", + label: "ESG평가 점수", + group: "점수 정보", + type: "number", + excelHeader: "Total ESG Score", + }, + { + id: "createdAt", + label: "생성일", + group: "메타데이터", + type: "date", + excelHeader: "Created At", + }, + { + id: "updatedAt", + label: "수정일", + group: "메타데이터", + type: "date", + excelHeader: "Updated At", + }, +] as const;
\ No newline at end of file |
