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

View File

@@ -44,7 +44,7 @@ interface DailyPhaseData {
interface ForecastData {
forecastSales: number
forecastRevenue: string
forecastRevenue: number
confidenceLevel: number
dailyForecasts: {
date: string
@@ -129,7 +129,7 @@ export function ForecastMetrics() {
<p className="text-sm font-medium text-muted-foreground">Forecast Revenue</p>
</div>
{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>

View File

@@ -17,10 +17,10 @@ interface PhaseBreakdown {
interface OverstockMetricsData {
overstockedProducts: number
total_excess_units: number
total_excess_cost: number
total_excess_retail: number
category_data: {
totalExcessUnits: number
totalExcessCost: number
totalExcessRetail: number
categoryData: {
category: string
products: number
units: number
@@ -69,7 +69,7 @@ export function OverstockMetrics() {
<p className="text-sm font-medium text-muted-foreground">Overstocked Units</p>
</div>
{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 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>
</div>
{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 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>
</div>
{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>
{data?.phaseBreakdown && data.phaseBreakdown.length > 0 && (

View File

@@ -175,7 +175,7 @@ function SortableImageCell({
src={src}
alt={`Image ${image.iid}`}
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"
)}
draggable={false}