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 { 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>;
}
};

View File

@@ -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"]
}
]

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 { 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);
// Group columns by their group property
const columnsByGroup = AVAILABLE_COLUMNS.reduce((acc, col) => {
if (!acc[col.group]) {
acc[col.group] = [];
// 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;
});
// 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)}
/>