Fix and update forecasting details

This commit is contained in:
2025-01-15 22:33:09 -05:00
parent c8c3d323a4
commit e4e23291ea
3 changed files with 76 additions and 73 deletions

View File

@@ -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,

View File

@@ -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>
{products.map((product: ProductDetail) => ( <TableHead>Average Price</TableHead>
<div key={product.product_id} className="grid grid-cols-6 gap-4 py-2 border-t"> </TableRow>
<div>{product.name}</div> </TableHeader>
<div>{product.sku}</div> <TableBody>
<div>{product.stock_quantity}</div> {products.map((product: ProductDetail) => (
<div>{product.total_sold}</div> <TableRow key={product.product_id}>
<div>${product.avg_price.toFixed(2)}</div> <TableCell>{product.name}</TableCell>
</div> <TableCell>{product.sku}</TableCell>
))} <TableCell>{product.stock_quantity.toLocaleString()}</TableCell>
</div> <TableCell>{product.total_sold.toLocaleString()}</TableCell>
<TableCell>${product.avg_price.toFixed(2)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</ScrollArea>
); );
}; };

View File

@@ -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>