Switch to daterangepicker for forecast and sales
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
135
inventory/src/components/ui/date-range-picker-narrow.tsx
Normal file
135
inventory/src/components/ui/date-range-picker-narrow.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user