Overview tweaks
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user