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 * 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,21 +87,18 @@ 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">
|
|
||||||
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2" onClick={() => onSort(column)}>
|
|
||||||
{columnDef?.label ?? column}
|
{columnDef?.label ?? column}
|
||||||
{sortColumn === column && (
|
{sortColumn === column && (
|
||||||
<ArrowUpDown className={cn(
|
sortDirection === 'desc'
|
||||||
"h-4 w-4",
|
? <SortDesc className="h-4 w-4" />
|
||||||
sortDirection === 'desc' && "rotate-180 transform"
|
: <SortAsc className="h-4 w-4" />
|
||||||
)} />
|
|
||||||
)}
|
)}
|
||||||
</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>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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[]> = {
|
||||||
|
all: [
|
||||||
'image',
|
'image',
|
||||||
'title',
|
'title',
|
||||||
'SKU',
|
|
||||||
'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);
|
||||||
|
|
||||||
// Group columns by their group property
|
// Store visible columns and order for each view
|
||||||
const columnsByGroup = AVAILABLE_COLUMNS.reduce((acc, col) => {
|
const [viewColumns, setViewColumns] = useState<Record<string, Set<ColumnKey>>>(() => {
|
||||||
if (!acc[col.group]) {
|
const initialColumns: Record<string, Set<ColumnKey>> = {};
|
||||||
acc[col.group] = [];
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 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,11 +322,12 @@ 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" />
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{Object.entries(columnsByGroup).map(([group, columns]) => (
|
{Object.entries(columnsByGroup).map(([group, columns]) => (
|
||||||
<div key={group}>
|
<div key={group}>
|
||||||
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
|
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
|
||||||
@@ -220,23 +338,22 @@ export function Products() {
|
|||||||
key={column.key}
|
key={column.key}
|
||||||
className="capitalize"
|
className="capitalize"
|
||||||
checked={visibleColumns.has(column.key)}
|
checked={visibleColumns.has(column.key)}
|
||||||
onSelect={(e) => {
|
onCheckedChange={(checked) => handleColumnVisibilityChange(column.key, checked)}
|
||||||
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}
|
{column.label}
|
||||||
</DropdownMenuCheckboxItem>
|
</DropdownMenuCheckboxItem>
|
||||||
))}
|
))}
|
||||||
<DropdownMenuSeparator />
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full justify-start"
|
||||||
|
onClick={resetColumnsToDefault}
|
||||||
|
>
|
||||||
|
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)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user