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",
|
"sonner": "^1.7.1",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tanstack": "^1.0.0"
|
"tanstack": "^1.0.0",
|
||||||
|
"vaul": "^1.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
@@ -6854,6 +6855,19 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/victory-vendor": {
|
||||||
"version": "36.9.2",
|
"version": "36.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
||||||
|
|||||||
@@ -48,7 +48,8 @@
|
|||||||
"sonner": "^1.7.1",
|
"sonner": "^1.7.1",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tanstack": "^1.0.0"
|
"tanstack": "^1.0.0",
|
||||||
|
"vaul": "^1.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@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[];
|
columnDefs: ColumnDef[];
|
||||||
columnOrder: (keyof Product | 'image')[];
|
columnOrder: (keyof Product | 'image')[];
|
||||||
onColumnOrderChange?: (columns: (keyof Product | 'image')[]) => void;
|
onColumnOrderChange?: (columns: (keyof Product | 'image')[]) => void;
|
||||||
|
onRowClick?: (product: Product) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SortableHeaderProps {
|
interface SortableHeaderProps {
|
||||||
@@ -159,8 +160,9 @@ export function ProductTable({
|
|||||||
sortDirection,
|
sortDirection,
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
columnDefs,
|
columnDefs,
|
||||||
columnOrder,
|
columnOrder = columnDefs.map(col => col.key),
|
||||||
onColumnOrderChange,
|
onColumnOrderChange,
|
||||||
|
onRowClick,
|
||||||
}: ProductTableProps) {
|
}: ProductTableProps) {
|
||||||
const [, setActiveId] = React.useState<keyof Product | null>(null);
|
const [, setActiveId] = React.useState<keyof Product | null>(null);
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
@@ -304,13 +306,13 @@ export function ProductTable({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border">
|
|
||||||
<Table>
|
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
@@ -330,10 +332,13 @@ export function ProductTable({
|
|||||||
</SortableContext>
|
</SortableContext>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
</DndContext>
|
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{products.map((product) => (
|
{products.map((product) => (
|
||||||
<TableRow key={product.product_id}>
|
<TableRow
|
||||||
|
key={product.product_id}
|
||||||
|
onClick={() => onRowClick?.(product)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
{orderedColumns.map((column) => (
|
{orderedColumns.map((column) => (
|
||||||
<TableCell key={`${product.product_id}-${column}`}>
|
<TableCell key={`${product.product_id}-${column}`}>
|
||||||
{formatColumnValue(product, column)}
|
{formatColumnValue(product, column)}
|
||||||
@@ -354,5 +359,6 @@ export function ProductTable({
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</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 { ProductFilters } from '@/components/products/ProductFilters';
|
||||||
import { ProductTable } from '@/components/products/ProductTable';
|
import { ProductTable } from '@/components/products/ProductTable';
|
||||||
import { ProductTableSkeleton } from '@/components/products/ProductTableSkeleton';
|
import { ProductTableSkeleton } from '@/components/products/ProductTableSkeleton';
|
||||||
|
import { ProductDetail } from '@/components/products/ProductDetail';
|
||||||
import {
|
import {
|
||||||
Pagination,
|
Pagination,
|
||||||
PaginationContent,
|
PaginationContent,
|
||||||
@@ -169,6 +170,7 @@ export function Products() {
|
|||||||
...DEFAULT_VISIBLE_COLUMNS,
|
...DEFAULT_VISIBLE_COLUMNS,
|
||||||
...AVAILABLE_COLUMNS.map(col => col.key).filter(key => !DEFAULT_VISIBLE_COLUMNS.includes(key))
|
...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
|
// Group columns by their group property
|
||||||
const columnsByGroup = AVAILABLE_COLUMNS.reduce((acc, col) => {
|
const columnsByGroup = AVAILABLE_COLUMNS.reduce((acc, col) => {
|
||||||
@@ -436,6 +438,7 @@ export function Products() {
|
|||||||
columnOrder={columnOrder}
|
columnOrder={columnOrder}
|
||||||
onSort={handleSort}
|
onSort={handleSort}
|
||||||
onColumnOrderChange={handleColumnOrderChange}
|
onColumnOrderChange={handleColumnOrderChange}
|
||||||
|
onRowClick={(product) => setSelectedProductId(product.product_id)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -443,6 +446,11 @@ export function Products() {
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
{renderPagination()}
|
{renderPagination()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ProductDetail
|
||||||
|
productId={selectedProductId}
|
||||||
|
onClose={() => setSelectedProductId(null)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user