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_OBJECT(
'product_id', pm.product_id,
'name', pm.title,
'title', pm.title,
'sku', pm.sku,
'stock_quantity', pm.stock_quantity,
'total_sold', pm.total_sold,

View File

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