summaryrefslogtreecommitdiff
path: root/lib/vendors/table/vendors-table-columns.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendors/table/vendors-table-columns.tsx')
-rw-r--r--lib/vendors/table/vendors-table-columns.tsx393
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