Update frontend to match part 1

This commit is contained in:
2025-01-28 01:30:48 -05:00
parent 8323ae7703
commit 64d9ab2f83
25 changed files with 936 additions and 686 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>
))}

View File

@@ -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,

View File

@@ -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

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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 && (

View File

@@ -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,

View File

@@ -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 && (

View File

@@ -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>;