diff --git a/inventory-server/src/routes/dashboard.js b/inventory-server/src/routes/dashboard.js index cb39cea..5bccb6e 100644 --- a/inventory-server/src/routes/dashboard.js +++ b/inventory-server/src/routes/dashboard.js @@ -209,7 +209,7 @@ router.get('/replenishment/metrics', async (req, res) => { // GET /dashboard/forecast/metrics // Returns sales forecasts for specified period 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 { // Get summary metrics 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(AVG(confidence_level), 0) as overall_confidence FROM sales_forecasts - WHERE forecast_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY) - `, [days]); + WHERE forecast_date BETWEEN ? AND ? + `, [startDate, endDate]); // Get daily forecasts const [dailyForecasts] = await executeQuery(` @@ -229,10 +229,10 @@ router.get('/forecast/metrics', async (req, res) => { COALESCE(SUM(forecast_revenue), 0) as revenue, COALESCE(AVG(confidence_level), 0) as confidence FROM sales_forecasts - WHERE forecast_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY) + WHERE forecast_date BETWEEN ? AND ? GROUP BY forecast_date ORDER BY forecast_date - `, [days]); + `, [startDate, endDate]); // Get category forecasts const [categoryForecasts] = await executeQuery(` @@ -243,10 +243,10 @@ router.get('/forecast/metrics', async (req, res) => { COALESCE(AVG(cf.confidence_level), 0) as confidence FROM category_forecasts cf 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 ORDER BY revenue DESC - `, [days]); + `, [startDate, endDate]); // Format response const response = { @@ -455,7 +455,7 @@ router.get('/best-sellers', async (req, res) => { // GET /dashboard/sales/metrics // Returns sales metrics for specified period 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 { const [dailyData] = await executeQuery(` SELECT JSON_ARRAYAGG( @@ -479,10 +479,10 @@ router.get('/sales/metrics', async (req, res) => { FROM orders o JOIN products p ON o.product_id = p.product_id WHERE o.canceled = false - AND o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY) + AND o.date BETWEEN ? AND ? GROUP BY DATE(o.date) ) d - `, [days]); + `, [startDate, endDate]); const [categoryData] = await executeQuery(` 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 categories c ON pc.category_id = c.id WHERE o.canceled = false - AND o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY) + AND o.date BETWEEN ? AND ? GROUP BY c.id, c.name ) c - `, [days]); + `, [startDate, endDate]); const [metrics] = await executeQuery(` SELECT @@ -533,8 +533,8 @@ router.get('/sales/metrics', async (req, res) => { GROUP BY DATE(date) ) daily ON DATE(o.date) = daily.sale_date WHERE o.canceled = false - AND o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY) - `, [days]); + AND o.date BETWEEN ? AND ? + `, [startDate, endDate]); const response = { totalOrders: parseInt(metrics.total_orders) || 0, diff --git a/inventory/src/components/dashboard/ForecastMetrics.tsx b/inventory/src/components/dashboard/ForecastMetrics.tsx index 5db4cde..baeda59 100644 --- a/inventory/src/components/dashboard/ForecastMetrics.tsx +++ b/inventory/src/components/dashboard/ForecastMetrics.tsx @@ -1,11 +1,13 @@ import { useQuery } from "@tanstack/react-query" import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip } from "recharts" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { useState } from "react" import config from "@/config" 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 { 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() { - const [period, setPeriod] = useState("30") + const [dateRange, setDateRange] = useState({ + from: new Date(), + to: addDays(new Date(), 30), + }); const { data } = useQuery({ - queryKey: ["forecast-metrics", period], + queryKey: ["forecast-metrics", dateRange], 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) { throw new Error("Failed to fetch forecast metrics") } @@ -41,20 +42,17 @@ export function ForecastMetrics() { return ( <> - + Forecast - +
+ { + if (range) setDateRange(range); + }} + future={true} + /> +
diff --git a/inventory/src/components/dashboard/SalesMetrics.tsx b/inventory/src/components/dashboard/SalesMetrics.tsx index 477fb14..671f776 100644 --- a/inventory/src/components/dashboard/SalesMetrics.tsx +++ b/inventory/src/components/dashboard/SalesMetrics.tsx @@ -1,11 +1,13 @@ import { useQuery } from "@tanstack/react-query" import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip } from "recharts" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { useState } from "react" import config from "@/config" import { formatCurrency } from "@/lib/utils" 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 { 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() { - const [period, setPeriod] = useState("30") + const [dateRange, setDateRange] = useState({ + from: addDays(new Date(), -30), + to: new Date(), + }); const { data } = useQuery({ - queryKey: ["sales-metrics", period], + queryKey: ["sales-metrics", dateRange], 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) { throw new Error("Failed to fetch sales metrics") } @@ -44,20 +45,17 @@ export function SalesMetrics() { return ( <> - + Sales Overview - +
+ { + if (range) setDateRange(range); + }} + future={false} + /> +
diff --git a/inventory/src/components/ui/date-range-picker-narrow.tsx b/inventory/src/components/ui/date-range-picker-narrow.tsx new file mode 100644 index 0000000..10162fb --- /dev/null +++ b/inventory/src/components/ui/date-range-picker-narrow.tsx @@ -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 ( +
+ + + + + +
+ {presets.map((preset) => ( + + ))} +
+ { + if (range) onChange(range); + }} + numberOfMonths={2} + /> +
+
+
+ ); +} \ No newline at end of file