Switch to daterangepicker for forecast and sales

This commit is contained in:
2025-01-17 20:35:53 -05:00
parent 0727529edf
commit de5bd785c1
4 changed files with 196 additions and 65 deletions

View File

@@ -209,7 +209,7 @@ router.get('/replenishment/metrics', async (req, res) => {
// GET /dashboard/forecast/metrics // GET /dashboard/forecast/metrics
// Returns sales forecasts for specified period // Returns sales forecasts for specified period
router.get('/forecast/metrics', async (req, res) => { router.get('/forecast/metrics', async (req, res) => {
const days = Math.max(1, Math.min(365, parseInt(req.query.days) || 30)); const { startDate, endDate } = req.query;
try { try {
// Get summary metrics // Get summary metrics
const [metrics] = await executeQuery(` const [metrics] = await executeQuery(`
@@ -218,8 +218,8 @@ router.get('/forecast/metrics', async (req, res) => {
COALESCE(SUM(forecast_revenue), 0) as total_forecast_revenue, COALESCE(SUM(forecast_revenue), 0) as total_forecast_revenue,
COALESCE(AVG(confidence_level), 0) as overall_confidence COALESCE(AVG(confidence_level), 0) as overall_confidence
FROM sales_forecasts FROM sales_forecasts
WHERE forecast_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY) WHERE forecast_date BETWEEN ? AND ?
`, [days]); `, [startDate, endDate]);
// Get daily forecasts // Get daily forecasts
const [dailyForecasts] = await executeQuery(` const [dailyForecasts] = await executeQuery(`
@@ -229,10 +229,10 @@ router.get('/forecast/metrics', async (req, res) => {
COALESCE(SUM(forecast_revenue), 0) as revenue, COALESCE(SUM(forecast_revenue), 0) as revenue,
COALESCE(AVG(confidence_level), 0) as confidence COALESCE(AVG(confidence_level), 0) as confidence
FROM sales_forecasts FROM sales_forecasts
WHERE forecast_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY) WHERE forecast_date BETWEEN ? AND ?
GROUP BY forecast_date GROUP BY forecast_date
ORDER BY forecast_date ORDER BY forecast_date
`, [days]); `, [startDate, endDate]);
// Get category forecasts // Get category forecasts
const [categoryForecasts] = await executeQuery(` const [categoryForecasts] = await executeQuery(`
@@ -243,10 +243,10 @@ router.get('/forecast/metrics', async (req, res) => {
COALESCE(AVG(cf.confidence_level), 0) as confidence COALESCE(AVG(cf.confidence_level), 0) as confidence
FROM category_forecasts cf FROM category_forecasts cf
JOIN categories c ON cf.category_id = c.id JOIN categories c ON cf.category_id = c.id
WHERE cf.forecast_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY) WHERE cf.forecast_date BETWEEN ? AND ?
GROUP BY c.id, c.name GROUP BY c.id, c.name
ORDER BY revenue DESC ORDER BY revenue DESC
`, [days]); `, [startDate, endDate]);
// Format response // Format response
const response = { const response = {
@@ -455,7 +455,7 @@ router.get('/best-sellers', async (req, res) => {
// GET /dashboard/sales/metrics // GET /dashboard/sales/metrics
// Returns sales metrics for specified period // Returns sales metrics for specified period
router.get('/sales/metrics', async (req, res) => { router.get('/sales/metrics', async (req, res) => {
const days = Math.max(1, Math.min(365, parseInt(req.query.days) || 30)); const { startDate, endDate } = req.query;
try { try {
const [dailyData] = await executeQuery(` const [dailyData] = await executeQuery(`
SELECT JSON_ARRAYAGG( SELECT JSON_ARRAYAGG(
@@ -479,10 +479,10 @@ router.get('/sales/metrics', async (req, res) => {
FROM orders o FROM orders o
JOIN products p ON o.product_id = p.product_id JOIN products p ON o.product_id = p.product_id
WHERE o.canceled = false WHERE o.canceled = false
AND o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY) AND o.date BETWEEN ? AND ?
GROUP BY DATE(o.date) GROUP BY DATE(o.date)
) d ) d
`, [days]); `, [startDate, endDate]);
const [categoryData] = await executeQuery(` const [categoryData] = await executeQuery(`
SELECT JSON_ARRAYAGG( SELECT JSON_ARRAYAGG(
@@ -504,10 +504,10 @@ router.get('/sales/metrics', async (req, res) => {
JOIN product_categories pc ON p.product_id = pc.product_id JOIN product_categories pc ON p.product_id = pc.product_id
JOIN categories c ON pc.category_id = c.id JOIN categories c ON pc.category_id = c.id
WHERE o.canceled = false WHERE o.canceled = false
AND o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY) AND o.date BETWEEN ? AND ?
GROUP BY c.id, c.name GROUP BY c.id, c.name
) c ) c
`, [days]); `, [startDate, endDate]);
const [metrics] = await executeQuery(` const [metrics] = await executeQuery(`
SELECT SELECT
@@ -533,8 +533,8 @@ router.get('/sales/metrics', async (req, res) => {
GROUP BY DATE(date) GROUP BY DATE(date)
) daily ON DATE(o.date) = daily.sale_date ) daily ON DATE(o.date) = daily.sale_date
WHERE o.canceled = false WHERE o.canceled = false
AND o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY) AND o.date BETWEEN ? AND ?
`, [days]); `, [startDate, endDate]);
const response = { const response = {
totalOrders: parseInt(metrics.total_orders) || 0, totalOrders: parseInt(metrics.total_orders) || 0,

View File

@@ -1,11 +1,13 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip } from "recharts" import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip } from "recharts"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { useState } from "react" import { useState } from "react"
import config from "@/config" import config from "@/config"
import { formatCurrency } from "@/lib/utils" import { formatCurrency } from "@/lib/utils"
import { TrendingUp, DollarSign } from "lucide-react" // Importing icons import { TrendingUp, DollarSign } from "lucide-react"
import { DateRange } from "react-day-picker"
import { addDays } from "date-fns"
import { DateRangePicker } from "@/components/ui/date-range-picker-narrow"
interface ForecastData { interface ForecastData {
forecastSales: number forecastSales: number
@@ -17,21 +19,20 @@ interface ForecastData {
}[] }[]
} }
const periods = [
{ value: "7", label: "7 Days" },
{ value: "14", label: "14 Days" },
{ value: "30", label: "30 Days" },
{ value: "60", label: "60 Days" },
{ value: "90", label: "90 Days" },
]
export function ForecastMetrics() { export function ForecastMetrics() {
const [period, setPeriod] = useState("30") const [dateRange, setDateRange] = useState<DateRange>({
from: new Date(),
to: addDays(new Date(), 30),
});
const { data } = useQuery<ForecastData>({ const { data } = useQuery<ForecastData>({
queryKey: ["forecast-metrics", period], queryKey: ["forecast-metrics", dateRange],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${config.apiUrl}/dashboard/forecast/metrics?days=${period}`) const params = new URLSearchParams({
startDate: dateRange.from?.toISOString() || "",
endDate: dateRange.to?.toISOString() || "",
});
const response = await fetch(`${config.apiUrl}/dashboard/forecast/metrics?${params}`)
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch forecast metrics") throw new Error("Failed to fetch forecast metrics")
} }
@@ -41,20 +42,17 @@ export function ForecastMetrics() {
return ( return (
<> <>
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between pr-4">
<CardTitle className="text-xl font-medium">Forecast</CardTitle> <CardTitle className="text-xl font-medium">Forecast</CardTitle>
<Select value={period} onValueChange={setPeriod}> <div className="w-[230px]">
<SelectTrigger className="w-[120px]"> <DateRangePicker
<SelectValue placeholder="Select period" /> value={dateRange}
</SelectTrigger> onChange={(range) => {
<SelectContent> if (range) setDateRange(range);
{periods.map((p) => ( }}
<SelectItem key={p.value} value={p.value}> future={true}
{p.label} />
</SelectItem> </div>
))}
</SelectContent>
</Select>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">

View File

@@ -1,11 +1,13 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip } from "recharts" import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip } from "recharts"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { useState } from "react" import { useState } from "react"
import config from "@/config" import config from "@/config"
import { formatCurrency } from "@/lib/utils" import { formatCurrency } from "@/lib/utils"
import { ClipboardList, Package, DollarSign, ShoppingCart } from "lucide-react" import { ClipboardList, Package, DollarSign, ShoppingCart } from "lucide-react"
import { DateRange } from "react-day-picker"
import { addDays } from "date-fns"
import { DateRangePicker } from "@/components/ui/date-range-picker-narrow"
interface SalesData { interface SalesData {
totalOrders: number totalOrders: number
@@ -20,21 +22,20 @@ interface SalesData {
}[] }[]
} }
const periods = [
{ value: "7", label: "7 Days" },
{ value: "14", label: "14 Days" },
{ value: "30", label: "30 Days" },
{ value: "60", label: "60 Days" },
{ value: "90", label: "90 Days" },
]
export function SalesMetrics() { export function SalesMetrics() {
const [period, setPeriod] = useState("30") const [dateRange, setDateRange] = useState<DateRange>({
from: addDays(new Date(), -30),
to: new Date(),
});
const { data } = useQuery<SalesData>({ const { data } = useQuery<SalesData>({
queryKey: ["sales-metrics", period], queryKey: ["sales-metrics", dateRange],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${config.apiUrl}/dashboard/sales/metrics?days=${period}`) const params = new URLSearchParams({
startDate: dateRange.from?.toISOString() || "",
endDate: dateRange.to?.toISOString() || "",
});
const response = await fetch(`${config.apiUrl}/dashboard/sales/metrics?${params}`)
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch sales metrics") throw new Error("Failed to fetch sales metrics")
} }
@@ -44,20 +45,17 @@ export function SalesMetrics() {
return ( return (
<> <>
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between pr-4">
<CardTitle className="text-xl font-medium">Sales Overview</CardTitle> <CardTitle className="text-xl font-medium">Sales Overview</CardTitle>
<Select value={period} onValueChange={setPeriod}> <div className="w-[230px]">
<SelectTrigger className="w-[120px]"> <DateRangePicker
<SelectValue placeholder="Select period" /> value={dateRange}
</SelectTrigger> onChange={(range) => {
<SelectContent> if (range) setDateRange(range);
{periods.map((p) => ( }}
<SelectItem key={p.value} value={p.value}> future={false}
{p.label} />
</SelectItem> </div>
))}
</SelectContent>
</Select>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">

View File

@@ -0,0 +1,135 @@
import { format, addDays, startOfYear, endOfYear, subDays } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import { DateRange } from "react-day-picker";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
interface DateRangePickerProps {
value: DateRange;
onChange: (range: DateRange | undefined) => void;
className?: string;
future?: boolean;
}
export function DateRangePicker({
value,
onChange,
className,
future = false,
}: DateRangePickerProps) {
const today = new Date();
const presets = future ? [
{
label: "Next 30 days",
range: {
from: today,
to: addDays(today, 30),
},
},
{
label: "Next 90 days",
range: {
from: today,
to: addDays(today, 90),
},
},
{
label: "Rest of year",
range: {
from: today,
to: endOfYear(today),
},
},
] : [
{
label: "Last 7 days",
range: {
from: subDays(today, 7),
to: today,
},
},
{
label: "Last 30 days",
range: {
from: subDays(today, 30),
to: today,
},
},
{
label: "Last 90 days",
range: {
from: subDays(today, 90),
to: today,
},
},
{
label: "Year to date",
range: {
from: startOfYear(today),
to: today,
},
},
];
return (
<div className={cn("grid gap-1", className)}>
<Popover>
<PopoverTrigger asChild className="p-3">
<Button
id="date"
variant={"outline"}
className={cn(
"h-8 w-[230px] justify-start text-left font-normal",
!value && "text-muted-foreground"
)}
>
<CalendarIcon className="h-4 w-4" />
{value?.from ? (
value.to ? (
<>
{format(value.from, "LLL d, y")} -{" "}
{format(value.to, "LLL d, y")}
</>
) : (
format(value.from, "LLL dd, y")
)
) : (
<span>Pick a date range</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-3" align="start">
<div className="flex gap-2 pb-4">
{presets.map((preset) => (
<Button
key={preset.label}
variant="outline"
size="sm"
onClick={() => onChange(preset.range)}
>
{preset.label}
</Button>
))}
</div>
<Calendar
initialFocus
mode="range"
defaultMonth={value?.from}
selected={value}
onSelect={(range) => {
if (range) onChange(range);
}}
numberOfMonths={2}
/>
</PopoverContent>
</Popover>
</div>
);
}