Files
inventory/inventory/src/pages/Forecasting.tsx

190 lines
6.7 KiB
TypeScript

import { useState, Fragment } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useQuery } from "@tanstack/react-query";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import {
flexRender,
getCoreRowModel,
getSortedRowModel,
SortingState,
useReactTable,
getExpandedRowModel,
Row,
Header,
HeaderGroup,
} from "@tanstack/react-table";
import { columns, ForecastItem, renderSubComponent } from "@/components/forecasting/columns";
import { DateRange } from "react-day-picker";
import { addDays } from "date-fns";
import { DateRangePicker } from "@/components/ui/date-range-picker";
export default function Forecasting() {
const [selectedBrand, setSelectedBrand] = useState<string>("");
const [dateRange, setDateRange] = useState<DateRange>({
from: addDays(new Date(), -30),
to: new Date(),
});
const [sorting, setSorting] = useState<SortingState>([]);
const handleDateRangeChange = (range: DateRange | undefined) => {
if (range) {
setDateRange(range);
}
};
const { data: brands = [], isLoading: brandsLoading } = useQuery({
queryKey: ["brands"],
queryFn: async () => {
const response = await fetch("/api/products/brands");
if (!response.ok) {
throw new Error("Failed to fetch brands");
}
const data = await response.json();
return Array.isArray(data) ? data : [];
},
});
const { data: forecastData, isLoading: forecastLoading } = useQuery({
queryKey: ["forecast", selectedBrand, dateRange],
queryFn: async () => {
const params = new URLSearchParams({
brand: selectedBrand,
startDate: dateRange.from?.toISOString() || "",
endDate: dateRange.to?.toISOString() || "",
});
const response = await fetch(`/api/analytics/forecast?${params}`);
if (!response.ok) {
throw new Error("Failed to fetch forecast data");
}
const data = await response.json();
return data.map((item: any) => ({
category: item.category_name,
categoryPath: item.path,
avgDailySales: Number(item.avg_daily_sales) || 0,
totalSold: Number(item.total_sold) || 0,
numProducts: Number(item.num_products) || 0,
avgPrice: Number(item.avg_price) || 0,
avgTotalSold: Number(item.avgTotalSold) || 0,
products: item.products?.map((p: any) => ({
pid: p.pid,
title: p.title,
sku: p.sku,
stock_quantity: Number(p.stock_quantity) || 0,
total_sold: Number(p.total_sold) || 0,
daily_sales_avg: Number(p.daily_sales_avg) || 0,
forecast_units: Number(p.forecast_units) || 0,
forecast_revenue: Number(p.forecast_revenue) || 0,
confidence_level: Number(p.confidence_level) || 0,
categoryPath: item.path
}))
}));
},
enabled: !!selectedBrand && !!dateRange.from && !!dateRange.to,
});
const table = useReactTable({
data: forecastData || [],
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: setSorting,
getRowCanExpand: () => true,
state: {
sorting,
},
});
return (
<div className="container mx-auto py-10">
<Card>
<CardHeader>
<CardTitle>Sales Forecasting</CardTitle>
</CardHeader>
<CardContent>
<div className="flex gap-4 mb-6">
<div className="w-[200px]">
<Select value={selectedBrand} onValueChange={setSelectedBrand}>
<SelectTrigger disabled={brandsLoading}>
<SelectValue placeholder="Select brand" />
</SelectTrigger>
<SelectContent>
{brands.map((brand: string) => (
<SelectItem key={brand} value={brand}>
{brand}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<DateRangePicker
value={dateRange}
onChange={handleDateRangeChange}
/>
</div>
{forecastLoading ? (
<div className="h-24 flex items-center justify-center">
Loading forecast data...
</div>
) : forecastData && (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup: HeaderGroup<ForecastItem>) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header: Header<ForecastItem, unknown>) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row: Row<ForecastItem>) => (
<Fragment key={row.id}>
<TableRow
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
{row.getIsExpanded() && (
<TableRow>
<TableCell colSpan={columns.length} className="p-0">
{renderSubComponent({ row })}
</TableCell>
</TableRow>
)}
</Fragment>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
</div>
);
}