Add product detail drawer
This commit is contained in:
16
inventory/package-lock.json
generated
16
inventory/package-lock.json
generated
@@ -46,7 +46,8 @@
|
||||
"sonner": "^1.7.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tanstack": "^1.0.0"
|
||||
"tanstack": "^1.0.0",
|
||||
"vaul": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
@@ -6854,6 +6855,19 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vaul": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
|
||||
"integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/victory-vendor": {
|
||||
"version": "36.9.2",
|
||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"sonner": "^1.7.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tanstack": "^1.0.0"
|
||||
"tanstack": "^1.0.0",
|
||||
"vaul": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
|
||||
352
inventory/src/components/products/ProductDetail.tsx
Normal file
352
inventory/src/components/products/ProductDetail.tsx
Normal file
@@ -0,0 +1,352 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from "@/components/ui/drawer";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
import config from "@/config";
|
||||
|
||||
interface Product {
|
||||
product_id: string;
|
||||
title: string;
|
||||
sku: string;
|
||||
stock_quantity: number;
|
||||
price: number;
|
||||
regular_price: number;
|
||||
cost_price: number;
|
||||
vendor: string;
|
||||
brand: string;
|
||||
categories: string[];
|
||||
// Metrics
|
||||
daily_sales_avg: number;
|
||||
weekly_sales_avg: number;
|
||||
monthly_sales_avg: number;
|
||||
days_of_inventory: number;
|
||||
reorder_point: number;
|
||||
safety_stock: number;
|
||||
avg_margin_percent: number;
|
||||
total_revenue: number;
|
||||
inventory_value: number;
|
||||
turnover_rate: number;
|
||||
abc_class: string;
|
||||
stock_status: string;
|
||||
// Time series data
|
||||
monthly_sales: Array<{
|
||||
month: string;
|
||||
quantity: number;
|
||||
revenue: number;
|
||||
}>;
|
||||
recent_orders: Array<{
|
||||
date: string;
|
||||
order_number: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
}>;
|
||||
recent_purchases: Array<{
|
||||
date: string;
|
||||
po_id: string;
|
||||
ordered: number;
|
||||
received: number;
|
||||
status: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface ProductDetailProps {
|
||||
productId: string | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ProductDetail({ productId, onClose }: ProductDetailProps) {
|
||||
const { data: product, isLoading } = useQuery<Product>({
|
||||
queryKey: ["product", productId],
|
||||
queryFn: async () => {
|
||||
if (!productId) return null;
|
||||
const response = await fetch(`${config.apiUrl}/products/${productId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch product details");
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
enabled: !!productId,
|
||||
});
|
||||
|
||||
if (!productId) return null;
|
||||
|
||||
return (
|
||||
<Drawer open={!!productId} onOpenChange={(open) => !open && onClose()}>
|
||||
<DrawerContent className="h-[85vh]">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-8 w-[200px]" />
|
||||
) : (
|
||||
product?.title
|
||||
)}
|
||||
</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
{isLoading ? (
|
||||
"\u00A0" // Non-breaking space for loading state
|
||||
) : (
|
||||
`SKU: ${product?.sku}`
|
||||
)}
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
|
||||
<div className="px-4">
|
||||
<Tabs defaultValue="overview" className="w-full">
|
||||
<TabsList className="w-full justify-start">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="inventory">Inventory</TabsTrigger>
|
||||
<TabsTrigger value="sales">Sales</TabsTrigger>
|
||||
<TabsTrigger value="purchase">Purchase History</TabsTrigger>
|
||||
<TabsTrigger value="metrics">Performance Metrics</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-24 w-full" />
|
||||
<Skeleton className="h-24 w-full" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Card className="p-4">
|
||||
<h3 className="font-semibold mb-2">Basic Information</h3>
|
||||
<dl className="space-y-2">
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Brand</dt>
|
||||
<dd>{product?.brand || "N/A"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Vendor</dt>
|
||||
<dd>{product?.vendor || "N/A"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Categories</dt>
|
||||
<dd>{Array.isArray(product?.categories) ? product.categories.join(", ") : "N/A"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<h3 className="font-semibold mb-2">Pricing</h3>
|
||||
<dl className="space-y-2">
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Price</dt>
|
||||
<dd>${typeof product?.price === 'number' ? product.price.toFixed(2) : 'N/A'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Regular Price</dt>
|
||||
<dd>${typeof product?.regular_price === 'number' ? product.regular_price.toFixed(2) : 'N/A'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Cost Price</dt>
|
||||
<dd>${typeof product?.cost_price === 'number' ? product.cost_price.toFixed(2) : 'N/A'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="inventory" className="space-y-4">
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-48 w-full" />
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<Card className="p-4">
|
||||
<h3 className="font-semibold mb-2">Current Stock</h3>
|
||||
<dl className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Stock Quantity</dt>
|
||||
<dd className="text-2xl font-semibold">{product?.stock_quantity}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Days of Inventory</dt>
|
||||
<dd className="text-2xl font-semibold">{product?.days_of_inventory || 0}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Status</dt>
|
||||
<dd className="text-2xl font-semibold">{product?.stock_status || "N/A"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<h3 className="font-semibold mb-2">Stock Thresholds</h3>
|
||||
<dl className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Reorder Point</dt>
|
||||
<dd>{product?.reorder_point || 0}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Safety Stock</dt>
|
||||
<dd>{product?.safety_stock || 0}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">ABC Class</dt>
|
||||
<dd>{product?.abc_class || "N/A"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="sales" className="space-y-4">
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-96 w-full" />
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<Card className="p-4">
|
||||
<h3 className="font-semibold mb-2">Sales Metrics</h3>
|
||||
<dl className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Daily Sales Avg</dt>
|
||||
<dd>{product?.daily_sales_avg?.toFixed(2) || 0}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Weekly Sales Avg</dt>
|
||||
<dd>{product?.weekly_sales_avg?.toFixed(2) || 0}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Monthly Sales Avg</dt>
|
||||
<dd>{product?.monthly_sales_avg?.toFixed(2) || 0}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<h3 className="font-semibold mb-2">Monthly Sales Trend</h3>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={product?.monthly_sales || []}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="month" />
|
||||
<YAxis yAxisId="left" />
|
||||
<YAxis yAxisId="right" orientation="right" />
|
||||
<Tooltip />
|
||||
<Line yAxisId="left" type="monotone" dataKey="quantity" stroke="#8884d8" name="Quantity" />
|
||||
<Line yAxisId="right" type="monotone" dataKey="revenue" stroke="#82ca9d" name="Revenue" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<h3 className="font-semibold mb-2">Recent Orders</h3>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Order #</TableHead>
|
||||
<TableHead>Quantity</TableHead>
|
||||
<TableHead>Price</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{product?.recent_orders?.map((order) => (
|
||||
<TableRow key={order.order_number}>
|
||||
<TableCell>{order.date}</TableCell>
|
||||
<TableCell>{order.order_number}</TableCell>
|
||||
<TableCell>{order.quantity}</TableCell>
|
||||
<TableCell>${order.price.toFixed(2)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="purchase" className="space-y-4">
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-96 w-full" />
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<Card className="p-4">
|
||||
<h3 className="font-semibold mb-2">Recent Purchase Orders</h3>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>PO #</TableHead>
|
||||
<TableHead>Ordered</TableHead>
|
||||
<TableHead>Received</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{product?.recent_purchases?.map((po) => (
|
||||
<TableRow key={po.po_id}>
|
||||
<TableCell>{po.date}</TableCell>
|
||||
<TableCell>{po.po_id}</TableCell>
|
||||
<TableCell>{po.ordered}</TableCell>
|
||||
<TableCell>{po.received}</TableCell>
|
||||
<TableCell>{po.status}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="metrics" className="space-y-4">
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-48 w-full" />
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Card className="p-4">
|
||||
<h3 className="font-semibold mb-2">Financial Metrics</h3>
|
||||
<dl className="space-y-2">
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Total Revenue</dt>
|
||||
<dd>${product?.total_revenue?.toFixed(2) || '0.00'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Margin %</dt>
|
||||
<dd>{product?.avg_margin_percent?.toFixed(2) || '0.00'}%</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Inventory Value</dt>
|
||||
<dd>${product?.inventory_value?.toFixed(2) || '0.00'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<h3 className="font-semibold mb-2">Performance Metrics</h3>
|
||||
<dl className="space-y-2">
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Turnover Rate</dt>
|
||||
<dd>{product?.turnover_rate?.toFixed(2) || '0.00'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">ABC Classification</dt>
|
||||
<dd>Class {product?.abc_class || 'N/A'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Stock Status</dt>
|
||||
<dd>{product?.stock_status || 'N/A'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@@ -95,6 +95,7 @@ interface ProductTableProps {
|
||||
columnDefs: ColumnDef[];
|
||||
columnOrder: (keyof Product | 'image')[];
|
||||
onColumnOrderChange?: (columns: (keyof Product | 'image')[]) => void;
|
||||
onRowClick?: (product: Product) => void;
|
||||
}
|
||||
|
||||
interface SortableHeaderProps {
|
||||
@@ -159,8 +160,9 @@ export function ProductTable({
|
||||
sortDirection,
|
||||
visibleColumns,
|
||||
columnDefs,
|
||||
columnOrder,
|
||||
columnOrder = columnDefs.map(col => col.key),
|
||||
onColumnOrderChange,
|
||||
onRowClick,
|
||||
}: ProductTableProps) {
|
||||
const [, setActiveId] = React.useState<keyof Product | null>(null);
|
||||
const sensors = useSensors(
|
||||
@@ -304,13 +306,13 @@ export function ProductTable({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<SortableContext
|
||||
@@ -330,10 +332,13 @@ export function ProductTable({
|
||||
</SortableContext>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</DndContext>
|
||||
<TableBody>
|
||||
{products.map((product) => (
|
||||
<TableRow key={product.product_id}>
|
||||
<TableRow
|
||||
key={product.product_id}
|
||||
onClick={() => onRowClick?.(product)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{orderedColumns.map((column) => (
|
||||
<TableCell key={`${product.product_id}-${column}`}>
|
||||
{formatColumnValue(product, column)}
|
||||
@@ -354,5 +359,6 @@ export function ProductTable({
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
125
inventory/src/components/ui/drawer.tsx
Normal file
125
inventory/src/components/ui/drawer.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Drawer = ({
|
||||
shouldScaleBackground = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
||||
<DrawerPrimitive.Root
|
||||
shouldScaleBackground={shouldScaleBackground}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
Drawer.displayName = "Drawer"
|
||||
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger
|
||||
|
||||
const DrawerPortal = DrawerPrimitive.Portal
|
||||
|
||||
const DrawerClose = DrawerPrimitive.Close
|
||||
|
||||
const DrawerOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
||||
|
||||
const DrawerContent = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> & {
|
||||
side?: "left" | "right" | "top" | "bottom"
|
||||
}
|
||||
>(({ className, children, side = "bottom", ...props }, ref) => (
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed z-50 gap-4 bg-background",
|
||||
{
|
||||
"inset-x-0 bottom-0 mt-24 rounded-t-[10px] border-t": side === "bottom",
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-xl": side === "right",
|
||||
"inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-xl": side === "left",
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{side === "bottom" && (
|
||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
)}
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
))
|
||||
DrawerContent.displayName = "DrawerContent"
|
||||
|
||||
const DrawerHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DrawerHeader.displayName = "DrawerHeader"
|
||||
|
||||
const DrawerFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DrawerFooter.displayName = "DrawerFooter"
|
||||
|
||||
const DrawerTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
||||
|
||||
const DrawerDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { useQuery, useQueryClient, keepPreviousData } from '@tanstack/react-quer
|
||||
import { ProductFilters } from '@/components/products/ProductFilters';
|
||||
import { ProductTable } from '@/components/products/ProductTable';
|
||||
import { ProductTableSkeleton } from '@/components/products/ProductTableSkeleton';
|
||||
import { ProductDetail } from '@/components/products/ProductDetail';
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
@@ -169,6 +170,7 @@ export function Products() {
|
||||
...DEFAULT_VISIBLE_COLUMNS,
|
||||
...AVAILABLE_COLUMNS.map(col => col.key).filter(key => !DEFAULT_VISIBLE_COLUMNS.includes(key))
|
||||
]);
|
||||
const [selectedProductId, setSelectedProductId] = useState<string | null>(null);
|
||||
|
||||
// Group columns by their group property
|
||||
const columnsByGroup = AVAILABLE_COLUMNS.reduce((acc, col) => {
|
||||
@@ -436,6 +438,7 @@ export function Products() {
|
||||
columnOrder={columnOrder}
|
||||
onSort={handleSort}
|
||||
onColumnOrderChange={handleColumnOrderChange}
|
||||
onRowClick={(product) => setSelectedProductId(product.product_id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -443,6 +446,11 @@ export function Products() {
|
||||
<div className="mt-4">
|
||||
{renderPagination()}
|
||||
</div>
|
||||
|
||||
<ProductDetail
|
||||
productId={selectedProductId}
|
||||
onClose={() => setSelectedProductId(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user