Replace drawer with vaul

This commit is contained in:
2025-01-14 16:59:39 -05:00
parent 0b6147a68d
commit ce2ed29e71
4 changed files with 574 additions and 395 deletions

View File

@@ -1,11 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
} from "@/components/ui/drawer";
import { Drawer as VaulDrawer } from "vaul";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Skeleton } from "@/components/ui/skeleton";
import { Card } from "@/components/ui/card";
@@ -188,409 +182,411 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
if (!productId) return null;
return (
<Drawer open={!!productId} onOpenChange={(open) => !open && onClose()}>
<DrawerContent className="h-[90vh] md:h-[90vh] overflow-y-auto">
<DrawerHeader>
<DrawerTitle>
{isLoading ? (
<Skeleton className="h-8 w-[200px]" />
) : (
product?.title
)}
</DrawerTitle>
<DrawerDescription>
{isLoading ? (
"\u00A0"
) : (
`SKU: ${product?.SKU} | Stock: ${product?.stock_quantity}`
)}
</DrawerDescription>
</DrawerHeader>
<div className="px-4 pb-8">
<Tabs defaultValue="overview" className="w-full">
<TabsList className="w-full justify-start mb-4 sticky top-0 bg-background z-10">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="inventory">Inventory</TabsTrigger>
<TabsTrigger value="sales">Sales</TabsTrigger>
<TabsTrigger value="purchase">Purchase History</TabsTrigger>
<TabsTrigger value="financial">Financial</TabsTrigger>
<TabsTrigger value="vendor">Vendor</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<VaulDrawer.Root open={!!productId} onOpenChange={(open) => !open && onClose()} direction="right">
<VaulDrawer.Portal>
<VaulDrawer.Content className="fixed right-0 top-0 h-full w-[90%] max-w-[800px] bg-background p-6 shadow-lg">
<div className="mb-8">
<VaulDrawer.Title className="text-2xl font-bold">
{isLoading ? (
<div className="space-y-4">
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
</div>
<Skeleton className="h-8 w-[200px]" />
) : (
<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 className="flex flex-wrap gap-2">
{product?.categories?.map(category => (
<span key={category} className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium ring-1 ring-inset ring-muted">
{category}
</span>
)) || "N/A"}
</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Tags</dt>
<dd className="flex flex-wrap gap-2">
{product?.tags?.map(tag => (
<span key={tag} className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium ring-1 ring-inset ring-muted">
{tag}
</span>
)) || "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>${formatPrice(product?.price)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Regular Price</dt>
<dd>${formatPrice(product?.regular_price)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Cost Price</dt>
<dd>${formatPrice(product?.cost_price)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Landing Cost</dt>
<dd>${formatPrice(product?.landing_cost_price)}</dd>
</div>
</dl>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Stock Status</h3>
<dl className="space-y-2">
<div>
<dt className="text-sm text-muted-foreground">Current Stock</dt>
<dd className="text-2xl font-semibold">{product?.stock_quantity}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Status</dt>
<dd>{product?.metrics?.stock_status}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Days of Stock</dt>
<dd>{product?.metrics?.days_of_inventory} days</dd>
</div>
</dl>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Sales Velocity</h3>
<dl className="space-y-2">
<div>
<dt className="text-sm text-muted-foreground">Daily Sales</dt>
<dd>{product?.metrics?.daily_sales_avg?.toFixed(1)} units</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Weekly Sales</dt>
<dd>{product?.metrics?.weekly_sales_avg?.toFixed(1)} units</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Monthly Sales</dt>
<dd>{product?.metrics?.monthly_sales_avg?.toFixed(1)} units</dd>
</div>
</dl>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Sales Trend</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={combinedData?.monthly_sales || []}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="quantity" stroke="#8884d8" name="Quantity" />
<Line type="monotone" dataKey="revenue" stroke="#82ca9d" name="Revenue" />
</LineChart>
</ResponsiveContainer>
</div>
</Card>
</div>
product?.title
)}
</TabsContent>
<TabsContent value="inventory" className="space-y-4">
</VaulDrawer.Title>
<VaulDrawer.Description className="text-muted-foreground">
{isLoading ? (
<Skeleton className="h-48 w-full" />
"\u00A0"
) : (
<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?.metrics?.days_of_inventory || 0}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Status</dt>
<dd className="text-2xl font-semibold">{product?.metrics?.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?.metrics?.reorder_point || 0}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Safety Stock</dt>
<dd>{product?.metrics?.safety_stock || 0}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">ABC Class</dt>
<dd>{product?.metrics?.abc_class || "N/A"}</dd>
</div>
</dl>
</Card>
</div>
`SKU: ${product?.SKU} | Stock: ${product?.stock_quantity}`
)}
</TabsContent>
</VaulDrawer.Description>
</div>
<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">Recent Orders</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Order #</TableHead>
<TableHead>Customer</TableHead>
<TableHead>Quantity</TableHead>
<TableHead>Price</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{combinedData?.recent_orders?.map((order: NonNullable<Product['recent_orders']>[number]) => (
<TableRow key={order.order_number}>
<TableCell>{order.date}</TableCell>
<TableCell>{order.order_number}</TableCell>
<TableCell>{order.customer}</TableCell>
<TableCell>{order.quantity}</TableCell>
<TableCell>${formatPrice(order.price)}</TableCell>
<TableCell>{order.status}</TableCell>
</TableRow>
))}
{(!combinedData?.recent_orders || combinedData.recent_orders.length === 0) && (
<div className="pb-8">
<Tabs defaultValue="overview" className="w-full">
<TabsList className="w-full justify-start mb-4 sticky top-0 bg-background z-10">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="inventory">Inventory</TabsTrigger>
<TabsTrigger value="sales">Sales</TabsTrigger>
<TabsTrigger value="purchase">Purchase History</TabsTrigger>
<TabsTrigger value="financial">Financial</TabsTrigger>
<TabsTrigger value="vendor">Vendor</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 className="flex flex-wrap gap-2">
{product?.categories?.map(category => (
<span key={category} className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium ring-1 ring-inset ring-muted">
{category}
</span>
)) || "N/A"}
</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Tags</dt>
<dd className="flex flex-wrap gap-2">
{product?.tags?.map(tag => (
<span key={tag} className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium ring-1 ring-inset ring-muted">
{tag}
</span>
)) || "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>${formatPrice(product?.price)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Regular Price</dt>
<dd>${formatPrice(product?.regular_price)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Cost Price</dt>
<dd>${formatPrice(product?.cost_price)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Landing Cost</dt>
<dd>${formatPrice(product?.landing_cost_price)}</dd>
</div>
</dl>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Stock Status</h3>
<dl className="space-y-2">
<div>
<dt className="text-sm text-muted-foreground">Current Stock</dt>
<dd className="text-2xl font-semibold">{product?.stock_quantity}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Status</dt>
<dd>{product?.metrics?.stock_status}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Days of Stock</dt>
<dd>{product?.metrics?.days_of_inventory} days</dd>
</div>
</dl>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Sales Velocity</h3>
<dl className="space-y-2">
<div>
<dt className="text-sm text-muted-foreground">Daily Sales</dt>
<dd>{product?.metrics?.daily_sales_avg?.toFixed(1)} units</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Weekly Sales</dt>
<dd>{product?.metrics?.weekly_sales_avg?.toFixed(1)} units</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Monthly Sales</dt>
<dd>{product?.metrics?.monthly_sales_avg?.toFixed(1)} units</dd>
</div>
</dl>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Sales Trend</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={combinedData?.monthly_sales || []}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="quantity" stroke="#8884d8" name="Quantity" />
<Line type="monotone" dataKey="revenue" stroke="#82ca9d" name="Revenue" />
</LineChart>
</ResponsiveContainer>
</div>
</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?.metrics?.days_of_inventory || 0}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Status</dt>
<dd className="text-2xl font-semibold">{product?.metrics?.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?.metrics?.reorder_point || 0}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Safety Stock</dt>
<dd>{product?.metrics?.safety_stock || 0}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">ABC Class</dt>
<dd>{product?.metrics?.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">Recent Orders</h3>
<Table>
<TableHeader>
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground">
No recent orders
</TableCell>
<TableHead>Date</TableHead>
<TableHead>Order #</TableHead>
<TableHead>Customer</TableHead>
<TableHead>Quantity</TableHead>
<TableHead>Price</TableHead>
<TableHead>Status</TableHead>
</TableRow>
)}
</TableBody>
</Table>
</Card>
</TableHeader>
<TableBody>
{combinedData?.recent_orders?.map((order: NonNullable<Product['recent_orders']>[number]) => (
<TableRow key={order.order_number}>
<TableCell>{order.date}</TableCell>
<TableCell>{order.order_number}</TableCell>
<TableCell>{order.customer}</TableCell>
<TableCell>{order.quantity}</TableCell>
<TableCell>${formatPrice(order.price)}</TableCell>
<TableCell>{order.status}</TableCell>
</TableRow>
))}
{(!combinedData?.recent_orders || combinedData.recent_orders.length === 0) && (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground">
No recent orders
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</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={combinedData?.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>
</div>
)}
</TabsContent>
<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={combinedData?.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>
</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>
<TableHead>Lead Time</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{combinedData?.recent_purchases?.map((po: NonNullable<Product['recent_purchases']>[number]) => (
<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>
<TableCell>{po.lead_time_days ? `${po.lead_time_days} days` : 'N/A'}</TableCell>
</TableRow>
))}
{(!combinedData?.recent_purchases || combinedData.recent_purchases.length === 0) && (
<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>
<TableCell colSpan={6} className="text-center text-muted-foreground">
No recent purchase orders
</TableCell>
<TableHead>Date</TableHead>
<TableHead>PO #</TableHead>
<TableHead>Ordered</TableHead>
<TableHead>Received</TableHead>
<TableHead>Status</TableHead>
<TableHead>Lead Time</TableHead>
</TableRow>
)}
</TableBody>
</Table>
</Card>
</div>
)}
</TabsContent>
</TableHeader>
<TableBody>
{combinedData?.recent_purchases?.map((po: NonNullable<Product['recent_purchases']>[number]) => (
<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>
<TableCell>{po.lead_time_days ? `${po.lead_time_days} days` : 'N/A'}</TableCell>
</TableRow>
))}
{(!combinedData?.recent_purchases || combinedData.recent_purchases.length === 0) && (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground">
No recent purchase orders
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</Card>
</div>
)}
</TabsContent>
<TabsContent value="financial" 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">Financial Overview</h3>
<dl className="grid grid-cols-3 gap-4">
<div>
<dt className="text-sm text-muted-foreground">Gross Profit</dt>
<dd className="text-2xl font-semibold">${formatPrice(product?.metrics.gross_profit)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">GMROI</dt>
<dd className="text-2xl font-semibold">{product?.metrics.gmroi.toFixed(2)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Margin %</dt>
<dd className="text-2xl font-semibold">{product?.metrics.avg_margin_percent.toFixed(2)}%</dd>
</div>
</dl>
</Card>
<TabsContent value="financial" 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">Financial Overview</h3>
<dl className="grid grid-cols-3 gap-4">
<div>
<dt className="text-sm text-muted-foreground">Gross Profit</dt>
<dd className="text-2xl font-semibold">${formatPrice(product?.metrics.gross_profit)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">GMROI</dt>
<dd className="text-2xl font-semibold">{product?.metrics.gmroi.toFixed(2)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Margin %</dt>
<dd className="text-2xl font-semibold">{product?.metrics.avg_margin_percent.toFixed(2)}%</dd>
</div>
</dl>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Cost Breakdown</h3>
<dl className="grid grid-cols-2 gap-4">
<div>
<dt className="text-sm text-muted-foreground">Cost of Goods Sold</dt>
<dd>${formatPrice(product?.metrics.cost_of_goods_sold)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Landing Cost</dt>
<dd>${formatPrice(product?.landing_cost_price)}</dd>
</div>
</dl>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Cost Breakdown</h3>
<dl className="grid grid-cols-2 gap-4">
<div>
<dt className="text-sm text-muted-foreground">Cost of Goods Sold</dt>
<dd>${formatPrice(product?.metrics.cost_of_goods_sold)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Landing Cost</dt>
<dd>${formatPrice(product?.landing_cost_price)}</dd>
</div>
</dl>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Profit Margin Trend</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={combinedData?.monthly_sales || []}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis domain={[0, 100]} />
<Tooltip />
<Line type="monotone" dataKey="profit_margin" stroke="#82ca9d" name="Profit Margin %" />
</LineChart>
</ResponsiveContainer>
</div>
</Card>
</div>
)}
</TabsContent>
<Card className="p-4">
<h3 className="font-semibold mb-2">Profit Margin Trend</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={combinedData?.monthly_sales || []}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis domain={[0, 100]} />
<Tooltip />
<Line type="monotone" dataKey="profit_margin" stroke="#82ca9d" name="Profit Margin %" />
</LineChart>
</ResponsiveContainer>
</div>
</Card>
</div>
)}
</TabsContent>
<TabsContent value="vendor" className="space-y-4">
{isLoading ? (
<Skeleton className="h-48 w-full" />
) : product?.vendor_performance ? (
<div className="space-y-4">
<Card className="p-4">
<h3 className="font-semibold mb-2">Vendor Performance</h3>
<dl className="grid grid-cols-3 gap-4">
<div>
<dt className="text-sm text-muted-foreground">On-Time Delivery</dt>
<dd className="text-2xl font-semibold">{product.vendor_performance.on_time_delivery_rate.toFixed(1)}%</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Order Fill Rate</dt>
<dd className="text-2xl font-semibold">{product.vendor_performance.order_fill_rate.toFixed(1)}%</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Avg Lead Time</dt>
<dd className="text-2xl font-semibold">{product.vendor_performance.avg_lead_time_days} days</dd>
</div>
</dl>
</Card>
<TabsContent value="vendor" className="space-y-4">
{isLoading ? (
<Skeleton className="h-48 w-full" />
) : product?.vendor_performance ? (
<div className="space-y-4">
<Card className="p-4">
<h3 className="font-semibold mb-2">Vendor Performance</h3>
<dl className="grid grid-cols-3 gap-4">
<div>
<dt className="text-sm text-muted-foreground">On-Time Delivery</dt>
<dd className="text-2xl font-semibold">{product.vendor_performance.on_time_delivery_rate.toFixed(1)}%</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Order Fill Rate</dt>
<dd className="text-2xl font-semibold">{product.vendor_performance.order_fill_rate.toFixed(1)}%</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Avg Lead Time</dt>
<dd className="text-2xl font-semibold">{product.vendor_performance.avg_lead_time_days} days</dd>
</div>
</dl>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Order History</h3>
<dl className="grid grid-cols-2 gap-4">
<div>
<dt className="text-sm text-muted-foreground">Total Orders</dt>
<dd>{product.vendor_performance.total_orders}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Late Orders</dt>
<dd>{product.vendor_performance.total_late_orders}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Total Purchase Value</dt>
<dd>${formatPrice(product.vendor_performance.total_purchase_value)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Avg Order Value</dt>
<dd>${formatPrice(product.vendor_performance.avg_order_value)}</dd>
</div>
</dl>
</Card>
</div>
) : (
<div className="text-center text-muted-foreground">No vendor performance data available</div>
)}
</TabsContent>
</Tabs>
</div>
</DrawerContent>
</Drawer>
<Card className="p-4">
<h3 className="font-semibold mb-2">Order History</h3>
<dl className="grid grid-cols-2 gap-4">
<div>
<dt className="text-sm text-muted-foreground">Total Orders</dt>
<dd>{product.vendor_performance.total_orders}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Late Orders</dt>
<dd>{product.vendor_performance.total_late_orders}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Total Purchase Value</dt>
<dd>${formatPrice(product.vendor_performance.total_purchase_value)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Avg Order Value</dt>
<dd>${formatPrice(product.vendor_performance.avg_order_value)}</dd>
</div>
</dl>
</Card>
</div>
) : (
<div className="text-center text-muted-foreground">No vendor performance data available</div>
)}
</TabsContent>
</Tabs>
</div>
</VaulDrawer.Content>
</VaulDrawer.Portal>
</VaulDrawer.Root>
);
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/analytics/categoryperformance.tsx","./src/components/analytics/priceanalysis.tsx","./src/components/analytics/profitanalysis.tsx","./src/components/analytics/stockanalysis.tsx","./src/components/analytics/vendorperformance.tsx","./src/components/dashboard/inventorystats.tsx","./src/components/dashboard/overview.tsx","./src/components/dashboard/recentsales.tsx","./src/components/dashboard/salesbycategory.tsx","./src/components/dashboard/trendingproducts.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/products/producteditdialog.tsx","./src/components/products/productfilters.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/settings/calculationsettings.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/performancemetrics.tsx","./src/components/settings/stockmanagement.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/hooks/use-mobile.tsx","./src/lib/utils.ts","./src/pages/analytics.tsx","./src/pages/dashboard.tsx","./src/pages/import.tsx","./src/pages/login.tsx","./src/pages/orders.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/settings.tsx"],"version":"5.6.3"}
{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/analytics/categoryperformance.tsx","./src/components/analytics/priceanalysis.tsx","./src/components/analytics/profitanalysis.tsx","./src/components/analytics/stockanalysis.tsx","./src/components/analytics/vendorperformance.tsx","./src/components/dashboard/inventoryhealthsummary.tsx","./src/components/dashboard/inventorystats.tsx","./src/components/dashboard/keymetricscharts.tsx","./src/components/dashboard/lowstockalerts.tsx","./src/components/dashboard/overview.tsx","./src/components/dashboard/recentsales.tsx","./src/components/dashboard/salesbycategory.tsx","./src/components/dashboard/trendingproducts.tsx","./src/components/dashboard/vendorperformance.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/products/productdetail.tsx","./src/components/products/producteditdialog.tsx","./src/components/products/productfilters.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/settings/calculationsettings.tsx","./src/components/settings/configuration.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/performancemetrics.tsx","./src/components/settings/stockmanagement.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/command.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/hooks/use-mobile.tsx","./src/lib/utils.ts","./src/pages/analytics.tsx","./src/pages/dashboard.tsx","./src/pages/import.tsx","./src/pages/login.tsx","./src/pages/orders.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/settings.tsx"],"version":"5.6.3"}

183
inventory/vite.config.js Normal file
View File

@@ -0,0 +1,183 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
import path from "path";
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { loadEnv } from "vite";
import fs from 'fs-extra';
// https://vitejs.dev/config/
export default defineConfig(function (_a) {
var mode = _a.mode;
var env = loadEnv(mode, process.cwd(), "");
var isDev = mode === 'development';
return {
plugins: [
react(),
{
name: 'copy-build',
closeBundle: function () { return __awaiter(void 0, void 0, void 0, function () {
var sourcePath, targetPath, error_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!!isDev) return [3 /*break*/, 6];
sourcePath = path.resolve(__dirname, 'build');
targetPath = path.resolve(__dirname, '../inventory-server/frontend/build');
_a.label = 1;
case 1:
_a.trys.push([1, 5, , 6]);
return [4 /*yield*/, fs.ensureDir(path.dirname(targetPath))];
case 2:
_a.sent();
return [4 /*yield*/, fs.remove(targetPath)];
case 3:
_a.sent();
return [4 /*yield*/, fs.copy(sourcePath, targetPath)];
case 4:
_a.sent();
console.log('Build files copied successfully to server directory!');
return [3 /*break*/, 6];
case 5:
error_1 = _a.sent();
console.error('Error copying build files:', error_1);
process.exit(1);
return [3 /*break*/, 6];
case 6: return [2 /*return*/];
}
});
}); }
}
],
define: {
'process.env.NODE_ENV': JSON.stringify(mode)
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
server: {
host: "0.0.0.0",
port: 5173,
proxy: {
"/api": {
target: "https://inventory.kent.pw",
changeOrigin: true,
secure: false,
ws: true,
xfwd: true,
cookieDomainRewrite: "",
withCredentials: true,
rewrite: function (path) { return path.replace(/^\/api/, "/api"); },
configure: function (proxy, _options) {
proxy.on("error", function (err, req, res) {
console.log("API proxy error:", err);
res.writeHead(500, {
"Content-Type": "application/json",
});
res.end(JSON.stringify({ error: "Proxy Error", message: err.message }));
});
proxy.on("proxyReq", function (proxyReq, req, _res) {
console.log("Outgoing request to API:", {
method: req.method,
url: req.url,
headers: proxyReq.getHeaders(),
});
});
proxy.on("proxyRes", function (proxyRes, req, _res) {
console.log("API Proxy response:", {
statusCode: proxyRes.statusCode,
url: req.url,
headers: proxyRes.headers,
});
});
},
},
"/auth-inv": {
target: "https://inventory.kent.pw",
changeOrigin: true,
secure: false,
ws: true,
xfwd: true,
cookieDomainRewrite: {
"inventory.kent.pw": "localhost"
},
withCredentials: true,
onProxyReq: function (proxyReq, req) {
// Add origin header to match CORS policy
proxyReq.setHeader('Origin', 'http://localhost:5173');
},
rewrite: function (path) { return path.replace(/^\/auth-inv/, "/auth-inv"); },
configure: function (proxy, _options) {
proxy.on("error", function (err, req, res) {
console.log("Auth proxy error:", err);
res.writeHead(500, {
"Content-Type": "application/json",
});
res.end(JSON.stringify({ error: "Proxy Error", message: err.message }));
});
proxy.on("proxyReq", function (proxyReq, req, _res) {
console.log("Outgoing request to Auth:", {
method: req.method,
url: req.url,
headers: proxyReq.getHeaders(),
});
});
proxy.on("proxyRes", function (proxyRes, req, _res) {
console.log("Auth Proxy response:", {
statusCode: proxyRes.statusCode,
url: req.url,
headers: proxyRes.headers,
});
});
},
},
},
},
build: {
outDir: "build",
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ["react", "react-dom", "react-router-dom"],
},
},
},
},
};
});