diff --git a/inventory-server/scripts/calculate-metrics.js b/inventory-server/scripts/calculate-metrics.js index 27477bd..a77de13 100644 --- a/inventory-server/scripts/calculate-metrics.js +++ b/inventory-server/scripts/calculate-metrics.js @@ -3,7 +3,7 @@ const path = require('path'); require('dotenv').config({ path: path.resolve(__dirname, '..', '.env') }); const fs = require('fs'); -// Configuration flags +// Set to 1 to skip product metrics and only calculate the remaining metrics const SKIP_PRODUCT_METRICS = 0; // Helper function to format elapsed time @@ -1149,8 +1149,12 @@ async function calculateSalesForecasts(connection, startTime, totalProducts) { DATE_ADD(CURRENT_DATE, INTERVAL n DAY) as forecast_date FROM ( SELECT 0 as n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION - SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 14 UNION SELECT 30 UNION - SELECT 60 UNION SELECT 90 + SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION + SELECT 10 UNION SELECT 11 UNION SELECT 12 UNION SELECT 13 UNION SELECT 14 UNION + SELECT 15 UNION SELECT 16 UNION SELECT 17 UNION SELECT 18 UNION SELECT 19 UNION + SELECT 20 UNION SELECT 21 UNION SELECT 22 UNION SELECT 23 UNION SELECT 24 UNION + SELECT 25 UNION SELECT 26 UNION SELECT 27 UNION SELECT 28 UNION SELECT 29 UNION + SELECT 30 ) numbers ), product_stats AS ( @@ -1160,7 +1164,15 @@ async function calculateSalesForecasts(connection, startTime, totalProducts) { STDDEV_SAMP(ds.daily_quantity) as std_daily_quantity, AVG(ds.daily_revenue) as avg_daily_revenue, STDDEV_SAMP(ds.daily_revenue) as std_daily_revenue, - COUNT(*) as data_points + COUNT(*) as data_points, + -- Calculate day-of-week averages + AVG(CASE WHEN DAYOFWEEK(ds.sale_date) = 1 THEN ds.daily_revenue END) as sunday_avg, + AVG(CASE WHEN DAYOFWEEK(ds.sale_date) = 2 THEN ds.daily_revenue END) as monday_avg, + AVG(CASE WHEN DAYOFWEEK(ds.sale_date) = 3 THEN ds.daily_revenue END) as tuesday_avg, + AVG(CASE WHEN DAYOFWEEK(ds.sale_date) = 4 THEN ds.daily_revenue END) as wednesday_avg, + AVG(CASE WHEN DAYOFWEEK(ds.sale_date) = 5 THEN ds.daily_revenue END) as thursday_avg, + AVG(CASE WHEN DAYOFWEEK(ds.sale_date) = 6 THEN ds.daily_revenue END) as friday_avg, + AVG(CASE WHEN DAYOFWEEK(ds.sale_date) = 7 THEN ds.daily_revenue END) as saturday_avg FROM daily_sales ds GROUP BY ds.product_id ) @@ -1178,14 +1190,24 @@ async function calculateSalesForecasts(connection, startTime, totalProducts) { )) ) as forecast_units, GREATEST(0, - ps.avg_daily_revenue * + CASE DAYOFWEEK(fd.forecast_date) + WHEN 1 THEN COALESCE(ps.sunday_avg, ps.avg_daily_revenue) + WHEN 2 THEN COALESCE(ps.monday_avg, ps.avg_daily_revenue) + WHEN 3 THEN COALESCE(ps.tuesday_avg, ps.avg_daily_revenue) + WHEN 4 THEN COALESCE(ps.wednesday_avg, ps.avg_daily_revenue) + WHEN 5 THEN COALESCE(ps.thursday_avg, ps.avg_daily_revenue) + WHEN 6 THEN COALESCE(ps.friday_avg, ps.avg_daily_revenue) + WHEN 7 THEN COALESCE(ps.saturday_avg, ps.avg_daily_revenue) + END * (1 + COALESCE( (SELECT seasonality_factor FROM sales_seasonality WHERE MONTH(fd.forecast_date) = month LIMIT 1), 0 - )) + )) * + -- Add some randomness within a small range (±5%) + (0.95 + (RAND() * 0.1)) ) as forecast_revenue, CASE WHEN ps.data_points >= 60 THEN 90 @@ -1231,8 +1253,12 @@ async function calculateSalesForecasts(connection, startTime, totalProducts) { DATE_ADD(CURRENT_DATE, INTERVAL n DAY) as forecast_date FROM ( SELECT 0 as n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION - SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 14 UNION SELECT 30 UNION - SELECT 60 UNION SELECT 90 + SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION + SELECT 10 UNION SELECT 11 UNION SELECT 12 UNION SELECT 13 UNION SELECT 14 UNION + SELECT 15 UNION SELECT 16 UNION SELECT 17 UNION SELECT 18 UNION SELECT 19 UNION + SELECT 20 UNION SELECT 21 UNION SELECT 22 UNION SELECT 23 UNION SELECT 24 UNION + SELECT 25 UNION SELECT 26 UNION SELECT 27 UNION SELECT 28 UNION SELECT 29 UNION + SELECT 30 ) numbers ), category_stats AS ( @@ -1242,7 +1268,15 @@ async function calculateSalesForecasts(connection, startTime, totalProducts) { STDDEV_SAMP(cds.daily_quantity) as std_daily_quantity, AVG(cds.daily_revenue) as avg_daily_revenue, STDDEV_SAMP(cds.daily_revenue) as std_daily_revenue, - COUNT(*) as data_points + COUNT(*) as data_points, + -- Calculate day-of-week averages + AVG(CASE WHEN DAYOFWEEK(cds.sale_date) = 1 THEN cds.daily_revenue END) as sunday_avg, + AVG(CASE WHEN DAYOFWEEK(cds.sale_date) = 2 THEN cds.daily_revenue END) as monday_avg, + AVG(CASE WHEN DAYOFWEEK(cds.sale_date) = 3 THEN cds.daily_revenue END) as tuesday_avg, + AVG(CASE WHEN DAYOFWEEK(cds.sale_date) = 4 THEN cds.daily_revenue END) as wednesday_avg, + AVG(CASE WHEN DAYOFWEEK(cds.sale_date) = 5 THEN cds.daily_revenue END) as thursday_avg, + AVG(CASE WHEN DAYOFWEEK(cds.sale_date) = 6 THEN cds.daily_revenue END) as friday_avg, + AVG(CASE WHEN DAYOFWEEK(cds.sale_date) = 7 THEN cds.daily_revenue END) as saturday_avg FROM category_daily_sales cds GROUP BY cds.category_id ) @@ -1260,14 +1294,24 @@ async function calculateSalesForecasts(connection, startTime, totalProducts) { )) ) as forecast_units, GREATEST(0, - cs.avg_daily_revenue * + CASE DAYOFWEEK(fd.forecast_date) + WHEN 1 THEN COALESCE(cs.sunday_avg, cs.avg_daily_revenue) + WHEN 2 THEN COALESCE(cs.monday_avg, cs.avg_daily_revenue) + WHEN 3 THEN COALESCE(cs.tuesday_avg, cs.avg_daily_revenue) + WHEN 4 THEN COALESCE(cs.wednesday_avg, cs.avg_daily_revenue) + WHEN 5 THEN COALESCE(cs.thursday_avg, cs.avg_daily_revenue) + WHEN 6 THEN COALESCE(cs.friday_avg, cs.avg_daily_revenue) + WHEN 7 THEN COALESCE(cs.saturday_avg, cs.avg_daily_revenue) + END * (1 + COALESCE( (SELECT seasonality_factor FROM sales_seasonality WHERE MONTH(fd.forecast_date) = month LIMIT 1), 0 - )) + )) * + -- Add some randomness within a small range (±5%) + (0.95 + (RAND() * 0.1)) ) as forecast_revenue, CASE WHEN cs.data_points >= 60 THEN 90 diff --git a/inventory-server/src/routes/dashboard.js b/inventory-server/src/routes/dashboard.js index 237d345..19a1194 100644 --- a/inventory-server/src/routes/dashboard.js +++ b/inventory-server/src/routes/dashboard.js @@ -270,14 +270,13 @@ router.get('/forecast/metrics', async (req, res) => { // Get daily forecasts const [dailyForecasts] = await executeQuery(` SELECT - forecast_date as date, - COALESCE(SUM(forecast_units), 0) as units, + DATE(forecast_date) as date, COALESCE(SUM(forecast_revenue), 0) as revenue, COALESCE(AVG(confidence_level), 0) as confidence FROM sales_forecasts WHERE forecast_date BETWEEN ? AND ? - GROUP BY forecast_date - ORDER BY forecast_date + GROUP BY DATE(forecast_date) + ORDER BY date `, [startDate, endDate]); // Get category forecasts @@ -296,12 +295,11 @@ router.get('/forecast/metrics', async (req, res) => { // Format response const response = { - forecastSales: parseInt(metrics.total_forecast_units) || 0, - forecastRevenue: parseFloat(metrics.total_forecast_revenue) || 0, - confidenceLevel: parseFloat(metrics.overall_confidence) || 0, + forecastSales: parseInt(metrics[0].total_forecast_units) || 0, + forecastRevenue: parseFloat(metrics[0].total_forecast_revenue) || 0, + confidenceLevel: parseFloat(metrics[0].overall_confidence) || 0, dailyForecasts: dailyForecasts.map(d => ({ date: d.date, - units: parseInt(d.units) || 0, revenue: parseFloat(d.revenue) || 0, confidence: parseFloat(d.confidence) || 0 })), diff --git a/inventory/src/components/dashboard/ForecastMetrics.tsx b/inventory/src/components/dashboard/ForecastMetrics.tsx index baeda59..87fe747 100644 --- a/inventory/src/components/dashboard/ForecastMetrics.tsx +++ b/inventory/src/components/dashboard/ForecastMetrics.tsx @@ -6,16 +6,24 @@ import config from "@/config" import { formatCurrency } from "@/lib/utils" import { TrendingUp, DollarSign } from "lucide-react" import { DateRange } from "react-day-picker" -import { addDays } from "date-fns" +import { addDays, format } from "date-fns" import { DateRangePicker } from "@/components/ui/date-range-picker-narrow" interface ForecastData { forecastSales: number forecastRevenue: number - dailyForecast: { + confidenceLevel: number + dailyForecasts: { date: string - sales: number + units: number revenue: number + confidence: number + }[] + categoryForecasts: { + category: string + units: number + revenue: number + confidence: number }[] } @@ -25,24 +33,28 @@ export function ForecastMetrics() { to: addDays(new Date(), 30), }); - const { data } = useQuery({ + const { data, error, isLoading } = useQuery({ queryKey: ["forecast-metrics", dateRange], queryFn: async () => { const params = new URLSearchParams({ startDate: dateRange.from?.toISOString() || "", endDate: dateRange.to?.toISOString() || "", }); + console.log('Fetching forecast metrics with params:', params.toString()); const response = await fetch(`${config.apiUrl}/dashboard/forecast/metrics?${params}`) if (!response.ok) { - throw new Error("Failed to fetch forecast metrics") + const text = await response.text(); + throw new Error(`Failed to fetch forecast metrics: ${text}`); } - return response.json() + const data = await response.json(); + console.log('Forecast metrics response:', data); + return data; }, }) return ( <> - + Forecast
- -
-
-
- -

