Fix and update forecasting details
This commit is contained in:
@@ -575,7 +575,7 @@ router.get('/forecast', async (req, res) => {
|
|||||||
JSON_ARRAYAGG(
|
JSON_ARRAYAGG(
|
||||||
JSON_OBJECT(
|
JSON_OBJECT(
|
||||||
'product_id', pm.product_id,
|
'product_id', pm.product_id,
|
||||||
'name', pm.title,
|
'title', pm.title,
|
||||||
'sku', pm.sku,
|
'sku', pm.sku,
|
||||||
'stock_quantity', pm.stock_quantity,
|
'stock_quantity', pm.stock_quantity,
|
||||||
'total_sold', pm.total_sold,
|
'total_sold', pm.total_sold,
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { ColumnDef, Column } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ArrowUpDown, ChevronDown, ChevronRight } from "lucide-react";
|
import { ArrowUpDown, ChevronDown, ChevronRight } from "lucide-react";
|
||||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
export type ProductDetail = {
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
|
interface ProductDetail {
|
||||||
product_id: string;
|
product_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
sku: string;
|
sku: string;
|
||||||
stock_quantity: number;
|
stock_quantity: number;
|
||||||
total_sold: number;
|
total_sold: number;
|
||||||
avg_price: number;
|
avg_price: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type ForecastItem = {
|
export interface ForecastItem {
|
||||||
category: string;
|
category: string;
|
||||||
avgDailySales: number;
|
avgDailySales: number;
|
||||||
totalSold: number;
|
totalSold: number;
|
||||||
@@ -20,49 +20,36 @@ export type ForecastItem = {
|
|||||||
avgPrice: number;
|
avgPrice: number;
|
||||||
avgTotalSold: number;
|
avgTotalSold: number;
|
||||||
products?: ProductDetail[];
|
products?: ProductDetail[];
|
||||||
};
|
}
|
||||||
|
|
||||||
export const columns: ColumnDef<ForecastItem>[] = [
|
export const columns: ColumnDef<ForecastItem>[] = [
|
||||||
{
|
{
|
||||||
id: "expander",
|
id: "expander",
|
||||||
header: () => null,
|
header: () => null,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return row.original.products?.length ? (
|
return row.getCanExpand() ? (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => row.toggleExpanded()}
|
onClick={() => row.toggleExpanded()}
|
||||||
className="p-0 hover:bg-transparent"
|
className="p-0 h-auto"
|
||||||
>
|
>
|
||||||
{row.getIsExpanded() ? (
|
{row.getIsExpanded() ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
) : null;
|
) : null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "category",
|
accessorKey: "category",
|
||||||
header: ({ column }: { column: Column<ForecastItem> }) => {
|
header: "Category",
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
Category
|
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "avgDailySales",
|
accessorKey: "avgDailySales",
|
||||||
header: ({ column }: { column: Column<ForecastItem> }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
className="whitespace-nowrap"
|
||||||
>
|
>
|
||||||
Avg Daily Sales
|
Avg Daily Sales
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
@@ -71,12 +58,12 @@ export const columns: ColumnDef<ForecastItem>[] = [
|
|||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const value = row.getValue("avgDailySales") as number;
|
const value = row.getValue("avgDailySales") as number;
|
||||||
return value ? value.toFixed(2) : '0.00';
|
return value?.toFixed(2) || "0.00";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "totalSold",
|
accessorKey: "totalSold",
|
||||||
header: ({ column }: { column: Column<ForecastItem> }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -89,16 +76,36 @@ export const columns: ColumnDef<ForecastItem>[] = [
|
|||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const value = row.getValue("totalSold") as number;
|
const value = row.getValue("totalSold") as number;
|
||||||
return value ? value.toLocaleString() : '0';
|
return value?.toLocaleString() || "0";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "avgTotalSold",
|
accessorKey: "numProducts",
|
||||||
header: ({ column }: { column: Column<ForecastItem> }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
# Products
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const value = row.getValue("numProducts") as number;
|
||||||
|
return value?.toLocaleString() || "0";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "avgTotalSold",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
className="whitespace-nowrap"
|
||||||
>
|
>
|
||||||
Avg Total Sold
|
Avg Total Sold
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
@@ -107,30 +114,17 @@ export const columns: ColumnDef<ForecastItem>[] = [
|
|||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const value = row.getValue("avgTotalSold") as number;
|
const value = row.getValue("avgTotalSold") as number;
|
||||||
return value ? value.toFixed(2) : '0.00';
|
return value?.toFixed(2) || "0.00";
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "numProducts",
|
|
||||||
header: ({ column }: { column: Column<ForecastItem> }) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
# of Products
|
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "avgPrice",
|
accessorKey: "avgPrice",
|
||||||
header: ({ column }: { column: Column<ForecastItem> }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
className="whitespace-nowrap"
|
||||||
>
|
>
|
||||||
Avg Price
|
Avg Price
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
@@ -139,7 +133,7 @@ export const columns: ColumnDef<ForecastItem>[] = [
|
|||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const value = row.getValue("avgPrice") as number;
|
const value = row.getValue("avgPrice") as number;
|
||||||
return value ? `$${value.toFixed(2)}` : '$0.00';
|
return `$${value?.toFixed(2) || "0.00"}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -148,23 +142,29 @@ export const renderSubComponent = ({ row }: { row: any }) => {
|
|||||||
const products = row.original.products || [];
|
const products = row.original.products || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<ScrollArea className="h-[400px] w-full rounded-md border p-4">
|
||||||
<div className="grid grid-cols-6 gap-4 font-medium mb-2">
|
<Table>
|
||||||
<div>Name</div>
|
<TableHeader>
|
||||||
<div>SKU</div>
|
<TableRow>
|
||||||
<div>Stock</div>
|
<TableHead>Product Name</TableHead>
|
||||||
<div>Total Sold</div>
|
<TableHead>SKU</TableHead>
|
||||||
<div>Avg Price</div>
|
<TableHead>Stock Quantity</TableHead>
|
||||||
</div>
|
<TableHead>Total Sold</TableHead>
|
||||||
|
<TableHead>Average Price</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
{products.map((product: ProductDetail) => (
|
{products.map((product: ProductDetail) => (
|
||||||
<div key={product.product_id} className="grid grid-cols-6 gap-4 py-2 border-t">
|
<TableRow key={product.product_id}>
|
||||||
<div>{product.name}</div>
|
<TableCell>{product.name}</TableCell>
|
||||||
<div>{product.sku}</div>
|
<TableCell>{product.sku}</TableCell>
|
||||||
<div>{product.stock_quantity}</div>
|
<TableCell>{product.stock_quantity.toLocaleString()}</TableCell>
|
||||||
<div>{product.total_sold}</div>
|
<TableCell>{product.total_sold.toLocaleString()}</TableCell>
|
||||||
<div>${product.avg_price.toFixed(2)}</div>
|
<TableCell>${product.avg_price.toFixed(2)}</TableCell>
|
||||||
</div>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</div>
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -9,10 +9,10 @@ import {
|
|||||||
getSortedRowModel,
|
getSortedRowModel,
|
||||||
SortingState,
|
SortingState,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
|
getExpandedRowModel,
|
||||||
Row,
|
Row,
|
||||||
Header,
|
Header,
|
||||||
HeaderGroup,
|
HeaderGroup,
|
||||||
getExpandedRowModel,
|
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import { columns, ForecastItem, renderSubComponent } from "@/components/forecasting/columns";
|
import { columns, ForecastItem, renderSubComponent } from "@/components/forecasting/columns";
|
||||||
import { DateRange } from "react-day-picker";
|
import { DateRange } from "react-day-picker";
|
||||||
@@ -67,7 +67,7 @@ export default function Forecasting() {
|
|||||||
avgTotalSold: Number(item.avgTotalSold) || 0,
|
avgTotalSold: Number(item.avgTotalSold) || 0,
|
||||||
products: item.products?.map((p: any) => ({
|
products: item.products?.map((p: any) => ({
|
||||||
product_id: p.product_id,
|
product_id: p.product_id,
|
||||||
name: p.product_name,
|
name: p.title,
|
||||||
sku: p.sku,
|
sku: p.sku,
|
||||||
stock_quantity: Number(p.stock_quantity) || 0,
|
stock_quantity: Number(p.stock_quantity) || 0,
|
||||||
total_sold: Number(p.total_sold) || 0,
|
total_sold: Number(p.total_sold) || 0,
|
||||||
@@ -85,10 +85,10 @@ export default function Forecasting() {
|
|||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
getExpandedRowModel: getExpandedRowModel(),
|
getExpandedRowModel: getExpandedRowModel(),
|
||||||
onSortingChange: setSorting,
|
onSortingChange: setSorting,
|
||||||
|
getRowCanExpand: () => true,
|
||||||
state: {
|
state: {
|
||||||
sorting,
|
sorting,
|
||||||
},
|
},
|
||||||
getRowCanExpand: () => true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -158,7 +158,7 @@ export default function Forecasting() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
{row.getIsExpanded() && (
|
{row.getIsExpanded() && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={columns.length}>
|
<TableCell colSpan={columns.length} className="p-0">
|
||||||
{renderSubComponent({ row })}
|
{renderSubComponent({ row })}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -167,7 +167,10 @@ export default function Forecasting() {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="h-24 text-center"
|
||||||
|
>
|
||||||
No results.
|
No results.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
Reference in New Issue
Block a user