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
|
||||
// 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,
|
||||
|
||||
@@ -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<DateRange>({
|
||||
from: new Date(),
|
||||
to: addDays(new Date(), 30),
|
||||
});
|
||||
|
||||
const { data } = useQuery<ForecastData>({
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
<Select value={period} onValueChange={setPeriod}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="Select period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{periods.map((p) => (
|
||||
<SelectItem key={p.value} value={p.value}>
|
||||
{p.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="w-[230px]">
|
||||
<DateRangePicker
|
||||
value={dateRange}
|
||||
onChange={(range) => {
|
||||
if (range) setDateRange(range);
|
||||
}}
|
||||
future={true}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
@@ -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<DateRange>({
|
||||
from: addDays(new Date(), -30),
|
||||
to: new Date(),
|
||||
});
|
||||
|
||||
const { data } = useQuery<SalesData>({
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
<Select value={period} onValueChange={setPeriod}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="Select period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{periods.map((p) => (
|
||||
<SelectItem key={p.value} value={p.value}>
|
||||
{p.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="w-[230px]">
|
||||
<DateRangePicker
|
||||
value={dateRange}
|
||||
onChange={(range) => {
|
||||
if (range) setDateRange(range);
|
||||
}}
|
||||
future={false}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<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