Forecast Sales

+ + {error ? ( +
Error: {error.message}
+ ) : isLoading ? ( +
Loading forecast metrics...
+ ) : ( + <> +
+
+
+ +

Forecast Sales

+
+

{data?.forecastSales.toLocaleString() || 0}

+
+
+
+ +

Forecast Revenue

+
+

{formatCurrency(data?.forecastRevenue || 0)}

+
-

{data?.forecastSales.toLocaleString() || 0}

-
-
-
- -

Forecast Revenue

-
-

{formatCurrency(data?.forecastRevenue || 0)}

-
-
-
- - - - value.toLocaleString()} - /> - formatCurrency(value)} - /> - [ - name === "revenue" ? formatCurrency(value) : value.toLocaleString(), - name === "revenue" ? "Revenue" : "Sales" - ]} - labelFormatter={(label) => `Date: ${label}`} - /> - - - - -
+
+ + + + + [formatCurrency(value), "Revenue"]} + labelFormatter={(date) => format(new Date(date), 'MMM d, yyyy')} + /> + + + +
+ + )} ) diff --git a/inventory/src/components/dashboard/TopReplenishProducts.tsx b/inventory/src/components/dashboard/TopReplenishProducts.tsx index 9597596..f420b24 100644 --- a/inventory/src/components/dashboard/TopReplenishProducts.tsx +++ b/inventory/src/components/dashboard/TopReplenishProducts.tsx @@ -33,7 +33,7 @@ export function TopReplenishProducts() { Top Products To Replenish - +