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:
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { ArrowUpDown, GripVertical } from "lucide-react";
|
||||
import { SortAsc, SortDesc } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Table,
|
||||
@@ -87,20 +87,17 @@ function SortableHeader({ column, columnDef, onSort, sortColumn, sortDirection }
|
||||
"cursor-pointer select-none",
|
||||
columnDef?.width
|
||||
)}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
onClick={() => onSort(column)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div {...attributes} {...listeners} className="cursor-grab">
|
||||
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2" onClick={() => onSort(column)}>
|
||||
{columnDef?.label ?? column}
|
||||
{sortColumn === column && (
|
||||
<ArrowUpDown className={cn(
|
||||
"h-4 w-4",
|
||||
sortDirection === 'desc' && "rotate-180 transform"
|
||||
)} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{columnDef?.label ?? column}
|
||||
{sortColumn === column && (
|
||||
sortDirection === 'desc'
|
||||
? <SortDesc className="h-4 w-4" />
|
||||
: <SortAsc className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</TableHead>
|
||||
);
|
||||
@@ -161,15 +158,19 @@ export function ProductTable({
|
||||
case 'critical':
|
||||
return <Badge variant="destructive">Critical</Badge>;
|
||||
case 'reorder':
|
||||
return <Badge variant="outline">Reorder</Badge>;
|
||||
return <Badge variant="warning">Reorder</Badge>;
|
||||
case 'healthy':
|
||||
return <Badge variant="secondary">Healthy</Badge>;
|
||||
return <Badge variant="success">Healthy</Badge>;
|
||||
case 'overstocked':
|
||||
return <Badge variant="secondary">Overstocked</Badge>;
|
||||
case 'new':
|
||||
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:
|
||||
return null;
|
||||
return <Badge variant="outline">{status}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
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 = {
|
||||
id: string
|
||||
@@ -15,42 +15,42 @@ export const PRODUCT_VIEWS: ProductView[] = [
|
||||
id: "all",
|
||||
label: "All Products",
|
||||
icon: PackageSearch,
|
||||
iconClassName: "text-muted-foreground",
|
||||
iconClassName: "",
|
||||
columns: ["image", "title", "SKU", "stock_quantity", "price", "stock_status"]
|
||||
},
|
||||
{
|
||||
id: "Critical",
|
||||
label: "Critical Stock",
|
||||
icon: AlertTriangle,
|
||||
iconClassName: "text-destructive",
|
||||
iconClassName: "",
|
||||
columns: ["image", "title", "SKU", "stock_quantity", "daily_sales_avg", "reorder_qty", "replenishable", "last_purchase_date", "lead_time_status"]
|
||||
},
|
||||
{
|
||||
id: "Reorder",
|
||||
label: "Reorder Soon",
|
||||
icon: AlertCircle,
|
||||
iconClassName: "text-warning",
|
||||
iconClassName: "",
|
||||
columns: ["image", "title", "SKU", "stock_quantity", "daily_sales_avg", "reorder_qty", "replenishable", "last_purchase_date", "lead_time_status"]
|
||||
},
|
||||
{
|
||||
id: "Healthy",
|
||||
label: "Healthy Stock",
|
||||
icon: CheckCircle2,
|
||||
iconClassName: "text-success",
|
||||
iconClassName: "",
|
||||
columns: ["image", "title", "stock_quantity", "daily_sales_avg", "stock_status", "abc_class"]
|
||||
},
|
||||
{
|
||||
id: "Overstocked",
|
||||
label: "Overstock",
|
||||
icon: PackageSearch,
|
||||
iconClassName: "text-muted-foreground",
|
||||
iconClassName: "",
|
||||
columns: ["image", "title", "stock_quantity", "daily_sales_avg", "overstocked_amt", "replenishable", "last_sale_date", "abc_class"]
|
||||
},
|
||||
{
|
||||
id: "New",
|
||||
label: "New Products",
|
||||
icon: Star,
|
||||
iconClassName: "text-accent",
|
||||
icon: Sparkles,
|
||||
iconClassName: "",
|
||||
columns: ["image", "title", "stock_quantity", "daily_sales_avg", "stock_status", "abc_class"]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useQuery, keepPreviousData } from '@tanstack/react-query';
|
||||
import { ProductFilters } from '@/components/products/ProductFilters';
|
||||
import { ProductTable } from '@/components/products/ProductTable';
|
||||
@@ -75,47 +75,155 @@ const AVAILABLE_COLUMNS: ColumnDef[] = [
|
||||
{ key: 'last_purchase_date', label: 'Last Purchase', group: 'Lead Time' },
|
||||
];
|
||||
|
||||
// Default visible columns
|
||||
const DEFAULT_VISIBLE_COLUMNS: ColumnKey[] = [
|
||||
'image',
|
||||
'title',
|
||||
'SKU',
|
||||
'brand',
|
||||
'vendor',
|
||||
'stock_quantity',
|
||||
'stock_status',
|
||||
'replenishable',
|
||||
'reorder_qty',
|
||||
'price',
|
||||
'regular_price',
|
||||
'daily_sales_avg',
|
||||
'weekly_sales_avg',
|
||||
'monthly_sales_avg',
|
||||
];
|
||||
// Define default columns for each view
|
||||
const VIEW_COLUMNS: Record<string, ColumnKey[]> = {
|
||||
all: [
|
||||
'image',
|
||||
'title',
|
||||
'brand',
|
||||
'vendor',
|
||||
'stock_quantity',
|
||||
'stock_status',
|
||||
'reorder_qty',
|
||||
'price',
|
||||
'regular_price',
|
||||
'daily_sales_avg',
|
||||
'weekly_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() {
|
||||
const [filters, setFilters] = useState<Record<string, string | number | boolean>>({});
|
||||
const [sortColumn, setSortColumn] = useState<ColumnKey>('title');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
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 [pageSize] = useState(50);
|
||||
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
|
||||
const columnsByGroup = AVAILABLE_COLUMNS.reduce((acc, col) => {
|
||||
if (!acc[col.group]) {
|
||||
acc[col.group] = [];
|
||||
// Get current view's columns
|
||||
const visibleColumns = useMemo(() => {
|
||||
const columns = new Set(viewColumns[activeView] || VIEW_COLUMNS.all);
|
||||
if (showNonReplenishable) {
|
||||
columns.add('replenishable');
|
||||
}
|
||||
acc[col.group].push(col);
|
||||
return acc;
|
||||
}, {} as Record<string, typeof AVAILABLE_COLUMNS>);
|
||||
return columns;
|
||||
}, [viewColumns, activeView, showNonReplenishable]);
|
||||
|
||||
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
|
||||
const fetchProducts = async () => {
|
||||
@@ -195,6 +303,15 @@ export function Products() {
|
||||
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 = () => (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -205,38 +322,38 @@ export function Products() {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
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()}
|
||||
>
|
||||
<DropdownMenuLabel className="sticky top-0 bg-background z-10">Toggle columns</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="sticky top-[29px] bg-background z-10" />
|
||||
{Object.entries(columnsByGroup).map(([group, columns]) => (
|
||||
<div key={group}>
|
||||
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
|
||||
{group}
|
||||
</DropdownMenuLabel>
|
||||
{columns.map((column) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.key}
|
||||
className="capitalize"
|
||||
checked={visibleColumns.has(column.key)}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
const newVisibleColumns = new Set(visibleColumns);
|
||||
if (newVisibleColumns.has(column.key)) {
|
||||
newVisibleColumns.delete(column.key);
|
||||
} else {
|
||||
newVisibleColumns.add(column.key);
|
||||
}
|
||||
setVisibleColumns(newVisibleColumns);
|
||||
}}
|
||||
>
|
||||
{column.label}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
</div>
|
||||
))}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{Object.entries(columnsByGroup).map(([group, columns]) => (
|
||||
<div key={group}>
|
||||
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
|
||||
{group}
|
||||
</DropdownMenuLabel>
|
||||
{columns.map((column) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.key}
|
||||
className="capitalize"
|
||||
checked={visibleColumns.has(column.key)}
|
||||
onCheckedChange={(checked) => handleColumnVisibilityChange(column.key, checked)}
|
||||
>
|
||||
{column.label}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start"
|
||||
onClick={resetColumnsToDefault}
|
||||
>
|
||||
Reset to Default
|
||||
</Button>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
@@ -311,7 +428,7 @@ export function Products() {
|
||||
visibleColumns={visibleColumns}
|
||||
columnDefs={AVAILABLE_COLUMNS}
|
||||
columnOrder={columnOrder}
|
||||
onColumnOrderChange={setColumnOrder}
|
||||
onColumnOrderChange={handleColumnOrderChange}
|
||||
onRowClick={(product) => setSelectedProductId(product.product_id)}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user