Overview tweaks

This commit is contained in:
2026-02-13 23:18:45 -05:00
parent 45ded53530
commit bae8c575bc
4 changed files with 43 additions and 39 deletions

View File

@@ -21,8 +21,8 @@ router.get('/stock/metrics', async (req, res) => {
COALESCE(COUNT(*), 0)::integer as total_products, COALESCE(COUNT(*), 0)::integer as total_products,
COALESCE(COUNT(CASE WHEN current_stock > 0 THEN 1 END), 0)::integer as products_in_stock, COALESCE(COUNT(CASE WHEN current_stock > 0 THEN 1 END), 0)::integer as products_in_stock,
COALESCE(SUM(CASE WHEN current_stock > 0 THEN current_stock END), 0)::integer as total_units, COALESCE(SUM(CASE WHEN current_stock > 0 THEN current_stock END), 0)::integer as total_units,
ROUND(COALESCE(SUM(CASE WHEN current_stock > 0 THEN current_stock_cost END), 0)::numeric, 3) as total_cost, ROUND(COALESCE(SUM(CASE WHEN current_stock > 0 THEN current_stock_cost END), 0)::numeric, 2) as total_cost,
ROUND(COALESCE(SUM(CASE WHEN current_stock > 0 THEN current_stock_retail END), 0)::numeric, 3) as total_retail ROUND(COALESCE(SUM(CASE WHEN current_stock > 0 THEN current_stock_retail END), 0)::numeric, 2) as total_retail
FROM product_metrics FROM product_metrics
WHERE is_visible = true WHERE is_visible = true
`); `);
@@ -34,21 +34,21 @@ router.get('/stock/metrics', async (req, res) => {
COALESCE(brand, 'Unbranded') as brand, COALESCE(brand, 'Unbranded') as brand,
COUNT(DISTINCT pid)::integer as variant_count, COUNT(DISTINCT pid)::integer as variant_count,
COALESCE(SUM(current_stock), 0)::integer as stock_units, COALESCE(SUM(current_stock), 0)::integer as stock_units,
ROUND(COALESCE(SUM(current_stock_cost), 0)::numeric, 3) as stock_cost, ROUND(COALESCE(SUM(current_stock_cost), 0)::numeric, 2) as stock_cost,
ROUND(COALESCE(SUM(current_stock_retail), 0)::numeric, 3) as stock_retail ROUND(COALESCE(SUM(current_stock_retail), 0)::numeric, 2) as stock_retail
FROM product_metrics FROM product_metrics
WHERE current_stock > 0 WHERE current_stock > 0
AND is_visible = true AND is_visible = true
GROUP BY COALESCE(brand, 'Unbranded') GROUP BY COALESCE(brand, 'Unbranded')
HAVING ROUND(COALESCE(SUM(current_stock_cost), 0)::numeric, 3) > 0 HAVING ROUND(COALESCE(SUM(current_stock_cost), 0)::numeric, 2) > 0
), ),
other_brands AS ( other_brands AS (
SELECT SELECT
'Other' as brand, 'Other' as brand,
SUM(variant_count)::integer as variant_count, SUM(variant_count)::integer as variant_count,
SUM(stock_units)::integer as stock_units, SUM(stock_units)::integer as stock_units,
ROUND(SUM(stock_cost)::numeric, 3) as stock_cost, ROUND(SUM(stock_cost)::numeric, 2) as stock_cost,
ROUND(SUM(stock_retail)::numeric, 3) as stock_retail ROUND(SUM(stock_retail)::numeric, 2) as stock_retail
FROM brand_totals FROM brand_totals
WHERE stock_cost <= 5000 WHERE stock_cost <= 5000
), ),
@@ -154,7 +154,10 @@ router.get('/purchase/metrics', async (req, res) => {
vendor, vendor,
SUM(on_order_qty)::integer AS units, SUM(on_order_qty)::integer AS units,
ROUND(SUM(on_order_cost)::numeric, 2) AS cost, ROUND(SUM(on_order_cost)::numeric, 2) AS cost,
ROUND(SUM(on_order_retail)::numeric, 2) AS retail ROUND(SUM(on_order_retail)::numeric, 2) AS retail,
SUM(SUM(on_order_qty)::integer) OVER () AS total_units,
ROUND(SUM(SUM(on_order_cost)) OVER ()::numeric, 2) AS total_cost,
ROUND(SUM(SUM(on_order_retail)) OVER ()::numeric, 2) AS total_retail
FROM product_metrics FROM product_metrics
WHERE is_visible = true AND on_order_qty > 0 WHERE is_visible = true AND on_order_qty > 0
GROUP BY vendor GROUP BY vendor
@@ -169,9 +172,10 @@ router.get('/purchase/metrics', async (req, res) => {
retail: parseFloat(v.retail) || 0 retail: parseFloat(v.retail) || 0
})); }));
const onOrderUnits = vendorOrders.reduce((sum, v) => sum + v.units, 0); const firstRow = vendorRows[0];
const onOrderCost = vendorOrders.reduce((sum, v) => sum + v.cost, 0); const onOrderUnits = firstRow ? parseInt(firstRow.total_units) || 0 : 0;
const onOrderRetail = vendorOrders.reduce((sum, v) => sum + v.retail, 0); const onOrderCost = firstRow ? parseFloat(firstRow.total_cost) || 0 : 0;
const onOrderRetail = firstRow ? parseFloat(firstRow.total_retail) || 0 : 0;
// Format response to match PurchaseMetricsData interface // Format response to match PurchaseMetricsData interface
const response = { const response = {
@@ -199,8 +203,8 @@ router.get('/replenishment/metrics', async (req, res) => {
SELECT SELECT
COUNT(DISTINCT pm.pid)::integer as products_to_replenish, COUNT(DISTINCT pm.pid)::integer as products_to_replenish,
COALESCE(SUM(pm.replenishment_units), 0)::integer as total_units_needed, COALESCE(SUM(pm.replenishment_units), 0)::integer as total_units_needed,
ROUND(COALESCE(SUM(pm.replenishment_cost), 0)::numeric, 3) as total_cost, ROUND(COALESCE(SUM(pm.replenishment_cost), 0)::numeric, 2) as total_cost,
ROUND(COALESCE(SUM(pm.replenishment_retail), 0)::numeric, 3) as total_retail ROUND(COALESCE(SUM(pm.replenishment_retail), 0)::numeric, 2) as total_retail
FROM product_metrics pm FROM product_metrics pm
WHERE pm.is_visible = true WHERE pm.is_visible = true
AND pm.is_replenishable = true AND pm.is_replenishable = true
@@ -216,8 +220,8 @@ router.get('/replenishment/metrics', async (req, res) => {
pm.title, pm.title,
pm.current_stock::integer as current_stock, pm.current_stock::integer as current_stock,
pm.replenishment_units::integer as replenish_qty, pm.replenishment_units::integer as replenish_qty,
ROUND(pm.replenishment_cost::numeric, 3) as replenish_cost, ROUND(pm.replenishment_cost::numeric, 2) as replenish_cost,
ROUND(pm.replenishment_retail::numeric, 3) as replenish_retail, ROUND(pm.replenishment_retail::numeric, 2) as replenish_retail,
pm.status, pm.status,
pm.planning_period_days::text as planning_period pm.planning_period_days::text as planning_period
FROM product_metrics pm FROM product_metrics pm
@@ -552,7 +556,7 @@ router.get('/forecast/metrics', async (req, res) => {
return res.json({ return res.json({
forecastSales: Math.round(totalUnits), forecastSales: Math.round(totalUnits),
forecastRevenue: totalRevenue.toFixed(2), forecastRevenue: parseFloat(totalRevenue.toFixed(2)),
confidenceLevel, confidenceLevel,
dailyForecasts, dailyForecasts,
dailyForecastsByPhase, dailyForecastsByPhase,
@@ -611,7 +615,7 @@ router.get('/forecast/metrics', async (req, res) => {
res.json({ res.json({
forecastSales: Math.round(dailyUnits * days), forecastSales: Math.round(dailyUnits * days),
forecastRevenue: (dailyRevenue * days).toFixed(2), forecastRevenue: parseFloat((dailyRevenue * days).toFixed(2)),
confidenceLevel: 0, confidenceLevel: 0,
dailyForecasts, dailyForecasts,
categoryForecasts: categoryRows.map(c => ({ categoryForecasts: categoryRows.map(c => ({
@@ -794,10 +798,10 @@ router.get('/overstock/metrics', async (req, res) => {
if (parseInt(countCheck.overstock_count) === 0) { if (parseInt(countCheck.overstock_count) === 0) {
return res.json({ return res.json({
overstockedProducts: 0, overstockedProducts: 0,
total_excess_units: 0, totalExcessUnits: 0,
total_excess_cost: 0, totalExcessCost: 0,
total_excess_retail: 0, totalExcessRetail: 0,
category_data: [] categoryData: []
}); });
} }
@@ -806,8 +810,8 @@ router.get('/overstock/metrics', async (req, res) => {
SELECT SELECT
COUNT(DISTINCT pid)::integer as total_overstocked, COUNT(DISTINCT pid)::integer as total_overstocked,
SUM(overstocked_units)::integer as total_excess_units, SUM(overstocked_units)::integer as total_excess_units,
ROUND(SUM(overstocked_cost)::numeric, 3) as total_excess_cost, ROUND(SUM(overstocked_cost)::numeric, 2) as total_excess_cost,
ROUND(SUM(overstocked_retail)::numeric, 3) as total_excess_retail ROUND(SUM(overstocked_retail)::numeric, 2) as total_excess_retail
FROM product_metrics FROM product_metrics
WHERE status = 'Overstock' WHERE status = 'Overstock'
AND is_visible = true AND is_visible = true
@@ -819,8 +823,8 @@ router.get('/overstock/metrics', async (req, res) => {
c.name as category_name, c.name as category_name,
COUNT(DISTINCT pm.pid)::integer as overstocked_products, COUNT(DISTINCT pm.pid)::integer as overstocked_products,
SUM(pm.overstocked_units)::integer as total_excess_units, SUM(pm.overstocked_units)::integer as total_excess_units,
ROUND(SUM(pm.overstocked_cost)::numeric, 3) as total_excess_cost, ROUND(SUM(pm.overstocked_cost)::numeric, 2) as total_excess_cost,
ROUND(SUM(pm.overstocked_retail)::numeric, 3) as total_excess_retail ROUND(SUM(pm.overstocked_retail)::numeric, 2) as total_excess_retail
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 product_metrics pm ON pc.pid = pm.pid JOIN product_metrics pm ON pc.pid = pm.pid
@@ -850,10 +854,10 @@ router.get('/overstock/metrics', async (req, res) => {
// Format response with explicit type conversion // Format response with explicit type conversion
const response = { const response = {
overstockedProducts: parseInt(summaryMetrics.total_overstocked) || 0, overstockedProducts: parseInt(summaryMetrics.total_overstocked) || 0,
total_excess_units: parseInt(summaryMetrics.total_excess_units) || 0, totalExcessUnits: parseInt(summaryMetrics.total_excess_units) || 0,
total_excess_cost: parseFloat(summaryMetrics.total_excess_cost) || 0, totalExcessCost: parseFloat(summaryMetrics.total_excess_cost) || 0,
total_excess_retail: parseFloat(summaryMetrics.total_excess_retail) || 0, totalExcessRetail: parseFloat(summaryMetrics.total_excess_retail) || 0,
category_data: categoryData.map(cat => ({ categoryData: categoryData.map(cat => ({
category: cat.category_name, category: cat.category_name,
products: parseInt(cat.overstocked_products) || 0, products: parseInt(cat.overstocked_products) || 0,
units: parseInt(cat.total_excess_units) || 0, units: parseInt(cat.total_excess_units) || 0,

View File

@@ -44,7 +44,7 @@ interface DailyPhaseData {
interface ForecastData { interface ForecastData {
forecastSales: number forecastSales: number
forecastRevenue: string forecastRevenue: number
confidenceLevel: number confidenceLevel: number
dailyForecasts: { dailyForecasts: {
date: string date: string
@@ -129,7 +129,7 @@ export function ForecastMetrics() {
<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>
{isLoading || !data ? <MetricSkeleton /> : ( {isLoading || !data ? <MetricSkeleton /> : (
<p className="text-lg font-bold">{formatCurrency(Number(data.forecastRevenue) || 0)}</p> <p className="text-lg font-bold">{formatCurrency(data.forecastRevenue)}</p>
)} )}
</div> </div>
</div> </div>

View File

@@ -17,10 +17,10 @@ interface PhaseBreakdown {
interface OverstockMetricsData { interface OverstockMetricsData {
overstockedProducts: number overstockedProducts: number
total_excess_units: number totalExcessUnits: number
total_excess_cost: number totalExcessCost: number
total_excess_retail: number totalExcessRetail: number
category_data: { categoryData: {
category: string category: string
products: number products: number
units: number units: number
@@ -69,7 +69,7 @@ export function OverstockMetrics() {
<p className="text-sm font-medium text-muted-foreground">Overstocked Units</p> <p className="text-sm font-medium text-muted-foreground">Overstocked Units</p>
</div> </div>
{isLoading || !data ? <MetricSkeleton /> : ( {isLoading || !data ? <MetricSkeleton /> : (
<p className="text-lg font-bold">{data.total_excess_units.toLocaleString()}</p> <p className="text-lg font-bold">{data.totalExcessUnits.toLocaleString()}</p>
)} )}
</div> </div>
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
@@ -78,7 +78,7 @@ export function OverstockMetrics() {
<p className="text-sm font-medium text-muted-foreground">Overstocked Cost</p> <p className="text-sm font-medium text-muted-foreground">Overstocked Cost</p>
</div> </div>
{isLoading || !data ? <MetricSkeleton /> : ( {isLoading || !data ? <MetricSkeleton /> : (
<p className="text-lg font-bold">{formatCurrency(data.total_excess_cost)}</p> <p className="text-lg font-bold">{formatCurrency(data.totalExcessCost)}</p>
)} )}
</div> </div>
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
@@ -87,7 +87,7 @@ export function OverstockMetrics() {
<p className="text-sm font-medium text-muted-foreground">Overstocked Retail</p> <p className="text-sm font-medium text-muted-foreground">Overstocked Retail</p>
</div> </div>
{isLoading || !data ? <MetricSkeleton /> : ( {isLoading || !data ? <MetricSkeleton /> : (
<p className="text-lg font-bold">{formatCurrency(data.total_excess_retail)}</p> <p className="text-lg font-bold">{formatCurrency(data.totalExcessRetail)}</p>
)} )}
</div> </div>
{data?.phaseBreakdown && data.phaseBreakdown.length > 0 && ( {data?.phaseBreakdown && data.phaseBreakdown.length > 0 && (

View File

@@ -175,7 +175,7 @@ function SortableImageCell({
src={src} src={src}
alt={`Image ${image.iid}`} alt={`Image ${image.iid}`}
className={cn( className={cn(
"w-full h-full object-cover pointer-events-none select-none", "w-full h-full object-contain pointer-events-none select-none",
isMain ? "rounded-lg" : "rounded-md" isMain ? "rounded-lg" : "rounded-md"
)} )}
draggable={false} draggable={false}