Refactor ProductTable and ProductViews components; update icons and badge variants. Enhance Products page with dynamic column visibility and order management for different views.

This commit is contained in:
2025-01-15 14:24:03 -05:00
parent ad71fe02a5
commit 5747ce3ded
3 changed files with 203 additions and 85 deletions

View File

@@ -1,5 +1,5 @@
import * as React from "react"; import * as React from "react";
import { ArrowUpDown, GripVertical } from "lucide-react"; import { SortAsc, SortDesc } from "lucide-react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { import {
Table, Table,
@@ -87,20 +87,17 @@ function SortableHeader({ column, columnDef, onSort, sortColumn, sortDirection }
"cursor-pointer select-none", "cursor-pointer select-none",
columnDef?.width columnDef?.width
)} )}
{...attributes}
{...listeners}
onClick={() => onSort(column)}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-1">
<div {...attributes} {...listeners} className="cursor-grab"> {columnDef?.label ?? column}
<GripVertical className="h-4 w-4 text-muted-foreground" /> {sortColumn === column && (
</div> sortDirection === 'desc'
<div className="flex items-center gap-2" onClick={() => onSort(column)}> ? <SortDesc className="h-4 w-4" />
{columnDef?.label ?? column} : <SortAsc className="h-4 w-4" />
{sortColumn === column && ( )}
<ArrowUpDown className={cn(
"h-4 w-4",
sortDirection === 'desc' && "rotate-180 transform"
)} />
)}
</div>
</div> </div>
</TableHead> </TableHead>
); );
@@ -161,15 +158,19 @@ export function ProductTable({
case 'critical': case 'critical':
return <Badge variant="destructive">Critical</Badge>; return <Badge variant="destructive">Critical</Badge>;
case 'reorder': case 'reorder':
return <Badge variant="outline">Reorder</Badge>; return <Badge variant="warning">Reorder</Badge>;
case 'healthy': case 'healthy':
return <Badge variant="secondary">Healthy</Badge>; return <Badge variant="success">Healthy</Badge>;
case 'overstocked': case 'overstocked':
return <Badge variant="secondary">Overstocked</Badge>; return <Badge variant="secondary">Overstocked</Badge>;
case 'new': case 'new':
return <Badge variant="default">New</Badge>; return <Badge variant="default">New</Badge>;
case 'out of stock':
return <Badge variant="destructive">Out of Stock</Badge>;
case 'at-risk':
return <Badge variant="warning">At Risk</Badge>;
default: default:
return null; return <Badge variant="outline">{status}</Badge>;
} }
}; };

View File

