diff options
Diffstat (limited to 'lib/tech-vendor-rfq-response/vendor-rfq-table/rfqs-table-columns.tsx')
| -rw-r--r-- | lib/tech-vendor-rfq-response/vendor-rfq-table/rfqs-table-columns.tsx | 424 |
1 files changed, 424 insertions, 0 deletions
diff --git a/lib/tech-vendor-rfq-response/vendor-rfq-table/rfqs-table-columns.tsx b/lib/tech-vendor-rfq-response/vendor-rfq-table/rfqs-table-columns.tsx new file mode 100644 index 00000000..69a5e7e7 --- /dev/null +++ b/lib/tech-vendor-rfq-response/vendor-rfq-table/rfqs-table-columns.tsx @@ -0,0 +1,424 @@ +"use client" + +import * as React from "react" +import { useRouter } from "next/navigation" +import { ColumnDef } from "@tanstack/react-table" +import { + Ellipsis, + MessageSquare, + Package, + Paperclip, +} from "lucide-react" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger +} from "@/components/ui/dropdown-menu" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { Badge } from "@/components/ui/badge" + +import { getErrorMessage } from "@/lib/handle-error" +import { formatDate, formatDateTime } from "@/lib/utils" +import { modifyRfqVendor } from "../../rfqs-tech/service" +import type { RfqWithAll } from "../types" +import type { DataTableRowAction } from "@/types/table" + +type NextRouter = ReturnType<typeof useRouter> + +interface GetColumnsProps { + setRowAction: React.Dispatch< + React.SetStateAction<DataTableRowAction<RfqWithAll> | null> + > + router: NextRouter + openAttachmentsSheet: (rfqId: number) => void + openCommentSheet: (rfqId: number) => void +} + +/** + * tanstack table 컬럼 정의 (Nested Header) + */ +export function getColumns({ + setRowAction, + router, + openAttachmentsSheet, + openCommentSheet, +}: GetColumnsProps): ColumnDef<RfqWithAll>[] { + // 1) 체크박스(Select) 컬럼 + const selectColumn: ColumnDef<RfqWithAll> = { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + size: 40, + enableSorting: false, + enableHiding: false, + } + + // 2) Actions (Dropdown) + const actionsColumn: ColumnDef<RfqWithAll> = { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" size="icon"> + <Ellipsis className="h-4 w-4" aria-hidden="true" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-56"> + <DropdownMenuSub> + <DropdownMenuSubTrigger>RFQ Response</DropdownMenuSubTrigger> + <DropdownMenuSubContent> + <DropdownMenuRadioGroup + value={row.original.responseStatus} + onValueChange={(value) => { + startUpdateTransition(async () => { + let newStatus: + | "ACCEPTED" + | "DECLINED" + | "REVIEWING" + + switch (value) { + case "ACCEPTED": + newStatus = "ACCEPTED" + break + case "DECLINED": + newStatus = "DECLINED" + break + default: + newStatus = "REVIEWING" + } + + await toast.promise( + modifyRfqVendor({ + id: row.original.responseId, + status: newStatus, + }), + { + loading: "Updating response status...", + success: "Response status updated", + error: (err) => getErrorMessage(err), + } + ) + }) + }} + > + {[ + { value: "ACCEPTED", label: "Accept RFQ" }, + { value: "DECLINED", label: "Decline RFQ" }, + ].map((rep) => ( + <DropdownMenuRadioItem + key={rep.value} + value={rep.value} + className="capitalize" + disabled={isUpdatePending} + > + {rep.label} + </DropdownMenuRadioItem> + ))} + </DropdownMenuRadioGroup> + </DropdownMenuSubContent> + </DropdownMenuSub> + {/* <DropdownMenuItem + onClick={() => { + router.push(`/vendor/rfqs/${row.original.rfqId}`) + }} + > + View Details + </DropdownMenuItem> */} + {/* <DropdownMenuItem onClick={() => openAttachmentsSheet(row.original.rfqId)}> + View Attachments + </DropdownMenuItem> + <DropdownMenuItem onClick={() => openCommentSheet(row.original.rfqId)}> + View Comments + </DropdownMenuItem> + <DropdownMenuItem onClick={() => setRowAction({ row, type: "items" })}> + View Items + </DropdownMenuItem> */} + </DropdownMenuContent> + </DropdownMenu> + ) + }, + size: 40, + } + + // 3) RFQ Code 컬럼 + const rfqCodeColumn: ColumnDef<RfqWithAll> = { + id: "rfqCode", + accessorKey: "rfqCode", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="RFQ Code" /> + ), + // cell: ({ row }) => { + // return ( + // <Button + // variant="link" + // className="p-0 h-auto font-medium" + // onClick={() => router.push(`/vendor/rfqs/${row.original.rfqId}`)} + // > + // {row.original.rfqCode} + // </Button> + // ) + // }, + cell: ({ row }) => row.original.rfqCode || "-", + size: 150, + } + + + + // 4) 응답 상태 컬럼 + const responseStatusColumn: ColumnDef<RfqWithAll> = { + id: "responseStatus", + accessorKey: "responseStatus", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Response Status" /> + ), + cell: ({ row }) => { + const status = row.original.responseStatus; + let variant: "default" | "secondary" | "destructive" | "outline"; + + switch (status) { + case "REVIEWING": + variant = "default"; + break; + case "ACCEPTED": + variant = "secondary"; + break; + case "DECLINED": + variant = "destructive"; + break; + default: + variant = "outline"; + } + + return <Badge variant={variant}>{status}</Badge>; + }, + size: 150, + } + + // 5) 프로젝트 이름 컬럼 + const projectNameColumn: ColumnDef<RfqWithAll> = { + id: "projectName", + accessorKey: "projectName", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Project" /> + ), + cell: ({ row }) => row.original.projectName || "-", + size: 150, + } + + // 6) RFQ Description 컬럼 + const descriptionColumn: ColumnDef<RfqWithAll> = { + id: "rfqDescription", + accessorKey: "rfqDescription", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Description" /> + ), + cell: ({ row }) => row.original.rfqDescription || "-", + size: 200, + } + + // 7) Due Date 컬럼 + const dueDateColumn: ColumnDef<RfqWithAll> = { + id: "rfqDueDate", + accessorKey: "rfqDueDate", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Due Date" /> + ), + cell: ({ row }) => { + const date = row.original.rfqDueDate; + return date ? formatDate(date) : "-"; + }, + size: 120, + } + + // 8) Last Updated 컬럼 + const updatedAtColumn: ColumnDef<RfqWithAll> = { + id: "respondedAt", + accessorKey: "respondedAt", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Last Updated" /> + ), + cell: ({ row }) => { + const date = row.original.respondedAt; + return date ? formatDateTime(date) : "-"; + }, + size: 150, + } + + // 9) Items 컬럼 - 뱃지로 아이템 개수 표시 + const itemsColumn: ColumnDef<RfqWithAll> = { + id: "items", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Items" /> + ), + cell: ({ row }) => { + const rfq = row.original + const count = rfq.items?.length ?? 0 + + function handleClick() { + setRowAction({ row, type: "items" }) + } + + return ( + <Button + variant="ghost" + size="sm" + className="relative h-8 w-8 p-0 group" + onClick={handleClick} + aria-label={count > 0 ? `View ${count} items` : "No items"} + > + <Package className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" /> + {count > 0 && ( + <Badge + variant="secondary" + className="pointer-events-none absolute -top-1 -right-1 h-4 min-w-[1rem] p-0 text-[0.625rem] leading-none flex items-center justify-center" + > + {count} + </Badge> + )} + + <span className="sr-only"> + {count > 0 ? `${count} Items` : "No Items"} + </span> + </Button> + ) + }, + enableSorting: false, + maxSize: 80, + } + + // 10) Attachments 컬럼 - 뱃지로 파일 개수 표시 + const attachmentsColumn: ColumnDef<RfqWithAll> = { + id: "attachments", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Attachments" /> + ), + cell: ({ row }) => { + const attachCount = row.original.attachments?.length ?? 0 + + function handleClick(e: React.MouseEvent<HTMLButtonElement>) { + e.preventDefault() + openAttachmentsSheet(row.original.rfqId) + } + + return ( + <Button + variant="ghost" + size="sm" + className="relative h-8 w-8 p-0 group" + onClick={handleClick} + aria-label={ + attachCount > 0 ? `View ${attachCount} files` : "No files" + } + > + <Paperclip className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" /> + {attachCount > 0 && ( + <Badge + variant="secondary" + className="pointer-events-none absolute -top-1 -right-1 h-4 min-w-[1rem] p-0 text-[0.625rem] leading-none flex items-center justify-center" + > + {attachCount} + </Badge> + )} + <span className="sr-only"> + {attachCount > 0 ? `${attachCount} Files` : "No Files"} + </span> + </Button> + ) + }, + enableSorting: false, + maxSize: 80, + } + + // 11) Comments 컬럼 - 뱃지로 댓글 개수 표시 + const commentsColumn: ColumnDef<RfqWithAll> = { + id: "comments", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Comments" /> + ), + cell: ({ row }) => { + const commCount = row.original.comments?.length ?? 0 + + function handleClick() { + setRowAction({ row, type: "comments" }) + openCommentSheet(row.original.rfqId) + } + + return ( + <Button + variant="ghost" + size="sm" + className="relative h-8 w-8 p-0 group" + onClick={handleClick} + aria-label={ + commCount > 0 ? `View ${commCount} comments` : "No comments" + } + > + <MessageSquare className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" /> + {commCount > 0 && ( + <Badge + variant="secondary" + className="pointer-events-none absolute -top-1 -right-1 h-4 min-w-[1rem] p-0 text-[0.625rem] leading-none flex items-center justify-center" + > + {commCount} + </Badge> + )} + <span className="sr-only"> + {commCount > 0 ? `${commCount} Comments` : "No Comments"} + </span> + </Button> + ) + }, + enableSorting: false, + maxSize: 80, + } + + // 최종 컬럼 구성 - TBE/CBE 관련 컬럼 제외 + return [ + selectColumn, + rfqCodeColumn, + responseStatusColumn, + projectNameColumn, + descriptionColumn, + dueDateColumn, + itemsColumn, + attachmentsColumn, + commentsColumn, + updatedAtColumn, + actionsColumn, + ] +}
\ No newline at end of file |
