Update stock status calculations and add restock/overstock qty fields and calculations
This commit is contained in:
@@ -39,6 +39,8 @@ CREATE TABLE IF NOT EXISTS product_metrics (
|
||||
weeks_of_inventory INT,
|
||||
reorder_point INT,
|
||||
safety_stock INT,
|
||||
reorder_qty INT DEFAULT 0,
|
||||
overstocked_amt INT DEFAULT 0,
|
||||
-- Financial metrics
|
||||
avg_margin_percent DECIMAL(10,3),
|
||||
total_revenue DECIMAL(10,3),
|
||||
|
||||
@@ -760,11 +760,45 @@ async function calculateMetrics() {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Get current stock
|
||||
// Get current stock and stock age
|
||||
const [stockInfo] = await connection.query(`
|
||||
SELECT stock_quantity, cost_price
|
||||
FROM products
|
||||
WHERE product_id = ?
|
||||
SELECT
|
||||
p.stock_quantity,
|
||||
p.cost_price,
|
||||
p.created_at,
|
||||
p.replenishable,
|
||||
p.moq,
|
||||
DATEDIFF(CURDATE(), MIN(po.received_date)) as days_since_first_stock,
|
||||
DATEDIFF(CURDATE(), COALESCE(
|
||||
(SELECT MAX(o2.date)
|
||||
FROM orders o2
|
||||
WHERE o2.product_id = p.product_id
|
||||
AND o2.canceled = false),
|
||||
CURDATE() -- If no sales, use current date
|
||||
)) as days_since_last_sale,
|
||||
(SELECT SUM(quantity)
|
||||
FROM orders o3
|
||||
WHERE o3.product_id = p.product_id
|
||||
AND o3.canceled = false) as total_quantity_sold,
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM orders o
|
||||
WHERE o.product_id = p.product_id
|
||||
AND o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
AND o.canceled = false
|
||||
AND (SELECT SUM(quantity) FROM orders o2
|
||||
WHERE o2.product_id = p.product_id
|
||||
AND o2.date >= o.date
|
||||
AND o2.canceled = false) = 0
|
||||
) THEN true
|
||||
ELSE false
|
||||
END as had_recent_stockout
|
||||
FROM products p
|
||||
LEFT JOIN purchase_orders po ON p.product_id = po.product_id
|
||||
AND po.status = 'closed'
|
||||
AND po.received > 0
|
||||
WHERE p.product_id = ?
|
||||
GROUP BY p.product_id
|
||||
`, [product.product_id]).catch(err => {
|
||||
logError(err, `Failed to get stock info for product ${product.product_id}`);
|
||||
throw err;
|
||||
@@ -787,17 +821,118 @@ async function calculateMetrics() {
|
||||
// Calculate current inventory value
|
||||
const inventory_value = (stock.stock_quantity || 0) * (stock.cost_price || 0);
|
||||
|
||||
// Calculate stock status using configurable thresholds with proper handling of zero sales
|
||||
const stock_status = daily_sales_avg === 0 ? 'New' :
|
||||
stock.stock_quantity <= Math.max(1, Math.ceil(daily_sales_avg * config.critical_days)) ? 'Critical' :
|
||||
stock.stock_quantity <= Math.max(1, Math.ceil(daily_sales_avg * config.reorder_days)) ? 'Reorder' :
|
||||
stock.stock_quantity > Math.max(1, daily_sales_avg * config.overstock_days) ? 'Overstocked' : 'Healthy';
|
||||
// Calculate stock status with improved handling
|
||||
const stock_status = (() => {
|
||||
const days_since_first_stock = stockInfo[0]?.days_since_first_stock || 0;
|
||||
const days_since_last_sale = stockInfo[0]?.days_since_last_sale || 9999;
|
||||
const total_quantity_sold = stockInfo[0]?.total_quantity_sold || 0;
|
||||
const had_recent_stockout = stockInfo[0]?.had_recent_stockout || false;
|
||||
const dq = stock.stock_quantity || 0;
|
||||
const ds = daily_sales_avg || 0;
|
||||
const ws = weekly_sales_avg || 0;
|
||||
const ms = monthly_sales_avg || 0;
|
||||
|
||||
// If no stock, return immediately
|
||||
if (dq === 0) {
|
||||
return had_recent_stockout ? 'Critical' : 'Out of Stock';
|
||||
}
|
||||
|
||||
// 1. Check if truly "New" (≤30 days and no sales)
|
||||
if (days_since_first_stock <= 30 && total_quantity_sold === 0) {
|
||||
return 'New';
|
||||
}
|
||||
|
||||
// 2. Handle zero or very low sales velocity cases
|
||||
if (ds === 0 || (ds < 0.1 && ws < 0.5)) { // Less than 1 sale per 10 days and less than 0.5 per week
|
||||
if (days_since_first_stock > config.overstock_days) {
|
||||
return 'Overstocked';
|
||||
}
|
||||
if (days_since_first_stock > 30) {
|
||||
return 'At Risk';
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Calculate days of supply and check velocity trends
|
||||
const days_of_supply = ds > 0 ? dq / ds : 999;
|
||||
const velocity_trend = ds > 0 ? (ds / (ms || ds) - 1) * 100 : 0; // Percent change from monthly to daily avg
|
||||
|
||||
// Critical stock level
|
||||
if (days_of_supply <= config.critical_days) {
|
||||
return 'Critical';
|
||||
}
|
||||
|
||||
// Reorder cases
|
||||
if (days_of_supply <= config.reorder_days ||
|
||||
(had_recent_stockout && days_of_supply <= config.reorder_days * 1.5)) {
|
||||
return 'Reorder';
|
||||
}
|
||||
|
||||
// At Risk cases (multiple scenarios)
|
||||
if (
|
||||
// Approaching overstock threshold
|
||||
(days_of_supply >= config.overstock_days * 0.8) ||
|
||||
// Significant sales decline
|
||||
(velocity_trend <= -50 && days_of_supply > config.reorder_days * 2) ||
|
||||
// No recent sales
|
||||
(days_since_last_sale > 45 && dq > 0) ||
|
||||
// Very low velocity with significant stock
|
||||
(ds > 0 && ds < 0.2 && dq > ds * config.overstock_days * 0.5)
|
||||
) {
|
||||
return 'At Risk';
|
||||
}
|
||||
|
||||
// Overstock cases
|
||||
if (days_of_supply >= config.overstock_days) {
|
||||
return 'Overstocked';
|
||||
}
|
||||
|
||||
// If none of the above conditions are met
|
||||
return 'Healthy';
|
||||
})();
|
||||
|
||||
// Calculate safety stock using configured values with proper defaults
|
||||
const safety_stock = daily_sales_avg > 0 ?
|
||||
Math.max(1, Math.ceil(daily_sales_avg * (config.safety_stock_days || 14) * ((config.service_level || 95.0) / 100))) :
|
||||
null;
|
||||
|
||||
// Calculate reorder quantity and overstocked amount
|
||||
let reorder_qty = 0;
|
||||
let overstocked_amt = 0;
|
||||
|
||||
// Only calculate reorder quantity for replenishable products
|
||||
if (stock.replenishable && (stock_status === 'Critical' || stock_status === 'Reorder')) {
|
||||
const ds = daily_sales_avg || 0;
|
||||
const lt = purchases.avg_lead_time_days || 14; // Default to 14 days if no lead time data
|
||||
const sc = config.safety_stock_days || 14;
|
||||
const ss = safety_stock || 0;
|
||||
const dq = stock.stock_quantity || 0;
|
||||
const moq = stock.moq || 1;
|
||||
|
||||
// Calculate desired stock level based on daily sales, lead time, coverage days, and safety stock
|
||||
const desired_stock = (ds * (lt + sc)) + ss;
|
||||
|
||||
// Calculate raw reorder amount
|
||||
const raw_reorder = Math.max(0, desired_stock - dq);
|
||||
|
||||
// Round up to nearest MOQ
|
||||
reorder_qty = Math.ceil(raw_reorder / moq) * moq;
|
||||
}
|
||||
|
||||
// Calculate overstocked amount for overstocked products
|
||||
if (stock_status === 'Overstocked') {
|
||||
const ds = daily_sales_avg || 0;
|
||||
const dq = stock.stock_quantity || 0;
|
||||
const lt = purchases.avg_lead_time_days || 14;
|
||||
const sc = config.safety_stock_days || 14;
|
||||
const ss = safety_stock || 0;
|
||||
|
||||
// Calculate maximum desired stock based on overstock days configuration
|
||||
const max_desired_stock = (ds * config.overstock_days) + ss;
|
||||
|
||||
// Calculate excess inventory
|
||||
overstocked_amt = Math.max(0, dq - max_desired_stock);
|
||||
}
|
||||
|
||||
// Add to batch update
|
||||
metricsUpdates.push([
|
||||
product.product_id,
|
||||
@@ -818,7 +953,9 @@ async function calculateMetrics() {
|
||||
purchases.avg_lead_time_days || null,
|
||||
purchases.last_purchase_date || null,
|
||||
purchases.last_received_date || null,
|
||||
stock_status
|
||||
stock_status,
|
||||
reorder_qty,
|
||||
overstocked_amt
|
||||
]);
|
||||
} catch (err) {
|
||||
logError(err, `Failed processing product ${product.product_id}`);
|
||||
@@ -849,7 +986,9 @@ async function calculateMetrics() {
|
||||
avg_lead_time_days,
|
||||
last_purchase_date,
|
||||
last_received_date,
|
||||
stock_status
|
||||
stock_status,
|
||||
reorder_qty,
|
||||
overstocked_amt
|
||||
) VALUES ?
|
||||
ON DUPLICATE KEY UPDATE
|
||||
last_calculated_at = NOW(),
|
||||
@@ -870,7 +1009,9 @@ async function calculateMetrics() {
|
||||
avg_lead_time_days = VALUES(avg_lead_time_days),
|
||||
last_purchase_date = VALUES(last_purchase_date),
|
||||
last_received_date = VALUES(last_received_date),
|
||||
stock_status = VALUES(stock_status)
|
||||
stock_status = VALUES(stock_status),
|
||||
reorder_qty = VALUES(reorder_qty),
|
||||
overstocked_amt = VALUES(overstocked_amt)
|
||||
`, [metricsUpdates]).catch(err => {
|
||||
logError(err, `Failed to batch update metrics for ${metricsUpdates.length} products`);
|
||||
throw err;
|
||||
|
||||
@@ -275,6 +275,8 @@ router.get('/', async (req, res) => {
|
||||
pm.current_lead_time,
|
||||
pm.target_lead_time,
|
||||
pm.lead_time_status,
|
||||
pm.reorder_qty,
|
||||
pm.overstocked_amt,
|
||||
COALESCE(pm.days_of_inventory / NULLIF(pt.target_days, 0), 0) as stock_coverage_ratio
|
||||
FROM products p
|
||||
LEFT JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
@@ -323,7 +325,9 @@ router.get('/', async (req, res) => {
|
||||
current_lead_time: parseFloat(row.current_lead_time) || 0,
|
||||
target_lead_time: parseFloat(row.target_lead_time) || 0,
|
||||
lead_time_status: row.lead_time_status || null,
|
||||
stock_coverage_ratio: parseFloat(row.stock_coverage_ratio) || 0
|
||||
stock_coverage_ratio: parseFloat(row.stock_coverage_ratio) || 0,
|
||||
reorder_qty: parseInt(row.reorder_qty) || 0,
|
||||
overstocked_amt: parseInt(row.overstocked_amt) || 0
|
||||
}));
|
||||
|
||||
res.json({
|
||||
@@ -507,7 +511,9 @@ router.get('/:id', async (req, res) => {
|
||||
avg_lead_time_days: parseInt(rows[0].avg_lead_time_days) || 0,
|
||||
current_lead_time: parseInt(rows[0].current_lead_time) || 0,
|
||||
target_lead_time: parseInt(rows[0].target_lead_time) || 14,
|
||||
lead_time_status: rows[0].lead_time_status || 'Unknown'
|
||||
lead_time_status: rows[0].lead_time_status || 'Unknown',
|
||||
reorder_qty: parseInt(rows[0].reorder_qty) || 0,
|
||||
overstocked_amt: parseInt(rows[0].overstocked_amt) || 0
|
||||
},
|
||||
|
||||
// Vendor performance (if available)
|
||||
@@ -645,7 +651,9 @@ router.get('/:id/metrics', async (req, res) => {
|
||||
COALESCE(pm.avg_lead_time_days, 0) as avg_lead_time_days,
|
||||
COALESCE(pm.current_lead_time, 0) as current_lead_time,
|
||||
COALESCE(pm.target_lead_time, 14) as target_lead_time,
|
||||
COALESCE(pm.lead_time_status, 'Unknown') as lead_time_status
|
||||
COALESCE(pm.lead_time_status, 'Unknown') as lead_time_status,
|
||||
COALESCE(pm.reorder_qty, 0) as reorder_qty,
|
||||
COALESCE(pm.overstocked_amt, 0) as overstocked_amt
|
||||
FROM products p
|
||||
LEFT JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
LEFT JOIN inventory_status is ON p.product_id = is.product_id
|
||||
@@ -670,7 +678,9 @@ router.get('/:id/metrics', async (req, res) => {
|
||||
avg_lead_time_days: 0,
|
||||
current_lead_time: 0,
|
||||
target_lead_time: 14,
|
||||
lead_time_status: 'Unknown'
|
||||
lead_time_status: 'Unknown',
|
||||
reorder_qty: 0,
|
||||
overstocked_amt: 0
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,62 +26,9 @@ import {
|
||||
useSortable,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { Product } from "@/types/products";
|
||||
|
||||
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;
|
||||
days_of_inventory?: number;
|
||||
weeks_of_inventory?: number;
|
||||
reorder_point?: number;
|
||||
safety_stock?: number;
|
||||
avg_margin_percent?: number;
|
||||
total_revenue?: number;
|
||||
inventory_value?: number;
|
||||
cost_of_goods_sold?: number;
|
||||
gross_profit?: number;
|
||||
gmroi?: number;
|
||||
avg_lead_time_days?: number;
|
||||
last_purchase_date?: string;
|
||||
last_received_date?: string;
|
||||
abc_class?: string;
|
||||
stock_status?: string;
|
||||
turnover_rate?: number;
|
||||
current_lead_time?: number;
|
||||
target_lead_time?: number;
|
||||
lead_time_status?: string;
|
||||
}
|
||||
|
||||
type ColumnKey = keyof Product | 'image';
|
||||
export type ColumnKey = keyof Product | 'image';
|
||||
|
||||
interface ColumnDef {
|
||||
key: ColumnKey;
|
||||
@@ -296,6 +243,12 @@ export function ProductTable({
|
||||
) : (
|
||||
<Badge variant="outline">Hidden</Badge>
|
||||
);
|
||||
case 'replenishable':
|
||||
return value ? (
|
||||
<Badge variant="secondary">Replenishable</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">Non-Replenishable</Badge>
|
||||
);
|
||||
default:
|
||||
if (columnDef?.format && value !== undefined && value !== null) {
|
||||
// For numeric formats (those using toFixed), ensure the value is a number
|
||||
|
||||
@@ -23,14 +23,14 @@ export const PRODUCT_VIEWS: ProductView[] = [
|
||||
label: "Critical Stock",
|
||||
icon: AlertTriangle,
|
||||
iconClassName: "text-destructive",
|
||||
columns: ["image", "title", "SKU", "stock_quantity", "daily_sales_avg", "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",
|
||||
label: "Reorder Soon",
|
||||
icon: AlertCircle,
|
||||
iconClassName: "text-warning",
|
||||
columns: ["image", "title", "SKU", "stock_quantity", "daily_sales_avg", "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",
|
||||
@@ -44,7 +44,7 @@ export const PRODUCT_VIEWS: ProductView[] = [
|
||||
label: "Overstock",
|
||||
icon: PackageSearch,
|
||||
iconClassName: "text-muted-foreground",
|
||||
columns: ["image", "title", "stock_quantity", "daily_sales_avg", "last_sale_date", "abc_class"]
|
||||
columns: ["image", "title", "stock_quantity", "daily_sales_avg", "overstocked_amt", "replenishable", "last_sale_date", "abc_class"]
|
||||
},
|
||||
{
|
||||
id: "New",
|
||||
|
||||
@@ -6,6 +6,8 @@ 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 { Product } from '@/types/products';
|
||||
import type { ColumnKey } from '@/components/products/ProductTable';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
@@ -26,65 +28,9 @@ import {
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination"
|
||||
|
||||
// Enhanced Product interface with all possible fields
|
||||
interface Product {
|
||||
// Basic product info
|
||||
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;
|
||||
days_of_inventory?: number;
|
||||
weeks_of_inventory?: number;
|
||||
reorder_point?: number;
|
||||
safety_stock?: number;
|
||||
avg_margin_percent?: number;
|
||||
total_revenue?: number;
|
||||
inventory_value?: number;
|
||||
cost_of_goods_sold?: number;
|
||||
gross_profit?: number;
|
||||
gmroi?: number;
|
||||
avg_lead_time_days?: number;
|
||||
last_purchase_date?: string;
|
||||
last_received_date?: string;
|
||||
abc_class?: string;
|
||||
stock_status?: string;
|
||||
turnover_rate?: number;
|
||||
current_lead_time?: number;
|
||||
target_lead_time?: number;
|
||||
lead_time_status?: string;
|
||||
}
|
||||
|
||||
// Column definition type
|
||||
interface ColumnDef {
|
||||
key: keyof Product | 'image';
|
||||
key: ColumnKey;
|
||||
label: string;
|
||||
group: string;
|
||||
noLabel?: boolean;
|
||||
@@ -105,6 +51,10 @@ const AVAILABLE_COLUMNS: ColumnDef[] = [
|
||||
{ key: 'stock_status', label: 'Stock Status', group: 'Stock' },
|
||||
{ key: 'days_of_inventory', label: 'Days of Stock', group: 'Stock', format: (v) => v?.toFixed(1) ?? '-' },
|
||||
{ key: 'abc_class', label: 'ABC Class', group: 'Stock' },
|
||||
{ key: 'replenishable', label: 'Replenishable', group: 'Stock' },
|
||||
{ key: 'moq', label: 'MOQ', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
||||
{ key: 'reorder_qty', label: 'Reorder Qty', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
||||
{ key: 'overstocked_amt', label: 'Overstock Amt', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
||||
{ key: 'price', label: 'Price', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' },
|
||||
{ key: 'regular_price', label: 'Default Price', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' },
|
||||
{ key: 'cost_price', label: 'Cost', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' },
|
||||
@@ -124,7 +74,7 @@ const AVAILABLE_COLUMNS: ColumnDef[] = [
|
||||
];
|
||||
|
||||
// Default visible columns
|
||||
const DEFAULT_VISIBLE_COLUMNS: (keyof Product | 'image')[] = [
|
||||
const DEFAULT_VISIBLE_COLUMNS: ColumnKey[] = [
|
||||
'image',
|
||||
'title',
|
||||
'SKU',
|
||||
@@ -132,6 +82,8 @@ const DEFAULT_VISIBLE_COLUMNS: (keyof Product | 'image')[] = [
|
||||
'vendor',
|
||||
'stock_quantity',
|
||||
'stock_status',
|
||||
'replenishable',
|
||||
'reorder_qty',
|
||||
'price',
|
||||
'regular_price',
|
||||
'daily_sales_avg',
|
||||
@@ -141,11 +93,11 @@ const DEFAULT_VISIBLE_COLUMNS: (keyof Product | 'image')[] = [
|
||||
|
||||
export function Products() {
|
||||
const [filters, setFilters] = useState<Record<string, string | number | boolean>>({});
|
||||
const [sortColumn, setSortColumn] = useState<keyof Product>('title');
|
||||
const [sortColumn, setSortColumn] = useState<ColumnKey>('title');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [visibleColumns, setVisibleColumns] = useState<Set<keyof Product | 'image'>>(new Set(DEFAULT_VISIBLE_COLUMNS));
|
||||
const [columnOrder, setColumnOrder] = useState<(keyof Product | 'image')[]>([
|
||||
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))
|
||||
]);
|
||||
@@ -162,11 +114,6 @@ export function Products() {
|
||||
return acc;
|
||||
}, {} as Record<string, typeof AVAILABLE_COLUMNS>);
|
||||
|
||||
// Handle column reordering from drag and drop
|
||||
const handleColumnOrderChange = (newOrder: (keyof Product | 'image')[]) => {
|
||||
setColumnOrder(newOrder);
|
||||
};
|
||||
|
||||
// Function to fetch products data
|
||||
const fetchProducts = async () => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
@@ -32,11 +32,24 @@ export interface Product {
|
||||
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;
|
||||
days_of_inventory?: number;
|
||||
weeks_of_inventory?: number;
|
||||
reorder_point?: number;
|
||||
max_stock?: number;
|
||||
safety_stock?: number;
|
||||
avg_margin_percent?: number;
|
||||
total_revenue?: number;
|
||||
inventory_value?: number;
|
||||
cost_of_goods_sold?: number;
|
||||
gross_profit?: number;
|
||||
gmroi?: number;
|
||||
avg_lead_time_days?: number;
|
||||
last_received_date?: string;
|
||||
abc_class?: string;
|
||||
stock_status?: string;
|
||||
turnover_rate?: number;
|
||||
current_lead_time?: number;
|
||||
target_lead_time?: number;
|
||||
lead_time_status?: string;
|
||||
reorder_qty?: number;
|
||||
overstocked_amt?: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user