Add products filter views
This commit is contained in:
80
inventory/src/components/products/ProductViews.tsx
Normal file
80
inventory/src/components/products/ProductViews.tsx
Normal file
@@ -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 (
|
||||||
|
<Tabs value={activeView} onValueChange={onViewChange} className="w-full">
|
||||||
|
<TabsList className="inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground w-fit">
|
||||||
|
{PRODUCT_VIEWS.map((view) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={view.id}
|
||||||
|
value={view.id}
|
||||||
|
className="inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow"
|
||||||
|
>
|
||||||
|
<view.icon className={`h-4 w-4 ${view.iconClassName} mr-2`} />
|
||||||
|
{view.label}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } 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';
|
||||||
import { ProductTableSkeleton } from '@/components/products/ProductTableSkeleton';
|
import { ProductTableSkeleton } from '@/components/products/ProductTableSkeleton';
|
||||||
import { ProductDetail } from '@/components/products/ProductDetail';
|
import { ProductDetail } from '@/components/products/ProductDetail';
|
||||||
|
import { ProductViews } from '@/components/products/ProductViews';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -13,8 +14,17 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} 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 { motion } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
} from "@/components/ui/pagination"
|
||||||
|
|
||||||
// Enhanced Product interface with all possible fields
|
// Enhanced Product interface with all possible fields
|
||||||
interface Product {
|
interface Product {
|
||||||
@@ -133,13 +143,15 @@ 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<keyof Product>('title');
|
const [sortColumn, setSortColumn] = useState<keyof Product>('title');
|
||||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||||
const [page, setPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [visibleColumns, setVisibleColumns] = useState<Set<keyof Product | 'image'>>(new Set(DEFAULT_VISIBLE_COLUMNS));
|
const [visibleColumns, setVisibleColumns] = useState<Set<keyof Product | 'image'>>(new Set(DEFAULT_VISIBLE_COLUMNS));
|
||||||
const [columnOrder, setColumnOrder] = useState<(keyof Product | 'image')[]>([
|
const [columnOrder, setColumnOrder] = useState<(keyof Product | 'image')[]>([
|
||||||
...DEFAULT_VISIBLE_COLUMNS,
|
...DEFAULT_VISIBLE_COLUMNS,
|
||||||
...AVAILABLE_COLUMNS.map(col => col.key).filter(key => !DEFAULT_VISIBLE_COLUMNS.includes(key))
|
...AVAILABLE_COLUMNS.map(col => col.key).filter(key => !DEFAULT_VISIBLE_COLUMNS.includes(key))
|
||||||
]);
|
]);
|
||||||
const [selectedProductId, setSelectedProductId] = useState<number | null>(null);
|
const [selectedProductId, setSelectedProductId] = useState<number | null>(null);
|
||||||
|
const [activeView, setActiveView] = useState("all");
|
||||||
|
const [pageSize] = useState(50);
|
||||||
|
|
||||||
// Group columns by their group property
|
// Group columns by their group property
|
||||||
const columnsByGroup = AVAILABLE_COLUMNS.reduce((acc, col) => {
|
const columnsByGroup = AVAILABLE_COLUMNS.reduce((acc, col) => {
|
||||||
@@ -160,23 +172,28 @@ export function Products() {
|
|||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
// Add pagination params
|
// Add pagination params
|
||||||
params.append('page', page.toString());
|
params.append('page', currentPage.toString());
|
||||||
params.append('limit', '50');
|
params.append('limit', pageSize.toString());
|
||||||
|
|
||||||
// Add sorting params
|
// Add sorting params
|
||||||
if (sortColumn) {
|
if (sortColumn) {
|
||||||
params.append('sortColumn', sortColumn);
|
params.append('sort', sortColumn);
|
||||||
params.append('sortDirection', sortDirection);
|
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]) => {
|
Object.entries(filters).forEach(([key, value]) => {
|
||||||
if (value !== undefined && value !== null && value !== '') {
|
if (value !== undefined && value !== '') {
|
||||||
params.append(key, value.toString());
|
params.append(key, value.toString());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch(`/api/products?${params.toString()}`);
|
const response = await fetch('/api/products?' + params.toString());
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch products');
|
throw new Error('Failed to fetch products');
|
||||||
}
|
}
|
||||||
@@ -185,11 +202,18 @@ export function Products() {
|
|||||||
|
|
||||||
// Query for products data
|
// Query for products data
|
||||||
const { data, isFetching } = useQuery({
|
const { data, isFetching } = useQuery({
|
||||||
queryKey: ['products', page, sortColumn, sortDirection, filters],
|
queryKey: ['products', currentPage, pageSize, sortColumn, sortDirection, activeView, filters],
|
||||||
queryFn: fetchProducts,
|
queryFn: fetchProducts,
|
||||||
placeholderData: keepPreviousData,
|
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
|
// Handle sort column change
|
||||||
const handleSort = (column: keyof Product) => {
|
const handleSort = (column: keyof Product) => {
|
||||||
setSortDirection(prev => {
|
setSortDirection(prev => {
|
||||||
@@ -202,74 +226,20 @@ export function Products() {
|
|||||||
// Handle filter changes
|
// Handle filter changes
|
||||||
const handleFilterChange = (newFilters: Record<string, string | number | boolean>) => {
|
const handleFilterChange = (newFilters: Record<string, string | number | boolean>) => {
|
||||||
setFilters(newFilters);
|
setFilters(newFilters);
|
||||||
setPage(1);
|
setCurrentPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearFilters = () => {
|
const handleClearFilters = () => {
|
||||||
setFilters({});
|
setFilters({});
|
||||||
setPage(1);
|
setCurrentPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePageChange = (newPage: number) => {
|
// Function to handle page changes
|
||||||
setPage(newPage);
|
const handlePageChange = (page: number) => {
|
||||||
|
setCurrentPage(page);
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPagination = () => {
|
|
||||||
if (!data) return null;
|
|
||||||
|
|
||||||
const { total, pages } = data.pagination;
|
|
||||||
if (total === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between px-2">
|
|
||||||
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
|
||||||
Page {page} of {pages}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-6 lg:space-x-8">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="hidden h-8 w-8 p-0 lg:flex"
|
|
||||||
onClick={() => handlePageChange(1)}
|
|
||||||
disabled={page === 1}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Go to first page</span>
|
|
||||||
<ChevronsLeft className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
onClick={() => handlePageChange(page - 1)}
|
|
||||||
disabled={page === 1}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Go to previous page</span>
|
|
||||||
<ChevronLeft className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
onClick={() => handlePageChange(page + 1)}
|
|
||||||
disabled={page === pages}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Go to next page</span>
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="hidden h-8 w-8 p-0 lg:flex"
|
|
||||||
onClick={() => handlePageChange(pages)}
|
|
||||||
disabled={page === pages}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Go to last page</span>
|
|
||||||
<ChevronsRight className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderColumnToggle = () => (
|
const renderColumnToggle = () => (
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -316,14 +286,32 @@ export function Products() {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="p-8 space-y-8"
|
className="container mx-auto py-6 space-y-4"
|
||||||
>
|
>
|
||||||
<h1 className="text-2xl font-bold">Products</h1>
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">Products</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ProductViews activeView={activeView} onViewChange={(view) => {
|
||||||
|
setActiveView(view);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}} />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
@@ -336,7 +324,7 @@ export function Products() {
|
|||||||
activeFilters={filters}
|
activeFilters={filters}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{data?.pagination.total && (
|
{data?.pagination.total > 0 && (
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{data.pagination.total.toLocaleString()} products
|
{data.pagination.total.toLocaleString()} products
|
||||||
</div>
|
</div>
|
||||||
@@ -344,26 +332,105 @@ export function Products() {
|
|||||||
{renderColumnToggle()}
|
{renderColumnToggle()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
|
||||||
{isFetching ? (
|
{isFetching ? (
|
||||||
<ProductTableSkeleton />
|
<ProductTableSkeleton />
|
||||||
) : (
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
<ProductTable
|
<ProductTable
|
||||||
products={data?.products ?? []}
|
products={data?.products || []}
|
||||||
visibleColumns={visibleColumns}
|
onSort={handleSort}
|
||||||
sortColumn={sortColumn}
|
sortColumn={sortColumn}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
|
visibleColumns={visibleColumns}
|
||||||
columnDefs={AVAILABLE_COLUMNS}
|
columnDefs={AVAILABLE_COLUMNS}
|
||||||
columnOrder={columnOrder}
|
columnOrder={columnOrder}
|
||||||
onSort={handleSort}
|
onColumnOrderChange={setColumnOrder}
|
||||||
onColumnOrderChange={handleColumnOrderChange}
|
|
||||||
onRowClick={(product) => setSelectedProductId(product.product_id)}
|
onRowClick={(product) => setSelectedProductId(product.product_id)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Pagination>
|
||||||
|
<PaginationContent>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationPrevious
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlePageChange(Math.max(1, currentPage - 1));
|
||||||
|
}}
|
||||||
|
aria-disabled={currentPage === 1}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
|
||||||
|
{showEllipsis && currentPage > 4 && (
|
||||||
|
<>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationLink
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlePageChange(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationEllipsis />
|
||||||
|
</PaginationItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pageNumbers.map((page) => (
|
||||||
|
<PaginationItem key={page}>
|
||||||
|
<PaginationLink
|
||||||
|
href="#"
|
||||||
|
isActive={currentPage === page}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlePageChange(page);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{showEllipsis && currentPage < totalPages - 3 && (
|
||||||
|
<>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationEllipsis />
|
||||||
|
</PaginationItem>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationLink
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlePageChange(totalPages);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{totalPages}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlePageChange(Math.min(totalPages, currentPage + 1));
|
||||||
|
}}
|
||||||
|
aria-disabled={currentPage === totalPages}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className="mt-4">
|
|
||||||
{renderPagination()}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProductDetail
|
<ProductDetail
|
||||||
|
|||||||
42
inventory/src/types/products.ts
Normal file
42
inventory/src/types/products.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export interface Product {
|
||||||
|
product_id: number;
|
||||||
|
title: string;
|
||||||
|
SKU: string;
|
||||||
|
stock_quantity: number;
|
||||||
|
price: number;
|
||||||
|
regular_price: number;
|
||||||
|
cost_price: number;
|
||||||
|
landing_cost_price: number | null;
|
||||||
|
barcode: string;
|
||||||
|
vendor: string;
|
||||||
|
vendor_reference: string;
|
||||||
|
brand: string;
|
||||||
|
categories: string[];
|
||||||
|
tags: string[];
|
||||||
|
options: Record<string, any>;
|
||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user