Fix and restyle forecast component
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
})),
|
||||
|
||||
@@ -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<ForecastData>({
|
||||
const { data, error, isLoading } = useQuery<ForecastData>({
|
||||
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 (
|
||||
<>
|
||||
<CardHeader className="flex flex-row items-center justify-between pr-4">
|
||||
<CardHeader className="flex flex-row items-center justify-between pr-5">
|
||||
<CardTitle className="text-xl font-medium">Forecast</CardTitle>
|
||||
<div className="w-[230px]">
|
||||
<DateRangePicker
|
||||
@@ -54,76 +66,64 @@ export function ForecastMetrics() {
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium text-muted-foreground">Forecast Sales</p>
|
||||
<CardContent className="py-0 -mb-2">
|
||||
{error ? (
|
||||
<div className="text-sm text-red-500">Error: {error.message}</div>
|
||||
) : isLoading ? (
|
||||
<div className="text-sm">Loading forecast metrics...</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium text-muted-foreground">Forecast Sales</p>
|
||||
</div>
|
||||
<p className="text-lg font-bold">{data?.forecastSales.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium text-muted-foreground">Forecast Revenue</p>
|
||||
</div>
|
||||
<p className="text-lg font-bold">{formatCurrency(data?.forecastRevenue || 0)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">{data?.forecastSales.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium text-muted-foreground">Forecast Revenue</p>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">{formatCurrency(data?.forecastRevenue || 0)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[300px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={data?.dailyForecast || []}>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={12}
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="left"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={12}
|
||||
tickFormatter={(value) => value.toLocaleString()}
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={12}
|
||||
tickFormatter={(value) => formatCurrency(value)}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value: number, name: string) => [
|
||||
name === "revenue" ? formatCurrency(value) : value.toLocaleString(),
|
||||
name === "revenue" ? "Revenue" : "Sales"
|
||||
]}
|
||||
labelFormatter={(label) => `Date: ${label}`}
|
||||
/>
|
||||
<Area
|
||||
yAxisId="left"
|
||||
type="monotone"
|
||||
dataKey="sales"
|
||||
name="Sales"
|
||||
stroke="#0088FE"
|
||||
fill="#0088FE"
|
||||
fillOpacity={0.2}
|
||||
/>
|
||||
<Area
|
||||
yAxisId="right"
|
||||
type="monotone"
|
||||
dataKey="revenue"
|
||||
name="Revenue"
|
||||
stroke="#00C49F"
|
||||
fill="#00C49F"
|
||||
fillOpacity={0.2}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="h-[250px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart
|
||||
data={data?.dailyForecasts || []}
|
||||
margin={{ top: 30, right: 0, left: -60, bottom: 0 }}
|
||||
>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tick={false}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tick={false}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value: number) => [formatCurrency(value), "Revenue"]}
|
||||
labelFormatter={(date) => format(new Date(date), 'MMM d, yyyy')}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="revenue"
|
||||
name="Revenue"
|
||||
stroke="#00C49F"
|
||||
fill="#00C49F"
|
||||
fillOpacity={0.2}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@ export function TopReplenishProducts() {
|
||||
<CardTitle className="text-xl font-medium">Top Products To Replenish</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="max-h-[650px] w-full overflow-y-auto">
|
||||
<ScrollArea className="max-h-[530px] w-full overflow-y-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
|
||||
Reference in New Issue
Block a user