Add some additional existing data points to products table (partly broken)
This commit is contained in:
@@ -78,6 +78,55 @@ router.get('/', async (req, res) => {
|
|||||||
paramCounter++;
|
paramCounter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add new text filters for the additional fields
|
||||||
|
if (req.query.description) {
|
||||||
|
conditions.push(`p.description ILIKE $${paramCounter}`);
|
||||||
|
params.push(`%${req.query.description}%`);
|
||||||
|
paramCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.harmonized_tariff_code) {
|
||||||
|
conditions.push(`p.harmonized_tariff_code ILIKE $${paramCounter}`);
|
||||||
|
params.push(`%${req.query.harmonized_tariff_code}%`);
|
||||||
|
paramCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.notions_reference) {
|
||||||
|
conditions.push(`p.notions_reference ILIKE $${paramCounter}`);
|
||||||
|
params.push(`%${req.query.notions_reference}%`);
|
||||||
|
paramCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.line) {
|
||||||
|
conditions.push(`p.line ILIKE $${paramCounter}`);
|
||||||
|
params.push(`%${req.query.line}%`);
|
||||||
|
paramCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.subline) {
|
||||||
|
conditions.push(`p.subline ILIKE $${paramCounter}`);
|
||||||
|
params.push(`%${req.query.subline}%`);
|
||||||
|
paramCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.artist) {
|
||||||
|
conditions.push(`p.artist ILIKE $${paramCounter}`);
|
||||||
|
params.push(`%${req.query.artist}%`);
|
||||||
|
paramCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.country_of_origin) {
|
||||||
|
conditions.push(`p.country_of_origin ILIKE $${paramCounter}`);
|
||||||
|
params.push(`%${req.query.country_of_origin}%`);
|
||||||
|
paramCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.location) {
|
||||||
|
conditions.push(`p.location ILIKE $${paramCounter}`);
|
||||||
|
params.push(`%${req.query.location}%`);
|
||||||
|
paramCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle numeric filters with operators
|
// Handle numeric filters with operators
|
||||||
const numericFields = {
|
const numericFields = {
|
||||||
stock: 'p.stock_quantity',
|
stock: 'p.stock_quantity',
|
||||||
@@ -102,7 +151,16 @@ router.get('/', async (req, res) => {
|
|||||||
daysOfStock: 'pm.days_of_inventory',
|
daysOfStock: 'pm.days_of_inventory',
|
||||||
weeksOfStock: 'pm.weeks_of_inventory',
|
weeksOfStock: 'pm.weeks_of_inventory',
|
||||||
reorderPoint: 'pm.reorder_point',
|
reorderPoint: 'pm.reorder_point',
|
||||||
safetyStock: 'pm.safety_stock'
|
safetyStock: 'pm.safety_stock',
|
||||||
|
// Add new numeric fields
|
||||||
|
preorderCount: 'p.preorder_count',
|
||||||
|
notionsInvCount: 'p.notions_inv_count',
|
||||||
|
rating: 'p.rating',
|
||||||
|
reviews: 'p.reviews',
|
||||||
|
weight: 'p.weight',
|
||||||
|
totalSold: 'p.total_sold',
|
||||||
|
baskets: 'p.baskets',
|
||||||
|
notifies: 'p.notifies'
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(req.query).forEach(([key, value]) => {
|
Object.entries(req.query).forEach(([key, value]) => {
|
||||||
@@ -298,7 +356,8 @@ router.get('/', async (req, res) => {
|
|||||||
pm.last_received_date,
|
pm.last_received_date,
|
||||||
pm.abc_class,
|
pm.abc_class,
|
||||||
pm.stock_status,
|
pm.stock_status,
|
||||||
pm.turnover_rate
|
pm.turnover_rate,
|
||||||
|
p.date_last_sold
|
||||||
FROM products p
|
FROM products p
|
||||||
LEFT JOIN product_metrics pm ON p.pid = pm.pid
|
LEFT JOIN product_metrics pm ON p.pid = pm.pid
|
||||||
LEFT JOIN product_categories pc ON p.pid = pc.pid
|
LEFT JOIN product_categories pc ON p.pid = pc.pid
|
||||||
@@ -515,6 +574,29 @@ router.get('/:id', async (req, res) => {
|
|||||||
uom: parseInt(productRows[0].uom),
|
uom: parseInt(productRows[0].uom),
|
||||||
managing_stock: Boolean(productRows[0].managing_stock),
|
managing_stock: Boolean(productRows[0].managing_stock),
|
||||||
replenishable: Boolean(productRows[0].replenishable),
|
replenishable: Boolean(productRows[0].replenishable),
|
||||||
|
// Format new fields
|
||||||
|
preorder_count: parseInt(productRows[0].preorder_count || 0),
|
||||||
|
notions_inv_count: parseInt(productRows[0].notions_inv_count || 0),
|
||||||
|
harmonized_tariff_code: productRows[0].harmonized_tariff_code || '',
|
||||||
|
notions_reference: productRows[0].notions_reference || '',
|
||||||
|
line: productRows[0].line || '',
|
||||||
|
subline: productRows[0].subline || '',
|
||||||
|
artist: productRows[0].artist || '',
|
||||||
|
rating: parseFloat(productRows[0].rating || 0),
|
||||||
|
reviews: parseInt(productRows[0].reviews || 0),
|
||||||
|
weight: parseFloat(productRows[0].weight || 0),
|
||||||
|
dimensions: {
|
||||||
|
length: parseFloat(productRows[0].length || 0),
|
||||||
|
width: parseFloat(productRows[0].width || 0),
|
||||||
|
height: parseFloat(productRows[0].height || 0),
|
||||||
|
},
|
||||||
|
country_of_origin: productRows[0].country_of_origin || '',
|
||||||
|
location: productRows[0].location || '',
|
||||||
|
total_sold: parseInt(productRows[0].total_sold || 0),
|
||||||
|
baskets: parseInt(productRows[0].baskets || 0),
|
||||||
|
notifies: parseInt(productRows[0].notifies || 0),
|
||||||
|
date_last_sold: productRows[0].date_last_sold || null,
|
||||||
|
// Format existing analytics fields
|
||||||
daily_sales_avg: parseFloat(productRows[0].daily_sales_avg) || 0,
|
daily_sales_avg: parseFloat(productRows[0].daily_sales_avg) || 0,
|
||||||
weekly_sales_avg: parseFloat(productRows[0].weekly_sales_avg) || 0,
|
weekly_sales_avg: parseFloat(productRows[0].weekly_sales_avg) || 0,
|
||||||
monthly_sales_avg: parseFloat(productRows[0].monthly_sales_avg) || 0,
|
monthly_sales_avg: parseFloat(productRows[0].monthly_sales_avg) || 0,
|
||||||
|
|||||||
@@ -125,6 +125,11 @@ interface Product {
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
category_paths?: Record<string, string>;
|
category_paths?: Record<string, string>;
|
||||||
|
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
preorder_count: number;
|
||||||
|
notions_inv_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProductDetailProps {
|
interface ProductDetailProps {
|
||||||
@@ -225,6 +230,7 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
|
|||||||
<TabsTrigger value="purchase">Purchase History</TabsTrigger>
|
<TabsTrigger value="purchase">Purchase History</TabsTrigger>
|
||||||
<TabsTrigger value="financial">Financial</TabsTrigger>
|
<TabsTrigger value="financial">Financial</TabsTrigger>
|
||||||
<TabsTrigger value="vendor">Vendor</TabsTrigger>
|
<TabsTrigger value="vendor">Vendor</TabsTrigger>
|
||||||
|
<TabsTrigger value="details">Additional Info</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -255,6 +261,12 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
|
|||||||
<dt className="text-sm text-muted-foreground">UPC</dt>
|
<dt className="text-sm text-muted-foreground">UPC</dt>
|
||||||
<dd>{product?.barcode || "N/A"}</dd>
|
<dd>{product?.barcode || "N/A"}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
{product?.description && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Description</dt>
|
||||||
|
<dd>{product.description}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-sm text-muted-foreground">Categories</dt>
|
<dt className="text-sm text-muted-foreground">Categories</dt>
|
||||||
<dd className="flex flex-col gap-2">
|
<dd className="flex flex-col gap-2">
|
||||||
@@ -359,6 +371,51 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<h3 className="font-semibold mb-2">Customer Engagement</h3>
|
||||||
|
<dl className="grid grid-cols-3 gap-4">
|
||||||
|
{product?.total_sold > 0 && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Total Sold</dt>
|
||||||
|
<dd className="text-2xl font-semibold">{product.total_sold}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{product?.rating > 0 && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Rating</dt>
|
||||||
|
<dd className="text-2xl font-semibold flex items-center">
|
||||||
|
{product.rating.toFixed(1)}
|
||||||
|
<span className="ml-1 text-yellow-500">★</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{product?.reviews > 0 && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Reviews</dt>
|
||||||
|
<dd className="text-2xl font-semibold">{product.reviews}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{product?.baskets > 0 && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">In Baskets</dt>
|
||||||
|
<dd className="text-2xl font-semibold">{product.baskets}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{product?.notifies > 0 && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Notify Requests</dt>
|
||||||
|
<dd className="text-2xl font-semibold">{product.notifies}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{product?.date_last_sold && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Last Sold</dt>
|
||||||
|
<dd className="text-xl font-semibold">{formatDate(product.date_last_sold)}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</dl>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
<h3 className="font-semibold mb-2">Financial Metrics</h3>
|
<h3 className="font-semibold mb-2">Financial Metrics</h3>
|
||||||
<dl className="space-y-2">
|
<dl className="space-y-2">
|
||||||
@@ -426,6 +483,18 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
|
|||||||
<dt className="text-sm text-muted-foreground">Days of Inventory</dt>
|
<dt className="text-sm text-muted-foreground">Days of Inventory</dt>
|
||||||
<dd className="text-2xl font-semibold">{product?.metrics?.days_of_inventory || 0}</dd>
|
<dd className="text-2xl font-semibold">{product?.metrics?.days_of_inventory || 0}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
{product?.preorder_count > 0 && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Preorders</dt>
|
||||||
|
<dd className="text-2xl font-semibold">{product?.preorder_count}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{product?.notions_inv_count > 0 && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Notions Inventory</dt>
|
||||||
|
<dd className="text-2xl font-semibold">{product?.notions_inv_count}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</dl>
|
</dl>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -506,6 +575,51 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
|
|||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<h3 className="font-semibold mb-2">Customer Engagement</h3>
|
||||||
|
<dl className="grid grid-cols-3 gap-4">
|
||||||
|
{product?.total_sold > 0 && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Total Sold</dt>
|
||||||
|
<dd className="text-2xl font-semibold">{product.total_sold}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{product?.rating > 0 && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Rating</dt>
|
||||||
|
<dd className="text-2xl font-semibold flex items-center">
|
||||||
|
{product.rating.toFixed(1)}
|
||||||
|
<span className="ml-1 text-yellow-500">★</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{product?.reviews > 0 && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Reviews</dt>
|
||||||
|
<dd className="text-2xl font-semibold">{product.reviews}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{product?.baskets > 0 && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">In Baskets</dt>
|
||||||
|
<dd className="text-2xl font-semibold">{product.baskets}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{product?.notifies > 0 && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Notify Requests</dt>
|
||||||
|
<dd className="text-2xl font-semibold">{product.notifies}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{product?.date_last_sold && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Last Sold</dt>
|
||||||
|
<dd className="text-xl font-semibold">{formatDate(product.date_last_sold)}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</dl>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@@ -661,6 +775,123 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
|
|||||||
<div className="text-center text-muted-foreground">No vendor performance data available</div>
|
<div className="text-center text-muted-foreground">No vendor performance data available</div>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="details" className="p-4">
|
||||||
|
{isLoading ? (
|
||||||
|
<Skeleton className="h-48 w-full" />
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Card className="p-4">
|
||||||
|
<h3 className="font-semibold mb-2">Product Details</h3>
|
||||||
|
<dl className="grid grid-cols-2 gap-4">
|
||||||
|
{product?.description && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<dt className="text-sm text-muted-foreground">Description</dt>
|
||||||
|
<dd>{product.description}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Created Date</dt>
|
||||||
|
<dd>{formatDate(product?.created_at)}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Last Updated</dt>
|
||||||
|
<dd>{formatDate(product?.updated_at)}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Product ID</dt>
|
||||||
|
<dd>{product?.pid}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Line</dt>
|
||||||
|
<dd>{product?.line || 'N/A'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Subline</dt>
|
||||||
|
<dd>{product?.subline || 'N/A'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Artist</dt>
|
||||||
|
<dd>{product?.artist || 'N/A'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Country of Origin</dt>
|
||||||
|
<dd>{product?.country_of_origin || 'N/A'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Location</dt>
|
||||||
|
<dd>{product?.location || 'N/A'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">HTS Code</dt>
|
||||||
|
<dd>{product?.harmonized_tariff_code || 'N/A'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Notions Reference</dt>
|
||||||
|
<dd>{product?.notions_reference || 'N/A'}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<h3 className="font-semibold mb-2">Physical Attributes</h3>
|
||||||
|
<dl className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Weight</dt>
|
||||||
|
<dd>{product?.weight ? `${product.weight} kg` : 'N/A'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Dimensions</dt>
|
||||||
|
<dd>
|
||||||
|
{product?.dimensions
|
||||||
|
? `${product.dimensions.length} × ${product.dimensions.width} × ${product.dimensions.height} cm`
|
||||||
|
: 'N/A'
|
||||||
|
}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<h3 className="font-semibold mb-2">Customer Metrics</h3>
|
||||||
|
<dl className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Rating</dt>
|
||||||
|
<dd className="flex items-center">
|
||||||
|
{product?.rating
|
||||||
|
? <>
|
||||||
|
{product.rating.toFixed(1)}
|
||||||
|
<span className="ml-1 text-yellow-500">★</span>
|
||||||
|
</>
|
||||||
|
: 'N/A'
|
||||||
|
}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Review Count</dt>
|
||||||
|
<dd>{product?.reviews || 'N/A'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Total Sold</dt>
|
||||||
|
<dd>{product?.total_sold || 'N/A'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Currently in Baskets</dt>
|
||||||
|
<dd>{product?.baskets || 'N/A'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Notify Requests</dt>
|
||||||
|
<dd>{product?.notifies || 'N/A'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-muted-foreground">Date Last Sold</dt>
|
||||||
|
<dd>{formatDate(product?.date_last_sold) || 'N/A'}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</VaulDrawer.Content>
|
</VaulDrawer.Content>
|
||||||
</VaulDrawer.Portal>
|
</VaulDrawer.Portal>
|
||||||
|
|||||||
@@ -56,6 +56,23 @@ const FILTER_OPTIONS: FilterOption[] = [
|
|||||||
{ id: "vendor_reference", label: "Supplier #", type: "text", group: "Basic Info" },
|
{ id: "vendor_reference", label: "Supplier #", type: "text", group: "Basic Info" },
|
||||||
{ id: "brand", label: "Brand", type: "select", group: "Basic Info" },
|
{ id: "brand", label: "Brand", type: "select", group: "Basic Info" },
|
||||||
{ id: "category", label: "Category", type: "select", group: "Basic Info" },
|
{ id: "category", label: "Category", type: "select", group: "Basic Info" },
|
||||||
|
{ id: "description", label: "Description", type: "text", group: "Basic Info" },
|
||||||
|
{ id: "harmonized_tariff_code", label: "HTS Code", type: "text", group: "Basic Info" },
|
||||||
|
{ id: "notions_reference", label: "Notions Ref", type: "text", group: "Basic Info" },
|
||||||
|
{ id: "line", label: "Line", type: "text", group: "Basic Info" },
|
||||||
|
{ id: "subline", label: "Subline", type: "text", group: "Basic Info" },
|
||||||
|
{ id: "artist", label: "Artist", type: "text", group: "Basic Info" },
|
||||||
|
{ id: "country_of_origin", label: "Origin", type: "text", group: "Basic Info" },
|
||||||
|
{ id: "location", label: "Location", type: "text", group: "Basic Info" },
|
||||||
|
|
||||||
|
// Physical Properties
|
||||||
|
{
|
||||||
|
id: "weight",
|
||||||
|
label: "Weight",
|
||||||
|
type: "number",
|
||||||
|
group: "Physical Properties",
|
||||||
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
|
},
|
||||||
|
|
||||||
// Inventory Group
|
// Inventory Group
|
||||||
{
|
{
|
||||||
@@ -79,6 +96,20 @@ const FILTER_OPTIONS: FilterOption[] = [
|
|||||||
group: "Inventory",
|
group: "Inventory",
|
||||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "preorderCount",
|
||||||
|
label: "Preorder Count",
|
||||||
|
type: "number",
|
||||||
|
group: "Inventory",
|
||||||
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "notionsInvCount",
|
||||||
|
label: "Notions Inventory",
|
||||||
|
type: "number",
|
||||||
|
group: "Inventory",
|
||||||
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "daysOfStock",
|
id: "daysOfStock",
|
||||||
label: "Days of Stock",
|
label: "Days of Stock",
|
||||||
@@ -200,6 +231,47 @@ const FILTER_OPTIONS: FilterOption[] = [
|
|||||||
type: "text",
|
type: "text",
|
||||||
group: "Sales Metrics",
|
group: "Sales Metrics",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "date_last_sold",
|
||||||
|
label: "Date Last Sold",
|
||||||
|
type: "text",
|
||||||
|
group: "Sales Metrics",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "total_sold",
|
||||||
|
label: "Total Sold",
|
||||||
|
type: "number",
|
||||||
|
group: "Sales Metrics",
|
||||||
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "baskets",
|
||||||
|
label: "In Baskets",
|
||||||
|
type: "number",
|
||||||
|
group: "Sales Metrics",
|
||||||
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "notifies",
|
||||||
|
label: "Notifies",
|
||||||
|
type: "number",
|
||||||
|
group: "Sales Metrics",
|
||||||
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "rating",
|
||||||
|
label: "Rating",
|
||||||
|
type: "number",
|
||||||
|
group: "Sales Metrics",
|
||||||
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "reviews",
|
||||||
|
label: "Reviews",
|
||||||
|
type: "number",
|
||||||
|
group: "Sales Metrics",
|
||||||
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
|
},
|
||||||
|
|
||||||
// Financial Metrics Group
|
// Financial Metrics Group
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -234,6 +234,11 @@ export function ProductTable({
|
|||||||
)) || '-'}
|
)) || '-'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
case 'dimensions':
|
||||||
|
if (value) {
|
||||||
|
return `${value.length}×${value.width}×${value.height}`;
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
case 'stock_status':
|
case 'stock_status':
|
||||||
return getStockStatus(product.stock_status);
|
return getStockStatus(product.stock_status);
|
||||||
case 'abc_class':
|
case 'abc_class':
|
||||||
@@ -252,6 +257,14 @@ export function ProductTable({
|
|||||||
) : (
|
) : (
|
||||||
<Badge variant="outline">Non-Replenishable</Badge>
|
<Badge variant="outline">Non-Replenishable</Badge>
|
||||||
);
|
);
|
||||||
|
case 'rating':
|
||||||
|
if (value === undefined || value === null) return '-';
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
{value.toFixed(1)}
|
||||||
|
<span className="ml-1 text-yellow-500">★</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
if (columnDef?.format && value !== undefined && value !== null) {
|
if (columnDef?.format && value !== undefined && value !== null) {
|
||||||
// For numeric formats (those using toFixed), ensure the value is a number
|
// For numeric formats (those using toFixed), ensure the value is a number
|
||||||
|
|||||||
@@ -52,8 +52,25 @@ const AVAILABLE_COLUMNS: ColumnDef[] = [
|
|||||||
{ key: 'vendor', label: 'Supplier', group: 'Basic Info' },
|
{ key: 'vendor', label: 'Supplier', group: 'Basic Info' },
|
||||||
{ key: 'vendor_reference', label: 'Supplier #', group: 'Basic Info' },
|
{ key: 'vendor_reference', label: 'Supplier #', group: 'Basic Info' },
|
||||||
{ key: 'barcode', label: 'UPC', group: 'Basic Info' },
|
{ key: 'barcode', label: 'UPC', group: 'Basic Info' },
|
||||||
|
{ key: 'description', label: 'Description', group: 'Basic Info' },
|
||||||
|
{ key: 'created_at', label: 'Created', group: 'Basic Info' },
|
||||||
|
{ key: 'harmonized_tariff_code', label: 'HTS Code', group: 'Basic Info' },
|
||||||
|
{ key: 'notions_reference', label: 'Notions Ref', group: 'Basic Info' },
|
||||||
|
{ key: 'line', label: 'Line', group: 'Basic Info' },
|
||||||
|
{ key: 'subline', label: 'Subline', group: 'Basic Info' },
|
||||||
|
{ key: 'artist', label: 'Artist', group: 'Basic Info' },
|
||||||
|
{ key: 'country_of_origin', label: 'Origin', group: 'Basic Info' },
|
||||||
|
{ key: 'location', label: 'Location', group: 'Basic Info' },
|
||||||
|
|
||||||
|
// Physical properties
|
||||||
|
{ key: 'weight', label: 'Weight', group: 'Physical', format: (v) => v?.toString() ?? '-' },
|
||||||
|
{ key: 'dimensions', label: 'Dimensions', group: 'Physical', format: (v) => v ? `${v.length}x${v.width}x${v.height}` : '-' },
|
||||||
|
|
||||||
|
// Stock columns
|
||||||
{ key: 'stock_quantity', label: 'Shelf Count', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
{ key: 'stock_quantity', label: 'Shelf Count', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
||||||
{ key: 'stock_status', label: 'Stock Status', group: 'Stock' },
|
{ key: 'stock_status', label: 'Stock Status', group: 'Stock' },
|
||||||
|
{ key: 'preorder_count', label: 'Preorders', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
||||||
|
{ key: 'notions_inv_count', label: 'Notions Inv', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
||||||
{ key: 'days_of_inventory', label: 'Days of Stock', group: 'Stock', format: (v) => v?.toFixed(1) ?? '-' },
|
{ key: 'days_of_inventory', label: 'Days of Stock', group: 'Stock', format: (v) => v?.toFixed(1) ?? '-' },
|
||||||
{ key: 'weeks_of_inventory', label: 'Weeks of Stock', group: 'Stock', format: (v) => v?.toFixed(1) ?? '-' },
|
{ key: 'weeks_of_inventory', label: 'Weeks of Stock', group: 'Stock', format: (v) => v?.toFixed(1) ?? '-' },
|
||||||
{ key: 'abc_class', label: 'ABC Class', group: 'Stock' },
|
{ key: 'abc_class', label: 'ABC Class', group: 'Stock' },
|
||||||
@@ -63,10 +80,14 @@ const AVAILABLE_COLUMNS: ColumnDef[] = [
|
|||||||
{ key: 'reorder_point', label: 'Reorder Point', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
{ key: 'reorder_point', label: 'Reorder Point', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
||||||
{ key: 'safety_stock', label: 'Safety Stock', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
{ key: 'safety_stock', label: 'Safety Stock', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
||||||
{ key: 'overstocked_amt', label: 'Overstock Amt', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
{ key: 'overstocked_amt', label: 'Overstock Amt', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
||||||
|
|
||||||
|
// Pricing columns
|
||||||
{ key: 'price', label: 'Price', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' },
|
{ key: 'price', label: 'Price', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' },
|
||||||
{ key: 'regular_price', label: 'Default Price', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' },
|
{ key: 'regular_price', label: 'Default Price', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' },
|
||||||
{ key: 'cost_price', label: 'Cost', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' },
|
{ key: 'cost_price', label: 'Cost', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' },
|
||||||
{ key: 'landing_cost_price', label: 'Landing Cost', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' },
|
{ key: 'landing_cost_price', label: 'Landing Cost', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' },
|
||||||
|
|
||||||
|
// Sales columns
|
||||||
{ key: 'daily_sales_avg', label: 'Daily Sales', group: 'Sales', format: (v) => v?.toFixed(1) ?? '-' },
|
{ key: 'daily_sales_avg', label: 'Daily Sales', group: 'Sales', format: (v) => v?.toFixed(1) ?? '-' },
|
||||||
{ key: 'weekly_sales_avg', label: 'Weekly Sales', group: 'Sales', format: (v) => v?.toFixed(1) ?? '-' },
|
{ key: 'weekly_sales_avg', label: 'Weekly Sales', group: 'Sales', format: (v) => v?.toFixed(1) ?? '-' },
|
||||||
{ key: 'monthly_sales_avg', label: 'Monthly Sales', group: 'Sales', format: (v) => v?.toFixed(1) ?? '-' },
|
{ key: 'monthly_sales_avg', label: 'Monthly Sales', group: 'Sales', format: (v) => v?.toFixed(1) ?? '-' },
|
||||||
@@ -74,12 +95,22 @@ const AVAILABLE_COLUMNS: ColumnDef[] = [
|
|||||||
{ key: 'number_of_orders', label: 'Order Count', group: 'Sales', format: (v) => v?.toString() ?? '-' },
|
{ key: 'number_of_orders', label: 'Order Count', group: 'Sales', format: (v) => v?.toString() ?? '-' },
|
||||||
{ key: 'first_sale_date', label: 'First Sale', group: 'Sales' },
|
{ key: 'first_sale_date', label: 'First Sale', group: 'Sales' },
|
||||||
{ key: 'last_sale_date', label: 'Last Sale', group: 'Sales' },
|
{ key: 'last_sale_date', label: 'Last Sale', group: 'Sales' },
|
||||||
|
{ key: 'date_last_sold', label: 'Date Last Sold', group: 'Sales' },
|
||||||
|
{ key: 'total_sold', label: 'Total Sold', group: 'Sales', format: (v) => v?.toString() ?? '-' },
|
||||||
|
{ key: 'baskets', label: 'In Baskets', group: 'Sales', format: (v) => v?.toString() ?? '-' },
|
||||||
|
{ key: 'notifies', label: 'Notifies', group: 'Sales', format: (v) => v?.toString() ?? '-' },
|
||||||
|
{ key: 'rating', label: 'Rating', group: 'Sales', format: (v) => v ? v.toFixed(1) : '-' },
|
||||||
|
{ key: 'reviews', label: 'Reviews', group: 'Sales', format: (v) => v?.toString() ?? '-' },
|
||||||
|
|
||||||
|
// Financial columns
|
||||||
{ key: 'gmroi', label: 'GMROI', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' },
|
{ key: 'gmroi', label: 'GMROI', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' },
|
||||||
{ key: 'turnover_rate', label: 'Turnover Rate', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' },
|
{ key: 'turnover_rate', label: 'Turnover Rate', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' },
|
||||||
{ key: 'avg_margin_percent', label: 'Margin %', group: 'Financial', format: (v) => v ? `${v.toFixed(1)}%` : '-' },
|
{ key: 'avg_margin_percent', label: 'Margin %', group: 'Financial', format: (v) => v ? `${v.toFixed(1)}%` : '-' },
|
||||||
{ key: 'inventory_value', label: 'Inventory Value', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' },
|
{ key: 'inventory_value', label: 'Inventory Value', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' },
|
||||||
{ key: 'cost_of_goods_sold', label: 'COGS', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' },
|
{ key: 'cost_of_goods_sold', label: 'COGS', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' },
|
||||||
{ key: 'gross_profit', label: 'Gross Profit', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' },
|
{ key: 'gross_profit', label: 'Gross Profit', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' },
|
||||||
|
|
||||||
|
// Lead Time columns
|
||||||
{ key: 'current_lead_time', label: 'Current Lead Time', group: 'Lead Time', format: (v) => v?.toFixed(1) ?? '-' },
|
{ key: 'current_lead_time', label: 'Current Lead Time', group: 'Lead Time', format: (v) => v?.toFixed(1) ?? '-' },
|
||||||
{ key: 'target_lead_time', label: 'Target Lead Time', group: 'Lead Time', format: (v) => v?.toFixed(1) ?? '-' },
|
{ key: 'target_lead_time', label: 'Target Lead Time', group: 'Lead Time', format: (v) => v?.toFixed(1) ?? '-' },
|
||||||
{ key: 'lead_time_status', label: 'Lead Time Status', group: 'Lead Time' },
|
{ key: 'lead_time_status', label: 'Lead Time Status', group: 'Lead Time' },
|
||||||
|
|||||||
@@ -23,6 +23,30 @@ export interface Product {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|
||||||
|
// New fields
|
||||||
|
description?: string;
|
||||||
|
preorder_count?: number;
|
||||||
|
notions_inv_count?: number;
|
||||||
|
harmonized_tariff_code?: string;
|
||||||
|
notions_reference?: string;
|
||||||
|
line?: string;
|
||||||
|
subline?: string;
|
||||||
|
artist?: string;
|
||||||
|
rating?: number;
|
||||||
|
reviews?: number;
|
||||||
|
weight?: number;
|
||||||
|
dimensions?: {
|
||||||
|
length: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
country_of_origin?: string;
|
||||||
|
location?: string;
|
||||||
|
total_sold?: number;
|
||||||
|
baskets?: number;
|
||||||
|
notifies?: number;
|
||||||
|
date_last_sold?: string;
|
||||||
|
|
||||||
// Metrics
|
// Metrics
|
||||||
daily_sales_avg?: string; // numeric(15,3)
|
daily_sales_avg?: string; // numeric(15,3)
|
||||||
weekly_sales_avg?: string; // numeric(15,3)
|
weekly_sales_avg?: string; // numeric(15,3)
|
||||||
|
|||||||
Reference in New Issue
Block a user