Category calculation fixes

This commit is contained in:
2025-04-02 15:42:20 -04:00
parent 6051b849d6
commit 2601a04211
4 changed files with 304 additions and 122 deletions

View File

@@ -106,6 +106,11 @@ interface CategoryMetric {
lifetimeRevenue: string | number;
avgMargin_30d: string | number | null;
stockTurn_30d: string | number | null;
direct_active_product_count: number;
direct_current_stock_units: number;
direct_stock_cost: string | number;
direct_revenue_30d: string | number;
direct_profit_30d: string | number;
}
// Define response type to avoid type errors
@@ -455,7 +460,84 @@ export function Categories() {
// Build the hierarchical tree structure
const hierarchicalCategories = useMemo(() => {
if (!categories || categories.length === 0) return [];
// DIRECT CALCULATION: Create a map to directly calculate accurate totals
// This approach doesn't rely on the tree structure for calculations
const directTotalsMap = new Map<string | number, {
revenue30d: number,
profit30d: number,
activeProductCount: number,
currentStockUnits: number,
currentStockCost: number
}>();
// First, identify all parent-child relationships
const childToParentMap = new Map<string | number, string | number>();
categories.forEach(cat => {
if (cat.parent_id) {
childToParentMap.set(cat.category_id, cat.parent_id);
}
});
// Build sets of all descendants for each category
const allDescendantsMap = new Map<string | number, Set<string | number>>();
// Helper to get all descendants recursively
const getAllDescendants = (categoryId: string | number): Set<string | number> => {
if (allDescendantsMap.has(categoryId)) {
return allDescendantsMap.get(categoryId)!;
}
const descendants = new Set<string | number>();
// Find direct children
categories.forEach(cat => {
if (cat.parent_id === categoryId) {
descendants.add(cat.category_id);
// Add their descendants recursively
const childDescendants = getAllDescendants(cat.category_id);
childDescendants.forEach(id => descendants.add(id));
}
});
// Cache and return
allDescendantsMap.set(categoryId, descendants);
return descendants;
};
// Calculate descendants for all categories
categories.forEach(cat => {
if (!allDescendantsMap.has(cat.category_id)) {
getAllDescendants(cat.category_id);
}
});
// Now use pre-calculated database values instead of calculating totals directly
categories.forEach(cat => {
// Use the pre-calculated values from the database
const totalRevenue = parseFloat(cat.revenue_30d?.toString() || '0');
const totalProfit = parseFloat(cat.profit_30d?.toString() || '0');
const totalActiveProducts = cat.active_product_count || 0;
const totalStockUnits = cat.current_stock_units || 0;
const totalStockCost = parseFloat(cat.current_stock_cost?.toString() || '0');
// Direct values (for display in tooltips)
const directRevenue = parseFloat(cat.direct_revenue_30d?.toString() || '0');
const directProfit = parseFloat(cat.direct_profit_30d?.toString() || '0');
const directActiveProducts = cat.direct_active_product_count || 0;
const directStockUnits = cat.direct_current_stock_units || 0;
const directStockCost = parseFloat(cat.direct_stock_cost?.toString() || '0');
// Set the pre-calculated totals
directTotalsMap.set(cat.category_id, {
revenue30d: totalRevenue,
profit30d: totalProfit,
activeProductCount: totalActiveProducts,
currentStockUnits: totalStockUnits,
currentStockCost: totalStockCost
});
});
// First, create a lookup map by category ID
const categoryMap = new Map<string | number, CategoryWithChildren>();
categories.forEach((cat) => {
@@ -476,14 +558,16 @@ export function Categories() {
if (parent) {
parent.children.push(processedCat);
}
} else if (cat.parent_id) {
// This is an orphaned category
} else {
// This is a root category
rootCategories.push(processedCat);
}
});
// Compute hierarchy levels and aggregate stats
const computeHierarchyAndStats = (
// Set hierarchy levels and use the direct aggregates we calculated
const computeHierarchyAndLevels = (
categories: CategoryWithChildren[],
level = 0
) => {
@@ -493,60 +577,49 @@ export function Categories() {
cat.isLast = index === arr.length - 1;
cat.isExpanded = expandedCategories.has(cat.category_id);
// Process children first to ensure we have their aggregated values
// Process children to set their hierarchy levels
const children =
cat.children.length > 0
? computeHierarchyAndStats(cat.children, level + 1)
? computeHierarchyAndLevels(cat.children, level + 1)
: [];
// Calculate this category's own direct stats for clarity
const ownStats = {
activeProductCount: cat.active_product_count || 0,
currentStockUnits: cat.current_stock_units || 0,
currentStockCost: parseFloat(
cat.current_stock_cost?.toString() || "0"
),
revenue30d: parseFloat(cat.revenue_30d?.toString() || "0"),
profit30d: parseFloat(cat.profit_30d?.toString() || "0"),
avg_margin_30d: parseFloat(cat.avg_margin_30d?.toString() || "0"),
};
// For leaf nodes (no children), aggregated stats = own stats
if (children.length === 0) {
cat.aggregatedStats = { ...ownStats };
return cat;
// Make sure we set aggregatedStats for ALL categories, not just those with children
// First check if we have pre-calculated values
const totals = directTotalsMap.get(cat.category_id);
if (totals) {
// Use our pre-calculated totals for all metrics
cat.aggregatedStats = {
activeProductCount: totals.activeProductCount,
currentStockUnits: totals.currentStockUnits,
currentStockCost: totals.currentStockCost,
revenue30d: totals.revenue30d,
profit30d: totals.profit30d,
avg_margin_30d: totals.revenue30d > 0
? (totals.profit30d / totals.revenue30d) * 100
: 0
};
} else {
// If we don't have pre-calculated values (shouldn't happen with our algorithm)
// Ensure every category still has aggregatedStats set with its direct values
cat.aggregatedStats = {
activeProductCount: cat.direct_active_product_count || 0,
currentStockUnits: cat.direct_current_stock_units || 0,
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")
};
}
// For parents, calculate aggregated stats = own stats + sum of all children's aggregated stats
const aggregatedStats = { ...ownStats }; // Start with own stats
// Add all children's AGGREGATED stats (not direct stats)
children.forEach((child) => {
if (child.aggregatedStats) {
aggregatedStats.activeProductCount +=
child.aggregatedStats.activeProductCount;
aggregatedStats.currentStockUnits +=
child.aggregatedStats.currentStockUnits;
aggregatedStats.currentStockCost +=
child.aggregatedStats.currentStockCost;
aggregatedStats.revenue30d += child.aggregatedStats.revenue30d;
aggregatedStats.profit30d += child.aggregatedStats.profit30d;
}
});
// Recalculate margin based on total profit and revenue
if (aggregatedStats.revenue30d > 0) {
aggregatedStats.avg_margin_30d =
(aggregatedStats.profit30d / aggregatedStats.revenue30d) * 100;
}
cat.aggregatedStats = aggregatedStats;
return cat;
});
};
// Compute hierarchy levels and aggregate stats for all categories
return computeHierarchyAndStats(rootCategories);
// Apply hierarchy levels and use our pre-calculated totals
const result = computeHierarchyAndLevels(rootCategories);
return result;
}, [categories, expandedCategories]);
// Recursive function to render category rows with streamlined stat display
@@ -636,7 +709,7 @@ export function Categories() {
</p>
<p>
Directly in '{category.category_name}':{" "}
{formatNumber(category.active_product_count)}
{formatNumber(category.direct_active_product_count)}
</p>
</TooltipContent>
</Tooltip>
@@ -662,7 +735,7 @@ export function Categories() {
</p>
<p>
Directly in '{category.category_name}':{" "}
{formatNumber(category.current_stock_units)}
{formatNumber(category.direct_current_stock_units)}
</p>
</TooltipContent>
</Tooltip>
@@ -687,7 +760,7 @@ export function Categories() {
</p>
<p>
Directly in '{category.category_name}':{" "}
{formatCurrency(category.current_stock_cost)}
{formatCurrency(category.direct_stock_cost)}
</p>
</TooltipContent>
</Tooltip>
@@ -712,12 +785,12 @@ export function Categories() {
</p>
<p>
Directly from '{category.category_name}':{" "}
{formatCurrency(category.revenue_30d)}
{formatCurrency(category.direct_revenue_30d)}
</p>
</TooltipContent>
</Tooltip>
) : (
formatCurrency(category.revenue_30d)
formatCurrency(category.aggregatedStats ? category.aggregatedStats.revenue30d : category.revenue_30d)
)}
</TableCell>
@@ -737,12 +810,12 @@ export function Categories() {
</p>
<p>
Directly from '{category.category_name}':{" "}
{formatCurrency(category.profit_30d)}
{formatCurrency(category.direct_profit_30d)}
</p>
</TooltipContent>
</Tooltip>
) : (
formatCurrency(category.profit_30d)
formatCurrency(category.aggregatedStats ? category.aggregatedStats.profit30d : category.profit_30d)
)}
</TableCell>