From 005d2eafac365970e11bcf1f6bec45dcc4b98ce6 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Tue, 7 Jul 2026 01:20:55 +0300 Subject: [PATCH] Fix product performance grouped JSON build --- svc/queries/product_performance.go | 58 ++++++++++++------- svc/routes/product_performance_excel.go | 14 ++--- .../pages/ProductPerformanceProfitability.vue | 42 +++++++------- 3 files changed, 65 insertions(+), 49 deletions(-) diff --git a/svc/queries/product_performance.go b/svc/queries/product_performance.go index 0b73137..e832ebe 100644 --- a/svc/queries/product_performance.go +++ b/svc/queries/product_performance.go @@ -4361,7 +4361,11 @@ func listProductPerformanceGroupedSQL(ctx context.Context, pg *sql.DB, req Produ out := make([]map[string]any, 0, 512) filters := productPerformanceGroupedBaseFilters(req) err := appendProductPerformanceGroupedSQLRows(ctx, pg, &out, mode, levels, 0, 0, []string{"tab:" + mode}, filters, req.ExpandedKeys, req.ExpandThroughLevel, req.Limit) - return out, true, err + if err != nil { + return out, true, err + } + out = sortProductPerformancePreparedGroupedRows(out, req.SortBy, req.Descending) + return out, true, nil } func productPerformanceGroupedBaseFilters(req ProductPerformanceGroupedRequest) []productPerformanceSQLGroupFilter { @@ -4439,6 +4443,7 @@ func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out row["label"] = value row["__group"] = true row[field] = value + deriveProductPerformanceGroupMetrics(row, field) *out = append(*out, row) if expandedKeys[key] || level <= expandThroughLevel { nextFilters := append(append([]productPerformanceSQLGroupFilter{}, filters...), productPerformanceSQLGroupFilter{Field: field, Value: value}) @@ -4464,6 +4469,8 @@ SELECT ( jsonb_build_object( 'group_value', group_value, 'label', group_value, + 'period_start', '2022-01-01', + 'period_end', COALESCE(to_char((SELECT kpi_date FROM LatestKPIDate),'YYYY-MM-DD'), ''), 'count', row_count, 'recommendation', CASE WHEN recommendation <> '' THEN recommendation ELSE row_count::text || ' satir' END, 'image_product_code', image_product_code, @@ -4482,6 +4489,10 @@ SELECT ( ) || jsonb_build_object( 'stock_qty', stock_qty, + 'avg_stock_90d', avg_stock_90d, + 'avg_stock_180d', avg_stock_180d, + 'avg_stock_365d', avg_stock_365d, + 'avg_stock_total', avg_stock_total, 'sales_qty_90d', sales_qty_90d, 'sales_qty_180d', sales_qty_180d, 'sales_qty_365d', sales_qty_365d, @@ -4506,8 +4517,17 @@ SELECT ( 'unit_profit_base_90d', unit_profit_base_90d, 'unit_profit_base_180d', unit_profit_base_180d, 'market_count_90d', market_count_90d, + 'market_count_180d', market_count_180d, + 'market_count_365d', market_count_365d, + 'market_count_total', market_count_total, 'customer_count_90d', customer_count_90d, + 'customer_count_180d', customer_count_180d, + 'customer_count_365d', customer_count_365d, + 'customer_count_total', customer_count_total, 'sales_index_90d', sales_index_90d, + 'sales_index_180d', sales_index_180d, + 'sales_index_365d', sales_index_365d, + 'sales_index_total', sales_index_total, 'price_index_90d', price_index_90d, 'margin_index_90d', margin_index_90d, 'performance_score', performance_score, @@ -4586,8 +4606,17 @@ FROM ( 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, COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_90d,0) > 0)::integer AS market_count_90d, + COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_180d,0) > 0)::integer AS market_count_180d, + COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_365d,0) > 0)::integer AS market_count_365d, + COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_total,0) > 0)::integer AS market_count_total, COALESCE(SUM(CASE WHEN COALESCE(sales_usd_90d,0) > 0 THEN customer_count_90d ELSE 0 END),0)::integer AS customer_count_90d, + COALESCE(SUM(CASE WHEN COALESCE(sales_usd_180d,0) > 0 THEN customer_count_180d ELSE 0 END),0)::integer AS customer_count_180d, + COALESCE(SUM(CASE WHEN COALESCE(sales_usd_365d,0) > 0 THEN customer_count_365d ELSE 0 END),0)::integer AS customer_count_365d, + COALESCE(SUM(CASE WHEN COALESCE(sales_usd_total,0) > 0 THEN customer_count_total ELSE 0 END),0)::integer AS customer_count_total, 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_180d) > 0 THEN SUM(sales_index_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS sales_index_180d, + CASE WHEN SUM(sales_qty_365d) > 0 THEN SUM(sales_index_365d * sales_qty_365d) / NULLIF(SUM(sales_qty_365d),0) ELSE 0 END AS sales_index_365d, + CASE WHEN SUM(sales_qty_total) > 0 THEN SUM(sales_index_total * sales_qty_total) / NULLIF(SUM(sales_qty_total),0) ELSE 0 END AS sales_index_total, 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 @@ -5966,15 +5995,8 @@ func productPerformanceMapVariantKey(row map[string]any) string { func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string) { normalizeProductPerformanceCostFields(out) - preservedPerformanceScores := map[string]float64{} - if score, ok := productPerformanceOptionalFloat(out, "performance_score"); ok { - preservedPerformanceScores["90d"] = score - } preservedCustomerScores := map[string]float64{} for _, suffix := range productPerformancePeriodSuffixes() { - if score, ok := productPerformanceOptionalFloat(out, "performance_score_"+suffix); ok { - preservedPerformanceScores[suffix] = score - } if score, ok := productPerformanceOptionalFloat(out, "customer_score_"+suffix); ok { preservedCustomerScores[suffix] = score } @@ -6058,11 +6080,7 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string) } else { out["customer_score_"+suffix] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, suffix)) } - if score, ok := preservedPerformanceScores[suffix]; ok { - out["performance_score_"+suffix] = score - } else { - out["performance_score_"+suffix] = productPerformanceSalesPeriodScore(out, suffix) - } + out["performance_score_"+suffix] = productPerformanceSalesPeriodScore(out, suffix) } out["performance_score"] = out["performance_score_90d"] if floatFromMap(out, "order_qty") > 0 || floatFromMap(out, "order_usd") > 0 { @@ -6282,8 +6300,8 @@ func productPerformanceProductScore(suffix string, salesUSD, salesIndex, margin, score := 0.30*productPerformanceMarginComponentScore(margin) + 0.20*productPerformanceRatioScore(stockTurnover, productPerformanceStockTurnoverTarget(suffix)) + 0.20*revenueScore + - 0.15*productPerformanceRatioScore(marketCount, productPerformanceMarketSpreadTarget(suffix)) + - 0.15*productPerformanceRatioScore(customerCount, productPerformanceCustomerSpreadTarget(suffix)) + 0.05*productPerformanceRatioScore(marketCount, productPerformanceMarketSpreadTarget(suffix)) + + 0.25*productPerformanceRatioScore(customerCount, productPerformanceCustomerSpreadTarget(suffix)) return productPerformanceRoundScore(score) } @@ -6296,11 +6314,11 @@ func productPerformanceMarketSpreadTarget(suffix string) float64 { case "90d": return 3 case "180d": - return 5 + return 3 case "365d": return 6 default: - return 8 + return 6 } } @@ -6309,11 +6327,11 @@ func productPerformanceCustomerSpreadTarget(suffix string) float64 { case "90d": return 8 case "180d": - return 15 + return 20 case "365d": - return 18 + return 50 default: - return 25 + return 50 } } diff --git a/svc/routes/product_performance_excel.go b/svc/routes/product_performance_excel.go index 66a3d42..2e41c57 100644 --- a/svc/routes/product_performance_excel.go +++ b/svc/routes/product_performance_excel.go @@ -962,8 +962,8 @@ func productPerformanceExcelProductScore(suffix string, salesUSD, stockTurnover, score := 0.30*productPerformanceExcelMarginScore(margin) + 0.20*productPerformanceExcelRatioScore(stockTurnover, 4) + 0.20*revenueScore + - 0.15*productPerformanceExcelRatioScore(marketCount, productPerformanceExcelMarketSpreadTarget(suffix)) + - 0.15*productPerformanceExcelRatioScore(customerCount, productPerformanceExcelCustomerSpreadTarget(suffix)) + 0.05*productPerformanceExcelRatioScore(marketCount, productPerformanceExcelMarketSpreadTarget(suffix)) + + 0.25*productPerformanceExcelRatioScore(customerCount, productPerformanceExcelCustomerSpreadTarget(suffix)) return productPerformanceExcelRoundScore(score) } @@ -972,11 +972,11 @@ func productPerformanceExcelMarketSpreadTarget(suffix string) float64 { case "90d": return 3 case "180d": - return 5 + return 3 case "365d": return 6 default: - return 8 + return 6 } } @@ -985,11 +985,11 @@ func productPerformanceExcelCustomerSpreadTarget(suffix string) float64 { case "90d": return 8 case "180d": - return 15 + return 20 case "365d": - return 18 + return 50 default: - return 25 + return 50 } } diff --git a/ui/src/pages/ProductPerformanceProfitability.vue b/ui/src/pages/ProductPerformanceProfitability.vue index 3a85a6f..f423859 100644 --- a/ui/src/pages/ProductPerformanceProfitability.vue +++ b/ui/src/pages/ProductPerformanceProfitability.vue @@ -1850,7 +1850,6 @@ const columns = [ { name: 'customer_count_total', label: 'Genel Müşteri', field: row => formatNumber(row.customer_count_total, 0), align: 'right', sortable: true }, { name: 'sales_index_90d', label: 'Piyasa End.', field: row => formatNumber(row.sales_index_90d, 2), align: 'right', sortable: true }, { name: 'sales_index_total', label: 'Genel Endeks', field: row => formatNumber(row.sales_index_total, 2), align: 'right', sortable: true }, - { name: 'performance_score', label: 'Skor', field: row => formatNumber(row.performance_score, 2), align: 'right', sortable: true }, { name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left' }, { name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' } ] @@ -2085,7 +2084,7 @@ const generalColumns = [ { name: 'customer_count_total', label: 'Toplam Müşteri', field: row => formatNumber(row.customer_count_total, 0), align: 'right', sortable: true }, { name: 'invoice_count_total', label: 'Fatura', field: row => formatNumber(row.invoice_count_total, 0), align: 'right', sortable: true }, { name: 'sales_index_total', label: 'Genel Endeks', field: row => formatNumber(row.sales_index_total, 2), align: 'right', sortable: true }, - { name: 'performance_score', label: 'Genel Skor', field: row => formatNumber(row.performance_score, 2), align: 'right', sortable: true }, + { name: 'performance_score_total', label: 'Genel Skor', field: row => formatNumber(row.performance_score_total ?? row.performance_score, 2), align: 'right', sortable: true }, { name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true }, { name: 'first_sale_date', label: 'İlk Satış', field: 'first_sale_date', align: 'left', sortable: true }, { name: 'last_sale_date', label: 'Son Satış', field: 'last_sale_date', align: 'left', sortable: true }, @@ -2355,7 +2354,6 @@ const salesBreakdownColumns = [ { name: 'gross_margin_cost_total', label: 'Genel Çıplak Marj', field: row => formatPercent(row.gross_margin_cost_total), align: 'right', sortable: true }, { name: 'customer_score_total', label: 'Genel Müşteri Skor', field: row => formatNumber(row.customer_score_total, 2), align: 'right', sortable: true }, { name: 'sales_index_90d', label: 'Endeks', field: row => formatNumber(row.sales_index_90d, 2), align: 'right', sortable: true }, - { name: 'performance_score', label: 'Skor', field: row => formatNumber(row.performance_score, 2), align: 'right', sortable: true }, { name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true }, { name: 'last_sale_date', label: 'Son Satış', field: 'last_sale_date', align: 'left', sortable: true }, { name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' } @@ -2427,7 +2425,6 @@ const metricLabelOverrides = { gross_margin_cost_365d: '360G Çıplak Marj', gross_margin_base_total: 'Genel Taban Marj', gross_margin_cost_total: 'Genel Çıplak Marj', - performance_score: 'Skor', performance_score_90d: '90G Ürün Skor', performance_score_180d: '180G Ürün Skor', performance_score_365d: '360G Ürün Skor', @@ -2666,7 +2663,10 @@ const visibleOrderAnalysisColumns = computed(() => { function performanceCardsForRow (row) { const source = row || {} return [ - { key: 'score', label: 'Skor', value: formatNumber(source.performance_score, 2) }, + { key: 'score90', label: '90G Skor', value: formatNumber(source.performance_score_90d ?? source.performance_score, 2) }, + { key: 'score180', label: '180G Skor', value: formatNumber(source.performance_score_180d, 2) }, + { key: 'score360', label: '360G Skor', value: formatNumber(source.performance_score_365d, 2) }, + { key: 'scoreTotal', label: 'Genel Skor', value: formatNumber(source.performance_score_total ?? source.performance_score, 2) }, { key: 'stock', label: 'Stok', value: formatNumber(source.stock_qty, 0) }, { key: 'sales90', label: '90G Satış', value: formatNumber(source.sales_qty_90d, 0) }, { key: 'sales180', label: '180G Satış', value: formatNumber(source.sales_qty_180d, 0) }, @@ -3285,6 +3285,9 @@ function compareGroupedBucketsForTable (tableKey, left, right, groupDef) { function groupedBucketSortValue (sourceRows, sortBy) { if (!sourceRows?.length) return 0 + if (shouldUseDerivedGroupSortValue(sortBy)) { + return sortValueForColumnName(aggregateGroupFields(sourceRows), sortBy) + } if (sortBy === 'stock_qty') return distinctVariantStockQty(sourceRows) if (sortBy === 'idle_cost_usd') return distinctVariantStockCost(sourceRows) if (sortBy === 'base_price_usd' || sortBy === 'cost_price_usd') return weightedAverageProductCost(sourceRows, sortBy) @@ -3293,6 +3296,10 @@ function groupedBucketSortValue (sourceRows, sortBy) { return sortValueForColumnName(sourceRows[0], sortBy) } +function shouldUseDerivedGroupSortValue (sortBy) { + return /^(performance_score|stock_turnover|stock_days|avg_price_usd|unit_profit|gross_profit|gross_margin|market_count|customer_count|sales_index)/i.test(String(sortBy || '')) +} + function sortProductGroupedTableRows (tableRows, sortBy, descending) { const source = Array.isArray(tableRows) ? tableRows : [] if (backendGroupedSupportedTab(activeTab.value)) return source @@ -3528,15 +3535,8 @@ function weightFieldForMetric (field) { function applyDerivedGroupMetrics (out, sourceRows, groupField = '') { Object.assign(out, normalizeProductCostFields(out)) - const preservedPerformanceScores = {} - if (Object.prototype.hasOwnProperty.call(out, 'performance_score')) { - preservedPerformanceScores['90d'] = Number(out.performance_score || 0) - } const preservedCustomerScores = {} for (const suffix of ['90d', '180d', '365d', 'total']) { - if (Object.prototype.hasOwnProperty.call(out, `performance_score_${suffix}`)) { - preservedPerformanceScores[suffix] = Number(out[`performance_score_${suffix}`] || 0) - } if (Object.prototype.hasOwnProperty.call(out, `customer_score_${suffix}`)) { preservedCustomerScores[suffix] = Number(out[`customer_score_${suffix}`] || 0) } @@ -3596,9 +3596,7 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') { out[`customer_score_${suffix}`] = Object.prototype.hasOwnProperty.call(preservedCustomerScores, suffix) ? preservedCustomerScores[suffix] : customerSalesPeriodScore(periodMetricSource(out, suffix)) - out[`performance_score_${suffix}`] = Object.prototype.hasOwnProperty.call(preservedPerformanceScores, suffix) - ? preservedPerformanceScores[suffix] - : productSalesPeriodScore(productPeriodMetricSource(out, suffix)) + out[`performance_score_${suffix}`] = productSalesPeriodScore(productPeriodMetricSource(out, suffix)) } out.performance_score = Number(out.performance_score_90d || 0) } @@ -3707,8 +3705,8 @@ function productScore100 ({ suffix = '90d', salesUSD = 0, margin = 0, stockTurno 0.30 * marginScore(margin) + 0.20 * ratioScore(stockTurnover, stockTurnoverTarget(suffix)) + 0.20 * revenue + - 0.15 * ratioScore(marketCount, marketSpreadTarget(suffix)) + - 0.15 * ratioScore(customerCount, customerSpreadTarget(suffix)) + 0.05 * ratioScore(marketCount, marketSpreadTarget(suffix)) + + 0.25 * ratioScore(customerCount, customerSpreadTarget(suffix)) ) } @@ -3777,16 +3775,16 @@ function stockTurnoverTarget () { function marketSpreadTarget (suffix) { if (suffix === '90d') return 3 - if (suffix === '180d') return 5 + if (suffix === '180d') return 3 if (suffix === '365d') return 6 - return 8 + return 6 } function customerSpreadTarget (suffix) { if (suffix === '90d') return 8 - if (suffix === '180d') return 15 - if (suffix === '365d') return 18 - return 25 + if (suffix === '180d') return 20 + if (suffix === '365d') return 50 + return 50 } function periodCount (row, prefix, suffix) {