diff options
Diffstat (limited to 'lib/vendors/table/vendors-table-columns.tsx')
| -rw-r--r-- | lib/vendors/table/vendors-table-columns.tsx | 393 |
1 files changed, 272 insertions, 121 deletions
diff --git a/lib/vendors/table/vendors-table-columns.tsx b/lib/vendors/table/vendors-table-columns.tsx index 77750c47..c768b587 100644 --- a/lib/vendors/table/vendors-table-columns.tsx +++ b/lib/vendors/table/vendors-table-columns.tsx @@ -27,30 +27,41 @@ import { import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header" import { useRouter } from "next/navigation" -import { Vendor, vendors, VendorWithAttachments } from "@/db/schema/vendors" +import { VendorWithType, vendors, VendorWithAttachments } from "@/db/schema/vendors" import { modifyVendor } from "../service" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" import { vendorColumnsConfig } from "@/config/vendorColumnsConfig" import { Separator } from "@/components/ui/separator" import { AttachmentsButton } from "./attachmentButton" +import { getVendorStatusIcon } from "../utils" +// 타입 정의 추가 +type StatusType = (typeof vendors.status.enumValues)[number]; +type BadgeVariantType = "default" | "secondary" | "destructive" | "outline"; +type StatusConfig = { + variant: BadgeVariantType; + className: string; +}; +type StatusDisplayMap = { + [key in StatusType]: string; +}; type NextRouter = ReturnType<typeof useRouter>; - interface GetColumnsProps { - setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<Vendor> | null>>; + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<VendorWithType> | null>>; router: NextRouter; + userId: number; } /** * tanstack table 컬럼 정의 (중첩 헤더 버전) */ -export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef<Vendor>[] { +export function getColumns({ setRowAction, router, userId }: GetColumnsProps): ColumnDef<VendorWithType>[] { // ---------------------------------------------------------------- // 1) select 컬럼 (체크박스) // ---------------------------------------------------------------- - const selectColumn: ColumnDef<Vendor> = { + const selectColumn: ColumnDef<VendorWithType> = { id: "select", header: ({ table }) => ( <Checkbox @@ -79,102 +90,103 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef // ---------------------------------------------------------------- // 2) actions 컬럼 (Dropdown 메뉴) // ---------------------------------------------------------------- -// ---------------------------------------------------------------- -// 2) actions 컬럼 (Dropdown 메뉴) -// ---------------------------------------------------------------- -const actionsColumn: ColumnDef<Vendor> = { - id: "actions", - enableHiding: false, - cell: function Cell({ row }) { - const [isUpdatePending, startUpdateTransition] = React.useTransition() - const isApproved = row.original.status === "APPROVED"; - - return ( - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button - aria-label="Open menu" - variant="ghost" - className="flex size-8 p-0 data-[state=open]:bg-muted" - > - <Ellipsis className="size-4" aria-hidden="true" /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="end" className="w-56"> - <DropdownMenuItem - onSelect={() => setRowAction({ row, type: "update" })} - > - Edit - </DropdownMenuItem> - <DropdownMenuItem - onSelect={() => { - // 1) 만약 rowAction을 열고 싶다면 - // setRowAction({ row, type: "update" }) - - // 2) 자세히 보기 페이지로 클라이언트 라우팅 - router.push(`/evcp/vendors/${row.original.id}/info`); - }} - > - Details - </DropdownMenuItem> - - {/* APPROVED 상태일 때만 추가 정보 기입 메뉴 표시 */} - {isApproved && ( + const actionsColumn: ColumnDef<VendorWithType> = { + id: "actions", + enableHiding: false, + cell: function Cell({ row }) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + const isApproved = row.original.status === "PQ_APPROVED"; + const afterApproved = row.original.status === "ACTIVE"; + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + aria-label="Open menu" + variant="ghost" + className="flex size-8 p-0 data-[state=open]:bg-muted" + > + <Ellipsis className="size-4" aria-hidden="true" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-56"> + {(isApproved ||afterApproved) && ( <DropdownMenuItem - onSelect={() => setRowAction({ row, type: "requestInfo" })} - className="text-blue-600 font-medium" + onSelect={() => setRowAction({ row, type: "update" })} > - 추가 정보 기입 + 레코드 편집 </DropdownMenuItem> - )} - - <Separator /> - <DropdownMenuSub> - <DropdownMenuSubTrigger>Status</DropdownMenuSubTrigger> - <DropdownMenuSubContent> - <DropdownMenuRadioGroup - value={row.original.status} - onValueChange={(value) => { - startUpdateTransition(() => { - toast.promise( - modifyVendor({ - id: String(row.original.id), - status: value as Vendor["status"], - }), - { - loading: "Updating...", - success: "Label updated", - error: (err) => getErrorMessage(err), - } - ) - }) - }} - > - {vendors.status.enumValues.map((status) => ( - <DropdownMenuRadioItem - key={status} - value={status} - className="capitalize" - disabled={isUpdatePending} - > - {status} - </DropdownMenuRadioItem> - ))} - </DropdownMenuRadioGroup> - </DropdownMenuSubContent> - </DropdownMenuSub> - </DropdownMenuContent> - </DropdownMenu> - ) - }, - size: 40, -} + )} + + <DropdownMenuItem + onSelect={() => { + // 1) 만약 rowAction을 열고 싶다면 + // setRowAction({ row, type: "update" }) + + // 2) 자세히 보기 페이지로 클라이언트 라우팅 + router.push(`/evcp/vendors/${row.original.id}/info`); + }} + > + 상세보기 + </DropdownMenuItem> + <DropdownMenuItem + onSelect={() => setRowAction({ row, type: "log" })} + > + 감사 로그 보기 + </DropdownMenuItem> + + <Separator /> + <DropdownMenuSub> + <DropdownMenuSubTrigger>Status</DropdownMenuSubTrigger> + <DropdownMenuSubContent> + <DropdownMenuRadioGroup + value={row.original.status} + onValueChange={(value) => { + startUpdateTransition(() => { + toast.promise( + modifyVendor({ + id: String(row.original.id), + status: value as VendorWithType["status"], + userId, + vendorName: row.original.vendorName, // Required field from UpdateVendorSchema + comment: `Status changed to ${value}` + }), + { + loading: "Updating...", + success: "Label updated", + error: (err) => getErrorMessage(err), + } + ) + }) + }} + > + {vendors.status.enumValues.map((status) => ( + <DropdownMenuRadioItem + key={status} + value={status} + className="capitalize" + disabled={isUpdatePending} + > + {status} + </DropdownMenuRadioItem> + ))} + </DropdownMenuRadioGroup> + </DropdownMenuSubContent> + </DropdownMenuSub> + + + </DropdownMenuContent> + </DropdownMenu> + ) + }, + size: 40, + } // ---------------------------------------------------------------- // 3) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성 // ---------------------------------------------------------------- - // 3-1) groupMap: { [groupName]: ColumnDef<Vendor>[] } - const groupMap: Record<string, ColumnDef<Vendor>[]> = {} + // 3-1) groupMap: { [groupName]: ColumnDef<VendorWithType>[] } + const groupMap: Record<string, ColumnDef<VendorWithType>[]> = {} vendorColumnsConfig.forEach((cfg) => { // 만약 group가 없으면 "_noGroup" 처리 @@ -185,7 +197,7 @@ const actionsColumn: ColumnDef<Vendor> = { } // child column 정의 - const childCol: ColumnDef<Vendor> = { + const childCol: ColumnDef<VendorWithType> = { accessorKey: cfg.id, enableResizing: true, header: ({ column }) => ( @@ -197,20 +209,158 @@ const actionsColumn: ColumnDef<Vendor> = { type: cfg.type, }, cell: ({ row, cell }) => { + // Status 컬럼 렌더링 개선 - 아이콘과 더 선명한 배경색 사용 + if (cfg.id === "status") { + const statusVal = row.original.status as StatusType; + if (!statusVal) return null; + // Status badge variant mapping - 더 뚜렷한 색상으로 변경 + const getStatusConfig = (status: StatusType): StatusConfig & { iconColor: string } => { + switch (status) { + case "PENDING_REVIEW": + return { + variant: "outline", + className: "bg-yellow-100 text-yellow-800 border-yellow-300", + iconColor: "text-yellow-600" + }; + case "IN_REVIEW": + return { + variant: "outline", + className: "bg-blue-100 text-blue-800 border-blue-300", + iconColor: "text-blue-600" + }; + case "REJECTED": + return { + variant: "outline", + className: "bg-red-100 text-red-800 border-red-300", + iconColor: "text-red-600" + }; + case "IN_PQ": + return { + variant: "outline", + className: "bg-purple-100 text-purple-800 border-purple-300", + iconColor: "text-purple-600" + }; + case "PQ_SUBMITTED": + return { + variant: "outline", + className: "bg-indigo-100 text-indigo-800 border-indigo-300", + iconColor: "text-indigo-600" + }; + case "PQ_FAILED": + return { + variant: "outline", + className: "bg-red-100 text-red-800 border-red-300", + iconColor: "text-red-600" + }; + case "PQ_APPROVED": + return { + variant: "outline", + className: "bg-green-100 text-green-800 border-green-300", + iconColor: "text-green-600" + }; + case "APPROVED": + return { + variant: "outline", + className: "bg-green-100 text-green-800 border-green-300", + iconColor: "text-green-600" + }; + case "READY_TO_SEND": + return { + variant: "outline", + className: "bg-emerald-100 text-emerald-800 border-emerald-300", + iconColor: "text-emerald-600" + }; + case "ACTIVE": + return { + variant: "outline", + className: "bg-emerald-100 text-emerald-800 border-emerald-300 font-semibold", + iconColor: "text-emerald-600" + }; + case "INACTIVE": + return { + variant: "outline", + className: "bg-gray-100 text-gray-800 border-gray-300", + iconColor: "text-gray-600" + }; + case "BLACKLISTED": + return { + variant: "outline", + className: "bg-slate-800 text-white border-slate-900", + iconColor: "text-white" + }; + default: + return { + variant: "outline", + className: "bg-gray-100 text-gray-800 border-gray-300", + iconColor: "text-gray-600" + }; + } + }; + + // Translate status for display + const getStatusDisplay = (status: StatusType): string => { + const statusMap: StatusDisplayMap = { + "PENDING_REVIEW": "가입 신청 중", + "IN_REVIEW": "심사 중", + "REJECTED": "심사 거부됨", + "IN_PQ": "PQ 진행 중", + "PQ_SUBMITTED": "PQ 제출", + "PQ_FAILED": "PQ 실패", + "PQ_APPROVED": "PQ 통과", + "APPROVED": "승인됨", + "READY_TO_SEND": "MDG 송부대기", + "ACTIVE": "활성 상태", + "INACTIVE": "비활성 상태", + "BLACKLISTED": "거래 금지" + }; + + return statusMap[status] || status; + }; + + const config = getStatusConfig(statusVal); + const displayText = getStatusDisplay(statusVal); + const StatusIcon = getVendorStatusIcon(statusVal); - if (cfg.id === "status") { - const statusVal = row.original.status - if (!statusVal) return null - // const Icon = getStatusIcon(statusVal) return ( - <div className="flex w-[6.25rem] items-center"> - {/* <Icon className="mr-2 size-4 text-muted-foreground" aria-hidden="true" /> */} - <span className="capitalize">{statusVal}</span> - </div> - ) + <Badge variant={config.variant} className={`flex items-center px-2 py-1 ${config.className}`}> + <StatusIcon className={`mr-1 h-3.5 w-3.5 ${config.iconColor}`} /> + <span>{displayText}</span> + </Badge> + ); } + // 업체 유형 컬럼 처리 + if (cfg.id === "vendorTypeName") { + const typeVal = row.original.vendorTypeName as string | null; + return typeVal ? ( + <span className="text-sm font-medium"> + {typeVal} + </span> + ) : ( + <span className="text-sm text-gray-400">미지정</span> + ); + } + + // 업체 분류 컬럼 처리 (별도로 표시하고 싶은 경우) + if (cfg.id === "vendorCategory") { + const categoryVal = row.original.vendorCategory as string | null; + if (!categoryVal) return null; + + let badgeClass = ""; + + if (categoryVal === "정규업체") { + badgeClass = "bg-green-50 text-green-700 border-green-200"; + } else if (categoryVal === "잠재업체") { + badgeClass = "bg-blue-50 text-blue-700 border-blue-200"; + } + + return ( + <Badge variant="outline" className={badgeClass}> + {categoryVal} + </Badge> + ); + } if (cfg.id === "createdAt") { const dateVal = cell.getValue() as Date @@ -222,10 +372,10 @@ const actionsColumn: ColumnDef<Vendor> = { return formatDate(dateVal) } - // code etc... return row.getValue(cfg.id) ?? "" }, + minSize: 150 } groupMap[groupName].push(childCol) @@ -234,7 +384,7 @@ const actionsColumn: ColumnDef<Vendor> = { // ---------------------------------------------------------------- // 3-2) groupMap에서 실제 상위 컬럼(그룹)을 만들기 // ---------------------------------------------------------------- - const nestedColumns: ColumnDef<Vendor>[] = [] + const nestedColumns: ColumnDef<VendorWithType>[] = [] // 순서를 고정하고 싶다면 group 순서를 미리 정의하거나 sort해야 함 // 여기서는 그냥 Object.entries 순서 @@ -252,34 +402,35 @@ const actionsColumn: ColumnDef<Vendor> = { } }) - const attachmentsColumn: ColumnDef<VendorWithAttachments> = { + // attachments 컬럼 타입 문제 해결을 위한 타입 단언 + const attachmentsColumn: ColumnDef<VendorWithType> = { id: "attachments", header: ({ column }) => ( <DataTableColumnHeaderSimple column={column} title="" /> ), cell: ({ row }) => { // hasAttachments 및 attachmentsList 속성이 추가되었다고 가정 - const hasAttachments = row.original.hasAttachments; - const attachmentsList = row.original.attachmentsList || []; - - if(hasAttachments){ + const hasAttachments = (row.original as VendorWithAttachments).hasAttachments; + const attachmentsList = (row.original as VendorWithAttachments).attachmentsList || []; - // 서버 액션을 사용하는 컴포넌트로 교체 - return ( - <AttachmentsButton - vendorId={row.original.id} - hasAttachments={hasAttachments} - attachmentsList={attachmentsList} - /> - );}{ - return null + if (hasAttachments) { + // 서버 액션을 사용하는 컴포넌트로 교체 + return ( + <AttachmentsButton + vendorId={row.original.id} + hasAttachments={hasAttachments} + attachmentsList={attachmentsList} + /> + ); + } else { + return null; } }, enableSorting: false, enableHiding: false, minSize: 45, }; - + // ---------------------------------------------------------------- // 4) 최종 컬럼 배열: select, nestedColumns, actions |
