Update frontend to match part 1
This commit is contained in:
@@ -6,14 +6,21 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import config from "@/config"
|
||||
import { formatCurrency } from "@/lib/utils"
|
||||
|
||||
interface BestSellerProduct {
|
||||
product_id: number
|
||||
sku: string
|
||||
title: string
|
||||
units_sold: number
|
||||
revenue: number
|
||||
profit: number
|
||||
growth_rate: number
|
||||
interface Product {
|
||||
pid: number;
|
||||
sku: string;
|
||||
title: string;
|
||||
units_sold: number;
|
||||
revenue: number;
|
||||
profit: number;
|
||||
}
|
||||
|
||||
interface Category {
|
||||
cat_id: number;
|
||||
name: string;
|
||||
total_revenue: number;
|
||||
total_profit: number;
|
||||
total_units: number;
|
||||
}
|
||||
|
||||
interface BestSellerBrand {
|
||||
@@ -25,18 +32,18 @@ interface BestSellerBrand {
|
||||
}
|
||||
|
||||
interface BestSellerCategory {
|
||||
category_id: number
|
||||
name: string
|
||||
units_sold: number
|
||||
revenue: number
|
||||
profit: number
|
||||
growth_rate: number
|
||||
cat_id: number;
|
||||
name: string;
|
||||
units_sold: number;
|
||||
revenue: number;
|
||||
profit: number;
|
||||
growth_rate: number;
|
||||
}
|
||||
|
||||
interface BestSellersData {
|
||||
products: BestSellerProduct[]
|
||||
products: Product[]
|
||||
brands: BestSellerBrand[]
|
||||
categories: BestSellerCategory[]
|
||||
categories: Category[]
|
||||
}
|
||||
|
||||
export function BestSellers() {
|
||||
@@ -70,41 +77,29 @@ export function BestSellers() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[40%]">Product</TableHead>
|
||||
<TableHead className="w-[15%] text-right">Sales</TableHead>
|
||||
<TableHead className="w-[15%] text-right">Revenue</TableHead>
|
||||
<TableHead className="w-[15%] text-right">Profit</TableHead>
|
||||
<TableHead className="w-[15%] text-right">Growth</TableHead>
|
||||
<TableHead>Product</TableHead>
|
||||
<TableHead className="text-right">Units Sold</TableHead>
|
||||
<TableHead className="text-right">Revenue</TableHead>
|
||||
<TableHead className="text-right">Profit</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.products.map((product) => (
|
||||
<TableRow key={product.product_id}>
|
||||
<TableCell className="w-[40%]">
|
||||
<div>
|
||||
<a
|
||||
href={`https://backend.acherryontop.com/product/${product.product_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium hover:underline"
|
||||
>
|
||||
{product.title}
|
||||
</a>
|
||||
<p className="text-sm text-muted-foreground">{product.sku}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="w-[15%] text-right">
|
||||
{product.units_sold.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="w-[15%] text-right">
|
||||
{formatCurrency(product.revenue)}
|
||||
</TableCell>
|
||||
<TableCell className="w-[15%] text-right">
|
||||
{formatCurrency(product.profit)}
|
||||
</TableCell>
|
||||
<TableCell className="w-[15%] text-right">
|
||||
{product.growth_rate > 0 ? '+' : ''}{product.growth_rate.toFixed(1)}%
|
||||
<TableRow key={product.pid}>
|
||||
<TableCell>
|
||||
<a
|
||||
href={`https://backend.acherryontop.com/product/${product.pid}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
{product.title}
|
||||
</a>
|
||||
<div className="text-sm text-muted-foreground">{product.sku}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{product.units_sold}</TableCell>
|
||||
<TableCell className="text-right">{formatCurrency(product.revenue)}</TableCell>
|
||||
<TableCell className="text-right">{formatCurrency(product.profit)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -154,31 +149,19 @@ export function BestSellers() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[40%]">Category</TableHead>
|
||||
<TableHead className="w-[15%] text-right">Sales</TableHead>
|
||||
<TableHead className="w-[15%] text-right">Revenue</TableHead>
|
||||
<TableHead className="w-[15%] text-right">Profit</TableHead>
|
||||
<TableHead className="w-[15%] text-right">Growth</TableHead>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead className="text-right">Units Sold</TableHead>
|
||||
<TableHead className="text-right">Revenue</TableHead>
|
||||
<TableHead className="text-right">Profit</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.categories.map((category) => (
|
||||
<TableRow key={category.category_id}>
|
||||
<TableCell className="w-[40%]">
|
||||
<p className="font-medium">{category.name}</p>
|
||||
</TableCell>
|
||||
<TableCell className="w-[15%] text-right">
|
||||
{category.units_sold.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="w-[15%] text-right">
|
||||
{formatCurrency(category.revenue)}
|
||||
</TableCell>
|
||||
<TableCell className="w-[15%] text-right">
|
||||
{formatCurrency(category.profit)}
|
||||
</TableCell>
|
||||
<TableCell className="w-[15%] text-right">
|
||||
{category.growth_rate > 0 ? '+' : ''}{category.growth_rate.toFixed(1)}%
|
||||
</TableCell>
|
||||
<TableRow key={category.cat_id}>
|
||||
<TableCell>{category.name}</TableCell>
|
||||
<TableCell className="text-right">{category.total_units}</TableCell>
|
||||
<TableCell className="text-right">{formatCurrency(category.total_revenue)}</TableCell>
|
||||
<TableCell className="text-right">{formatCurrency(category.total_profit)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -12,19 +12,20 @@ import { Badge } from "@/components/ui/badge"
|
||||
import { AlertCircle, AlertTriangle } from "lucide-react"
|
||||
import config from "@/config"
|
||||
|
||||
interface LowStockProduct {
|
||||
product_id: number
|
||||
SKU: string
|
||||
title: string
|
||||
stock_quantity: number
|
||||
reorder_qty: number
|
||||
days_of_inventory: number
|
||||
stock_status: "Critical" | "Reorder"
|
||||
daily_sales_avg: number
|
||||
interface Product {
|
||||
pid: number;
|
||||
sku: string;
|
||||
title: string;
|
||||
stock_quantity: number;
|
||||
daily_sales_avg: number;
|
||||
days_of_inventory: number;
|
||||
reorder_qty: number;
|
||||
last_purchase_date: string | null;
|
||||
lead_time_status: string;
|
||||
}
|
||||
|
||||
export function LowStockAlerts() {
|
||||
const { data: products } = useQuery<LowStockProduct[]>({
|
||||
const { data: products } = useQuery<Product[]>({
|
||||
queryKey: ["low-stock"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${config.apiUrl}/dashboard/low-stock/products`)
|
||||
@@ -45,35 +46,37 @@ export function LowStockAlerts() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>SKU</TableHead>
|
||||
<TableHead>Product</TableHead>
|
||||
<TableHead className="text-right">Stock</TableHead>
|
||||
<TableHead className="text-right">Status</TableHead>
|
||||
<TableHead className="text-right">Daily Sales</TableHead>
|
||||
<TableHead className="text-right">Days Left</TableHead>
|
||||
<TableHead className="text-right">Reorder Qty</TableHead>
|
||||
<TableHead>Last Purchase</TableHead>
|
||||
<TableHead>Lead Time</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{products?.map((product) => (
|
||||
<TableRow key={product.product_id}>
|
||||
<TableCell className="font-medium">{product.SKU}</TableCell>
|
||||
<TableCell>{product.title}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{product.stock_quantity} / {product.reorder_qty}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
product.stock_status === "Critical"
|
||||
? "border-destructive text-destructive"
|
||||
: "border-warning text-warning"
|
||||
}
|
||||
<TableRow key={product.pid}>
|
||||
<TableCell>
|
||||
<a
|
||||
href={`https://backend.acherryontop.com/product/${product.pid}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
{product.stock_status === "Critical" ? (
|
||||
<AlertCircle className="mr-1 h-3 w-3" />
|
||||
) : (
|
||||
<AlertTriangle className="mr-1 h-3 w-3" />
|
||||
)}
|
||||
{product.stock_status}
|
||||
{product.title}
|
||||
</a>
|
||||
<div className="text-sm text-muted-foreground">{product.sku}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{product.stock_quantity}</TableCell>
|
||||
<TableCell className="text-right">{product.daily_sales_avg.toFixed(1)}</TableCell>
|
||||
<TableCell className="text-right">{product.days_of_inventory.toFixed(1)}</TableCell>
|
||||
<TableCell className="text-right">{product.reorder_qty}</TableCell>
|
||||
<TableCell>{product.last_purchase_date ? formatDate(product.last_purchase_date) : '-'}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={getLeadTimeVariant(product.lead_time_status)}>
|
||||
{product.lead_time_status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -5,18 +5,18 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
||||
import config from "@/config"
|
||||
import { formatCurrency } from "@/lib/utils"
|
||||
|
||||
interface OverstockedProduct {
|
||||
product_id: number
|
||||
SKU: string
|
||||
title: string
|
||||
stock_quantity: number
|
||||
overstocked_amt: number
|
||||
excess_cost: number
|
||||
excess_retail: number
|
||||
interface Product {
|
||||
pid: number;
|
||||
sku: string;
|
||||
title: string;
|
||||
stock_quantity: number;
|
||||
overstocked_amt: number;
|
||||
excess_cost: number;
|
||||
excess_retail: number;
|
||||
}
|
||||
|
||||
export function TopOverstockedProducts() {
|
||||
const { data } = useQuery<OverstockedProduct[]>({
|
||||
const { data } = useQuery<Product[]>({
|
||||
queryKey: ["top-overstocked-products"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${config.apiUrl}/dashboard/overstock/products?limit=50`)
|
||||
@@ -38,40 +38,30 @@ export function TopOverstockedProducts() {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Product</TableHead>
|
||||
<TableHead className="text-right">Current Stock</TableHead>
|
||||
<TableHead className="text-right">Overstock Amt</TableHead>
|
||||
<TableHead className="text-right">Overstock Cost</TableHead>
|
||||
<TableHead className="text-right">Overstock Retail</TableHead>
|
||||
<TableHead className="text-right">Stock</TableHead>
|
||||
<TableHead className="text-right">Excess</TableHead>
|
||||
<TableHead className="text-right">Cost</TableHead>
|
||||
<TableHead className="text-right">Retail</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.map((product) => (
|
||||
<TableRow key={product.product_id}>
|
||||
<TableRow key={product.pid}>
|
||||
<TableCell>
|
||||
<div>
|
||||
<a
|
||||
href={`https://backend.acherryontop.com/product/${product.product_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium hover:underline"
|
||||
>
|
||||
{product.title}
|
||||
</a>
|
||||
<p className="text-sm text-muted-foreground">{product.SKU}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{product.stock_quantity.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{product.overstocked_amt.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatCurrency(product.excess_cost)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatCurrency(product.excess_retail)}
|
||||
<a
|
||||
href={`https://backend.acherryontop.com/product/${product.pid}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
{product.title}
|
||||
</a>
|
||||
<div className="text-sm text-muted-foreground">{product.sku}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{product.stock_quantity}</TableCell>
|
||||
<TableCell className="text-right">{product.overstocked_amt}</TableCell>
|
||||
<TableCell className="text-right">{formatCurrency(product.excess_cost)}</TableCell>
|
||||
<TableCell className="text-right">{formatCurrency(product.excess_retail)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -3,20 +3,19 @@ import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import config from "@/config"
|
||||
import { formatCurrency } from "@/lib/utils"
|
||||
|
||||
interface ReplenishProduct {
|
||||
product_id: number
|
||||
SKU: string
|
||||
title: string
|
||||
current_stock: number
|
||||
replenish_qty: number
|
||||
replenish_cost: number
|
||||
replenish_retail: number
|
||||
interface Product {
|
||||
pid: number;
|
||||
sku: string;
|
||||
title: string;
|
||||
stock_quantity: number;
|
||||
daily_sales_avg: number;
|
||||
reorder_qty: number;
|
||||
last_purchase_date: string | null;
|
||||
}
|
||||
|
||||
export function TopReplenishProducts() {
|
||||
const { data } = useQuery<ReplenishProduct[]>({
|
||||
const { data } = useQuery<Product[]>({
|
||||
queryKey: ["top-replenish-products"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${config.apiUrl}/dashboard/replenish/products?limit=50`)
|
||||
@@ -39,39 +38,29 @@ export function TopReplenishProducts() {
|
||||
<TableRow>
|
||||
<TableHead>Product</TableHead>
|
||||
<TableHead className="text-right">Stock</TableHead>
|
||||
<TableHead className="text-right">Replenish</TableHead>
|
||||
<TableHead className="text-right">Cost</TableHead>
|
||||
<TableHead className="text-right">Retail</TableHead>
|
||||
<TableHead className="text-right">Daily Sales</TableHead>
|
||||
<TableHead className="text-right">Reorder Qty</TableHead>
|
||||
<TableHead>Last Purchase</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.map((product) => (
|
||||
<TableRow key={product.product_id}>
|
||||
<TableRow key={product.pid}>
|
||||
<TableCell>
|
||||
<div>
|
||||
<a
|
||||
href={`https://backend.acherryontop.com/product/${product.product_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium hover:underline"
|
||||
>
|
||||
{product.title}
|
||||
</a>
|
||||
<p className="text-sm text-muted-foreground">{product.SKU}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{product.current_stock.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{product.replenish_qty.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatCurrency(product.replenish_cost)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatCurrency(product.replenish_retail)}
|
||||
<a
|
||||
href={`https://backend.acherryontop.com/product/${product.pid}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
{product.title}
|
||||
</a>
|
||||
<div className="text-sm text-muted-foreground">{product.sku}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{product.stock_quantity}</TableCell>
|
||||
<TableCell className="text-right">{product.daily_sales_avg.toFixed(1)}</TableCell>
|
||||
<TableCell className="text-right">{product.reorder_qty}</TableCell>
|
||||
<TableCell>{product.last_purchase_date ? product.last_purchase_date : '-'}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -11,18 +11,18 @@ import {
|
||||
import { TrendingUp, TrendingDown } from "lucide-react"
|
||||
import config from "@/config"
|
||||
|
||||
interface TrendingProduct {
|
||||
product_id: number
|
||||
sku: string
|
||||
title: string
|
||||
daily_sales_avg: string
|
||||
weekly_sales_avg: string
|
||||
growth_rate: string
|
||||
total_revenue: string
|
||||
interface Product {
|
||||
pid: number;
|
||||
sku: string;
|
||||
title: string;
|
||||
daily_sales_avg: number;
|
||||
weekly_sales_avg: number;
|
||||
growth_rate: number;
|
||||
total_revenue: number;
|
||||
}
|
||||
|
||||
export function TrendingProducts() {
|
||||
const { data: products } = useQuery<TrendingProduct[]>({
|
||||
const { data: products } = useQuery<Product[]>({
|
||||
queryKey: ["trending-products"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${config.apiUrl}/products/trending`)
|
||||
@@ -33,7 +33,6 @@ export function TrendingProducts() {
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const formatPercent = (value: number) =>
|
||||
new Intl.NumberFormat("en-US", {
|
||||
style: "percent",
|
||||
@@ -42,6 +41,14 @@ export function TrendingProducts() {
|
||||
signDisplay: "exceptZero",
|
||||
}).format(value / 100)
|
||||
|
||||
const formatCurrency = (value: number) =>
|
||||
new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value)
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader>
|
||||
@@ -59,7 +66,7 @@ export function TrendingProducts() {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{products?.map((product) => (
|
||||
<TableRow key={product.product_id}>
|
||||
<TableRow key={product.pid}>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{product.title}</span>
|
||||
|
||||
@@ -3,14 +3,16 @@ import { ArrowUpDown, ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
interface ProductDetail {
|
||||
product_id: string;
|
||||
name: string;
|
||||
|
||||
interface Product {
|
||||
pid: string;
|
||||
sku: string;
|
||||
title: string;
|
||||
stock_quantity: number;
|
||||
total_sold: number;
|
||||
avg_price: number;
|
||||
first_received_date: string;
|
||||
daily_sales_avg: number;
|
||||
forecast_units: number;
|
||||
forecast_revenue: number;
|
||||
confidence_level: number;
|
||||
}
|
||||
|
||||
export interface ForecastItem {
|
||||
@@ -20,7 +22,7 @@ export interface ForecastItem {
|
||||
numProducts: number;
|
||||
avgPrice: number;
|
||||
avgTotalSold: number;
|
||||
products?: ProductDetail[];
|
||||
products?: Product[];
|
||||
}
|
||||
|
||||
export const columns: ColumnDef<ForecastItem>[] = [
|
||||
@@ -147,23 +149,33 @@ export const renderSubComponent = ({ row }: { row: any }) => {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Product Name</TableHead>
|
||||
<TableHead>SKU</TableHead>
|
||||
<TableHead>First Received</TableHead>
|
||||
<TableHead>Stock Quantity</TableHead>
|
||||
<TableHead>Total Sold</TableHead>
|
||||
<TableHead>Average Price</TableHead>
|
||||
<TableHead>Product</TableHead>
|
||||
<TableHead className="text-right">Stock</TableHead>
|
||||
<TableHead className="text-right">Daily Sales</TableHead>
|
||||
<TableHead className="text-right">Forecast Units</TableHead>
|
||||
<TableHead className="text-right">Forecast Revenue</TableHead>
|
||||
<TableHead className="text-right">Confidence</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{products.map((product: ProductDetail) => (
|
||||
<TableRow key={product.product_id}>
|
||||
<TableCell>{product.name}</TableCell>
|
||||
<TableCell>{product.sku}</TableCell>
|
||||
<TableCell>{product.first_received_date}</TableCell>
|
||||
<TableCell>{product.stock_quantity.toLocaleString()}</TableCell>
|
||||
<TableCell>{product.total_sold.toLocaleString()}</TableCell>
|
||||
<TableCell>${product.avg_price.toFixed(2)}</TableCell>
|
||||
{products.map((product) => (
|
||||
<TableRow key={product.pid}>
|
||||
<TableCell>
|
||||
<a
|
||||
href={`https://backend.acherryontop.com/product/${product.pid}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
{product.title}
|
||||
</a>
|
||||
<div className="text-sm text-muted-foreground">{product.sku}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{product.stock_quantity}</TableCell>
|
||||
<TableCell className="text-right">{product.daily_sales_avg.toFixed(1)}</TableCell>
|
||||
<TableCell className="text-right">{product.forecast_units.toFixed(1)}</TableCell>
|
||||
<TableCell className="text-right">{product.forecast_revenue.toFixed(2)}</TableCell>
|
||||
<TableCell className="text-right">{product.confidence_level.toFixed(1)}%</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContai
|
||||
import config from "@/config";
|
||||
|
||||
interface Product {
|
||||
product_id: number;
|
||||
pid: number;
|
||||
title: string;
|
||||
SKU: string;
|
||||
barcode: string;
|
||||
@@ -38,7 +38,7 @@ interface Product {
|
||||
// Vendor info
|
||||
vendor: string;
|
||||
vendor_reference: string;
|
||||
brand: string;
|
||||
brand: string | 'Unbranded';
|
||||
|
||||
// URLs
|
||||
permalink: string;
|
||||
|
||||
@@ -230,7 +230,7 @@ export function ProductTable({
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{Array.from(new Set(value as string[])).map((category) => (
|
||||
<Badge key={`${product.product_id}-${category}`} variant="outline">{category}</Badge>
|
||||
<Badge key={`${product.pid}-${category}`} variant="outline">{category}</Badge>
|
||||
)) || '-'}
|
||||
</div>
|
||||
);
|
||||
@@ -297,12 +297,12 @@ export function ProductTable({
|
||||
<TableBody>
|
||||
{products.map((product) => (
|
||||
<TableRow
|
||||
key={product.product_id}
|
||||
key={product.pid}
|
||||
onClick={() => onRowClick?.(product)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{orderedColumns.map((column) => (
|
||||
<TableCell key={`${product.product_id}-${column}`}>
|
||||
<TableCell key={`${product.pid}-${column}`}>
|
||||
{formatColumnValue(product, column)}
|
||||
</TableCell>
|
||||
))}
|
||||
|
||||
@@ -8,7 +8,7 @@ import config from '../../config';
|
||||
|
||||
interface SalesVelocityConfig {
|
||||
id: number;
|
||||
category_id: number | null;
|
||||
cat_id: number | null;
|
||||
vendor: string | null;
|
||||
daily_window_days: number;
|
||||
weekly_window_days: number;
|
||||
@@ -18,7 +18,7 @@ interface SalesVelocityConfig {
|
||||
export function CalculationSettings() {
|
||||
const [salesVelocityConfig, setSalesVelocityConfig] = useState<SalesVelocityConfig>({
|
||||
id: 1,
|
||||
category_id: null,
|
||||
cat_id: null,
|
||||
vendor: null,
|
||||
daily_window_days: 30,
|
||||
weekly_window_days: 7,
|
||||
|
||||
@@ -6,10 +6,11 @@ import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { toast } from "sonner";
|
||||
import config from '../../config';
|
||||
import { Table, TableBody, TableCell, TableHeader, TableRow } from "@/components/ui/table";
|
||||
|
||||
interface StockThreshold {
|
||||
id: number;
|
||||
category_id: number | null;
|
||||
cat_id: number | null;
|
||||
vendor: string | null;
|
||||
critical_days: number;
|
||||
reorder_days: number;
|
||||
@@ -22,7 +23,7 @@ interface StockThreshold {
|
||||
|
||||
interface LeadTimeThreshold {
|
||||
id: number;
|
||||
category_id: number | null;
|
||||
cat_id: number | null;
|
||||
vendor: string | null;
|
||||
target_days: number;
|
||||
warning_days: number;
|
||||
@@ -31,7 +32,7 @@ interface LeadTimeThreshold {
|
||||
|
||||
interface SalesVelocityConfig {
|
||||
id: number;
|
||||
category_id: number | null;
|
||||
cat_id: number | null;
|
||||
vendor: string | null;
|
||||
daily_window_days: number;
|
||||
weekly_window_days: number;
|
||||
@@ -47,7 +48,7 @@ interface ABCClassificationConfig {
|
||||
|
||||
interface SafetyStockConfig {
|
||||
id: number;
|
||||
category_id: number | null;
|
||||
cat_id: number | null;
|
||||
vendor: string | null;
|
||||
coverage_days: number;
|
||||
service_level: number;
|
||||
@@ -55,7 +56,7 @@ interface SafetyStockConfig {
|
||||
|
||||
interface TurnoverConfig {
|
||||
id: number;
|
||||
category_id: number | null;
|
||||
cat_id: number | null;
|
||||
vendor: string | null;
|
||||
calculation_period_days: number;
|
||||
target_rate: number;
|
||||
@@ -64,7 +65,7 @@ interface TurnoverConfig {
|
||||
export function Configuration() {
|
||||
const [stockThresholds, setStockThresholds] = useState<StockThreshold>({
|
||||
id: 1,
|
||||
category_id: null,
|
||||
cat_id: null,
|
||||
vendor: null,
|
||||
critical_days: 7,
|
||||
reorder_days: 14,
|
||||
@@ -75,7 +76,7 @@ export function Configuration() {
|
||||
|
||||
const [leadTimeThresholds, setLeadTimeThresholds] = useState<LeadTimeThreshold>({
|
||||
id: 1,
|
||||
category_id: null,
|
||||
cat_id: null,
|
||||
vendor: null,
|
||||
target_days: 14,
|
||||
warning_days: 21,
|
||||
@@ -84,7 +85,7 @@ export function Configuration() {
|
||||
|
||||
const [salesVelocityConfig, setSalesVelocityConfig] = useState<SalesVelocityConfig>({
|
||||
id: 1,
|
||||
category_id: null,
|
||||
cat_id: null,
|
||||
vendor: null,
|
||||
daily_window_days: 30,
|
||||
weekly_window_days: 7,
|
||||
@@ -100,7 +101,7 @@ export function Configuration() {
|
||||
|
||||
const [safetyStockConfig, setSafetyStockConfig] = useState<SafetyStockConfig>({
|
||||
id: 1,
|
||||
category_id: null,
|
||||
cat_id: null,
|
||||
vendor: null,
|
||||
coverage_days: 14,
|
||||
service_level: 95.0
|
||||
@@ -108,7 +109,7 @@ export function Configuration() {
|
||||
|
||||
const [turnoverConfig, setTurnoverConfig] = useState<TurnoverConfig>({
|
||||
id: 1,
|
||||
category_id: null,
|
||||
cat_id: null,
|
||||
vendor: null,
|
||||
calculation_period_days: 30,
|
||||
target_rate: 1.0
|
||||
|
||||
@@ -5,10 +5,11 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { toast } from "sonner";
|
||||
import config from '../../config';
|
||||
import { Table, TableBody, TableCell, TableHeader, TableRow } from "@/components/ui/table";
|
||||
|
||||
interface LeadTimeThreshold {
|
||||
id: number;
|
||||
category_id: number | null;
|
||||
cat_id: number | null;
|
||||
vendor: string | null;
|
||||
target_days: number;
|
||||
warning_days: number;
|
||||
@@ -17,6 +18,8 @@ interface LeadTimeThreshold {
|
||||
|
||||
interface ABCClassificationConfig {
|
||||
id: number;
|
||||
cat_id: number | null;
|
||||
vendor: string | null;
|
||||
a_threshold: number;
|
||||
b_threshold: number;
|
||||
classification_period_days: number;
|
||||
@@ -24,7 +27,7 @@ interface ABCClassificationConfig {
|
||||
|
||||
interface TurnoverConfig {
|
||||
id: number;
|
||||
category_id: number | null;
|
||||
cat_id: number | null;
|
||||
vendor: string | null;
|
||||
calculation_period_days: number;
|
||||
target_rate: number;
|
||||
@@ -33,27 +36,16 @@ interface TurnoverConfig {
|
||||
export function PerformanceMetrics() {
|
||||
const [leadTimeThresholds, setLeadTimeThresholds] = useState<LeadTimeThreshold>({
|
||||
id: 1,
|
||||
category_id: null,
|
||||
cat_id: null,
|
||||
vendor: null,
|
||||
target_days: 14,
|
||||
warning_days: 21,
|
||||
critical_days: 30
|
||||
});
|
||||
|
||||
const [abcConfig, setAbcConfig] = useState<ABCClassificationConfig>({
|
||||
id: 1,
|
||||
a_threshold: 20.0,
|
||||
b_threshold: 50.0,
|
||||
classification_period_days: 90
|
||||
});
|
||||
const [abcConfigs, setAbcConfigs] = useState<ABCClassificationConfig[]>([]);
|
||||
|
||||
const [turnoverConfig, setTurnoverConfig] = useState<TurnoverConfig>({
|
||||
id: 1,
|
||||
category_id: null,
|
||||
vendor: null,
|
||||
calculation_period_days: 30,
|
||||
target_rate: 1.0
|
||||
});
|
||||
const [turnoverConfigs, setTurnoverConfigs] = useState<TurnoverConfig[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
@@ -66,8 +58,8 @@ export function PerformanceMetrics() {
|
||||
}
|
||||
const data = await response.json();
|
||||
setLeadTimeThresholds(data.leadTimeThresholds);
|
||||
setAbcConfig(data.abcConfig);
|
||||
setTurnoverConfig(data.turnoverConfig);
|
||||
setAbcConfigs(data.abcConfigs);
|
||||
setTurnoverConfigs(data.turnoverConfigs);
|
||||
} catch (error) {
|
||||
toast.error(`Failed to load configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
@@ -105,7 +97,7 @@ export function PerformanceMetrics() {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(abcConfig)
|
||||
body: JSON.stringify(abcConfigs)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -127,7 +119,7 @@ export function PerformanceMetrics() {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(turnoverConfig)
|
||||
body: JSON.stringify(turnoverConfigs)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -210,54 +202,28 @@ export function PerformanceMetrics() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="a-threshold">A Threshold (%)</Label>
|
||||
<Input
|
||||
id="a-threshold"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
value={abcConfig.a_threshold}
|
||||
onChange={(e) => setAbcConfig(prev => ({
|
||||
...prev,
|
||||
a_threshold: parseFloat(e.target.value) || 0
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="b-threshold">B Threshold (%)</Label>
|
||||
<Input
|
||||
id="b-threshold"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
value={abcConfig.b_threshold}
|
||||
onChange={(e) => setAbcConfig(prev => ({
|
||||
...prev,
|
||||
b_threshold: parseFloat(e.target.value) || 0
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="classification-period">Classification Period (days)</Label>
|
||||
<Input
|
||||
id="classification-period"
|
||||
type="number"
|
||||
min="1"
|
||||
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
value={abcConfig.classification_period_days}
|
||||
onChange={(e) => setAbcConfig(prev => ({
|
||||
...prev,
|
||||
classification_period_days: parseInt(e.target.value) || 1
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead>Vendor</TableHead>
|
||||
<TableHead className="text-right">A Threshold</TableHead>
|
||||
<TableHead className="text-right">B Threshold</TableHead>
|
||||
<TableHead className="text-right">Period Days</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{abcConfigs.map((config) => (
|
||||
<TableRow key={`${config.cat_id}-${config.vendor}`}>
|
||||
<TableCell>{config.cat_id ? getCategoryName(config.cat_id) : 'Global'}</TableCell>
|
||||
<TableCell>{config.vendor || 'All Vendors'}</TableCell>
|
||||
<TableCell className="text-right">{config.a_threshold}%</TableCell>
|
||||
<TableCell className="text-right">{config.b_threshold}%</TableCell>
|
||||
<TableCell className="text-right">{config.classification_period_days}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Button onClick={handleUpdateABCConfig}>
|
||||
Update ABC Classification
|
||||
</Button>
|
||||
@@ -273,37 +239,26 @@ export function PerformanceMetrics() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="calculation-period">Calculation Period (days)</Label>
|
||||
<Input
|
||||
id="calculation-period"
|
||||
type="number"
|
||||
min="1"
|
||||
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
value={turnoverConfig.calculation_period_days}
|
||||
onChange={(e) => setTurnoverConfig(prev => ({
|
||||
...prev,
|
||||
calculation_period_days: parseInt(e.target.value) || 1
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="target-rate">Target Rate</Label>
|
||||
<Input
|
||||
id="target-rate"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
value={turnoverConfig.target_rate}
|
||||
onChange={(e) => setTurnoverConfig(prev => ({
|
||||
...prev,
|
||||
target_rate: parseFloat(e.target.value) || 0
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead>Vendor</TableHead>
|
||||
<TableHead className="text-right">Period Days</TableHead>
|
||||
<TableHead className="text-right">Target Rate</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{turnoverConfigs.map((config) => (
|
||||
<TableRow key={`${config.cat_id}-${config.vendor}`}>
|
||||
<TableCell>{config.cat_id ? getCategoryName(config.cat_id) : 'Global'}</TableCell>
|
||||
<TableCell>{config.vendor || 'All Vendors'}</TableCell>
|
||||
<TableCell className="text-right">{config.calculation_period_days}</TableCell>
|
||||
<TableCell className="text-right">{config.target_rate.toFixed(2)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Button onClick={handleUpdateTurnoverConfig}>
|
||||
Update Turnover Configuration
|
||||
</Button>
|
||||
|
||||
@@ -5,10 +5,11 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { toast } from "sonner";
|
||||
import config from '../../config';
|
||||
import { Table, TableBody, TableCell, TableHeader, TableRow } from "@/components/ui/table";
|
||||
|
||||
interface StockThreshold {
|
||||
id: number;
|
||||
category_id: number | null;
|
||||
cat_id: number | null;
|
||||
vendor: string | null;
|
||||
critical_days: number;
|
||||
reorder_days: number;
|
||||
@@ -19,7 +20,7 @@ interface StockThreshold {
|
||||
|
||||
interface SafetyStockConfig {
|
||||
id: number;
|
||||
category_id: number | null;
|
||||
cat_id: number | null;
|
||||
vendor: string | null;
|
||||
coverage_days: number;
|
||||
service_level: number;
|
||||
@@ -28,7 +29,7 @@ interface SafetyStockConfig {
|
||||
export function StockManagement() {
|
||||
const [stockThresholds, setStockThresholds] = useState<StockThreshold>({
|
||||
id: 1,
|
||||
category_id: null,
|
||||
cat_id: null,
|
||||
vendor: null,
|
||||
critical_days: 7,
|
||||
reorder_days: 14,
|
||||
@@ -39,7 +40,7 @@ export function StockManagement() {
|
||||
|
||||
const [safetyStockConfig, setSafetyStockConfig] = useState<SafetyStockConfig>({
|
||||
id: 1,
|
||||
category_id: null,
|
||||
cat_id: null,
|
||||
vendor: null,
|
||||
coverage_days: 14,
|
||||
service_level: 95.0
|
||||
@@ -243,6 +244,54 @@ export function StockManagement() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead>Vendor</TableHead>
|
||||
<TableHead className="text-right">Critical Days</TableHead>
|
||||
<TableHead className="text-right">Reorder Days</TableHead>
|
||||
<TableHead className="text-right">Overstock Days</TableHead>
|
||||
<TableHead className="text-right">Low Stock</TableHead>
|
||||
<TableHead className="text-right">Min Reorder</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{stockThresholds.map((threshold) => (
|
||||
<TableRow key={`${threshold.cat_id}-${threshold.vendor}`}>
|
||||
<TableCell>{threshold.cat_id ? getCategoryName(threshold.cat_id) : 'Global'}</TableCell>
|
||||
<TableCell>{threshold.vendor || 'All Vendors'}</TableCell>
|
||||
<TableCell className="text-right">{threshold.critical_days}</TableCell>
|
||||
<TableCell className="text-right">{threshold.reorder_days}</TableCell>
|
||||
<TableCell className="text-right">{threshold.overstock_days}</TableCell>
|
||||
<TableCell className="text-right">{threshold.low_stock_threshold}</TableCell>
|
||||
<TableCell className="text-right">{threshold.min_reorder_quantity}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead>Vendor</TableHead>
|
||||
<TableHead className="text-right">Coverage Days</TableHead>
|
||||
<TableHead className="text-right">Service Level</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{safetyStockConfigs.map((config) => (
|
||||
<TableRow key={`${config.cat_id}-${config.vendor}`}>
|
||||
<TableCell>{config.cat_id ? getCategoryName(config.cat_id) : 'Global'}</TableCell>
|
||||
<TableCell>{config.vendor || 'All Vendors'}</TableCell>
|
||||
<TableCell className="text-right">{config.coverage_days}</TableCell>
|
||||
<TableCell className="text-right">{config.service_level}%</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,16 +10,22 @@ import { motion } from "motion/react";
|
||||
import config from "../config";
|
||||
|
||||
interface Category {
|
||||
category_id: number;
|
||||
cat_id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
parent_category?: string;
|
||||
product_count: number;
|
||||
total_value: number;
|
||||
avg_margin: number;
|
||||
turnover_rate: number;
|
||||
growth_rate: number;
|
||||
type: number;
|
||||
parent_id: number | null;
|
||||
description: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
status: string;
|
||||
metrics?: {
|
||||
product_count: number;
|
||||
active_products: number;
|
||||
total_value: number;
|
||||
avg_margin: number;
|
||||
turnover_rate: number;
|
||||
growth_rate: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CategoryFilters {
|
||||
@@ -71,16 +77,16 @@ export function Categories() {
|
||||
// Apply parent filter
|
||||
if (filters.parent !== 'all') {
|
||||
if (filters.parent === 'none') {
|
||||
filtered = filtered.filter(category => !category.parent_category);
|
||||
filtered = filtered.filter(category => !category.parent_id);
|
||||
} else {
|
||||
filtered = filtered.filter(category => category.parent_category === filters.parent);
|
||||
filtered = filtered.filter(category => category.parent_id === Number(filters.parent));
|
||||
}
|
||||
}
|
||||
|
||||
// Apply performance filter
|
||||
if (filters.performance !== 'all') {
|
||||
filtered = filtered.filter(category => {
|
||||
const growth = category.growth_rate ?? 0;
|
||||
const growth = category.metrics?.growth_rate ?? 0;
|
||||
switch (filters.performance) {
|
||||
case 'high_growth': return growth >= 20;
|
||||
case 'growing': return growth >= 5 && growth < 20;
|
||||
@@ -123,9 +129,9 @@ export function Categories() {
|
||||
if (!filteredData.length) return data?.stats;
|
||||
|
||||
const activeCategories = filteredData.filter(c => c.status === 'active').length;
|
||||
const totalValue = filteredData.reduce((sum, c) => sum + (c.total_value || 0), 0);
|
||||
const margins = filteredData.map(c => c.avg_margin || 0).filter(m => m !== 0);
|
||||
const growthRates = filteredData.map(c => c.growth_rate || 0).filter(g => g !== 0);
|
||||
const totalValue = filteredData.reduce((sum, c) => sum + (c.metrics?.total_value || 0), 0);
|
||||
const margins = filteredData.map(c => c.metrics?.avg_margin || 0).filter(m => m !== 0);
|
||||
const growthRates = filteredData.map(c => c.metrics?.growth_rate || 0).filter(g => g !== 0);
|
||||
|
||||
return {
|
||||
totalCategories: filteredData.length,
|
||||
@@ -281,14 +287,16 @@ export function Categories() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead onClick={() => handleSort("name")} className="cursor-pointer">Name</TableHead>
|
||||
<TableHead onClick={() => handleSort("parent_category")} className="cursor-pointer">Parent</TableHead>
|
||||
<TableHead onClick={() => handleSort("product_count")} className="cursor-pointer">Products</TableHead>
|
||||
<TableHead onClick={() => handleSort("total_value")} className="cursor-pointer">Value</TableHead>
|
||||
<TableHead onClick={() => handleSort("avg_margin")} className="cursor-pointer">Margin</TableHead>
|
||||
<TableHead onClick={() => handleSort("turnover_rate")} className="cursor-pointer">Turnover</TableHead>
|
||||
<TableHead onClick={() => handleSort("growth_rate")} className="cursor-pointer">Growth</TableHead>
|
||||
<TableHead onClick={() => handleSort("status")} className="cursor-pointer">Status</TableHead>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Parent</TableHead>
|
||||
<TableHead className="text-right">Products</TableHead>
|
||||
<TableHead className="text-right">Active</TableHead>
|
||||
<TableHead className="text-right">Value</TableHead>
|
||||
<TableHead className="text-right">Margin</TableHead>
|
||||
<TableHead className="text-right">Turnover</TableHead>
|
||||
<TableHead className="text-right">Growth</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -299,25 +307,21 @@ export function Categories() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : paginatedData.map((category: Category) => (
|
||||
<TableRow key={category.category_id}>
|
||||
<TableRow key={category.cat_id}>
|
||||
<TableCell>{category.name}</TableCell>
|
||||
<TableCell>{getPerformanceBadge(category.metrics?.growth_rate ?? 0)}</TableCell>
|
||||
<TableCell>{category.parent_id ? getParentName(category.parent_id) : '-'}</TableCell>
|
||||
<TableCell className="text-right">{category.metrics?.product_count || 0}</TableCell>
|
||||
<TableCell className="text-right">{category.metrics?.active_products || 0}</TableCell>
|
||||
<TableCell className="text-right">{formatCurrency(category.metrics?.total_value || 0)}</TableCell>
|
||||
<TableCell className="text-right">{category.metrics?.avg_margin?.toFixed(1)}%</TableCell>
|
||||
<TableCell className="text-right">{category.metrics?.turnover_rate?.toFixed(2)}</TableCell>
|
||||
<TableCell className="text-right">{category.metrics?.growth_rate?.toFixed(1)}%</TableCell>
|
||||
<TableCell>
|
||||
<div className="font-medium">{category.name}</div>
|
||||
<div className="text-sm text-muted-foreground">{category.description}</div>
|
||||
<Badge variant={getCategoryStatusVariant(category.status)}>
|
||||
{category.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{category.parent_category || "—"}</TableCell>
|
||||
<TableCell>{category.product_count?.toLocaleString() ?? 0}</TableCell>
|
||||
<TableCell>{formatCurrency(category.total_value ?? 0)}</TableCell>
|
||||
<TableCell>{typeof category.avg_margin === 'number' ? category.avg_margin.toFixed(1) : "0.0"}%</TableCell>
|
||||
<TableCell>{typeof category.turnover_rate === 'number' ? category.turnover_rate.toFixed(1) : "0.0"}x</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2" style={{ minWidth: '120px' }}>
|
||||
<div style={{ width: '50px', textAlign: 'right' }}>
|
||||
{typeof category.growth_rate === 'number' ? category.growth_rate.toFixed(1) : "0.0"}%
|
||||
</div>
|
||||
{getPerformanceBadge(category.growth_rate ?? 0)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{category.status}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{!isLoading && !paginatedData.length && (
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function Forecasting() {
|
||||
avgPrice: Number(item.avg_price) || 0,
|
||||
avgTotalSold: Number(item.avgTotalSold) || 0,
|
||||
products: item.products?.map((p: any) => ({
|
||||
product_id: p.product_id,
|
||||
pid: p.pid,
|
||||
name: p.title,
|
||||
sku: p.sku,
|
||||
stock_quantity: Number(p.stock_quantity) || 0,
|
||||
|
||||
@@ -503,7 +503,7 @@ export function Products() {
|
||||
columnDefs={AVAILABLE_COLUMNS}
|
||||
columnOrder={columnOrder}
|
||||
onColumnOrderChange={handleColumnOrderChange}
|
||||
onRowClick={(product) => setSelectedProductId(product.product_id)}
|
||||
onRowClick={(product) => setSelectedProductId(product.pid)}
|
||||
/>
|
||||
|
||||
{totalPages > 1 && (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface Product {
|
||||
product_id: number;
|
||||
pid: number;
|
||||
title: string;
|
||||
SKU: string;
|
||||
stock_quantity: number;
|
||||
@@ -10,7 +10,7 @@ export interface Product {
|
||||
barcode: string;
|
||||
vendor: string;
|
||||
vendor_reference: string;
|
||||
brand: string;
|
||||
brand: string | 'Unbranded';
|
||||
categories: string[];
|
||||
tags: string[];
|
||||
options: Record<string, any>;
|
||||
|
||||
Reference in New Issue
Block a user