diff options
Diffstat (limited to 'lib/techsales-rfq/table/detail-table/rfq-detail-column.tsx')
| -rw-r--r-- | lib/techsales-rfq/table/detail-table/rfq-detail-column.tsx | 291 |
1 files changed, 291 insertions, 0 deletions
diff --git a/lib/techsales-rfq/table/detail-table/rfq-detail-column.tsx b/lib/techsales-rfq/table/detail-table/rfq-detail-column.tsx new file mode 100644 index 00000000..c4a7edde --- /dev/null +++ b/lib/techsales-rfq/table/detail-table/rfq-detail-column.tsx @@ -0,0 +1,291 @@ +"use client" + +import * as React from "react" +import type { ColumnDef, Row } from "@tanstack/react-table"; +import { formatDate } from "@/lib/utils" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Ellipsis, MessageCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; + +export interface DataTableRowAction<TData> { + row: Row<TData>; + type: "delete" | "update" | "communicate"; +} + +// 벤더 견적 데이터 타입 정의 +export interface RfqDetailView { + id: number + rfqId: number + vendorId?: number | null + vendorName: string | null + vendorCode: string | null + totalPrice: string | number | null + currency: string | null + validUntil: Date | null + status: string | null + remark: string | null + submittedAt: Date | null + acceptedAt: Date | null + rejectionReason: string | null + createdAt: Date | null + updatedAt: Date | null + createdByName: string | null +} + +interface GetColumnsProps<TData> { + setRowAction: React.Dispatch< + React.SetStateAction<DataTableRowAction<TData> | null> + >; + unreadMessages?: Record<number, number>; // 읽지 않은 메시지 개수 +} + +export function getRfqDetailColumns({ + setRowAction, + unreadMessages = {} +}: GetColumnsProps<RfqDetailView>): ColumnDef<RfqDetailView>[] { + return [ + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="모두 선택" + /> + ), + cell: ({ row }) => { + const status = row.original.status; + const isDraft = status === "Draft"; + + return ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + disabled={!isDraft} + aria-label="행 선택" + className={!isDraft ? "opacity-50 cursor-not-allowed" : ""} + /> + ); + }, + enableSorting: false, + enableHiding: false, + size: 40, + }, + { + accessorKey: "status", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="견적 상태" /> + ), + cell: ({ row }) => { + const status = row.getValue("status") as string; + // 상태에 따른 배지 색상 설정 + let variant: "default" | "secondary" | "outline" | "destructive" = "outline"; + + if (status === "Submitted") { + variant = "default"; // 제출됨 - 기본 색상 + } else if (status === "Accepted") { + variant = "secondary"; // 승인됨 - 보조 색상 + } else if (status === "Rejected") { + variant = "destructive"; // 거부됨 - 위험 색상 + } + + return ( + <Badge variant={variant}>{status || "Draft"}</Badge> + ); + }, + meta: { + excelHeader: "견적 상태" + }, + enableResizing: true, + size: 120, + }, + { + accessorKey: "vendorCode", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="벤더 코드" /> + ), + cell: ({ row }) => <div>{row.getValue("vendorCode")}</div>, + meta: { + excelHeader: "벤더 코드" + }, + enableResizing: true, + size: 120, + }, + { + accessorKey: "vendorName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="벤더명" /> + ), + cell: ({ row }) => <div>{row.getValue("vendorName")}</div>, + meta: { + excelHeader: "벤더명" + }, + enableResizing: true, + size: 160, + }, + { + accessorKey: "totalPrice", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="견적 금액" /> + ), + cell: ({ row }) => { + const value = row.getValue("totalPrice") as string | number | null; + const currency = row.getValue("currency") as string | null; + + if (value === null || value === undefined) return "-"; + + // 숫자로 변환 시도 + const numValue = typeof value === 'string' ? parseFloat(value) : value; + + return ( + <div className="font-medium"> + {isNaN(numValue) ? value : numValue.toLocaleString()} {currency} + </div> + ); + }, + meta: { + excelHeader: "견적 금액" + }, + enableResizing: true, + size: 140, + }, + { + accessorKey: "currency", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="통화" /> + ), + cell: ({ row }) => <div>{row.getValue("currency")}</div>, + meta: { + excelHeader: "통화" + }, + enableResizing: true, + size: 80, + }, + { + accessorKey: "validUntil", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="유효기간" /> + ), + cell: ({ cell }) => { + const value = cell.getValue() as Date | null; + return value ? formatDate(value, "KR") : "-"; + }, + meta: { + excelHeader: "유효기간" + }, + enableResizing: true, + size: 120, + }, + { + accessorKey: "submittedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="제출일" /> + ), + cell: ({ cell }) => { + const value = cell.getValue() as Date | null; + return value ? formatDate(value, "KR") : "-"; + }, + meta: { + excelHeader: "제출일" + }, + enableResizing: true, + size: 120, + }, + { + accessorKey: "createdByName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="등록자" /> + ), + cell: ({ row }) => <div>{row.getValue("createdByName")}</div>, + meta: { + excelHeader: "등록자" + }, + enableResizing: true, + size: 120, + }, + { + accessorKey: "remark", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="비고" /> + ), + cell: ({ row }) => <div>{row.getValue("remark") || "-"}</div>, + meta: { + excelHeader: "비고" + }, + enableResizing: true, + size: 200, + }, + { + id: "actions", + header: () => <div className="text-right">동작</div>, + cell: function Cell({ row }) { + const vendorId = row.original.vendorId; + const unreadCount = vendorId ? unreadMessages[vendorId] || 0 : 0; + + return ( + <div className="text-right flex items-center justify-end gap-1"> + {/* 커뮤니케이션 버튼 */} + <div className="relative"> + <Button + variant="ghost" + size="sm" + className="h-8 w-8 p-0" + onClick={() => setRowAction({ row, type: "communicate" })} + title="벤더와 커뮤니케이션" + > + <MessageCircle className="h-4 w-4" /> + </Button> + {unreadCount > 0 && ( + <Badge + variant="destructive" + className="absolute -top-1 -right-1 h-4 w-4 p-0 text-xs flex items-center justify-center" + > + {unreadCount > 9 ? '9+' : unreadCount} + </Badge> + )} + </div> + + {/* 기존 드롭다운 메뉴 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="ghost" + className="flex h-8 w-8 p-0 data-[state=open]:bg-muted" + > + <Ellipsis className="h-4 w-4" /> + <span className="sr-only">메뉴 열기</span> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-[160px]"> + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "update" })} + > + 벤더 수정 + </DropdownMenuItem> + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "delete" })} + className="text-destructive focus:text-destructive" + > + 벤더 제거 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + </div> + ); + }, + enableResizing: false, + size: 80, + }, + ]; +}
\ No newline at end of file |
