diff --git a/inventory/src/components/products/ProductTable.tsx b/inventory/src/components/products/ProductTable.tsx index e8cc85d..b569b34 100644 --- a/inventory/src/components/products/ProductTable.tsx +++ b/inventory/src/components/products/ProductTable.tsx @@ -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)} > -
-
- -
-
onSort(column)}> - {columnDef?.label ?? column} - {sortColumn === column && ( - - )} -
+
+ {columnDef?.label ?? column} + {sortColumn === column && ( + sortDirection === 'desc' + ? + : + )}
); @@ -161,15 +158,19 @@ export function ProductTable({ case 'critical': return Critical; case 'reorder': - return Reorder; + return Reorder; case 'healthy': - return Healthy; + return Healthy; case 'overstocked': return Overstocked; case 'new': return New; + case 'out of stock': + return Out of Stock; + case 'at-risk': + return At Risk; default: - return null; + return {status}; } }; diff --git a/inventory/src/components/products/ProductViews.tsx b/inventory/src/components/products/ProductViews.tsx index 176efb4..3eaa6e4 100644 --- a/inventory/src/components/products/ProductViews.tsx +++ b/inventory/src/components/products/ProductViews.tsx @@ -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"] } ] diff --git a/inventory/src/pages/Products.tsx b/inventory/src/pages/Products.tsx index cbc3570..053c4e6 100644 --- a/inventory/src/pages/Products.tsx +++ b/inventory/src/pages/Products.tsx @@ -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 = { + 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>({}); const [sortColumn, setSortColumn] = useState('title'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); const [currentPage, setCurrentPage] = useState(1); - const [visibleColumns, setVisibleColumns] = useState>(new Set(DEFAULT_VISIBLE_COLUMNS)); - const [columnOrder, setColumnOrder] = useState([ - ...DEFAULT_VISIBLE_COLUMNS, - ...AVAILABLE_COLUMNS.map(col => col.key).filter(key => !DEFAULT_VISIBLE_COLUMNS.includes(key)) - ]); - const [selectedProductId, setSelectedProductId] = useState(null); const [activeView, setActiveView] = useState("all"); const [pageSize] = useState(50); const [showNonReplenishable, setShowNonReplenishable] = useState(false); + const [selectedProductId, setSelectedProductId] = useState(null); + + // Store visible columns and order for each view + const [viewColumns, setViewColumns] = useState>>(() => { + const initialColumns: Record> = {}; + Object.entries(VIEW_COLUMNS).forEach(([view, columns]) => { + initialColumns[view] = new Set(columns); + }); + return initialColumns; + }); + + const [viewColumnOrder, setViewColumnOrder] = useState>(() => { + const initialOrder: Record = {}; + 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); + 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); + const renderColumnToggle = () => ( @@ -205,38 +322,38 @@ export function Products() { e.preventDefault()} > Toggle columns - {Object.entries(columnsByGroup).map(([group, columns]) => ( -
- - {group} - - {columns.map((column) => ( - { - 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} - - ))} - -
- ))} +
+ {Object.entries(columnsByGroup).map(([group, columns]) => ( +
+ + {group} + + {columns.map((column) => ( + handleColumnVisibilityChange(column.key, checked)} + > + {column.label} + + ))} +
+ ))} +
+ +
); @@ -311,7 +428,7 @@ export function Products() { visibleColumns={visibleColumns} columnDefs={AVAILABLE_COLUMNS} columnOrder={columnOrder} - onColumnOrderChange={setColumnOrder} + onColumnOrderChange={handleColumnOrderChange} onRowClick={(product) => setSelectedProductId(product.product_id)} />