diff --git a/inventory/src/components/products/ProductViews.tsx b/inventory/src/components/products/ProductViews.tsx new file mode 100644 index 0000000..57490f4 --- /dev/null +++ b/inventory/src/components/products/ProductViews.tsx @@ -0,0 +1,80 @@ +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Product } from "@/types/products" +import { AlertCircle, AlertTriangle, CheckCircle2, PackageSearch, Star } from "lucide-react" + +export type ProductView = { + id: string + label: string + icon: any + iconClassName: string + columns: (keyof Product)[] +} + +export const PRODUCT_VIEWS: ProductView[] = [ + { + id: "all", + label: "All Products", + icon: PackageSearch, + iconClassName: "text-muted-foreground", + columns: ["image", "title", "SKU", "stock_quantity", "price", "stock_status"] + }, + { + id: "Critical", + label: "Critical Stock", + icon: AlertTriangle, + iconClassName: "text-destructive", + columns: ["image", "title", "SKU", "stock_quantity", "daily_sales_avg", "last_purchase_date", "lead_time_status"] + }, + { + id: "Reorder", + label: "Reorder Soon", + icon: AlertCircle, + iconClassName: "text-warning", + columns: ["image", "title", "SKU", "stock_quantity", "daily_sales_avg", "last_purchase_date", "lead_time_status"] + }, + { + id: "Healthy", + label: "Healthy Stock", + icon: CheckCircle2, + iconClassName: "text-success", + columns: ["image", "title", "stock_quantity", "daily_sales_avg", "stock_status", "abc_class"] + }, + { + id: "Overstocked", + label: "Overstock", + icon: PackageSearch, + iconClassName: "text-muted-foreground", + columns: ["image", "title", "stock_quantity", "daily_sales_avg", "last_sale_date", "abc_class"] + }, + { + id: "New", + label: "New Products", + icon: Star, + iconClassName: "text-accent", + columns: ["image", "title", "stock_quantity", "daily_sales_avg", "stock_status", "abc_class"] + } +] + +interface ProductViewsProps { + activeView: string + onViewChange: (view: string) => void +} + +export function ProductViews({ activeView, onViewChange }: ProductViewsProps) { + return ( + + + {PRODUCT_VIEWS.map((view) => ( + + + {view.label} + + ))} + + + ) +} diff --git a/inventory/src/pages/Products.tsx b/inventory/src/pages/Products.tsx index 532c475..62c4456 100644 --- a/inventory/src/pages/Products.tsx +++ b/inventory/src/pages/Products.tsx @@ -1,9 +1,10 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useQuery, keepPreviousData } from '@tanstack/react-query'; import { ProductFilters } from '@/components/products/ProductFilters'; import { ProductTable } from '@/components/products/ProductTable'; import { ProductTableSkeleton } from '@/components/products/ProductTableSkeleton'; import { ProductDetail } from '@/components/products/ProductDetail'; +import { ProductViews } from '@/components/products/ProductViews'; import { Button } from '@/components/ui/button'; import { DropdownMenu, @@ -13,8 +14,17 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { Settings2, ChevronsLeft, ChevronLeft, ChevronRight, ChevronsRight } from 'lucide-react'; +import { Settings2 } from 'lucide-react'; import { motion } from 'framer-motion'; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination" // Enhanced Product interface with all possible fields interface Product { @@ -133,13 +143,15 @@ export function Products() { const [filters, setFilters] = useState>({}); const [sortColumn, setSortColumn] = useState('title'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); - const [page, setPage] = useState(1); + const [currentPage, setCurrentPage] = useState(1); const [visibleColumns, setVisibleColumns] = useState>(new Set(DEFAULT_VISIBLE_COLUMNS)); const [columnOrder, setColumnOrder] = useState<(keyof Product | 'image')[]>([ ...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); // Group columns by their group property const columnsByGroup = AVAILABLE_COLUMNS.reduce((acc, col) => { @@ -160,23 +172,28 @@ export function Products() { const params = new URLSearchParams(); // Add pagination params - params.append('page', page.toString()); - params.append('limit', '50'); + params.append('page', currentPage.toString()); + params.append('limit', pageSize.toString()); // Add sorting params if (sortColumn) { - params.append('sortColumn', sortColumn); - params.append('sortDirection', sortDirection); + params.append('sort', sortColumn); + params.append('order', sortDirection); } - // Add filter params + // Add view filter + if (activeView !== 'all') { + params.append('stockStatus', activeView); + } + + // Add other filters Object.entries(filters).forEach(([key, value]) => { - if (value !== undefined && value !== null && value !== '') { + if (value !== undefined && value !== '') { params.append(key, value.toString()); } }); - const response = await fetch(`/api/products?${params.toString()}`); + const response = await fetch('/api/products?' + params.toString()); if (!response.ok) { throw new Error('Failed to fetch products'); } @@ -185,11 +202,18 @@ export function Products() { // Query for products data const { data, isFetching } = useQuery({ - queryKey: ['products', page, sortColumn, sortDirection, filters], + queryKey: ['products', currentPage, pageSize, sortColumn, sortDirection, activeView, filters], queryFn: fetchProducts, placeholderData: keepPreviousData, }); + // Update current page if it exceeds the total pages + useEffect(() => { + if (data?.pagination.pages && currentPage > data.pagination.pages) { + setCurrentPage(1); + } + }, [currentPage, data?.pagination.pages]); + // Handle sort column change const handleSort = (column: keyof Product) => { setSortDirection(prev => { @@ -202,74 +226,20 @@ export function Products() { // Handle filter changes const handleFilterChange = (newFilters: Record) => { setFilters(newFilters); - setPage(1); + setCurrentPage(1); }; const handleClearFilters = () => { setFilters({}); - setPage(1); + setCurrentPage(1); }; - const handlePageChange = (newPage: number) => { - setPage(newPage); + // Function to handle page changes + const handlePageChange = (page: number) => { + setCurrentPage(page); window.scrollTo({ top: 0, behavior: 'smooth' }); }; - const renderPagination = () => { - if (!data) return null; - - const { total, pages } = data.pagination; - if (total === 0) return null; - - return ( -
-
- Page {page} of {pages} -
-
-
- - - - -
-
-
- ); - }; - const renderColumnToggle = () => ( @@ -316,14 +286,32 @@ export function Products() { ); + // Calculate pagination numbers + const totalPages = data?.pagination.pages || 1; + const showEllipsis = totalPages > 7; + const pageNumbers = showEllipsis + ? currentPage <= 4 + ? [1, 2, 3, 4, 5] + : currentPage >= totalPages - 3 + ? [totalPages - 4, totalPages - 3, totalPages - 2, totalPages - 1, totalPages] + : [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2] + : Array.from({ length: totalPages }, (_, i) => i + 1); + return ( -

Products

+
+

Products

+
+ + { + setActiveView(view); + setCurrentPage(1); + }} />
@@ -336,7 +324,7 @@ export function Products() { activeFilters={filters} />
- {data?.pagination.total && ( + {data?.pagination.total > 0 && (
{data.pagination.total.toLocaleString()} products
@@ -344,26 +332,105 @@ export function Products() { {renderColumnToggle()}
-
- {isFetching ? ( - - ) : ( + + {isFetching ? ( + + ) : ( +
setSelectedProductId(product.product_id)} /> - )} -
-
-
- {renderPagination()} + + {totalPages > 1 && ( + + + + { + e.preventDefault(); + handlePageChange(Math.max(1, currentPage - 1)); + }} + aria-disabled={currentPage === 1} + /> + + + {showEllipsis && currentPage > 4 && ( + <> + + { + e.preventDefault(); + handlePageChange(1); + }} + > + 1 + + + + + + + )} + + {pageNumbers.map((page) => ( + + { + e.preventDefault(); + handlePageChange(page); + }} + > + {page} + + + ))} + + {showEllipsis && currentPage < totalPages - 3 && ( + <> + + + + + { + e.preventDefault(); + handlePageChange(totalPages); + }} + > + {totalPages} + + + + )} + + + { + e.preventDefault(); + handlePageChange(Math.min(totalPages, currentPage + 1)); + }} + aria-disabled={currentPage === totalPages} + /> + + + + )} +
+ )}
; + image: string | null; + moq: number; + uom: number; + visible: boolean; + managing_stock: boolean; + replenishable: boolean; + created_at: string; + updated_at: string; + + // Metrics + daily_sales_avg?: number; + weekly_sales_avg?: number; + monthly_sales_avg?: number; + avg_quantity_per_order?: number; + number_of_orders?: number; + first_sale_date?: string; + last_sale_date?: string; + last_purchase_date?: string; + days_of_stock?: number; + stock_status?: string; + abc_class?: string; + profit_margin?: number; + reorder_point?: number; + max_stock?: number; + lead_time_status?: string; +}