Opus corrections/fixes/additions
This commit is contained in:
@@ -16,7 +16,8 @@ import { Badge } from "@/components/ui/badge";
|
||||
type BrandSortableColumns =
|
||||
| 'brandName' | 'productCount' | 'activeProductCount' | 'currentStockUnits'
|
||||
| 'currentStockCost' | 'currentStockRetail' | 'revenue_7d' | 'revenue_30d'
|
||||
| 'profit_30d' | 'sales_30d' | 'avg_margin_30d' | 'stock_turn_30d' | 'status'; // Add more as needed
|
||||
| 'profit_30d' | 'sales_30d' | 'avg_margin_30d' | 'stock_turn_30d'
|
||||
| 'salesGrowth30dVsPrev' | 'revenueGrowth30dVsPrev' | 'status';
|
||||
|
||||
interface BrandMetric {
|
||||
brand_id: string | number;
|
||||
@@ -40,6 +41,9 @@ interface BrandMetric {
|
||||
lifetime_revenue: string | number;
|
||||
avg_margin_30d: string | number | null;
|
||||
stock_turn_30d: string | number | null;
|
||||
// Growth metrics
|
||||
sales_growth_30d_vs_prev: string | number | null;
|
||||
revenue_growth_30d_vs_prev: string | number | null;
|
||||
status: string;
|
||||
brand_status: string;
|
||||
description: string;
|
||||
@@ -57,6 +61,8 @@ interface BrandMetric {
|
||||
lifetimeRevenue: string | number;
|
||||
avgMargin_30d: string | number | null;
|
||||
stockTurn_30d: string | number | null;
|
||||
salesGrowth30dVsPrev: string | number | null;
|
||||
revenueGrowth30dVsPrev: string | number | null;
|
||||
}
|
||||
|
||||
// Define response type to avoid type errors
|
||||
@@ -140,6 +146,19 @@ const formatPercentage = (value: number | string | null | undefined, digits = 1)
|
||||
return `${value.toFixed(digits)}%`;
|
||||
};
|
||||
|
||||
// Growth formatting with color coding
|
||||
const formatGrowth = (value: number | string | null | undefined, digits = 1) => {
|
||||
if (value == null) return <span className="text-muted-foreground">N/A</span>;
|
||||
|
||||
const numValue = typeof value === "string" ? parseFloat(value) : value;
|
||||
if (isNaN(numValue)) return <span className="text-muted-foreground">N/A</span>;
|
||||
|
||||
const formatted = `${numValue >= 0 ? '+' : ''}${numValue.toFixed(digits)}%`;
|
||||
const colorClass = numValue >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
|
||||
return <span className={colorClass}>{formatted}</span>;
|
||||
};
|
||||
|
||||
const getStatusVariant = (status: string): "default" | "secondary" | "outline" | "destructive" => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
@@ -361,6 +380,8 @@ export function Brands() {
|
||||
<TableHead onClick={() => handleSort("profit_30d")} className="cursor-pointer text-right">Profit (30d)</TableHead>
|
||||
<TableHead onClick={() => handleSort("avg_margin_30d")} className="cursor-pointer text-right">Margin (30d)</TableHead>
|
||||
<TableHead onClick={() => handleSort("stock_turn_30d")} className="cursor-pointer text-right">Stock Turn (30d)</TableHead>
|
||||
<TableHead onClick={() => handleSort("salesGrowth30dVsPrev")} className="cursor-pointer text-right">Sales Growth</TableHead>
|
||||
<TableHead onClick={() => handleSort("revenueGrowth30dVsPrev")} className="cursor-pointer text-right">Revenue Growth</TableHead>
|
||||
<TableHead onClick={() => handleSort("status")} className="cursor-pointer text-right">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -378,17 +399,19 @@ export function Brands() {
|
||||
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
|
||||
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
|
||||
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
|
||||
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
|
||||
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : listError ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-center py-8 text-destructive">
|
||||
<TableCell colSpan={12} className="text-center py-8 text-destructive">
|
||||
Error loading brands: {listError.message}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : brands.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-center py-8 text-muted-foreground">
|
||||
<TableCell colSpan={12} className="text-center py-8 text-muted-foreground">
|
||||
No brands found matching your criteria.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -404,6 +427,8 @@ export function Brands() {
|
||||
<TableCell className="text-right">{formatCurrency(brand.profit_30d as number)}</TableCell>
|
||||
<TableCell className="text-right">{formatPercentage(brand.avg_margin_30d as number)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(brand.stock_turn_30d, 2)}</TableCell>
|
||||
<TableCell className="text-right">{formatGrowth(brand.sales_growth_30d_vs_prev)}</TableCell>
|
||||
<TableCell className="text-right">{formatGrowth(brand.revenue_growth_30d_vs_prev)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Badge variant={getStatusVariant(brand.status)}>
|
||||
{brand.status || 'Unknown'}
|
||||
|
||||
@@ -60,6 +60,8 @@ type CategorySortableColumns =
|
||||
| "sales30d"
|
||||
| "avgMargin30d"
|
||||
| "stockTurn30d"
|
||||
| "salesGrowth30dVsPrev"
|
||||
| "revenueGrowth30dVsPrev"
|
||||
| "status";
|
||||
|
||||
interface CategoryMetric {
|
||||
@@ -88,6 +90,9 @@ interface CategoryMetric {
|
||||
lifetime_revenue: string | number;
|
||||
avg_margin_30d: string | number | null;
|
||||
stock_turn_30d: string | number | null;
|
||||
// Growth metrics
|
||||
sales_growth_30d_vs_prev: string | number | null;
|
||||
revenue_growth_30d_vs_prev: string | number | null;
|
||||
// Fields from categories table
|
||||
status: string;
|
||||
description: string;
|
||||
@@ -108,6 +113,8 @@ interface CategoryMetric {
|
||||
lifetimeRevenue: string | number;
|
||||
avgMargin_30d: string | number | null;
|
||||
stockTurn_30d: string | number | null;
|
||||
salesGrowth30dVsPrev: string | number | null;
|
||||
revenueGrowth30dVsPrev: string | number | null;
|
||||
direct_active_product_count: number;
|
||||
direct_current_stock_units: number;
|
||||
direct_stock_cost: string | number;
|
||||
@@ -208,6 +215,19 @@ const formatPercentage = (
|
||||
return `${value.toFixed(digits)}%`;
|
||||
};
|
||||
|
||||
// Growth formatting with color coding
|
||||
const formatGrowth = (value: number | string | null | undefined, digits = 1) => {
|
||||
if (value == null) return <span className="text-muted-foreground">N/A</span>;
|
||||
|
||||
const numValue = typeof value === "string" ? parseFloat(value) : value;
|
||||
if (isNaN(numValue)) return <span className="text-muted-foreground">N/A</span>;
|
||||
|
||||
const formatted = `${numValue >= 0 ? '+' : ''}${numValue.toFixed(digits)}%`;
|
||||
const colorClass = numValue >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
|
||||
return <span className={colorClass}>{formatted}</span>;
|
||||
};
|
||||
|
||||
// Define interfaces for hierarchical structure
|
||||
interface CategoryWithChildren extends CategoryMetric {
|
||||
children: CategoryWithChildren[];
|
||||
@@ -221,6 +241,8 @@ interface CategoryWithChildren extends CategoryMetric {
|
||||
revenue30d: number;
|
||||
profit30d: number;
|
||||
avg_margin_30d?: number;
|
||||
sales_growth_30d_vs_prev?: number;
|
||||
revenue_growth_30d_vs_prev?: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -683,7 +705,9 @@ export function Categories() {
|
||||
profit30d: totals.profit30d,
|
||||
avg_margin_30d: totals.revenue30d > 0
|
||||
? (totals.profit30d / totals.revenue30d) * 100
|
||||
: 0
|
||||
: 0,
|
||||
sales_growth_30d_vs_prev: parseFloat(cat.sales_growth_30d_vs_prev?.toString() || "0"),
|
||||
revenue_growth_30d_vs_prev: parseFloat(cat.revenue_growth_30d_vs_prev?.toString() || "0")
|
||||
};
|
||||
} else {
|
||||
// If we don't have pre-calculated values (shouldn't happen with our algorithm)
|
||||
@@ -694,7 +718,9 @@ export function Categories() {
|
||||
currentStockCost: parseFloat(cat.direct_stock_cost?.toString() || "0"),
|
||||
revenue30d: parseFloat(cat.direct_revenue_30d?.toString() || "0"),
|
||||
profit30d: parseFloat(cat.direct_profit_30d?.toString() || "0"),
|
||||
avg_margin_30d: parseFloat(cat.avg_margin_30d?.toString() || "0")
|
||||
avg_margin_30d: parseFloat(cat.avg_margin_30d?.toString() || "0"),
|
||||
sales_growth_30d_vs_prev: parseFloat(cat.sales_growth_30d_vs_prev?.toString() || "0"),
|
||||
revenue_growth_30d_vs_prev: parseFloat(cat.revenue_growth_30d_vs_prev?.toString() || "0")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -953,6 +979,56 @@ export function Categories() {
|
||||
formatPercentage(category.avg_margin_30d)}
|
||||
</TableCell>
|
||||
|
||||
{/* Sales Growth Cell */}
|
||||
<TableCell className="h-16 py-2 text-right">
|
||||
{hasChildren && category.aggregatedStats ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="font-medium cursor-help">
|
||||
{formatGrowth(category.aggregatedStats.sales_growth_30d_vs_prev)}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Sales Growth (incl. children):{" "}
|
||||
{formatGrowth(category.aggregatedStats.sales_growth_30d_vs_prev)}
|
||||
</p>
|
||||
<p>
|
||||
Directly from '{category.category_name}':{" "}
|
||||
{formatGrowth(category.sales_growth_30d_vs_prev)}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
formatGrowth(category.sales_growth_30d_vs_prev)
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
{/* Revenue Growth Cell */}
|
||||
<TableCell className="h-16 py-2 text-right">
|
||||
{hasChildren && category.aggregatedStats ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="font-medium cursor-help">
|
||||
{formatGrowth(category.aggregatedStats.revenue_growth_30d_vs_prev)}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Revenue Growth (incl. children):{" "}
|
||||
{formatGrowth(category.aggregatedStats.revenue_growth_30d_vs_prev)}
|
||||
</p>
|
||||
<p>
|
||||
Directly from '{category.category_name}':{" "}
|
||||
{formatGrowth(category.revenue_growth_30d_vs_prev)}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
formatGrowth(category.revenue_growth_30d_vs_prev)
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
{/* Stock Turn (30d) Cell - Display direct value */}
|
||||
<TableCell className="h-16 py-2 text-right">
|
||||
{formatNumber(category.stock_turn_30d, 2)}
|
||||
@@ -1009,6 +1085,9 @@ export function Categories() {
|
||||
<TableCell className="text-right w-[8%]">
|
||||
<Skeleton className="h-5 w-full ml-auto" />
|
||||
</TableCell>
|
||||
<TableCell className="text-right w-[8%]">
|
||||
<Skeleton className="h-5 w-full ml-auto" />
|
||||
</TableCell>
|
||||
<TableCell className="text-right w-[6%]">
|
||||
<Skeleton className="h-5 w-full ml-auto" />
|
||||
</TableCell>
|
||||
@@ -1027,7 +1106,7 @@ export function Categories() {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={11}
|
||||
colSpan={13}
|
||||
className="h-16 text-center py-8 text-muted-foreground"
|
||||
>
|
||||
{categories && categories.length > 0 ? (
|
||||
@@ -1321,6 +1400,20 @@ export function Categories() {
|
||||
Margin (30d)
|
||||
<SortIndicator active={sortColumn === "avgMargin30d"} />
|
||||
</TableHead>
|
||||
<TableHead
|
||||
onClick={() => handleSort("salesGrowth30dVsPrev")}
|
||||
className="cursor-pointer text-right w-[8%]"
|
||||
>
|
||||
Sales Growth
|
||||
<SortIndicator active={sortColumn === "salesGrowth30dVsPrev"} />
|
||||
</TableHead>
|
||||
<TableHead
|
||||
onClick={() => handleSort("revenueGrowth30dVsPrev")}
|
||||
className="cursor-pointer text-right w-[8%]"
|
||||
>
|
||||
Revenue Growth
|
||||
<SortIndicator active={sortColumn === "revenueGrowth30dVsPrev"} />
|
||||
</TableHead>
|
||||
<TableHead
|
||||
onClick={() => handleSort("stockTurn30d")}
|
||||
className="cursor-pointer text-right w-[6%]"
|
||||
|
||||
@@ -182,6 +182,32 @@ const AVAILABLE_COLUMNS: ColumnDef[] = [
|
||||
{ key: 'first60DaysRevenue', label: 'First 60 Days Revenue', group: 'First Period', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' },
|
||||
{ key: 'first90DaysSales', label: 'First 90 Days Sales', group: 'First Period', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' },
|
||||
{ key: 'first90DaysRevenue', label: 'First 90 Days Revenue', group: 'First Period', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' },
|
||||
|
||||
// Growth Metrics
|
||||
{ key: 'salesGrowth30dVsPrev', label: 'Sales Growth % (30d vs Prev)', group: 'Growth Analysis', format: (v) => v === 0 ? '0%' : v ? `${v.toFixed(1)}%` : '-' },
|
||||
{ key: 'revenueGrowth30dVsPrev', label: 'Revenue Growth % (30d vs Prev)', group: 'Growth Analysis', format: (v) => v === 0 ? '0%' : v ? `${v.toFixed(1)}%` : '-' },
|
||||
{ key: 'salesGrowthYoy', label: 'Sales Growth % YoY', group: 'Growth Analysis', format: (v) => v === 0 ? '0%' : v ? `${v.toFixed(1)}%` : '-' },
|
||||
{ key: 'revenueGrowthYoy', label: 'Revenue Growth % YoY', group: 'Growth Analysis', format: (v) => v === 0 ? '0%' : v ? `${v.toFixed(1)}%` : '-' },
|
||||
|
||||
// Demand Variability Metrics
|
||||
{ key: 'salesVariance30d', label: 'Sales Variance (30d)', group: 'Demand Variability', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' },
|
||||
{ key: 'salesStdDev30d', label: 'Sales Std Dev (30d)', group: 'Demand Variability', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' },
|
||||
{ key: 'salesCv30d', label: 'Sales CV % (30d)', group: 'Demand Variability', format: (v) => v === 0 ? '0%' : v ? `${v.toFixed(1)}%` : '-' },
|
||||
{ key: 'demandPattern', label: 'Demand Pattern', group: 'Demand Variability' },
|
||||
|
||||
// Service Level Metrics
|
||||
{ key: 'fillRate30d', label: 'Fill Rate % (30d)', group: 'Service Level', format: (v) => v === 0 ? '0%' : v ? `${v.toFixed(1)}%` : '-' },
|
||||
{ key: 'stockoutIncidents30d', label: 'Stockout Incidents (30d)', group: 'Service Level', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' },
|
||||
{ key: 'serviceLevel30d', label: 'Service Level % (30d)', group: 'Service Level', format: (v) => v === 0 ? '0%' : v ? `${v.toFixed(1)}%` : '-' },
|
||||
{ key: 'lostSalesIncidents30d', label: 'Lost Sales Incidents (30d)', group: 'Service Level', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' },
|
||||
|
||||
// Seasonality Metrics
|
||||
{ key: 'seasonalityIndex', label: 'Seasonality Index', group: 'Seasonality', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' },
|
||||
{ key: 'seasonalPattern', label: 'Seasonal Pattern', group: 'Seasonality' },
|
||||
{ key: 'peakSeason', label: 'Peak Season', group: 'Seasonality' },
|
||||
|
||||
// Quality Indicators
|
||||
{ key: 'lifetimeRevenueQuality', label: 'Lifetime Revenue Quality', group: 'Data Quality' },
|
||||
];
|
||||
|
||||
// Define default columns for each view
|
||||
@@ -198,7 +224,8 @@ const VIEW_COLUMNS: Record<string, ProductMetricColumnKey[]> = {
|
||||
'revenue30d',
|
||||
'profit30d',
|
||||
'stockCoverInDays',
|
||||
'currentStockCost'
|
||||
'currentStockCost',
|
||||
'salesGrowth30dVsPrev'
|
||||
],
|
||||
critical: [
|
||||
'status',
|
||||
@@ -214,7 +241,9 @@ const VIEW_COLUMNS: Record<string, ProductMetricColumnKey[]> = {
|
||||
'earliestExpectedDate',
|
||||
'vendor',
|
||||
'dateLastReceived',
|
||||
'avgLeadTimeDays'
|
||||
'avgLeadTimeDays',
|
||||
'serviceLevel30d',
|
||||
'stockoutIncidents30d'
|
||||
],
|
||||
reorder: [
|
||||
'status',
|
||||
@@ -229,7 +258,8 @@ const VIEW_COLUMNS: Record<string, ProductMetricColumnKey[]> = {
|
||||
'sales30d',
|
||||
'vendor',
|
||||
'avgLeadTimeDays',
|
||||
'dateLastReceived'
|
||||
'dateLastReceived',
|
||||
'demandPattern'
|
||||
],
|
||||
overstocked: [
|
||||
'status',
|
||||
@@ -244,7 +274,8 @@ const VIEW_COLUMNS: Record<string, ProductMetricColumnKey[]> = {
|
||||
'stockturn30d',
|
||||
'currentStockCost',
|
||||
'overstockedCost',
|
||||
'dateLastSold'
|
||||
'dateLastSold',
|
||||
'salesVariance30d'
|
||||
],
|
||||
'at-risk': [
|
||||
'status',
|
||||
@@ -259,7 +290,9 @@ const VIEW_COLUMNS: Record<string, ProductMetricColumnKey[]> = {
|
||||
'sellsOutInDays',
|
||||
'dateLastSold',
|
||||
'avgLeadTimeDays',
|
||||
'profit30d'
|
||||
'profit30d',
|
||||
'fillRate30d',
|
||||
'salesGrowth30dVsPrev'
|
||||
],
|
||||
new: [
|
||||
'status',
|
||||
@@ -274,7 +307,9 @@ const VIEW_COLUMNS: Record<string, ProductMetricColumnKey[]> = {
|
||||
'currentCostPrice',
|
||||
'dateFirstReceived',
|
||||
'ageDays',
|
||||
'abcClass'
|
||||
'abcClass',
|
||||
'first7DaysSales',
|
||||
'first30DaysSales'
|
||||
],
|
||||
healthy: [
|
||||
'status',
|
||||
@@ -288,7 +323,9 @@ const VIEW_COLUMNS: Record<string, ProductMetricColumnKey[]> = {
|
||||
'profit30d',
|
||||
'margin30d',
|
||||
'gmroi30d',
|
||||
'stockturn30d'
|
||||
'stockturn30d',
|
||||
'salesGrowth30dVsPrev',
|
||||
'serviceLevel30d'
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ import { Label } from "@/components/ui/label";
|
||||
type VendorSortableColumns =
|
||||
| 'vendorName' | 'productCount' | 'activeProductCount' | 'currentStockUnits'
|
||||
| 'currentStockCost' | 'onOrderUnits' | 'onOrderCost' | 'avgLeadTimeDays'
|
||||
| 'revenue_30d' | 'profit_30d' | 'avg_margin_30d' | 'po_count_365d' | 'status';
|
||||
| 'revenue_30d' | 'profit_30d' | 'avg_margin_30d' | 'po_count_365d'
|
||||
| 'salesGrowth30dVsPrev' | 'revenueGrowth30dVsPrev' | 'status';
|
||||
|
||||
interface VendorMetric {
|
||||
vendor_id: string | number;
|
||||
@@ -43,6 +44,9 @@ interface VendorMetric {
|
||||
lifetime_sales: number;
|
||||
lifetime_revenue: string | number;
|
||||
avg_margin_30d: string | number | null;
|
||||
// Growth metrics
|
||||
sales_growth_30d_vs_prev: string | number | null;
|
||||
revenue_growth_30d_vs_prev: string | number | null;
|
||||
// New fields added by vendorsAggregate
|
||||
status: string;
|
||||
vendor_status: string;
|
||||
@@ -68,6 +72,8 @@ interface VendorMetric {
|
||||
lifetimeSales: number;
|
||||
lifetimeRevenue: string | number;
|
||||
avgMargin_30d: string | number | null;
|
||||
salesGrowth30dVsPrev: string | number | null;
|
||||
revenueGrowth30dVsPrev: string | number | null;
|
||||
}
|
||||
|
||||
// Define response type to avoid type errors
|
||||
@@ -162,6 +168,19 @@ const formatDays = (value: number | string | null | undefined, digits = 1): stri
|
||||
return `${value.toFixed(digits)} days`;
|
||||
};
|
||||
|
||||
// Growth formatting with color coding
|
||||
const formatGrowth = (value: number | string | null | undefined, digits = 1) => {
|
||||
if (value == null) return <span className="text-muted-foreground">N/A</span>;
|
||||
|
||||
const numValue = typeof value === "string" ? parseFloat(value) : value;
|
||||
if (isNaN(numValue)) return <span className="text-muted-foreground">N/A</span>;
|
||||
|
||||
const formatted = `${numValue >= 0 ? '+' : ''}${numValue.toFixed(digits)}%`;
|
||||
const colorClass = numValue >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
|
||||
return <span className={colorClass}>{formatted}</span>;
|
||||
};
|
||||
|
||||
const getStatusVariant = (status: string): "default" | "secondary" | "outline" | "destructive" => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
@@ -381,6 +400,8 @@ export function Vendors() {
|
||||
<TableHead onClick={() => handleSort("profit_30d")} className="cursor-pointer text-right">Profit (30d)</TableHead>
|
||||
<TableHead onClick={() => handleSort("avg_margin_30d")} className="cursor-pointer text-right">Margin (30d)</TableHead>
|
||||
<TableHead onClick={() => handleSort("po_count_365d")} className="cursor-pointer text-right">POs (365d)</TableHead>
|
||||
<TableHead onClick={() => handleSort("salesGrowth30dVsPrev")} className="cursor-pointer text-right">Sales Growth</TableHead>
|
||||
<TableHead onClick={() => handleSort("revenueGrowth30dVsPrev")} className="cursor-pointer text-right">Revenue Growth</TableHead>
|
||||
<TableHead onClick={() => handleSort("status")} className="cursor-pointer text-right">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -399,17 +420,19 @@ export function Vendors() {
|
||||
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
|
||||
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
|
||||
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
|
||||
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
|
||||
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : listError ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={11} className="text-center py-8 text-destructive">
|
||||
<TableCell colSpan={13} className="text-center py-8 text-destructive">
|
||||
Error loading vendors: {listError.message}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : vendors.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={11} className="text-center py-8 text-muted-foreground">
|
||||
<TableCell colSpan={13} className="text-center py-8 text-muted-foreground">
|
||||
No vendors found matching your criteria.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -426,6 +449,8 @@ export function Vendors() {
|
||||
<TableCell className="text-right">{formatCurrency(vendor.profit_30d as number)}</TableCell>
|
||||
<TableCell className="text-right">{formatPercentage(vendor.avg_margin_30d as number)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(vendor.po_count_365d || vendor.poCount_365d)}</TableCell>
|
||||
<TableCell className="text-right">{formatGrowth(vendor.sales_growth_30d_vs_prev)}</TableCell>
|
||||
<TableCell className="text-right">{formatGrowth(vendor.revenue_growth_30d_vs_prev)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Badge variant={getStatusVariant(vendor.status)}>
|
||||
{vendor.status || 'Unknown'}
|
||||
|
||||
@@ -213,6 +213,44 @@ export interface ProductMetric {
|
||||
// Yesterday
|
||||
yesterdaySales: number | null;
|
||||
|
||||
// Growth Metrics (P3)
|
||||
salesGrowth30dVsPrev: number | null;
|
||||
revenueGrowth30dVsPrev: number | null;
|
||||
salesGrowthYoy: number | null;
|
||||
revenueGrowthYoy: number | null;
|
||||
|
||||
// Demand Variability Metrics (P3)
|
||||
salesVariance30d: number | null;
|
||||
salesStdDev30d: number | null;
|
||||
salesCv30d: number | null;
|
||||
demandPattern: string | null;
|
||||
|
||||
// Service Level Metrics (P5)
|
||||
fillRate30d: number | null;
|
||||
stockoutIncidents30d: number | null;
|
||||
serviceLevel30d: number | null;
|
||||
lostSalesIncidents30d: number | null;
|
||||
|
||||
// Seasonality Metrics (P5)
|
||||
seasonalityIndex: number | null;
|
||||
seasonalPattern: string | null;
|
||||
peakSeason: string | null;
|
||||
|
||||
// Lifetime Metrics
|
||||
lifetimeSales: number | null;
|
||||
lifetimeRevenue: number | null;
|
||||
lifetimeRevenueQuality: string | null;
|
||||
|
||||
// First Period Metrics
|
||||
first7DaysSales: number | null;
|
||||
first7DaysRevenue: number | null;
|
||||
first30DaysSales: number | null;
|
||||
first30DaysRevenue: number | null;
|
||||
first60DaysSales: number | null;
|
||||
first60DaysRevenue: number | null;
|
||||
first90DaysSales: number | null;
|
||||
first90DaysRevenue: number | null;
|
||||
|
||||
// Calculated status (added by frontend)
|
||||
status?: ProductStatus;
|
||||
}
|
||||
@@ -364,7 +402,24 @@ export type ProductMetricColumnKey =
|
||||
| 'dateLastReceived'
|
||||
| 'dateFirstReceived'
|
||||
| 'dateFirstSold'
|
||||
| 'imageUrl';
|
||||
| 'imageUrl'
|
||||
// New metrics from P3-P5 implementation
|
||||
| 'salesGrowth30dVsPrev'
|
||||
| 'revenueGrowth30dVsPrev'
|
||||
| 'salesGrowthYoy'
|
||||
| 'revenueGrowthYoy'
|
||||
| 'salesVariance30d'
|
||||
| 'salesStdDev30d'
|
||||
| 'salesCv30d'
|
||||
| 'demandPattern'
|
||||
| 'fillRate30d'
|
||||
| 'stockoutIncidents30d'
|
||||
| 'serviceLevel30d'
|
||||
| 'lostSalesIncidents30d'
|
||||
| 'seasonalityIndex'
|
||||
| 'seasonalPattern'
|
||||
| 'peakSeason'
|
||||
| 'lifetimeRevenueQuality';
|
||||
|
||||
// Mapping frontend keys to backend query param keys
|
||||
export const FRONTEND_TO_BACKEND_KEY_MAP: Record<string, string> = {
|
||||
@@ -427,7 +482,24 @@ export const FRONTEND_TO_BACKEND_KEY_MAP: Record<string, string> = {
|
||||
overstockedCost: 'overstockedCost',
|
||||
isOldStock: 'isOldStock',
|
||||
yesterdaySales: 'yesterdaySales',
|
||||
status: 'status' // Frontend-only field
|
||||
status: 'status', // Frontend-only field
|
||||
// New metrics from P3-P5 implementation
|
||||
salesGrowth30dVsPrev: 'salesGrowth30dVsPrev',
|
||||
revenueGrowth30dVsPrev: 'revenueGrowth30dVsPrev',
|
||||
salesGrowthYoy: 'salesGrowthYoy',
|
||||
revenueGrowthYoy: 'revenueGrowthYoy',
|
||||
salesVariance30d: 'salesVariance30d',
|
||||
salesStdDev30d: 'salesStdDev30d',
|
||||
salesCv30d: 'salesCv30d',
|
||||
demandPattern: 'demandPattern',
|
||||
fillRate30d: 'fillRate30d',
|
||||
stockoutIncidents30d: 'stockoutIncidents30d',
|
||||
serviceLevel30d: 'serviceLevel30d',
|
||||
lostSalesIncidents30d: 'lostSalesIncidents30d',
|
||||
seasonalityIndex: 'seasonalityIndex',
|
||||
seasonalPattern: 'seasonalPattern',
|
||||
peakSeason: 'peakSeason',
|
||||
lifetimeRevenueQuality: 'lifetimeRevenueQuality'
|
||||
};
|
||||
|
||||
// Function to get backend key safely
|
||||
|
||||
Reference in New Issue
Block a user