Opus corrections/fixes/additions

This commit is contained in:
2025-06-19 15:49:31 -04:00
parent 8606a90e34
commit 2e3e81a02b
17 changed files with 741 additions and 613 deletions

View File

@@ -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'}

View File

@@ -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%]"

View File

@@ -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'
],
};

View File

@@ -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'}

View File

@@ -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