@@ -1,6 +1,6 @@
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Product } from "@/types/products" import { Product } from "@/types/products"
import { AlertCircle, AlertTriangle, CheckCircle2, PackageSearch, Star } from "lucide-react" import { AlertCircle, AlertTriangle, CheckCircle2, PackageSearch, Sparkles } from "lucide-react"
export type ProductView = { export type ProductView = {
id: string id: string
@@ -15,42 +15,42 @@ export const PRODUCT_VIEWS: ProductView[] = [
id: "all", id: "all",
label: "All Products", label: "All Products",
icon: PackageSearch, icon: PackageSearch,
iconClassName: "text-muted-foreground", iconClassName: "",
columns: ["image", "title", "SKU", "stock_quantity", "price", "stock_status"] columns: ["image", "title", "SKU", "stock_quantity", "price", "stock_status"]
}, },
{ {
id: "Critical", id: "Critical",
label: "Critical Stock", label: "Critical Stock",
icon: AlertTriangle, icon: AlertTriangle,
iconClassName: "text-destructive", iconClassName: "",
columns: ["image", "title", "SKU", "stock_quantity", "daily_sales_avg", "reorder_qty", "replenishable", "last_purchase_date", "lead_time_status"] columns: ["image", "title", "SKU", "stock_quantity", "daily_sales_avg", "reorder_qty", "replenishable", "last_purchase_date", "lead_time_status"]
}, },
{ {
id: "Reorder", id: "Reorder",
label: "Reorder Soon", label: "Reorder Soon",
icon: AlertCircle, icon: AlertCircle,
iconClassName: "text-warning", iconClassName: "",
columns: ["image", "title", "SKU", "stock_quantity", "daily_sales_avg", "reorder_qty", "replenishable", "last_purchase_date", "lead_time_status"] columns: ["image", "title", "SKU", "stock_quantity", "daily_sales_avg", "reorder_qty", "replenishable", "last_purchase_date", "lead_time_status"]
}, },
{ {
id: "Healthy", id: "Healthy",
label: "Healthy Stock", label: "Healthy Stock",
icon: CheckCircle2, icon: CheckCircle2,
iconClassName: "text-success", iconClassName: "",
columns: ["image", "title", "stock_quantity", "daily_sales_avg", "stock_status", "abc_class"] columns: ["image", "title", "stock_quantity", "daily_sales_avg", "stock_status", "abc_class"]
}, },
{ {
id: "Overstocked", id: "Overstocked",
label: "Overstock", label: "Overstock",
icon: PackageSearch, icon: PackageSearch,
iconClassName: "text-muted-foreground", iconClassName: "",
columns: ["image", "title", "stock_quantity", "daily_sales_avg", "overstocked_amt", "replenishable", "last_sale_date", "abc_class"] columns: ["image", "title", "stock_quantity", "daily_sales_avg", "overstocked_amt", "replenishable", "last_sale_date", "abc_class"]
}, },
{ {
id: "New", id: "New",
label: "New Products", label: "New Products",
icon: Star, icon: Sparkles,
iconClassName: "text-accent", iconClassName: "",
columns: ["image", "title", "stock_quantity", "daily_sales_avg", "stock_status", "abc_class"] columns: ["image", "title", "stock_quantity", "daily_sales_avg", "stock_status", "abc_class"]
} }
] ]

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { useQuery, keepPreviousData } from '@tanstack/react-query'; import { useQuery, keepPreviousData } from '@tanstack/react-query';
import { ProductFilters } from '@/components/products/ProductFilters'; import { ProductFilters } from '@/components/products/ProductFilters';
import { ProductTable } from '@/components/products/ProductTable'; import { ProductTable } from '@/components/products/ProductTable';
@@ -75,47 +75,155 @@ const AVAILABLE_COLUMNS: ColumnDef[] = [
{ key: 'last_purchase_date', label: 'Last Purchase', group: 'Lead Time' }, { key: 'last_purchase_date', label: 'Last Purchase', group: 'Lead Time' },
]; ];
// Default visible columns // Define default columns for each view
const DEFAULT_VISIBLE_COLUMNS: ColumnKey[] = [ const VIEW_COLUMNS: Record<string, ColumnKey[]> = {
'image', all: [
'title', 'image',
'SKU', 'title',
'brand', 'brand',
'vendor', 'vendor',
'stock_quantity', 'stock_quantity',
'stock_status', 'stock_status',
'replenishable', 'reorder_qty',
'reorder_qty', 'price',
'price', 'regular_price',
'regular_price', 'daily_sales_avg',
'daily_sales_avg', 'weekly_sales_avg',
'weekly_sales_avg', 'monthly_sales_avg',
'monthly_sales_avg', ],
]; critical: [
'image',
'title',
'stock_quantity',
'daily_sales_avg',
'weekly_sales_avg',
'reorder_qty',
'vendor',
'last_purchase_date',
'current_lead_time',
],
reorder: [
'image',
'title',
'stock_quantity',
'daily_sales_avg',
'weekly_sales_avg',
'reorder_qty',
'vendor',
'last_purchase_date',
],
overstocked: [
'image',
'title',
'stock_quantity',
'daily_sales_avg',
'weekly_sales_avg',
'overstocked_amt',
'days_of_inventory',
],
'at-risk': [
'image',
'title',
'stock_quantity',
'daily_sales_avg',
'weekly_sales_avg',
'monthly_sales_avg',
'days_of_inventory',
],
new: [
'image',
'title',
'stock_quantity',
'vendor',
'brand',
'price',
'regular_price',
],
healthy: [
'image',
'title',
'stock_quantity',
'daily_sales_avg',
'weekly_sales_avg',
'monthly_sales_avg',
'days_of_inventory',
],
};
export function Products() { export function Products() {
const [filters, setFilters] = useState<Record<string, string | number | boolean>>({}); const [filters, setFilters] = useState<Record<string, string | number | boolean>>({});
const [sortColumn, setSortColumn] = useState<ColumnKey>('title'); const [sortColumn, setSortColumn] = useState<ColumnKey>('title');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [visibleColumns, setVisibleColumns] = useState<Set<ColumnKey>>(new Set(DEFAULT_VISIBLE_COLUMNS));
const [columnOrder, setColumnOrder] = useState<ColumnKey[]>([
...DEFAULT_VISIBLE_COLUMNS,
...AVAILABLE_COLUMNS.map(col => col.key).filter(key => !DEFAULT_VISIBLE_COLUMNS.includes(key))
]);
const [selectedProductId, setSelectedProductId] = useState<number | null>(null);
const [activeView, setActiveView] = useState("all"); const [activeView, setActiveView] = useState("all");
const [pageSize] = useState(50); const [pageSize] = useState(50);
const [showNonReplenishable, setShowNonReplenishable] = useState(false); const [showNonReplenishable, setShowNonReplenishable] = useState(false);
const [selectedProductId, setSelectedProductId] = useState<number | null>(null);
// Store visible columns and order for each view
const [viewColumns, setViewColumns] = useState<Record<string, Set<ColumnKey>>>(() => {
const initialColumns: Record<string, Set<ColumnKey>> = {};
Object.entries(VIEW_COLUMNS).forEach(([view, columns]) => {
initialColumns[view] = new Set(columns);
});
return initialColumns;
});
const [viewColumnOrder, setViewColumnOrder] = useState<Record<string, ColumnKey[]>>(() => {
const initialOrder: Record<string, ColumnKey[]> = {};
Object.entries(VIEW_COLUMNS).forEach(([view, defaultColumns]) => {
initialOrder[view] = [
...defaultColumns,
...AVAILABLE_COLUMNS.map(col => col.key).filter(key => !defaultColumns.includes(key))
];
});
return initialOrder;
});
// Group columns by their group property // Get current view's columns
const columnsByGroup = AVAILABLE_COLUMNS.reduce((acc, col) => { const visibleColumns = useMemo(() => {
if (!acc[col.group]) { const columns = new Set(viewColumns[activeView] || VIEW_COLUMNS.all);
acc[col.group] = []; if (showNonReplenishable) {
columns.add('replenishable');
} }
acc[col.group].push(col); return columns;
return acc; }, [viewColumns, activeView, showNonReplenishable]);
}, {} as Record<string, typeof AVAILABLE_COLUMNS>);
const columnOrder = viewColumnOrder[activeView] || viewColumnOrder.all;
// Handle column visibility changes
const handleColumnVisibilityChange = (column: ColumnKey, isVisible: boolean) => {
setViewColumns(prev => ({
...prev,
[activeView]: isVisible
? new Set([...prev[activeView], column])
: new Set([...prev[activeView]].filter(col => col !== column))
}));
};
// Handle column order changes
const handleColumnOrderChange = (newOrder: ColumnKey[]) => {
setViewColumnOrder(prev => ({
...prev,
[activeView]: newOrder
}));
};
// Reset columns to default for current view
const resetColumnsToDefault = () => {
setViewColumns(prev => ({
...prev,
[activeView]: new Set(VIEW_COLUMNS[activeView] || VIEW_COLUMNS.all)
}));
setViewColumnOrder(prev => ({
...prev,
[activeView]: [
...(VIEW_COLUMNS[activeView] || VIEW_COLUMNS.all),
...AVAILABLE_COLUMNS.map(col => col.key)
.filter(key => !(VIEW_COLUMNS[activeView] || VIEW_COLUMNS.all).includes(key))
]
}));
};
// Function to fetch products data // Function to fetch products data
const fetchProducts = async () => { const fetchProducts = async () => {
@@ -195,6 +303,15 @@ export function Products() {
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
}; };
// 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, typeof AVAILABLE_COLUMNS>);
const renderColumnToggle = () => ( const renderColumnToggle = () => (
<DropdownMenu modal={false}> <DropdownMenu modal={false}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -205,38 +322,38 @@ export function Products() {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="end" align="end"
className="w-[280px] max-h-[calc(100vh-4rem)] overflow-y-auto" className="w-[500px] max-h-[calc(100vh-4rem)] overflow-y-auto"
onCloseAutoFocus={(e) => e.preventDefault()} onCloseAutoFocus={(e) => e.preventDefault()}
> >
<DropdownMenuLabel className="sticky top-0 bg-background z-10">Toggle columns</DropdownMenuLabel> <DropdownMenuLabel className="sticky top-0 bg-background z-10">Toggle columns</DropdownMenuLabel>
<DropdownMenuSeparator className="sticky top-[29px] bg-background z-10" /> <DropdownMenuSeparator className="sticky top-[29px] bg-background z-10" />
{Object.entries(columnsByGroup).map(([group, columns]) => ( <div className="grid grid-cols-2 gap-4">
<div key={group}> {Object.entries(columnsByGroup).map(([group, columns]) => (
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground"> <div key={group}>
{group} <DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
</DropdownMenuLabel> {group}
{columns.map((column) => ( </DropdownMenuLabel>
<DropdownMenuCheckboxItem {columns.map((column) => (
key={column.key} <DropdownMenuCheckboxItem
className="capitalize" key={column.key}
checked={visibleColumns.has(column.key)} className="capitalize"
onSelect={(e) => { checked={visibleColumns.has(column.key)}
e.preventDefault(); onCheckedChange={(checked) => handleColumnVisibilityChange(column.key, checked)}
const newVisibleColumns = new Set(visibleColumns); >
if (newVisibleColumns.has(column.key)) { {column.label}
newVisibleColumns.delete(column.key); </DropdownMenuCheckboxItem>
} else { ))}
newVisibleColumns.add(column.key); </div>
} ))}
setVisibleColumns(newVisibleColumns); </div>
}} <DropdownMenuSeparator />
> <Button
{column.label} variant="ghost"
</DropdownMenuCheckboxItem> className="w-full justify-start"
))} onClick={resetColumnsToDefault}
<DropdownMenuSeparator /> >
</div> Reset to Default
))} </Button>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );
@@ -311,7 +428,7 @@ export function Products() {
visibleColumns={visibleColumns} visibleColumns={visibleColumns}
columnDefs={AVAILABLE_COLUMNS} columnDefs={AVAILABLE_COLUMNS}
columnOrder={columnOrder} columnOrder={columnOrder}
onColumnOrderChange={setColumnOrder} onColumnOrderChange={handleColumnOrderChange}
onRowClick={(product) => setSelectedProductId(product.product_id)} onRowClick={(product) => setSelectedProductId(product.product_id)}
/> />