Add more columns and column selector to product table

This commit is contained in:
2025-01-13 11:55:30 -05:00
parent ab9b5cb48c
commit dd882490c8
6 changed files with 620 additions and 150 deletions

View File

@@ -2,7 +2,6 @@ import { Home, Package, ShoppingCart, BarChart2, Settings, Box, ClipboardList }
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarHeader,

View File

@@ -19,19 +19,62 @@ interface Product {
price: number;
regular_price: number;
cost_price: number;
landing_cost_price: number;
barcode: string;
vendor: string;
vendor_reference: string;
brand: string;
categories: string[];
image: string | null;
moq: number;
uom: number;
visible: boolean;
managing_stock: boolean;
image?: string;
replenishable: boolean;
// Metrics
daily_sales_avg?: number;
weekly_sales_avg?: number;
monthly_sales_avg?: number;
avg_quantity_per_order?: number;
number_of_orders?: number;
first_sale_date?: string;
last_sale_date?: string;
days_of_inventory?: number;
weeks_of_inventory?: number;
reorder_point?: number;
safety_stock?: number;
avg_margin_percent?: number;
total_revenue?: number;
inventory_value?: number;
cost_of_goods_sold?: number;
gross_profit?: number;
gmroi?: number;
avg_lead_time_days?: number;
last_purchase_date?: string;
last_received_date?: string;
abc_class?: string;
stock_status?: string;
turnover_rate?: number;
current_lead_time?: number;
target_lead_time?: number;
lead_time_status?: string;
}
interface ColumnDef {
key: keyof Product;
label: string;
group: string;
format?: (value: any) => string | number;
}
interface ProductTableProps {
products: Product[];
onSort: (column: keyof Product) => void;
sortColumn: keyof Product | null;
sortColumn: keyof Product;
sortDirection: 'asc' | 'desc';
visibleColumns: Set<keyof Product>;
columnDefs: ColumnDef[];
}
export function ProductTable({
@@ -39,6 +82,8 @@ export function ProductTable({
onSort,
sortColumn,
sortDirection,
visibleColumns,
columnDefs,
}: ProductTableProps) {
const getSortIcon = (column: keyof Product) => {
if (sortColumn !== column) return <ArrowUpDown className="ml-2 h-4 w-4" />;
@@ -49,149 +94,135 @@ export function ProductTable({
);
};
const getStockStatus = (quantity: number) => {
if (quantity === 0) {
return <Badge variant="destructive">Out of Stock</Badge>;
const getStockStatus = (status: string | undefined) => {
if (!status) return null;
switch (status.toLowerCase()) {
case 'critical':
return <Badge variant="destructive">Critical</Badge>;
case 'reorder':
return <Badge variant="outline">Reorder</Badge>;
case 'healthy':
return <Badge variant="secondary">Healthy</Badge>;
case 'overstocked':
return <Badge variant="secondary">Overstocked</Badge>;
case 'new':
return <Badge variant="default">New</Badge>;
default:
return null;
}
if (quantity <= 5) {
return <Badge variant="outline">Low Stock</Badge>;
}
return <Badge variant="secondary">In Stock</Badge>;
};
const getABCClass = (abcClass: string | undefined) => {
if (!abcClass) return null;
switch (abcClass.toUpperCase()) {
case 'A':
return <Badge variant="default">A</Badge>;
case 'B':
return <Badge variant="secondary">B</Badge>;
case 'C':
return <Badge variant="outline">C</Badge>;
default:
return null;
}
};
const getLeadTimeStatus = (status: string | undefined) => {
if (!status) return null;
switch (status.toLowerCase()) {
case 'critical':
return <Badge variant="destructive">Critical</Badge>;
case 'warning':
return <Badge variant="secondary">Warning</Badge>;
case 'good':
return <Badge variant="default">Good</Badge>;
default:
return null;
}
};
const formatColumnValue = (product: Product, column: ColumnDef) => {
const value = product[column.key];
// Special formatting for specific columns
switch (column.key) {
case 'title':
return (
<div className="flex items-center gap-2">
<Avatar className="h-8 w-8">
<AvatarImage src={product.image || undefined} alt={product.title} />
<AvatarFallback>{product.title.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
<span className="font-medium">{value as string}</span>
</div>
);
case 'categories':
return (
<div className="flex flex-wrap gap-1">
{Array.from(new Set(value as string[])).map((category) => (
<Badge key={`${product.product_id}-${category}`} variant="outline">{category}</Badge>
)) || '-'}
</div>
);
case 'stock_status':
return getStockStatus(value as string);
case 'abc_class':
return getABCClass(value as string);
case 'lead_time_status':
return getLeadTimeStatus(value as string);
case 'visible':
return value ? (
<Badge variant="secondary">Active</Badge>
) : (
<Badge variant="outline">Hidden</Badge>
);
default:
if (column.format && value !== undefined && value !== null) {
return column.format(value);
}
return value || '-';
}
};
// Get visible column definitions in order
const visibleColumnDefs = columnDefs.filter(col => visibleColumns.has(col.key));
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>
<Button
variant="ghost"
onClick={() => onSort('title')}
>
Product
{getSortIcon('title')}
</Button>
</TableHead>
<TableHead>
<Button
variant="ghost"
onClick={() => onSort('sku')}
>
SKU
{getSortIcon('sku')}
</Button>
</TableHead>
<TableHead>
<Button
variant="ghost"
onClick={() => onSort('stock_quantity')}
>
Stock
{getSortIcon('stock_quantity')}
</Button>
</TableHead>
<TableHead>
<Button
variant="ghost"
onClick={() => onSort('price')}
>
Price
{getSortIcon('price')}
</Button>
</TableHead>
<TableHead>
<Button
variant="ghost"
onClick={() => onSort('regular_price')}
>
Regular Price
{getSortIcon('regular_price')}
</Button>
</TableHead>
<TableHead>
<Button
variant="ghost"
onClick={() => onSort('cost_price')}
>
Cost
{getSortIcon('cost_price')}
</Button>
</TableHead>
<TableHead>
<Button
variant="ghost"
onClick={() => onSort('vendor')}
>
Vendor
{getSortIcon('vendor')}
</Button>
</TableHead>
<TableHead>
<Button
variant="ghost"
onClick={() => onSort('brand')}
>
Brand
{getSortIcon('brand')}
</Button>
</TableHead>
<TableHead>
<Button
variant="ghost"
onClick={() => onSort('categories')}
>
Categories
{getSortIcon('categories')}
</Button>
</TableHead>
<TableHead>Status</TableHead>
{visibleColumnDefs.map((column) => (
<TableHead key={column.key}>
<Button
variant="ghost"
onClick={() => onSort(column.key)}
>
{column.label}
{getSortIcon(column.key)}
</Button>
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{products.map((product) => (
<TableRow key={product.product_id}>
<TableCell>
<div className="flex items-center gap-2">
<Avatar className="h-8 w-8">
<AvatarImage src={product.image} alt={product.title} />
<AvatarFallback>{product.title.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
<span className="font-medium">{product.title}</span>
</div>
</TableCell>
<TableCell>{product.sku}</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<span>{product.stock_quantity}</span>
{getStockStatus(product.stock_quantity)}
</div>
</TableCell>
<TableCell>${product.price.toFixed(2)}</TableCell>
<TableCell>${product.regular_price.toFixed(2)}</TableCell>
<TableCell>${product.cost_price.toFixed(2)}</TableCell>
<TableCell>{product.vendor || '-'}</TableCell>
<TableCell>{product.brand || '-'}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{product.categories?.map((category) => (
<Badge key={category} variant="outline">{category}</Badge>
)) || '-'}
</div>
</TableCell>
<TableCell>
{product.visible ? (
<Badge variant="secondary">Active</Badge>
) : (
<Badge variant="outline">Hidden</Badge>
)}
</TableCell>
</TableRow>
))}
{products.map((product) => {
console.log('Rendering product:', product.product_id, product.title, product.categories);
return (
<TableRow key={product.product_id}>
{visibleColumnDefs.map((column) => (
<TableCell key={`${product.product_id}-${column.key}`}>
{formatColumnValue(product, column)}
</TableCell>
))}
</TableRow>
);
})}
{!products.length && (
<TableRow>
<TableCell colSpan={10} className="text-center py-8 text-muted-foreground">
<TableCell
colSpan={visibleColumnDefs.length}
className="text-center py-8 text-muted-foreground"
>
No products found
</TableCell>
</TableRow>

View File

@@ -0,0 +1,199 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@@ -12,9 +12,21 @@ import {
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Settings2 } from "lucide-react";
import config from '../config';
// Enhanced Product interface with all possible fields
interface Product {
// Basic product info (from products table)
product_id: string;
title: string;
sku: string;
@@ -22,12 +34,46 @@ interface Product {
price: number;
regular_price: number;
cost_price: number;
landing_cost_price: number;
barcode: string;
vendor: string;
vendor_reference: string;
brand: string;
categories: string[];
image: string | null;
moq: number;
uom: number;
visible: boolean;
managing_stock: boolean;
image: string | null;
replenishable: boolean;
// Metrics (from product_metrics table)
daily_sales_avg?: number;
weekly_sales_avg?: number;
monthly_sales_avg?: number;
avg_quantity_per_order?: number;
number_of_orders?: number;
first_sale_date?: string;
last_sale_date?: string;
days_of_inventory?: number;
weeks_of_inventory?: number;
reorder_point?: number;
safety_stock?: number;
avg_margin_percent?: number;
total_revenue?: number;
inventory_value?: number;
cost_of_goods_sold?: number;
gross_profit?: number;
gmroi?: number;
avg_lead_time_days?: number;
last_purchase_date?: string;
last_received_date?: string;
abc_class?: string;
stock_status?: string;
turnover_rate?: number;
current_lead_time?: number;
target_lead_time?: number;
lead_time_status?: string;
}
interface ProductFiltersState {
@@ -39,6 +85,82 @@ interface ProductFiltersState {
maxPrice: string;
}
// Column definition interface
interface ColumnDef {
key: keyof Product;
label: string;
group: string;
format?: (value: any) => string | number;
}
// Define available columns with grouping
const AVAILABLE_COLUMNS: ColumnDef[] = [
// Basic Info Group
{ key: 'title', label: 'Title', group: 'Basic Info' },
{ key: 'sku', label: 'SKU', group: 'Basic Info' },
{ key: 'brand', label: 'Brand', group: 'Basic Info' },
{ key: 'categories', label: 'Categories', group: 'Basic Info' },
{ key: 'vendor', label: 'Vendor', group: 'Basic Info' },
{ key: 'vendor_reference', label: 'Vendor Reference', group: 'Basic Info' },
{ key: 'barcode', label: 'Barcode', group: 'Basic Info' },
// Inventory Group
{ key: 'stock_quantity', label: 'Stock Quantity', group: 'Inventory' },
{ key: 'stock_status', label: 'Stock Status', group: 'Inventory' },
{ key: 'days_of_inventory', label: 'Days of Inventory', group: 'Inventory' },
{ key: 'reorder_point', label: 'Reorder Point', group: 'Inventory' },
{ key: 'safety_stock', label: 'Safety Stock', group: 'Inventory' },
{ key: 'moq', label: 'MOQ', group: 'Inventory' },
{ key: 'uom', label: 'UOM', group: 'Inventory' },
// Pricing Group
{ key: 'price', label: 'Price', group: 'Pricing', format: (v) => v.toFixed(2) },
{ key: 'regular_price', label: 'Regular Price', group: 'Pricing', format: (v) => v.toFixed(2) },
{ key: 'cost_price', label: 'Cost Price', group: 'Pricing', format: (v) => v.toFixed(2) },
{ key: 'landing_cost_price', label: 'Landing Cost', group: 'Pricing', format: (v) => v.toFixed(2) },
// Sales Metrics Group
{ key: 'daily_sales_avg', label: 'Daily Sales Avg', group: 'Sales Metrics', format: (v) => v?.toFixed(2) ?? '-' },
{ key: 'weekly_sales_avg', label: 'Weekly Sales Avg', group: 'Sales Metrics', format: (v) => v?.toFixed(2) ?? '-' },
{ key: 'monthly_sales_avg', label: 'Monthly Sales Avg', group: 'Sales Metrics', format: (v) => v?.toFixed(2) ?? '-' },
{ key: 'avg_quantity_per_order', label: 'Avg Qty per Order', group: 'Sales Metrics', format: (v) => v?.toFixed(1) ?? '-' },
{ key: 'number_of_orders', label: 'Number of Orders', group: 'Sales Metrics' },
{ key: 'first_sale_date', label: 'First Sale', group: 'Sales Metrics' },
{ key: 'last_sale_date', label: 'Last Sale', group: 'Sales Metrics' },
// Financial Metrics Group
{ key: 'avg_margin_percent', label: 'Avg Margin %', group: 'Financial Metrics', format: (v) => v ? `${v.toFixed(1)}%` : '-' },
{ key: 'total_revenue', label: 'Total Revenue', group: 'Financial Metrics', format: (v) => v?.toFixed(2) ?? '-' },
{ key: 'inventory_value', label: 'Inventory Value', group: 'Financial Metrics', format: (v) => v?.toFixed(2) ?? '-' },
{ key: 'cost_of_goods_sold', label: 'COGS', group: 'Financial Metrics', format: (v) => v?.toFixed(2) ?? '-' },
{ key: 'gross_profit', label: 'Gross Profit', group: 'Financial Metrics', format: (v) => v?.toFixed(2) ?? '-' },
{ key: 'gmroi', label: 'GMROI', group: 'Financial Metrics', format: (v) => v?.toFixed(2) ?? '-' },
{ key: 'turnover_rate', label: 'Turnover Rate', group: 'Financial Metrics', format: (v) => v?.toFixed(2) ?? '-' },
// Purchase & Lead Time Group
{ key: 'avg_lead_time_days', label: 'Avg Lead Time (Days)', group: 'Purchase & Lead Time' },
{ key: 'current_lead_time', label: 'Current Lead Time', group: 'Purchase & Lead Time' },
{ key: 'target_lead_time', label: 'Target Lead Time', group: 'Purchase & Lead Time' },
{ key: 'lead_time_status', label: 'Lead Time Status', group: 'Purchase & Lead Time' },
{ key: 'last_purchase_date', label: 'Last Purchase', group: 'Purchase & Lead Time' },
{ key: 'last_received_date', label: 'Last Received', group: 'Purchase & Lead Time' },
// Classification Group
{ key: 'abc_class', label: 'ABC Class', group: 'Classification' },
];
// Default visible columns
const DEFAULT_VISIBLE_COLUMNS: (keyof Product)[] = [
'title',
'sku',
'stock_quantity',
'stock_status',
'price',
'vendor',
'brand',
'categories',
];
export function Products() {
const queryClient = useQueryClient();
const tableRef = useRef<HTMLDivElement>(null);
@@ -53,6 +175,29 @@ export function Products() {
const [sortColumn, setSortColumn] = useState<keyof Product>('title');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const [page, setPage] = useState(1);
const [visibleColumns, setVisibleColumns] = useState<Set<keyof Product>>(new Set(DEFAULT_VISIBLE_COLUMNS));
// Group columns by their group property
const columnsByGroup = AVAILABLE_COLUMNS.reduce((acc, col) => {
if (!acc[col.group]) {
acc[col.group] = [];
}
acc[col.group].push(col);
return acc;
}, {} as Record<string, ColumnDef[]>);
// Toggle column visibility
const toggleColumn = (columnKey: keyof Product) => {
setVisibleColumns(prev => {
const next = new Set(prev);
if (next.has(columnKey)) {
next.delete(columnKey);
} else {
next.add(columnKey);
}
return next;
});
};
// Function to fetch products data
const fetchProducts = async (pageNum: number) => {
@@ -288,8 +433,39 @@ export function Products() {
<div className="p-8 space-y-8">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Products</h1>
<div className="text-sm text-muted-foreground">
{data?.pagination.total.toLocaleString() ?? '...'} products
<div className="flex items-center gap-4">
<div className="text-sm text-muted-foreground">
{data?.pagination.total.toLocaleString() ?? '...'} products
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<Settings2 className="mr-2 h-4 w-4" />
Columns
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[280px]">
<DropdownMenuLabel>Toggle Columns</DropdownMenuLabel>
<DropdownMenuSeparator />
{Object.entries(columnsByGroup).map(([group, columns]) => (
<div key={group}>
<DropdownMenuLabel className="text-xs font-bold text-muted-foreground">
{group}
</DropdownMenuLabel>
{columns.map((col) => (
<DropdownMenuCheckboxItem
key={col.key}
checked={visibleColumns.has(col.key)}
onCheckedChange={() => toggleColumn(col.key)}
>
{col.label}
</DropdownMenuCheckboxItem>
))}
<DropdownMenuSeparator />
</div>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
@@ -317,6 +493,8 @@ export function Products() {
onSort={handleSort}
sortColumn={sortColumn}
sortDirection={sortDirection}
visibleColumns={visibleColumns}
columnDefs={AVAILABLE_COLUMNS}
/>
</div>
{renderPagination()}