Update frontend to match part 2

This commit is contained in:
2025-01-28 01:43:45 -05:00
parent 64d9ab2f83
commit 57b0e9a120
8 changed files with 82 additions and 76 deletions

View File

@@ -369,14 +369,16 @@ router.get('/pricing', async (req, res) => {
// Get price points analysis // Get price points analysis
const [pricePoints] = await pool.query(` const [pricePoints] = await pool.query(`
SELECT SELECT
p.price, CAST(p.price AS DECIMAL(15,3)) as price,
SUM(o.quantity) as salesVolume, CAST(SUM(o.quantity) AS DECIMAL(15,3)) as salesVolume,
SUM(o.price * o.quantity) as revenue, CAST(SUM(o.price * o.quantity) AS DECIMAL(15,3)) as revenue,
p.categories as category c.name as category
FROM products p FROM products p
LEFT JOIN orders o ON p.pid = o.pid LEFT JOIN orders o ON p.pid = o.pid
JOIN product_categories pc ON p.pid = pc.pid
JOIN categories c ON pc.cat_id = c.cat_id
WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY p.price, p.categories GROUP BY p.price, c.name
HAVING salesVolume > 0 HAVING salesVolume > 0
ORDER BY revenue DESC ORDER BY revenue DESC
LIMIT 50 LIMIT 50
@@ -386,8 +388,8 @@ router.get('/pricing', async (req, res) => {
const [elasticity] = await pool.query(` const [elasticity] = await pool.query(`
SELECT SELECT
DATE_FORMAT(o.date, '%Y-%m-%d') as date, DATE_FORMAT(o.date, '%Y-%m-%d') as date,
AVG(o.price) as price, CAST(AVG(o.price) AS DECIMAL(15,3)) as price,
SUM(o.quantity) as demand CAST(SUM(o.quantity) AS DECIMAL(15,3)) as demand
FROM orders o FROM orders o
WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY DATE_FORMAT(o.date, '%Y-%m-%d') GROUP BY DATE_FORMAT(o.date, '%Y-%m-%d')
@@ -398,21 +400,25 @@ router.get('/pricing', async (req, res) => {
const [recommendations] = await pool.query(` const [recommendations] = await pool.query(`
SELECT SELECT
p.title as product, p.title as product,
p.price as currentPrice, CAST(p.price AS DECIMAL(15,3)) as currentPrice,
ROUND( CAST(
CASE ROUND(
WHEN AVG(o.quantity) > 10 THEN p.price * 1.1 CASE
WHEN AVG(o.quantity) < 2 THEN p.price * 0.9 WHEN AVG(o.quantity) > 10 THEN p.price * 1.1
ELSE p.price WHEN AVG(o.quantity) < 2 THEN p.price * 0.9
END, 2 ELSE p.price
END, 2
) AS DECIMAL(15,3)
) as recommendedPrice, ) as recommendedPrice,
ROUND( CAST(
SUM(o.price * o.quantity) * ROUND(
CASE SUM(o.price * o.quantity) *
WHEN AVG(o.quantity) > 10 THEN 1.15 CASE
WHEN AVG(o.quantity) < 2 THEN 0.95 WHEN AVG(o.quantity) > 10 THEN 1.15
ELSE 1 WHEN AVG(o.quantity) < 2 THEN 0.95
END, 2 ELSE 1
END, 2
) AS DECIMAL(15,3)
) as potentialRevenue, ) as potentialRevenue,
CASE CASE
WHEN AVG(o.quantity) > 10 THEN 85 WHEN AVG(o.quantity) > 10 THEN 85
@@ -422,9 +428,9 @@ router.get('/pricing', async (req, res) => {
FROM products p FROM products p
LEFT JOIN orders o ON p.pid = o.pid LEFT JOIN orders o ON p.pid = o.pid
WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY p.pid GROUP BY p.pid, p.price
HAVING ABS(recommendedPrice - currentPrice) > 0 HAVING ABS(recommendedPrice - currentPrice) > 0
ORDER BY potentialRevenue - SUM(o.price * o.quantity) DESC ORDER BY potentialRevenue - CAST(SUM(o.price * o.quantity) AS DECIMAL(15,3)) DESC
LIMIT 10 LIMIT 10
`); `);
@@ -536,14 +542,14 @@ router.get('/forecast', async (req, res) => {
const [results] = await pool.query(` const [results] = await pool.query(`
WITH category_metrics AS ( WITH category_metrics AS (
SELECT SELECT
c.cat_id as category_id, c.cat_id,
c.name as category_name, c.name as category_name,
p.brand, p.brand,
COUNT(DISTINCT p.pid) as num_products, COUNT(DISTINCT p.pid) as num_products,
COALESCE(ROUND(SUM(o.quantity) / DATEDIFF(?, ?), 2), 0) as avg_daily_sales, CAST(COALESCE(ROUND(SUM(o.quantity) / DATEDIFF(?, ?), 2), 0) AS DECIMAL(15,3)) as avg_daily_sales,
COALESCE(SUM(o.quantity), 0) as total_sold, COALESCE(SUM(o.quantity), 0) as total_sold,
COALESCE(ROUND(SUM(o.quantity) / COUNT(DISTINCT p.pid), 2), 0) as avgTotalSold, CAST(COALESCE(ROUND(SUM(o.quantity) / COUNT(DISTINCT p.pid), 2), 0) AS DECIMAL(15,3)) as avgTotalSold,
COALESCE(ROUND(AVG(o.price), 2), 0) as avg_price CAST(COALESCE(ROUND(AVG(o.price), 2), 0) AS DECIMAL(15,3)) as avg_price
FROM categories c FROM categories c
JOIN product_categories pc ON c.cat_id = pc.cat_id JOIN product_categories pc ON c.cat_id = pc.cat_id
JOIN products p ON pc.pid = p.pid JOIN products p ON pc.pid = p.pid
@@ -564,7 +570,7 @@ router.get('/forecast', async (req, res) => {
pc.cat_id, pc.cat_id,
pm.first_received_date, pm.first_received_date,
COALESCE(SUM(o.quantity), 0) as total_sold, COALESCE(SUM(o.quantity), 0) as total_sold,
COALESCE(ROUND(AVG(o.price), 2), 0) as avg_price CAST(COALESCE(ROUND(AVG(o.price), 2), 0) AS DECIMAL(15,3)) as avg_price
FROM products p FROM products p
JOIN product_categories pc ON p.pid = pc.pid JOIN product_categories pc ON p.pid = pc.pid
JOIN product_metrics pm ON p.pid = pm.pid JOIN product_metrics pm ON p.pid = pm.pid
@@ -589,8 +595,8 @@ router.get('/forecast', async (req, res) => {
) )
) as products ) as products
FROM category_metrics cm FROM category_metrics cm
JOIN product_metrics pm ON cm.category_id = pm.cat_id JOIN product_metrics pm ON cm.cat_id = pm.cat_id
GROUP BY cm.category_id, cm.category_name, cm.brand, cm.num_products, cm.avg_daily_sales, cm.total_sold, cm.avgTotalSold, cm.avg_price GROUP BY cm.cat_id, cm.category_name, cm.brand, cm.num_products, cm.avg_daily_sales, cm.total_sold, cm.avgTotalSold, cm.avg_price
ORDER BY cm.total_sold DESC ORDER BY cm.total_sold DESC
`, [startDate, endDate, startDate, endDate, brand, startDate, endDate, startDate, endDate, brand, startDate, endDate]); `, [startDate, endDate, startDate, endDate, brand, startDate, endDate, startDate, endDate, brand, startDate, endDate]);

View File

@@ -11,33 +11,33 @@ interface Product {
sku: string; sku: string;
title: string; title: string;
units_sold: number; units_sold: number;
revenue: number; revenue: string;
profit: number; profit: string;
} }
interface Category { interface Category {
cat_id: number; cat_id: number;
name: string; name: string;
total_revenue: number; total_revenue: string;
total_profit: number; total_profit: string;
total_units: number; total_units: number;
} }
interface BestSellerBrand { interface BestSellerBrand {
brand: string brand: string
units_sold: number units_sold: number
revenue: number revenue: string
profit: number profit: string
growth_rate: number growth_rate: string
} }
interface BestSellerCategory { interface BestSellerCategory {
cat_id: number; cat_id: number;
name: string; name: string;
units_sold: number; units_sold: number;
revenue: number; revenue: string;
profit: number; profit: string;
growth_rate: number; growth_rate: string;
} }
interface BestSellersData { interface BestSellersData {
@@ -98,8 +98,8 @@ export function BestSellers() {
<div className="text-sm text-muted-foreground">{product.sku}</div> <div className="text-sm text-muted-foreground">{product.sku}</div>
</TableCell> </TableCell>
<TableCell className="text-right">{product.units_sold}</TableCell> <TableCell className="text-right">{product.units_sold}</TableCell>
<TableCell className="text-right">{formatCurrency(product.revenue)}</TableCell> <TableCell className="text-right">{formatCurrency(Number(product.revenue))}</TableCell>
<TableCell className="text-right">{formatCurrency(product.profit)}</TableCell> <TableCell className="text-right">{formatCurrency(Number(product.profit))}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -129,13 +129,13 @@ export function BestSellers() {
{brand.units_sold.toLocaleString()} {brand.units_sold.toLocaleString()}
</TableCell> </TableCell>
<TableCell className="w-[15%] text-right"> <TableCell className="w-[15%] text-right">
{formatCurrency(brand.revenue)} {formatCurrency(Number(brand.revenue))}
</TableCell> </TableCell>
<TableCell className="w-[15%] text-right"> <TableCell className="w-[15%] text-right">
{formatCurrency(brand.profit)} {formatCurrency(Number(brand.profit))}
</TableCell> </TableCell>
<TableCell className="w-[15%] text-right"> <TableCell className="w-[15%] text-right">
{brand.growth_rate > 0 ? '+' : ''}{brand.growth_rate.toFixed(1)}% {Number(brand.growth_rate) > 0 ? '+' : ''}{Number(brand.growth_rate).toFixed(1)}%
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
@@ -160,8 +160,8 @@ export function BestSellers() {
<TableRow key={category.cat_id}> <TableRow key={category.cat_id}>
<TableCell>{category.name}</TableCell> <TableCell>{category.name}</TableCell>
<TableCell className="text-right">{category.total_units}</TableCell> <TableCell className="text-right">{category.total_units}</TableCell>
<TableCell className="text-right">{formatCurrency(category.total_revenue)}</TableCell> <TableCell className="text-right">{formatCurrency(Number(category.total_revenue))}</TableCell>
<TableCell className="text-right">{formatCurrency(category.total_profit)}</TableCell> <TableCell className="text-right">{formatCurrency(Number(category.total_profit))}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>

View File

@@ -11,18 +11,18 @@ import { DateRangePicker } from "@/components/ui/date-range-picker-narrow"
interface ForecastData { interface ForecastData {
forecastSales: number forecastSales: number
forecastRevenue: number forecastRevenue: string
confidenceLevel: number confidenceLevel: number
dailyForecasts: { dailyForecasts: {
date: string date: string
units: number units: number
revenue: number revenue: string
confidence: number confidence: number
}[] }[]
categoryForecasts: { categoryForecasts: {
category: string category: string
units: number units: number
revenue: number revenue: string
confidence: number confidence: number
}[] }[]
} }
@@ -86,7 +86,7 @@ export function ForecastMetrics() {
<DollarSign className="h-4 w-4 text-muted-foreground" /> <DollarSign className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">Forecast Revenue</p> <p className="text-sm font-medium text-muted-foreground">Forecast Revenue</p>
</div> </div>
<p className="text-lg font-bold">{formatCurrency(data?.forecastRevenue || 0)}</p> <p className="text-lg font-bold">{formatCurrency(Number(data?.forecastRevenue) || 0)}</p>
</div> </div>
</div> </div>
@@ -108,7 +108,7 @@ export function ForecastMetrics() {
tick={false} tick={false}
/> />
<Tooltip <Tooltip
formatter={(value: number) => [formatCurrency(value), "Revenue"]} formatter={(value: string) => [formatCurrency(Number(value)), "Revenue"]}
labelFormatter={(date) => format(new Date(date), 'MMM d, yyyy')} labelFormatter={(date) => format(new Date(date), 'MMM d, yyyy')}
/> />
<Area <Area

View File

@@ -13,11 +13,11 @@ interface InventoryMetrics {
topVendors: { topVendors: {
vendor: string; vendor: string;
productCount: number; productCount: number;
averageStockLevel: number; averageStockLevel: string;
}[]; }[];
stockTurnover: { stockTurnover: {
category: string; category: string;
rate: number; rate: string;
}[]; }[];
} }
@@ -70,7 +70,7 @@ export function InventoryStats() {
<BarChart data={data?.stockTurnover}> <BarChart data={data?.stockTurnover}>
<XAxis dataKey="category" /> <XAxis dataKey="category" />
<YAxis /> <YAxis />
<Tooltip /> <Tooltip formatter={(value: string) => [Number(value).toFixed(2), "Rate"]} />
<Bar dataKey="rate" name="Turnover Rate" fill="#60a5fa" /> <Bar dataKey="rate" name="Turnover Rate" fill="#60a5fa" />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
@@ -93,7 +93,7 @@ export function InventoryStats() {
</div> </div>
<div className="ml-4 text-right"> <div className="ml-4 text-right">
<p className="text-sm font-medium"> <p className="text-sm font-medium">
Avg. Stock: {vendor.averageStockLevel.toFixed(0)} Avg. Stock: {Number(vendor.averageStockLevel).toFixed(0)}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -17,8 +17,8 @@ interface Product {
sku: string; sku: string;
title: string; title: string;
stock_quantity: number; stock_quantity: number;
daily_sales_avg: number; daily_sales_avg: string;
days_of_inventory: number; days_of_inventory: string;
reorder_qty: number; reorder_qty: number;
last_purchase_date: string | null; last_purchase_date: string | null;
lead_time_status: string; lead_time_status: string;
@@ -70,8 +70,8 @@ export function LowStockAlerts() {
<div className="text-sm text-muted-foreground">{product.sku}</div> <div className="text-sm text-muted-foreground">{product.sku}</div>
</TableCell> </TableCell>
<TableCell className="text-right">{product.stock_quantity}</TableCell> <TableCell className="text-right">{product.stock_quantity}</TableCell>
<TableCell className="text-right">{product.daily_sales_avg.toFixed(1)}</TableCell> <TableCell className="text-right">{Number(product.daily_sales_avg).toFixed(1)}</TableCell>
<TableCell className="text-right">{product.days_of_inventory.toFixed(1)}</TableCell> <TableCell className="text-right">{Number(product.days_of_inventory).toFixed(1)}</TableCell>
<TableCell className="text-right">{product.reorder_qty}</TableCell> <TableCell className="text-right">{product.reorder_qty}</TableCell>
<TableCell>{product.last_purchase_date ? formatDate(product.last_purchase_date) : '-'}</TableCell> <TableCell>{product.last_purchase_date ? formatDate(product.last_purchase_date) : '-'}</TableCell>
<TableCell> <TableCell>

View File

@@ -12,13 +12,13 @@ import { DateRangePicker } from "@/components/ui/date-range-picker-narrow"
interface SalesData { interface SalesData {
totalOrders: number totalOrders: number
totalUnitsSold: number totalUnitsSold: number
totalCogs: number totalCogs: string
totalRevenue: number totalRevenue: string
dailySales: { dailySales: {
date: string date: string
units: number units: number
revenue: number revenue: string
cogs: number cogs: string
}[] }[]
} }
@@ -78,14 +78,14 @@ export function SalesMetrics() {
<DollarSign className="h-4 w-4 text-muted-foreground" /> <DollarSign className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">Cost of Goods</p> <p className="text-sm font-medium text-muted-foreground">Cost of Goods</p>
</div> </div>
<p className="text-lg font-bold">{formatCurrency(data?.totalCogs || 0)}</p> <p className="text-lg font-bold">{formatCurrency(Number(data?.totalCogs) || 0)}</p>
</div> </div>
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ShoppingCart className="h-4 w-4 text-muted-foreground" /> <ShoppingCart className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">Revenue</p> <p className="text-sm font-medium text-muted-foreground">Revenue</p>
</div> </div>
<p className="text-lg font-bold">{formatCurrency(data?.totalRevenue || 0)}</p> <p className="text-lg font-bold">{formatCurrency(Number(data?.totalRevenue) || 0)}</p>
</div> </div>
</div> </div>
@@ -107,7 +107,7 @@ export function SalesMetrics() {
tick={false} tick={false}
/> />
<Tooltip <Tooltip
formatter={(value: number) => [formatCurrency(value), "Revenue"]} formatter={(value: string) => [formatCurrency(Number(value)), "Revenue"]}
labelFormatter={(date) => format(new Date(date), 'MMM d, yyyy')} labelFormatter={(date) => format(new Date(date), 'MMM d, yyyy')}
/> />
<Area <Area

View File

@@ -10,14 +10,14 @@ interface StockMetricsData {
totalProducts: number totalProducts: number
productsInStock: number productsInStock: number
totalStockUnits: number totalStockUnits: number
totalStockCost: number totalStockCost: string
totalStockRetail: number totalStockRetail: string
brandStock: { brandStock: {
brand: string brand: string
variants: number variants: number
units: number units: number
cost: number cost: string
retail: number retail: string
}[] }[]
} }
@@ -91,7 +91,7 @@ const renderActiveShape = (props: any) => {
fill="#000000" fill="#000000"
className="text-base font-medium" className="text-base font-medium"
> >
{formatCurrency(retail)} {formatCurrency(Number(retail))}
</text> </text>
</g> </g>
); );
@@ -154,14 +154,14 @@ export function StockMetrics() {
<DollarSign className="h-4 w-4 text-muted-foreground" /> <DollarSign className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">Stock Cost</p> <p className="text-sm font-medium text-muted-foreground">Stock Cost</p>
</div> </div>
<p className="text-lg font-bold">{formatCurrency(data?.totalStockCost || 0)}</p> <p className="text-lg font-bold">{formatCurrency(Number(data?.totalStockCost) || 0)}</p>
</div> </div>
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ShoppingCart className="h-4 w-4 text-muted-foreground" /> <ShoppingCart className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">Stock Retail</p> <p className="text-sm font-medium text-muted-foreground">Stock Retail</p>
</div> </div>
<p className="text-lg font-bold">{formatCurrency(data?.totalStockRetail || 0)}</p> <p className="text-lg font-bold">{formatCurrency(Number(data?.totalStockRetail) || 0)}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -9,7 +9,7 @@ interface Product {
sku: string; sku: string;
title: string; title: string;
stock_quantity: number; stock_quantity: number;
daily_sales_avg: number; daily_sales_avg: string;
reorder_qty: number; reorder_qty: number;
last_purchase_date: string | null; last_purchase_date: string | null;
} }
@@ -58,7 +58,7 @@ export function TopReplenishProducts() {
<div className="text-sm text-muted-foreground">{product.sku}</div> <div className="text-sm text-muted-foreground">{product.sku}</div>
</TableCell> </TableCell>
<TableCell className="text-right">{product.stock_quantity}</TableCell> <TableCell className="text-right">{product.stock_quantity}</TableCell>
<TableCell className="text-right">{product.daily_sales_avg.toFixed(1)}</TableCell> <TableCell className="text-right">{Number(product.daily_sales_avg).toFixed(1)}</TableCell>
<TableCell className="text-right">{product.reorder_qty}</TableCell> <TableCell className="text-right">{product.reorder_qty}</TableCell>
<TableCell>{product.last_purchase_date ? product.last_purchase_date : '-'}</TableCell> <TableCell>{product.last_purchase_date ? product.last_purchase_date : '-'}</TableCell>
</TableRow> </TableRow>