diff --git a/svc/queries/product_performance.go b/svc/queries/product_performance.go index 54bc491..813513d 100644 --- a/svc/queries/product_performance.go +++ b/svc/queries/product_performance.go @@ -2921,7 +2921,7 @@ SELECT jsonb_build_object( ) AS row_json FROM ( SELECT - COALESCE(NULLIF(%s,''), '-') AS group_value, + COALESCE(%s, '') AS group_value, COUNT(*)::integer AS row_count, (ARRAY_AGG(product_code ORDER BY performance_score DESC NULLS LAST))[1] AS image_product_code, (ARRAY_AGG(color_code ORDER BY performance_score DESC NULLS LAST))[1] AS image_color_code, @@ -2945,36 +2945,36 @@ FROM ( COALESCE(SUM(sales_usd_365d),0) AS sales_usd_365d, CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(sales_usd_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS avg_price_usd_90d, CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(sales_usd_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS avg_price_usd_180d, - CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN cost_price_usd * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE AVG(cost_price_usd) END AS cost_price_usd, - CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN base_price_usd * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE AVG(base_price_usd) END AS base_price_usd, - CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN base_price_try * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE AVG(base_price_try) END AS base_price_try, + CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN cost_price_usd * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE 0 END AS cost_price_usd, + CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN base_price_usd * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE 0 END AS base_price_usd, + CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN base_price_try * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE 0 END AS base_price_try, COALESCE(SUM(gross_profit_usd_90d),0) AS gross_profit_usd_90d, COALESCE(SUM(gross_profit_usd_180d),0) AS gross_profit_usd_180d, CASE WHEN SUM(sales_usd_90d) > 0 THEN SUM(gross_profit_usd_90d) / NULLIF(SUM(sales_usd_90d),0) ELSE 0 END AS gross_margin_90d, CASE WHEN SUM(sales_usd_180d) > 0 THEN SUM(gross_profit_usd_180d) / NULLIF(SUM(sales_usd_180d),0) ELSE 0 END AS gross_margin_180d, - CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(unit_profit_cost_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE AVG(unit_profit_cost_90d) END AS unit_profit_cost_90d, - CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_cost_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE AVG(unit_profit_cost_180d) END AS unit_profit_cost_180d, - CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(unit_profit_base_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE AVG(unit_profit_base_90d) END AS unit_profit_base_90d, - CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_base_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE AVG(unit_profit_base_180d) END AS unit_profit_base_180d, + CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(unit_profit_cost_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS unit_profit_cost_90d, + CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_cost_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS unit_profit_cost_180d, + CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(unit_profit_base_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS unit_profit_base_90d, + CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_base_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS unit_profit_base_180d, COALESCE(SUM(market_count_90d),0)::integer AS market_count_90d, COALESCE(SUM(customer_count_90d),0)::integer AS customer_count_90d, - CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(sales_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE AVG(sales_index_90d) END AS sales_index_90d, - CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(price_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE AVG(price_index_90d) END AS price_index_90d, - CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(margin_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE AVG(margin_index_90d) END AS margin_index_90d, - CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(performance_score * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE AVG(performance_score) END AS performance_score, + CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(sales_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS sales_index_90d, + CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(price_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS price_index_90d, + CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(margin_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS margin_index_90d, + CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(performance_score * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS performance_score, MODE() WITHIN GROUP (ORDER BY performance_bucket) AS performance_bucket, COALESCE(SUM(CASE WHEN stock_variant_rank = 1 THEN idle_cost_usd ELSE 0 END),0) AS idle_cost_usd FROM ( SELECT Source.*, ROW_NUMBER() OVER ( - PARTITION BY COALESCE(NULLIF(%s,''), '-'), product_code, color_code, yaka_kodu + PARTITION BY COALESCE(%s, ''), product_code, color_code, yaka_kodu ORDER BY performance_score DESC NULLS LAST ) AS stock_variant_rank FROM Source %s ) s - GROUP BY COALESCE(NULLIF(%s,''), '-') + GROUP BY COALESCE(%s, '') ) g ORDER BY group_value `, len(args)+1, len(args)+1, len(args)+1, len(args)+1, len(args)+1, len(args)+1, len(args)+1, len(args)+1, len(args)+1, groupExpr, groupExpr, whereSQL, groupExpr) @@ -3107,7 +3107,7 @@ func productPerformanceSQLFilterWhere(filters []productPerformanceSQLGroupFilter return "", nil, fmt.Errorf("unsupported product performance filter field: %s", filter.Field) } args = append(args, normalizeProductPerformanceGroupValue(filter.Value)) - parts = append(parts, fmt.Sprintf("COALESCE(NULLIF(%s,''), '-') = $%d", expr, len(args))) + parts = append(parts, fmt.Sprintf("COALESCE(%s, '') = $%d", expr, len(args))) } return "WHERE " + strings.Join(parts, " AND "), args, nil } @@ -3310,17 +3310,8 @@ func distinctProductPerformanceVariantStockCost(rows []map[string]any) float64 { func productPerformanceMapVariantKey(row map[string]any) string { productCode := stringFromMap(row, "product_code") - if productCode == "" { - productCode = stringFromMap(row, "image_product_code") - } colorCode := stringFromMap(row, "color_code") - if colorCode == "" { - colorCode = stringFromMap(row, "image_color_code") - } yakaKodu := stringFromMap(row, "yaka_kodu") - if yakaKodu == "" { - yakaKodu = stringFromMap(row, "image_yaka_kodu") - } if productCode == "" && colorCode == "" && yakaKodu == "" { return "" } @@ -3391,9 +3382,6 @@ func isProductPerformanceCustomerGroup(groupField string) bool { func productPerformanceSalesGroupScore(row map[string]any) float64 { salesIndex := floatFromMap(row, "sales_index_90d") margin := floatFromMap(row, "gross_margin_cost_90d") - if margin == 0 { - margin = floatFromMap(row, "gross_margin_90d") - } invoiceCount := floatFromMap(row, "invoice_count_90d") salesQty := floatFromMap(row, "sales_qty_90d") stockTurnover := floatFromMap(row, "stock_turnover_90d") @@ -3435,9 +3423,6 @@ func productPerformanceCustomerSalesGroupScore(row map[string]any) float64 { func productPerformanceCustomerSalesPeriodScore(row map[string]any) float64 { salesUSD := floatFromMap(row, "sales_usd") margin := floatFromMap(row, "gross_margin_cost") - if margin == 0 { - margin = floatFromMap(row, "gross_margin") - } invoiceCount := floatFromMap(row, "invoice_count") salesQty := floatFromMap(row, "sales_qty") productCount := floatFromMap(row, "product_count") @@ -3589,8 +3574,7 @@ func productPerformanceMetricWeightField(field string) string { } func weightedAverageProductPerformanceRows(rows []map[string]any, valueField, qtyField string) float64 { - var weighted, qty, sum float64 - var count int + var weighted, qty float64 for _, row := range rows { value := floatFromMap(row, valueField) weight := floatFromMap(row, qtyField) @@ -3598,17 +3582,10 @@ func weightedAverageProductPerformanceRows(rows []map[string]any, valueField, qt weighted += value * weight qty += weight } - if value != 0 { - sum += value - count++ - } } if qty > 0 { return weighted / qty } - if count > 0 { - return sum / float64(count) - } return 0 } @@ -3667,11 +3644,7 @@ func cloneMap(row map[string]any) map[string]any { } func normalizeProductPerformanceGroupValue(value string) string { - value = strings.TrimSpace(value) - if value == "" { - return "-" - } - return value + return strings.TrimSpace(value) } func displayProductPerformanceMarketName(value string) string { @@ -3926,9 +3899,9 @@ func productPerformanceOrderBy(sortBy string, desc bool) string { "unit_profit_base_180d": "unit_profit_base_180d", "unit_profit_cost_180d": "unit_profit_cost_180d", "gross_margin_base_90d": "(CASE WHEN COALESCE(sales_usd_90d,0) <= 0 THEN 0 ELSE (sales_usd_90d - (sales_qty_90d * COALESCE(base_price_usd,0))) / NULLIF(sales_usd_90d,0) END)", - "gross_margin_cost_90d": "gross_margin_90d", + "gross_margin_cost_90d": "(CASE WHEN COALESCE(sales_usd_90d,0) <= 0 THEN 0 ELSE (sales_usd_90d - (sales_qty_90d * COALESCE(cost_price_usd,0))) / NULLIF(sales_usd_90d,0) END)", "gross_margin_base_180d": "(CASE WHEN COALESCE(sales_usd_180d,0) <= 0 THEN 0 ELSE (sales_usd_180d - (sales_qty_180d * COALESCE(base_price_usd,0))) / NULLIF(sales_usd_180d,0) END)", - "gross_margin_cost_180d": "gross_margin_180d", + "gross_margin_cost_180d": "(CASE WHEN COALESCE(sales_usd_180d,0) <= 0 THEN 0 ELSE (sales_usd_180d - (sales_qty_180d * COALESCE(cost_price_usd,0))) / NULLIF(sales_usd_180d,0) END)", "gross_margin_90d": "gross_margin_90d", "gross_margin_180d": "gross_margin_180d", "market_count_90d": "market_count_90d", diff --git a/ui/src/pages/ProductPerformanceProfitability.vue b/ui/src/pages/ProductPerformanceProfitability.vue index 23a3f48..02c330b 100644 --- a/ui/src/pages/ProductPerformanceProfitability.vue +++ b/ui/src/pages/ProductPerformanceProfitability.vue @@ -2789,7 +2789,7 @@ function salesGroupPerformanceScore (row) { function productSalesPeriodScore (row) { const salesIndex = Number(row?.sales_index_90d || 0) - const margin = Number(row?.gross_margin_cost_90d ?? row?.gross_margin_90d ?? 0) + const margin = Number(row?.gross_margin_cost_90d ?? 0) const invoices = Number(row?.invoice_count_90d || 0) const qty = Number(row?.sales_qty_90d || 0) let stockTurnover = Number(row?.stock_turnover_90d || 0) @@ -2803,8 +2803,7 @@ function productSalesPeriodScore (row) { } function productPeriodMetricSource (row, suffix) { - const salesUSD = Number(row?.[`sales_usd_${suffix}`] || 0) - const salesIndex = Number(row?.[`sales_index_${suffix}`] || 0) || (salesUSD > 0 ? salesUSD / 1000 : 0) + const salesIndex = Number(row?.[`sales_index_${suffix}`] || 0) const qty = Number(row?.[`sales_qty_${suffix}`] || 0) const stockQty = Number(row?.stock_qty || 0) return { @@ -2833,7 +2832,7 @@ function customerSalesGroupPerformanceScore (row) { function customerSalesPeriodScore (row) { const salesUSD = Number(row?.sales_usd || 0) - const margin = Number(row?.gross_margin_cost ?? row?.gross_margin ?? 0) + const margin = Number(row?.gross_margin_cost ?? 0) const invoices = Number(row?.invoice_count || 0) const qty = Number(row?.sales_qty || 0) const productCount = Number(row?.product_count || 0) @@ -2873,9 +2872,9 @@ function sumRows (sourceRows, field) { } function productVariantStockKey (row) { - const productCode = String(row?.product_code || row?.image_product_code || '').trim() - const colorCode = String(row?.color_code || row?.image_color_code || '').trim() - const yakaKodu = String(row?.yaka_kodu || row?.image_yaka_kodu || '').trim() + const productCode = String(row?.product_code || '').trim() + const colorCode = String(row?.color_code || '').trim() + const yakaKodu = String(row?.yaka_kodu || '').trim() if (!productCode && !colorCode && !yakaKodu) return '' return `${productCode}|${colorCode}|${yakaKodu}` } @@ -2980,7 +2979,7 @@ function weightedAverageOrAverage (sourceRows, valueField, qtyField) { if (qty > 0) { return weightedRows.reduce((sum, row) => sum + row.value * row.qty, 0) / qty } - return averageRows(sourceRows, valueField) + return 0 } function marginFromSalesCost (salesUSD, qty, unitCost) { @@ -2999,8 +2998,7 @@ function marginFromRows (sourceRows, salesField, qtyField, unitCostField) { } function normalizeGroupValue (value) { - const text = String(value || '').trim() - return text || '-' + return String(value || '').trim() } function isGroupExpanded (key) { @@ -3065,11 +3063,11 @@ function hasProductVariantImageKey (row) { } function canShowProductImage (row) { - return activeTab.value === 'products' && !row?.__group && hasProductVariantImageKey(row) + return activeTab.value === 'products' && hasProductVariantImageKey(row) } -function groupShowsImage () { - return false +function groupShowsImage (row) { + return activeTab.value === 'products' && row?.__group && hasProductVariantImageKey(row) } const groupDimensionFields = new Set(dimensionColumnNames)