diff --git a/svc/queries/product_performance.go b/svc/queries/product_performance.go index 232a780..a55ce14 100644 --- a/svc/queries/product_performance.go +++ b/svc/queries/product_performance.go @@ -1978,7 +1978,7 @@ SalesAgg AS ( MAX(last_ref_number) AS last_ref_number, COALESCE(SUM(sales_qty),0) AS sales_qty_total, COALESCE(SUM(sales_usd),0) AS sales_usd_total, - COALESCE(COUNT(DISTINCT NULLIF(customer_code, '-')),0)::integer AS customer_count_total, + COALESCE(COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE COALESCE(sales_usd,0) > 0),0)::integer AS customer_count_total, COALESCE(SUM(invoice_count),0)::integer AS invoice_count_total FROM mk_product_performance_sales_daily, Bounds WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end @@ -2102,8 +2102,8 @@ Spread AS ( product_code, color_code, yaka_kodu, - COUNT(DISTINCT NULLIF(market_key, 'STOK'))::integer AS market_count_total, - COUNT(DISTINCT NULLIF(customer_code, '-'))::integer AS customer_count_total_all + COUNT(DISTINCT NULLIF(market_key, 'STOK')) FILTER (WHERE COALESCE(sales_usd,0) > 0)::integer AS market_count_total, + COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE COALESCE(sales_usd,0) > 0)::integer AS customer_count_total_all FROM mk_product_performance_sales_daily, Bounds WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER') @@ -2140,8 +2140,8 @@ Scored AS ( CASE WHEN b.sales_usd_total <= 0 THEN 0 ELSE (b.sales_usd_total - (b.sales_qty_total * COALESCE(pd.cost_price_usd,0))) / NULLIF(b.sales_usd_total,0) END AS gross_margin_total, CASE WHEN b.sales_qty_total <= 0 THEN 0 ELSE (b.sales_usd_total / NULLIF(b.sales_qty_total,0)) - COALESCE(pd.cost_price_usd,0) END AS unit_profit_cost_total, CASE WHEN b.sales_qty_total <= 0 THEN 0 ELSE (b.sales_usd_total / NULLIF(b.sales_qty_total,0)) - COALESCE(pd.base_price_usd,0) END AS unit_profit_base_total, - COALESCE(sp.market_count_total,0) AS market_count_total, - COALESCE(sp.customer_count_total_all, b.customer_count_total) AS customer_count_total, + CASE WHEN b.market_key <> 'STOK' AND b.sales_usd_total > 0 THEN 1 ELSE 0 END AS market_count_total, + b.customer_count_total AS customer_count_total, b.invoice_count_total, CASE WHEN AVG(b.sales_qty_total) OVER (PARTITION BY b.market_key, b.kategori, b.urun_ana_grubu) <= 0 THEN 0 @@ -3174,7 +3174,7 @@ SELECT ELSE COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0) / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0) END AS avg_price_usd_90d, - COALESCE(SUM(customer_count) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)::integer AS customer_count_90d, + COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days' AND COALESCE(sales_usd,0) > 0)::integer AS customer_count_90d, COALESCE(SUM(invoice_count) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)::integer AS invoice_count_90d, COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '364 days'),0) AS sales_qty_365d, COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '364 days'),0) AS sales_usd_365d @@ -3507,16 +3507,16 @@ Agg AS ( COUNT(DISTINCT COALESCE(NULLIF(urun_ana_grubu,''), NULLIF(urun_alt_grubu,''), NULLIF(kategori,''), product_code)) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days')::integer AS product_group_count_180d, COUNT(DISTINCT COALESCE(NULLIF(urun_ana_grubu,''), NULLIF(urun_alt_grubu,''), NULLIF(kategori,''), product_code)) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days')::integer AS product_group_count_365d, COUNT(DISTINCT COALESCE(NULLIF(urun_ana_grubu,''), NULLIF(urun_alt_grubu,''), NULLIF(kategori,''), product_code)) FILTER (WHERE sales_date >= DATE '2022-01-01')::integer AS product_group_count_total, - COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days')::integer AS market_count_90d, - COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days')::integer AS market_count_180d, - COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days')::integer AS market_count_365d, - COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= DATE '2022-01-01')::integer AS market_count_total, + COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days' AND COALESCE(sales_usd,0) > 0)::integer AS market_count_90d, + COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days' AND COALESCE(sales_usd,0) > 0)::integer AS market_count_180d, + COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days' AND COALESCE(sales_usd,0) > 0)::integer AS market_count_365d, + COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= DATE '2022-01-01' AND COALESCE(sales_usd,0) > 0)::integer AS market_count_total, COALESCE(MAX(stock_qty),0) AS stock_qty, COALESCE(MAX(avg_stock_90d),0) AS avg_stock_90d, COALESCE(MAX(avg_stock_180d),0) AS avg_stock_180d, COALESCE(MAX(avg_stock_365d),0) AS avg_stock_365d, COALESCE(MAX(avg_stock_total),0) AS avg_stock_total, - COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days' AND COALESCE(customer_code,'') <> '')::integer AS customer_count_90d, + COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days' AND COALESCE(customer_code,'') <> '' AND COALESCE(sales_usd,0) > 0)::integer AS customer_count_90d, COALESCE(SUM(invoice_count) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)::integer AS invoice_count_90d, COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0) AS sales_qty_90d, COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0) AS sales_usd_90d, @@ -3532,7 +3532,7 @@ Agg AS ( ELSE COALESCE(SUM(sales_qty * cost_price_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0) / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0) END AS cost_price_usd_90d, - COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days' AND COALESCE(customer_code,'') <> '')::integer AS customer_count_180d, + COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days' AND COALESCE(customer_code,'') <> '' AND COALESCE(sales_usd,0) > 0)::integer AS customer_count_180d, COALESCE(SUM(invoice_count) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0)::integer AS invoice_count_180d, COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0) AS sales_qty_180d, COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0) AS sales_usd_180d, @@ -3550,7 +3550,7 @@ Agg AS ( END AS cost_price_usd_180d, COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0) AS sales_qty_365d, COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0) AS sales_usd_365d, - COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days' AND COALESCE(customer_code,'') <> '')::integer AS customer_count_365d, + COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days' AND COALESCE(customer_code,'') <> '' AND COALESCE(sales_usd,0) > 0)::integer AS customer_count_365d, COALESCE(SUM(invoice_count) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)::integer AS invoice_count_365d, CASE WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)=0 THEN 0 ELSE COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0) @@ -3564,7 +3564,7 @@ Agg AS ( ELSE COALESCE(SUM(sales_qty * cost_price_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0) / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0) END AS cost_price_usd_365d, - COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= DATE '2022-01-01' AND COALESCE(customer_code,'') <> '')::integer AS customer_count_total, + COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= DATE '2022-01-01' AND COALESCE(customer_code,'') <> '' AND COALESCE(sales_usd,0) > 0)::integer AS customer_count_total, COALESCE(SUM(invoice_count) FILTER (WHERE sales_date >= DATE '2022-01-01'),0)::integer AS invoice_count_total, COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= DATE '2022-01-01'),0) AS sales_qty_total, COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= DATE '2022-01-01'),0) AS sales_usd_total, @@ -3710,49 +3710,45 @@ SELECT CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) ELSE 0 END AS stock_turnover_total, has_cost, sales_index_90d, - CASE WHEN NOT has_cost THEN 0 ELSE + CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_90d <= 0 THEN 1 ELSE ROUND(( - LEAST(35, sales_usd_90d / 1000) - + LEAST(25, GREATEST(gross_margin_cost_90d,0) * 55) - + LEAST(20, invoice_count_90d * 2) - + LEAST(10, sales_qty_90d / 10) - + LEAST(10, product_count) + LEAST(35, sales_usd_90d / 25000 * 35) + + LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.40 * 30) + + LEAST(20, product_group_count_90d / 8.0 * 20) + + LEAST(15, sales_qty_90d / 500 * 15) )::numeric, 4) END AS customer_score_90d, - CASE WHEN NOT has_cost THEN 0 ELSE + CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_180d <= 0 THEN 1 ELSE ROUND(( - LEAST(35, sales_usd_180d / 1000) - + LEAST(25, GREATEST(gross_margin_cost_180d,0) * 55) - + LEAST(20, invoice_count_180d * 2) - + LEAST(10, sales_qty_180d / 10) - + LEAST(10, product_count) + LEAST(35, sales_usd_180d / 50000 * 35) + + LEAST(30, GREATEST(gross_margin_cost_180d,0) / 0.40 * 30) + + LEAST(20, product_group_count_180d / 8.0 * 20) + + LEAST(15, sales_qty_180d / 1000 * 15) )::numeric, 4) END AS customer_score_180d, - CASE WHEN NOT has_cost THEN 0 ELSE + CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_365d <= 0 THEN 1 ELSE ROUND(( - LEAST(35, sales_usd_365d / 1000) - + LEAST(25, GREATEST(gross_margin_cost_365d,0) * 55) - + LEAST(20, invoice_count_365d * 2) - + LEAST(10, sales_qty_365d / 10) - + LEAST(10, product_count) + LEAST(35, sales_usd_365d / 100000 * 35) + + LEAST(30, GREATEST(gross_margin_cost_365d,0) / 0.40 * 30) + + LEAST(20, product_group_count_365d / 8.0 * 20) + + LEAST(15, sales_qty_365d / 2000 * 15) )::numeric, 4) END AS customer_score_365d, - CASE WHEN NOT has_cost THEN 0 ELSE + CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_total <= 0 THEN 1 ELSE ROUND(( - LEAST(35, sales_usd_total / 1000) - + LEAST(25, GREATEST(gross_margin_cost_total,0) * 55) - + LEAST(20, invoice_count_total * 2) - + LEAST(10, sales_qty_total / 10) - + LEAST(10, product_count) + LEAST(35, sales_usd_total / 300000 * 35) + + LEAST(30, GREATEST(gross_margin_cost_total,0) / 0.40 * 30) + + LEAST(20, product_group_count_total / 8.0 * 20) + + LEAST(15, sales_qty_total / 6000 * 15) )::numeric, 4) END AS customer_score_total, - CASE WHEN NOT has_cost THEN 0 ELSE + CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_90d <= 0 THEN 1 ELSE ROUND( - LEAST(35, sales_index_90d * 18) - + LEAST(30, GREATEST(gross_margin_cost_90d,0) * 60) - + LEAST(15, invoice_count_90d * 1.5) - + LEAST(10, sales_qty_90d / 10) - + LEAST(10, (CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) ELSE 0 END) * 5), + LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.40 * 30) + + LEAST(20, (CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) ELSE 0 END) / 1.5 * 20) + + LEAST(20, sales_usd_90d / 10000 * 20) + + LEAST(15, market_count_90d / 8.0 * 15) + + LEAST(15, customer_count_90d / 25.0 * 15), 4 ) END AS performance_score, @@ -4485,8 +4481,8 @@ FROM ( 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, + COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_90d,0) > 0)::integer AS market_count_90d, + COALESCE(SUM(CASE WHEN COALESCE(sales_usd_90d,0) > 0 THEN customer_count_90d ELSE 0 END),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 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, @@ -5296,13 +5292,13 @@ func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) { func (n *productPerformanceGroupedSnapshotNode) addDistinctSpread(row map[string]any) { market := displayProductPerformanceMarketName(stringFromMap(row, "market_key")) if market != "" && market != "STOK" { - if floatFromMap(row, "sales_qty_90d") > 0 || floatFromMap(row, "sales_usd_90d") > 0 { + if floatFromMap(row, "sales_usd_90d") > 0 { if n.Market90Seen == nil { n.Market90Seen = map[string]bool{} } n.Market90Seen[market] = true } - if floatFromMap(row, "sales_qty_total") > 0 || floatFromMap(row, "sales_usd_total") > 0 { + if floatFromMap(row, "sales_usd_total") > 0 { if n.MarketTotalSeen == nil { n.MarketTotalSeen = map[string]bool{} } @@ -5311,13 +5307,13 @@ func (n *productPerformanceGroupedSnapshotNode) addDistinctSpread(row map[string } customer := strings.TrimSpace(stringFromMap(row, "customer_code")) if customer != "" && customer != "-" { - if floatFromMap(row, "sales_qty_90d") > 0 || floatFromMap(row, "sales_usd_90d") > 0 { + if floatFromMap(row, "sales_usd_90d") > 0 { if n.Customer90Seen == nil { n.Customer90Seen = map[string]bool{} } n.Customer90Seen[customer] = true } - if floatFromMap(row, "sales_qty_total") > 0 || floatFromMap(row, "sales_usd_total") > 0 { + if floatFromMap(row, "sales_usd_total") > 0 { if n.CustomerTotalSeen == nil { n.CustomerTotalSeen = map[string]bool{} } @@ -5530,19 +5526,19 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m func addProductPerformanceDistinctSpread(row map[string]any, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen map[string]bool) { market := displayProductPerformanceMarketName(stringFromMap(row, "market_key")) if market != "" && market != "STOK" { - if floatFromMap(row, "sales_qty_90d") > 0 || floatFromMap(row, "sales_usd_90d") > 0 { + if floatFromMap(row, "sales_usd_90d") > 0 { market90Seen[market] = true } - if floatFromMap(row, "sales_qty_total") > 0 || floatFromMap(row, "sales_usd_total") > 0 { + if floatFromMap(row, "sales_usd_total") > 0 { marketTotalSeen[market] = true } } customer := strings.TrimSpace(stringFromMap(row, "customer_code")) if customer != "" && customer != "-" { - if floatFromMap(row, "sales_qty_90d") > 0 || floatFromMap(row, "sales_usd_90d") > 0 { + if floatFromMap(row, "sales_usd_90d") > 0 { customer90Seen[customer] = true } - if floatFromMap(row, "sales_qty_total") > 0 || floatFromMap(row, "sales_usd_total") > 0 { + if floatFromMap(row, "sales_usd_total") > 0 { customerTotalSeen[customer] = true } } @@ -5561,7 +5557,7 @@ func applyProductPerformanceDistinctSpread(row map[string]any, market90Seen, mar if len(customerTotalSeen) > 0 { row["customer_count_total"] = len(customerTotalSeen) } - if floatFromMap(row, "sales_qty_90d") > 0 || floatFromMap(row, "sales_usd_90d") > 0 { + if floatFromMap(row, "sales_usd_90d") > 0 { if intFromMap(row, "market_count_90d") == 0 { row["market_count_90d"] = 1 } @@ -5569,7 +5565,7 @@ func applyProductPerformanceDistinctSpread(row map[string]any, market90Seen, mar row["customer_count_90d"] = 1 } } - if floatFromMap(row, "sales_qty_total") > 0 || floatFromMap(row, "sales_usd_total") > 0 { + if floatFromMap(row, "sales_usd_total") > 0 { if intFromMap(row, "market_count_total") == 0 { row["market_count_total"] = maxInt(1, intFromMap(row, "market_count_90d")) } @@ -5860,6 +5856,9 @@ func productPerformanceCustomerSalesPeriodScore(row map[string]any) float64 { } func productPerformanceProductScore(suffix string, salesUSD, salesIndex, margin, stockTurnover, marketCount, customerCount float64) float64 { + if salesUSD <= 0 { + return 1 + } revenueScore := productPerformanceRevenueScore(suffix, salesUSD, salesIndex, productPerformanceProductRevenueTarget(suffix)) score := 0.30*productPerformanceMarginComponentScore(margin) + 0.20*productPerformanceRatioScore(stockTurnover, 1.50) + @@ -5870,6 +5869,9 @@ func productPerformanceProductScore(suffix string, salesUSD, salesIndex, margin, } func productPerformanceCustomerScore(suffix string, salesUSD, margin, productGroupCount, salesQty float64) float64 { + if salesUSD <= 0 { + return 1 + } score := 0.35*productPerformanceRatioScore(salesUSD, productPerformanceCustomerRevenueTarget(suffix)) + 0.30*productPerformanceMarginComponentScore(margin) + 0.20*productPerformanceRatioScore(productGroupCount, 8) + @@ -5878,9 +5880,6 @@ func productPerformanceCustomerScore(suffix string, salesUSD, margin, productGro } func productPerformanceRevenueScore(suffix string, salesUSD, salesIndex, absoluteTarget float64) float64 { - if salesIndex > 0 { - return productPerformanceRatioScore(salesIndex, 2) - } return productPerformanceRatioScore(salesUSD, absoluteTarget) } diff --git a/svc/queries/product_performance_sql.go b/svc/queries/product_performance_sql.go index 16a9827..718edf4 100644 --- a/svc/queries/product_performance_sql.go +++ b/svc/queries/product_performance_sql.go @@ -76,16 +76,19 @@ func productPerformanceSalesSQL() string { ), SPACE(0))) END, Qty1 = ISNULL(I.Qty1, 0), - TLAmount = ISNULL(I.Doc_Amount, 0) * ISNULL(I.Loc_ExchangeRate, 0), - USDAmount = (ISNULL(I.Doc_Amount, 0) * ISNULL(I.Loc_ExchangeRate, 0)) / ISNULL(( - SELECT TOP 1 Rate - FROM AllExchangeRates WITH(NOLOCK) - WHERE CurrencyCode = 'USD' - AND RelationCurrencyCode = 'TRY' - AND ExchangeTypeCode = 6 - AND Rate > 1 - ORDER BY ABS(DATEDIFF(DAY, AllExchangeRates.Date, I.InvoiceDate)) ASC - ), 1), + TLAmount = CASE + WHEN Amount.DocCurrencyCode = 'TRY' THEN Amount.DocNetAmount + WHEN CurRate.Rate > 0 THEN Amount.DocNetAmount * CurRate.Rate + WHEN ISNULL(I.Loc_ExchangeRate, 0) > 0 THEN Amount.DocNetAmount * I.Loc_ExchangeRate + ELSE 0 + END, + USDAmount = CASE + WHEN Amount.DocCurrencyCode = 'USD' THEN Amount.DocNetAmount + WHEN Amount.DocCurrencyCode = 'TRY' AND UsdRate.Rate > 0 THEN Amount.DocNetAmount / UsdRate.Rate + WHEN CurRate.Rate > 0 AND UsdRate.Rate > 0 THEN (Amount.DocNetAmount * CurRate.Rate) / UsdRate.Rate + WHEN ISNULL(I.Loc_ExchangeRate, 0) > 0 AND UsdRate.Rate > 0 THEN (Amount.DocNetAmount * I.Loc_ExchangeRate) / UsdRate.Rate + ELSE 0 + END, RN = ROW_NUMBER() OVER ( PARTITION BY I.InvoiceLineID ORDER BY I.InvoiceDate DESC, I.InvoiceNumber @@ -96,6 +99,39 @@ func productPerformanceSalesSQL() string { AND CAF.CurrAccCode = I.CurrAccCode LEFT OUTER JOIN ProductAttributesFilter PAF WITH(NOLOCK) ON PAF.ItemCode = I.ItemCode + LEFT OUTER JOIN dbo.trInvoiceLineCurrency ILC WITH(NOLOCK) + ON ILC.InvoiceLineID = I.InvoiceLineID + AND UPPER(LTRIM(RTRIM(ILC.CurrencyCode))) = UPPER(LTRIM(RTRIM(ISNULL(NULLIF(LTRIM(RTRIM(I.Doc_CurrencyCode)), ''), 'TRY')))) + OUTER APPLY ( + SELECT + DocCurrencyCode = UPPER(LTRIM(RTRIM(ISNULL(NULLIF(LTRIM(RTRIM(I.Doc_CurrencyCode)), ''), ISNULL(NULLIF(LTRIM(RTRIM(ILC.CurrencyCode)), ''), 'TRY'))))), + DocNetAmount = COALESCE( + NULLIF(ISNULL(ILC.NetAmount, 0), 0), + NULLIF(ISNULL(I.Doc_Amount, 0), 0), + NULLIF(ISNULL(I.Doc_Price, 0) * ISNULL(I.Qty1, 0), 0), + 0 + ) + ) Amount + OUTER APPLY ( + SELECT TOP 1 Rate + FROM AllExchangeRates WITH(NOLOCK) + WHERE CurrencyCode = 'USD' + AND RelationCurrencyCode = 'TRY' + AND ExchangeTypeCode = 6 + AND Rate > 0 + AND Date <= CAST(I.InvoiceDate AS date) + ORDER BY Date DESC + ) UsdRate + OUTER APPLY ( + SELECT TOP 1 Rate + FROM AllExchangeRates WITH(NOLOCK) + WHERE CurrencyCode = Amount.DocCurrencyCode + AND RelationCurrencyCode = 'TRY' + AND ExchangeTypeCode = 6 + AND Rate > 0 + AND Date <= CAST(I.InvoiceDate AS date) + ORDER BY Date DESC + ) CurRate WHERE I.InvoiceDate >= @p1 AND I.InvoiceDate < DATEADD(DAY, 1, @p2) AND I.ItemTypeCode = 1 @@ -449,7 +485,7 @@ SalesAgg AS ( SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '179 days') AS sales_usd_180d, SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days') AS sales_usd_365d, SUM(sales_usd) FILTER (WHERE sales_date >= DATE '2022-01-01') AS sales_usd_total, - COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days') AS customer_count_90d, + COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days' AND COALESCE(sales_usd, 0) > 0) AS customer_count_90d, MAX(sales_date) AS last_sale_date, MAX(last_ref_number) AS last_ref_number FROM mk_product_performance_sales_daily @@ -566,9 +602,9 @@ Spread AS ( product_code, color_code, yaka_kodu, - COUNT(DISTINCT market_key) AS market_count_90d + COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) AS market_count_90d FROM SalesAgg - WHERE COALESCE(sales_qty_90d, 0) > 0 + WHERE COALESCE(sales_usd_90d, 0) > 0 GROUP BY product_code, color_code, yaka_kodu ), Market AS ( @@ -622,13 +658,15 @@ SELECT COALESCE(gross_profit_usd_90d, 0), COALESCE(gross_profit_usd_180d, 0), COALESCE(gross_margin_90d, 0), COALESCE(gross_margin_180d, 0), COALESCE(unit_profit_cost_90d, 0), COALESCE(unit_profit_cost_180d, 0), COALESCE(unit_profit_base_90d, 0), COALESCE(unit_profit_base_180d, 0), COALESCE(market_count_90d, 0), COALESCE(customer_count_90d, 0), COALESCE(sales_index_90d, 0), COALESCE(price_index_90d, 0), COALESCE(margin_index_90d, 0), - ROUND( - LEAST(35, COALESCE(sales_index_90d, 0) * 18) - + LEAST(25, GREATEST(COALESCE(gross_margin_90d, 0), 0) * 50) - + LEAST(20, COALESCE(customer_count_90d, 0) * 2) - + LEAST(10, COALESCE(market_count_90d, 0) * 3) - + CASE WHEN COALESCE(stock_days_90d, 0) BETWEEN 10 AND 90 THEN 10 WHEN COALESCE(stock_days_90d, 0) > 180 THEN -10 ELSE 0 END - , 4) AS performance_score, + CASE WHEN COALESCE(sales_usd_90d, 0) <= 0 THEN 1 ELSE + ROUND( + LEAST(35, COALESCE(sales_index_90d, 0) * 18) + + LEAST(25, GREATEST(COALESCE(gross_margin_90d, 0), 0) * 50) + + LEAST(20, COALESCE(customer_count_90d, 0) * 2) + + LEAST(10, COALESCE(market_count_90d, 0) * 3) + + CASE WHEN COALESCE(stock_days_90d, 0) BETWEEN 10 AND 90 THEN 10 WHEN COALESCE(stock_days_90d, 0) > 180 THEN -10 ELSE 0 END + , 4) + END AS performance_score, CASE WHEN COALESCE(sales_index_90d,0) >= 1.25 AND COALESCE(gross_margin_90d,0) >= 0.25 THEN 'YILDIZ_URUN' WHEN COALESCE(stock_qty,0) <= 0 AND COALESCE(sales_qty_90d,0) > 0 THEN 'STOKSUZ_TALEP' diff --git a/svc/routes/product_images.go b/svc/routes/product_images.go index e9bc8ae..11d6a1e 100644 --- a/svc/routes/product_images.go +++ b/svc/routes/product_images.go @@ -590,7 +590,8 @@ WHERE id = $1 return } - if storedInDB && len(binData) > 0 { + _ = storedInDB + if len(binData) > 0 { w.Header().Set("Content-Type", http.DetectContentType(binData)) w.Header().Set("Cache-Control", "public, max-age=3600") _, _ = w.Write(binData) diff --git a/svc/routes/product_performance_excel.go b/svc/routes/product_performance_excel.go index ea932f4..4917400 100644 --- a/svc/routes/product_performance_excel.go +++ b/svc/routes/product_performance_excel.go @@ -26,7 +26,7 @@ type productPerformanceExcelExportRequest struct { type productPerformanceExcelColumn struct { Header string Kind string - Value func(models.ProductPerformanceRow, models.ProductPerformanceGeneralRow, productPerformanceExcelStats) any + Value func(*productPerformanceExcelVariant, productPerformanceExcelStats) any } type productPerformanceExcelStats struct { @@ -36,6 +36,52 @@ type productPerformanceExcelStats struct { AvgSalesUSDTotal float64 } +type productPerformanceExcelVariant struct { + KpiDate string + ProductCode string + ColorCode string + ColorDescription string + YakaKodu string + ItemDescription string + Kategori string + Seri string + YasGrubu string + AskiliYan string + UrunIlkGrubu string + UrunAnaGrubu string + UrunAltGrubu string + StockQty float64 + BasePriceUSD float64 + CostPriceUSD float64 + + SalesQty90 float64 + SalesQty180 float64 + SalesQty365 float64 + SalesUSD90 float64 + SalesUSD180 float64 + SalesUSD365 float64 + + SalesQtyTotalProduct float64 + SalesUSDTotalProduct float64 + SalesQtyTotalGeneral float64 + SalesUSDTotalGeneral float64 + HasGeneralTotal bool + + MarketCount90 int + CustomerCount90 int + MarketCountTotal int + CustomerCountTotal int + Markets90 map[string]bool + MarketsTotal map[string]bool + MarketFiltered bool + + PerformanceBucket string + Recommendation string + LastSaleDate string + LastRefNumber string + UpdatedAt string +} + func ExportProductPerformanceExcelHandler(pg *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { traceID := utils.TraceIDFromRequest(r) @@ -50,7 +96,7 @@ func ExportProductPerformanceExcelHandler(pg *sql.DB) http.HandlerFunc { } } - rows, _, err := queries.ListProductPerformance(ctx, pg, queries.ProductPerformanceFilters{ + productRows, _, err := queries.ListProductPerformance(ctx, pg, queries.ProductPerformanceFilters{ Limit: 50000, Page: 1, SortBy: "product_code", @@ -66,13 +112,12 @@ func ExportProductPerformanceExcelHandler(pg *sql.DB) http.HandlerFunc { http.Error(w, "urun performans genel excel listesi alinamadi: "+err.Error(), http.StatusInternalServerError) return } - generalByKey := productPerformanceExcelGeneralMap(generalRows) - rows = filterProductPerformanceExcelRows(rows, generalByKey, req.Filters) - sortProductPerformanceExcelRows(rows, generalByKey, req.SortBy, req.Descending == nil || *req.Descending) - stats := productPerformanceExcelStatsFor(rows, generalByKey) + rows := buildProductPerformanceExcelVariants(productRows, generalRows, req.Filters) + sortProductPerformanceExcelVariants(rows, req.SortBy, req.Descending == nil || *req.Descending) + stats := productPerformanceExcelStatsFor(rows) - file, err := buildProductPerformanceExcelFile(rows, generalByKey, stats) + file, err := buildProductPerformanceExcelFile(rows, stats) if err != nil { http.Error(w, "urun performans excel dosyasi hazirlanamadi: "+err.Error(), http.StatusInternalServerError) return @@ -85,7 +130,7 @@ func ExportProductPerformanceExcelHandler(pg *sql.DB) http.HandlerFunc { return } - filename := fmt.Sprintf("product_performance_color_yaka_%s.xlsx", time.Now().Format("20060102_150405")) + filename := fmt.Sprintf("product_performance_renk_yaka_%s.xlsx", time.Now().Format("20060102_150405")) w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"") w.Header().Set("Content-Length", fmt.Sprint(len(buf.Bytes()))) @@ -94,7 +139,158 @@ func ExportProductPerformanceExcelHandler(pg *sql.DB) http.HandlerFunc { } } -func buildProductPerformanceExcelFile(rows []models.ProductPerformanceRow, generalByKey map[string]models.ProductPerformanceGeneralRow, stats productPerformanceExcelStats) (*excelize.File, error) { +func buildProductPerformanceExcelVariants(productRows []models.ProductPerformanceRow, generalRows []models.ProductPerformanceGeneralRow, filters map[string][]string) []*productPerformanceExcelVariant { + byKey := map[string]*productPerformanceExcelVariant{} + marketFiltered := productPerformanceExcelHasFilter(filters, "market_key") + for _, row := range productRows { + if !productPerformanceExcelProductRowMatchesFilters(row, filters) { + continue + } + key := productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu) + v := byKey[key] + if v == nil { + v = &productPerformanceExcelVariant{Markets90: map[string]bool{}, MarketsTotal: map[string]bool{}, MarketFiltered: marketFiltered} + byKey[key] = v + } + v.addProductRow(row) + } + for _, row := range generalRows { + if !productPerformanceExcelGeneralRowMatchesFilters(row, filters) { + continue + } + key := productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu) + v := byKey[key] + if v == nil { + v = &productPerformanceExcelVariant{Markets90: map[string]bool{}, MarketsTotal: map[string]bool{}, MarketFiltered: marketFiltered} + byKey[key] = v + } + v.addGeneralRow(row) + } + + out := make([]*productPerformanceExcelVariant, 0, len(byKey)) + for _, row := range byKey { + if productPerformanceExcelVariantMatchesPostFilters(row, filters) { + out = append(out, row) + } + } + return out +} + +func (v *productPerformanceExcelVariant) addProductRow(row models.ProductPerformanceRow) { + v.fillDimensions( + row.KpiDate, row.ProductCode, row.ColorCode, row.ColorDescription, row.YakaKodu, row.ItemDescription, + row.Kategori, row.Seri, row.YasGrubu, row.AskiliYan, row.UrunIlkGrubu, row.UrunAnaGrubu, row.UrunAltGrubu, + row.BasePriceUSD, row.CostPriceUSD, row.StockQty, row.LastSaleDate, row.LastRefNumber, row.UpdatedAt, + ) + v.SalesQty90 += row.SalesQty90 + v.SalesQty180 += row.SalesQty180 + v.SalesQty365 += row.SalesQty365 + v.SalesUSD90 += row.SalesUSD90 + v.SalesUSD180 += row.SalesUSD180 + v.SalesUSD365 += row.SalesUSD365 + v.SalesQtyTotalProduct += row.SalesQtyTotal + v.SalesUSDTotalProduct += row.SalesUSDTotal + v.MarketCount90 = productPerformanceExcelMaxInt(v.MarketCount90, row.MarketCount90) + if row.SalesUSD90 > 0 { + v.CustomerCount90 += row.CustomerCount90 + } + v.PerformanceBucket = productPerformanceExcelFirstNonEmpty(v.PerformanceBucket, row.PerformanceBucket) + v.Recommendation = productPerformanceExcelFirstNonEmpty(v.Recommendation, row.Recommendation) + v.addMarket(row.MarketKey, row.SalesQty90, row.SalesUSD90, row.SalesQtyTotal, row.SalesUSDTotal) +} + +func (v *productPerformanceExcelVariant) addGeneralRow(row models.ProductPerformanceGeneralRow) { + v.fillDimensions( + productPerformanceExcelFirstNonEmpty(row.PeriodEnd, row.PeriodStart), row.ProductCode, row.ColorCode, row.ColorDescription, row.YakaKodu, row.ItemDescription, + row.Kategori, row.Seri, row.YasGrubu, row.AskiliYan, row.UrunIlkGrubu, row.UrunAnaGrubu, row.UrunAltGrubu, + row.BasePriceUSD, row.CostPriceUSD, row.StockQty, row.LastSaleDate, row.LastRefNumber, "", + ) + v.SalesQtyTotalGeneral += row.SalesQtyTotal + v.SalesUSDTotalGeneral += row.SalesUSDTotal + v.HasGeneralTotal = true + v.MarketCountTotal = productPerformanceExcelMaxInt(v.MarketCountTotal, row.MarketCountTotal) + if row.SalesUSDTotal > 0 { + v.CustomerCountTotal += row.CustomerCountTotal + } + v.PerformanceBucket = productPerformanceExcelFirstNonEmpty(v.PerformanceBucket, row.PerformanceBucket) + v.Recommendation = productPerformanceExcelFirstNonEmpty(v.Recommendation, row.Recommendation) + v.addMarket(row.MarketKey, 0, 0, row.SalesQtyTotal, row.SalesUSDTotal) +} + +func (v *productPerformanceExcelVariant) fillDimensions(kpiDate, productCode, colorCode, colorDescription, yakaKodu, itemDescription, kategori, seri, yasGrubu, askiliYan, urunIlkGrubu, urunAnaGrubu, urunAltGrubu string, basePrice, costPrice, stockQty float64, lastSaleDate, lastRefNumber, updatedAt string) { + v.KpiDate = productPerformanceExcelFirstNonEmpty(v.KpiDate, kpiDate) + v.ProductCode = productPerformanceExcelFirstNonEmpty(v.ProductCode, productCode) + v.ColorCode = productPerformanceExcelFirstNonEmpty(v.ColorCode, colorCode) + v.ColorDescription = productPerformanceExcelFirstNonEmpty(v.ColorDescription, colorDescription) + v.YakaKodu = productPerformanceExcelFirstNonEmpty(v.YakaKodu, yakaKodu) + v.ItemDescription = productPerformanceExcelFirstNonEmpty(v.ItemDescription, itemDescription) + v.Kategori = productPerformanceExcelFirstNonEmpty(v.Kategori, kategori) + v.Seri = productPerformanceExcelFirstNonEmpty(v.Seri, seri) + v.YasGrubu = productPerformanceExcelFirstNonEmpty(v.YasGrubu, yasGrubu) + v.AskiliYan = productPerformanceExcelFirstNonEmpty(v.AskiliYan, productPerformanceExcelCleanOptional(askiliYan)) + v.UrunIlkGrubu = productPerformanceExcelFirstNonEmpty(v.UrunIlkGrubu, productPerformanceExcelCleanOptional(urunIlkGrubu)) + v.UrunAnaGrubu = productPerformanceExcelFirstNonEmpty(v.UrunAnaGrubu, urunAnaGrubu) + v.UrunAltGrubu = productPerformanceExcelFirstNonEmpty(v.UrunAltGrubu, urunAltGrubu) + v.BasePriceUSD = productPerformanceExcelFirstNonZero(v.BasePriceUSD, basePrice) + v.CostPriceUSD = productPerformanceExcelFirstNonZero(v.CostPriceUSD, costPrice) + if stockQty > v.StockQty { + v.StockQty = stockQty + } + if lastSaleDate > v.LastSaleDate { + v.LastSaleDate = lastSaleDate + v.LastRefNumber = lastRefNumber + } + if updatedAt > v.UpdatedAt { + v.UpdatedAt = updatedAt + } +} + +func (v *productPerformanceExcelVariant) addMarket(marketKey string, salesQty90, salesUSD90, salesQtyTotal, salesUSDTotal float64) { + market := productPerformanceExcelMarketName(marketKey) + if market == "" || market == "STOK" { + return + } + if salesUSD90 > 0 { + v.Markets90[market] = true + } + if salesUSDTotal > 0 { + v.MarketsTotal[market] = true + } +} + +func (v *productPerformanceExcelVariant) totalQty() float64 { + if v.HasGeneralTotal { + return v.SalesQtyTotalGeneral + } + return v.SalesQtyTotalProduct +} + +func (v *productPerformanceExcelVariant) totalUSD() float64 { + if v.HasGeneralTotal { + return v.SalesUSDTotalGeneral + } + return v.SalesUSDTotalProduct +} + +func (v *productPerformanceExcelVariant) marketCount90() int { + if v.MarketFiltered { + return len(v.Markets90) + } + return productPerformanceExcelMaxInt(v.MarketCount90, len(v.Markets90)) +} + +func (v *productPerformanceExcelVariant) marketCountTotal() int { + if v.MarketFiltered { + return len(v.MarketsTotal) + } + return productPerformanceExcelMaxInt(v.MarketCountTotal, len(v.MarketsTotal)) +} + +func (v *productPerformanceExcelVariant) marketsLabel() string { + return productPerformanceExcelSortedSetLabel(v.MarketsTotal) +} + +func buildProductPerformanceExcelFile(rows []*productPerformanceExcelVariant, stats productPerformanceExcelStats) (*excelize.File, error) { f := excelize.NewFile() sheet := "RenkYaka" f.SetSheetName("Sheet1", sheet) @@ -121,350 +317,200 @@ func buildProductPerformanceExcelFile(rows []models.ProductPerformanceRow, gener for rowIndex, row := range rows { excelRow := rowIndex + 2 - general := generalByKey[productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)] for colIndex, col := range columns { cell, _ := excelize.CoordinatesToCellName(colIndex+1, excelRow) - value := col.Value(row, general, stats) - switch col.Kind { - case "text": + value := col.Value(row, stats) + if col.Kind == "text" { _ = f.SetCellStr(sheet, cell, strings.TrimSpace(fmt.Sprint(value))) - default: + } else { _ = f.SetCellValue(sheet, cell, productPerformanceExcelFloat(value)) } } } - lastRow := len(rows) + 1 + lastRow := productPerformanceExcelMaxInt(len(rows)+1, 2) if len(columns) > 0 { - filterLastRow := lastRow - if filterLastRow < 2 { - filterLastRow = 2 - } - lastFilterCell, _ := excelize.CoordinatesToCellName(len(columns), filterLastRow) + lastFilterCell, _ := excelize.CoordinatesToCellName(len(columns), lastRow) _ = f.AutoFilter(sheet, "A1:"+lastFilterCell, []excelize.AutoFilterOptions{}) } for i, col := range columns { colName, _ := excelize.ColumnNumberToName(i + 1) - width := productPerformanceExcelColumnWidth(col.Header, col.Kind) - _ = f.SetColWidth(sheet, colName, colName, width) - if lastRow >= 2 { - style := decimalStyle - switch col.Kind { - case "text": - style = textStyle - case "int": - style = intStyle - case "percent": - style = percentStyle - } - _ = f.SetCellStyle(sheet, fmt.Sprintf("%s2", colName), fmt.Sprintf("%s%d", colName, lastRow), style) + _ = f.SetColWidth(sheet, colName, colName, productPerformanceExcelColumnWidth(col.Header, col.Kind)) + style := decimalStyle + switch col.Kind { + case "text": + style = textStyle + case "int": + style = intStyle + case "percent": + style = percentStyle } + _ = f.SetCellStyle(sheet, fmt.Sprintf("%s2", colName), fmt.Sprintf("%s%d", colName, lastRow), style) } - _ = f.SetPanes(sheet, &excelize.Panes{ - Freeze: true, - Split: false, - XSplit: 0, - YSplit: 1, - TopLeftCell: "A2", - ActivePane: "bottomLeft", - }) + _ = f.SetPanes(sheet, &excelize.Panes{Freeze: true, YSplit: 1, TopLeftCell: "A2", ActivePane: "bottomLeft"}) return f, nil } func productPerformanceExcelColumns() []productPerformanceExcelColumn { + base := []productPerformanceExcelColumn{ + textExcelColumn("KPI Tarihi", func(v *productPerformanceExcelVariant) any { return v.KpiDate }), + textExcelColumn("Ürün İlk Grubu", func(v *productPerformanceExcelVariant) any { return v.UrunIlkGrubu }), + textExcelColumn("Askılı/Yan", func(v *productPerformanceExcelVariant) any { return v.AskiliYan }), + textExcelColumn("Kategori", func(v *productPerformanceExcelVariant) any { return v.Kategori }), + textExcelColumn("Ürün Ana Grubu", func(v *productPerformanceExcelVariant) any { return v.UrunAnaGrubu }), + textExcelColumn("Ürün Alt Grubu", func(v *productPerformanceExcelVariant) any { return v.UrunAltGrubu }), + textExcelColumn("Ürün", func(v *productPerformanceExcelVariant) any { return v.ProductCode }), + textExcelColumn("Açıklama", func(v *productPerformanceExcelVariant) any { return v.ItemDescription }), + textExcelColumn("Renk", func(v *productPerformanceExcelVariant) any { return v.ColorCode }), + textExcelColumn("Renk Açıklama", func(v *productPerformanceExcelVariant) any { return v.ColorDescription }), + textExcelColumn("Yaka", func(v *productPerformanceExcelVariant) any { return v.YakaKodu }), + textExcelColumn("Renk/Yaka", func(v *productPerformanceExcelVariant) any { + return productPerformanceExcelColorYaka(v.ColorCode, v.ColorDescription, v.YakaKodu) + }), + textExcelColumn("Piyasalar", func(v *productPerformanceExcelVariant) any { return v.marketsLabel() }), + numberExcelColumn("Toplam Stok", "int", func(v *productPerformanceExcelVariant) any { return v.StockQty }), + } + base = append(base, productPerformanceExcelPeriodColumns("90G", "90d", func(v *productPerformanceExcelVariant) (float64, float64, int, int) { + return v.SalesQty90, v.SalesUSD90, v.marketCount90(), v.CustomerCount90 + })...) + base = append(base, productPerformanceExcelPeriodColumns("180G", "180d", func(v *productPerformanceExcelVariant) (float64, float64, int, int) { + return v.SalesQty180, v.SalesUSD180, v.marketCount90(), v.CustomerCount90 + })...) + base = append(base, productPerformanceExcelPeriodColumns("360G", "365d", func(v *productPerformanceExcelVariant) (float64, float64, int, int) { + return v.SalesQty365, v.SalesUSD365, v.marketCount90(), v.CustomerCount90 + })...) + base = append(base, productPerformanceExcelPeriodColumns("Genel", "total", func(v *productPerformanceExcelVariant) (float64, float64, int, int) { + return v.totalQty(), v.totalUSD(), v.marketCountTotal(), v.CustomerCountTotal + })...) + base = append(base, + textExcelColumn("Durum", func(v *productPerformanceExcelVariant) any { return productPerformanceExcelStatus(v) }), + textExcelColumn("Öneri", func(v *productPerformanceExcelVariant) any { return productPerformanceExcelRecommendation(v) }), + textExcelColumn("Son Satış", func(v *productPerformanceExcelVariant) any { return v.LastSaleDate }), + textExcelColumn("Son Ref", func(v *productPerformanceExcelVariant) any { return v.LastRefNumber }), + textExcelColumn("Güncelleme", func(v *productPerformanceExcelVariant) any { return v.UpdatedAt }), + ) + return base +} + +func productPerformanceExcelPeriodColumns(label, suffix string, values func(*productPerformanceExcelVariant) (float64, float64, int, int)) []productPerformanceExcelColumn { return []productPerformanceExcelColumn{ - {Header: "KPI Tarihi", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.KpiDate - }}, - {Header: "Ürün İlk Grubu", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelCleanOptional(r.UrunIlkGrubu) - }}, - {Header: "Askılı/Yan", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelCleanOptional(r.AskiliYan) - }}, - {Header: "Kategori", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.Kategori - }}, - {Header: "Ürün Ana Grubu", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.UrunAnaGrubu - }}, - {Header: "Ürün Alt Grubu", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.UrunAltGrubu - }}, - {Header: "Ürün", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.ProductCode - }}, - {Header: "Açıklama", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.ItemDescription - }}, - {Header: "Renk", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.ColorCode - }}, - {Header: "Renk Açıklama", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.ColorDescription - }}, - {Header: "Yaka", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.YakaKodu - }}, - {Header: "Renk/Yaka", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelColorYaka(r) - }}, - {Header: "Toplam Stok", Kind: "int", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.StockQty - }}, - - {Header: "90G Toplam Adet", Kind: "int", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.SalesQty90 - }}, - {Header: "90G Toplam Ciro USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.SalesUSD90 - }}, - {Header: "90G Ağ. Ort. Fiyat USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelAvgPrice(r.SalesUSD90, r.SalesQty90) - }}, - {Header: "90G Ağ. Ort. Stok Gün", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.StockDays90 - }}, - {Header: "90G Stok Devir", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelFirstNonZero(r.StockTurnover90, productPerformanceExcelStockTurnover(r.SalesQty90, r.StockQty)) - }}, - {Header: "90G Ort. Taban Maliyet USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.BasePriceUSD - }}, - {Header: "90G Ort. Çıplak Maliyet USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.CostPriceUSD - }}, - {Header: "90G Ağ. Ort. Birim Taban K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelUnitProfit(r.SalesUSD90, r.SalesQty90, r.BasePriceUSD) - }}, - {Header: "90G Ağ. Ort. Birim Çıplak K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelUnitProfit(r.SalesUSD90, r.SalesQty90, r.CostPriceUSD) - }}, - {Header: "90G Taban Toplam K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelGrossProfit(r.SalesUSD90, r.SalesQty90, r.BasePriceUSD) - }}, - {Header: "90G Çıplak Toplam K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelGrossProfit(r.SalesUSD90, r.SalesQty90, r.CostPriceUSD) - }}, - {Header: "90G Taban Marj", Kind: "percent", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelMargin(r.SalesUSD90, r.SalesQty90, r.BasePriceUSD) - }}, - {Header: "90G Çıplak Marj", Kind: "percent", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelMargin(r.SalesUSD90, r.SalesQty90, r.CostPriceUSD) - }}, - {Header: "90G Tekil Piyasa", Kind: "int", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.MarketCount90 - }}, - {Header: "90G Tekil Müşteri", Kind: "int", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.CustomerCount90 - }}, - {Header: "90G Endeks", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, stats productPerformanceExcelStats) any { - return productPerformanceExcelFirstNonZero(r.SalesIndex90, productPerformanceExcelRelativeIndex(r.SalesUSD90, stats.AvgSalesUSD90)) - }}, - {Header: "90G Ürün Skor", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, stats productPerformanceExcelStats) any { - if strings.TrimSpace(r.PerformanceBucket) != "" { - return r.PerformanceScore - } - return productPerformanceExcelProductScore("90d", r.SalesUSD90, productPerformanceExcelRelativeIndex(r.SalesUSD90, stats.AvgSalesUSD90), productPerformanceExcelMargin(r.SalesUSD90, r.SalesQty90, r.CostPriceUSD), productPerformanceExcelStockTurnover(r.SalesQty90, r.StockQty), float64(r.MarketCount90), float64(r.CustomerCount90)) - }}, - - {Header: "180G Toplam Adet", Kind: "int", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.SalesQty180 - }}, - {Header: "180G Toplam Ciro USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.SalesUSD180 - }}, - {Header: "180G Ağ. Ort. Fiyat USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelAvgPrice(r.SalesUSD180, r.SalesQty180) - }}, - {Header: "180G Ağ. Ort. Stok Gün", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.StockDays180 - }}, - {Header: "180G Stok Devir", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelFirstNonZero(r.StockTurnover180, productPerformanceExcelStockTurnover(r.SalesQty180, r.StockQty)) - }}, - {Header: "180G Ort. Taban Maliyet USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.BasePriceUSD - }}, - {Header: "180G Ort. Çıplak Maliyet USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.CostPriceUSD - }}, - {Header: "180G Taban Toplam K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelGrossProfit(r.SalesUSD180, r.SalesQty180, r.BasePriceUSD) - }}, - {Header: "180G Çıplak Toplam K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelGrossProfit(r.SalesUSD180, r.SalesQty180, r.CostPriceUSD) - }}, - {Header: "180G Taban Marj", Kind: "percent", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelMargin(r.SalesUSD180, r.SalesQty180, r.BasePriceUSD) - }}, - {Header: "180G Çıplak Marj", Kind: "percent", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelMargin(r.SalesUSD180, r.SalesQty180, r.CostPriceUSD) - }}, - {Header: "180G Ürün Skor", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, stats productPerformanceExcelStats) any { - return productPerformanceExcelProductScore("180d", r.SalesUSD180, productPerformanceExcelRelativeIndex(r.SalesUSD180, stats.AvgSalesUSD180), productPerformanceExcelMargin(r.SalesUSD180, r.SalesQty180, r.CostPriceUSD), productPerformanceExcelStockTurnover(r.SalesQty180, r.StockQty), productPerformanceExcelFirstNonZero(float64(g.MarketCountTotal), float64(r.MarketCount90)), productPerformanceExcelFirstNonZero(float64(g.CustomerCountTotal), float64(r.CustomerCount90))) - }}, - - {Header: "360G Toplam Adet", Kind: "int", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.SalesQty365 - }}, - {Header: "360G Toplam Ciro USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.SalesUSD365 - }}, - {Header: "360G Ağ. Ort. Fiyat USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelAvgPrice(r.SalesUSD365, r.SalesQty365) - }}, - {Header: "360G Ağ. Ort. Stok Gün", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.StockDays365 - }}, - {Header: "360G Stok Devir", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelFirstNonZero(r.StockTurnover365, productPerformanceExcelStockTurnover(r.SalesQty365, r.StockQty)) - }}, - {Header: "360G Ort. Taban Maliyet USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.BasePriceUSD - }}, - {Header: "360G Ort. Çıplak Maliyet USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.CostPriceUSD - }}, - {Header: "360G Taban Toplam K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelGrossProfit(r.SalesUSD365, r.SalesQty365, r.BasePriceUSD) - }}, - {Header: "360G Çıplak Toplam K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelGrossProfit(r.SalesUSD365, r.SalesQty365, r.CostPriceUSD) - }}, - {Header: "360G Taban Marj", Kind: "percent", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelMargin(r.SalesUSD365, r.SalesQty365, r.BasePriceUSD) - }}, - {Header: "360G Çıplak Marj", Kind: "percent", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelMargin(r.SalesUSD365, r.SalesQty365, r.CostPriceUSD) - }}, - {Header: "360G Ürün Skor", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, stats productPerformanceExcelStats) any { - return productPerformanceExcelProductScore("365d", r.SalesUSD365, productPerformanceExcelRelativeIndex(r.SalesUSD365, stats.AvgSalesUSD365), productPerformanceExcelMargin(r.SalesUSD365, r.SalesQty365, r.CostPriceUSD), productPerformanceExcelStockTurnover(r.SalesQty365, r.StockQty), productPerformanceExcelFirstNonZero(float64(g.MarketCountTotal), float64(r.MarketCount90)), productPerformanceExcelFirstNonZero(float64(g.CustomerCountTotal), float64(r.CustomerCount90))) - }}, - - {Header: "Genel Toplam Adet", Kind: "int", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelTotalSalesQty(r, g) - }}, - {Header: "Genel Toplam Ciro USD", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelTotalSalesUSD(r, g) - }}, - {Header: "Genel Ağ. Ort. Fiyat USD", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelFirstNonZero(g.AvgPriceUSDTotal, productPerformanceExcelAvgPrice(productPerformanceExcelTotalSalesUSD(r, g), productPerformanceExcelTotalSalesQty(r, g))) - }}, - {Header: "Genel Ağ. Ort. Stok Gün", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelFirstNonZero(g.StockDaysTotal, r.StockDaysTotal) - }}, - {Header: "Genel Stok Devir", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelFirstNonZero(r.StockTurnoverTotal, productPerformanceExcelStockTurnover(productPerformanceExcelTotalSalesQty(r, g), r.StockQty)) - }}, - {Header: "Genel Ort. Taban Maliyet USD", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelFirstNonZero(g.BasePriceUSD, r.BasePriceUSD) - }}, - {Header: "Genel Ort. Çıplak Maliyet USD", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelFirstNonZero(g.CostPriceUSD, r.CostPriceUSD) - }}, - {Header: "Genel Taban Toplam K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelGrossProfit(productPerformanceExcelTotalSalesUSD(r, g), productPerformanceExcelTotalSalesQty(r, g), productPerformanceExcelFirstNonZero(g.BasePriceUSD, r.BasePriceUSD)) - }}, - {Header: "Genel Çıplak Toplam K/Z USD", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelGrossProfit(productPerformanceExcelTotalSalesUSD(r, g), productPerformanceExcelTotalSalesQty(r, g), productPerformanceExcelFirstNonZero(g.CostPriceUSD, r.CostPriceUSD)) - }}, - {Header: "Genel Taban Marj", Kind: "percent", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelMargin(productPerformanceExcelTotalSalesUSD(r, g), productPerformanceExcelTotalSalesQty(r, g), productPerformanceExcelFirstNonZero(g.BasePriceUSD, r.BasePriceUSD)) - }}, - {Header: "Genel Çıplak Marj", Kind: "percent", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelMargin(productPerformanceExcelTotalSalesUSD(r, g), productPerformanceExcelTotalSalesQty(r, g), productPerformanceExcelFirstNonZero(g.CostPriceUSD, r.CostPriceUSD)) - }}, - {Header: "Genel Tekil Piyasa", Kind: "int", Value: func(_ models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return g.MarketCountTotal - }}, - {Header: "Genel Tekil Müşteri", Kind: "int", Value: func(_ models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return g.CustomerCountTotal - }}, - {Header: "Genel Fatura", Kind: "int", Value: func(_ models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return g.InvoiceCountTotal - }}, - {Header: "Genel Endeks", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, stats productPerformanceExcelStats) any { - return productPerformanceExcelFirstNonZero(g.SalesIndexTotal, productPerformanceExcelRelativeIndex(productPerformanceExcelTotalSalesUSD(r, g), stats.AvgSalesUSDTotal)) - }}, - {Header: "Genel Ürün Skor", Kind: "number", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, stats productPerformanceExcelStats) any { - if strings.TrimSpace(g.PerformanceBucket) != "" { - return g.PerformanceScore - } - return productPerformanceExcelProductScore("total", productPerformanceExcelTotalSalesUSD(r, g), productPerformanceExcelRelativeIndex(productPerformanceExcelTotalSalesUSD(r, g), stats.AvgSalesUSDTotal), productPerformanceExcelMargin(productPerformanceExcelTotalSalesUSD(r, g), productPerformanceExcelTotalSalesQty(r, g), productPerformanceExcelFirstNonZero(g.CostPriceUSD, r.CostPriceUSD)), productPerformanceExcelStockTurnover(productPerformanceExcelTotalSalesQty(r, g), r.StockQty), float64(g.MarketCountTotal), float64(g.CustomerCountTotal)) - }}, - {Header: "Durum", Kind: "text", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelBucketLabel(productPerformanceExcelFirstNonEmpty(g.PerformanceBucket, r.PerformanceBucket)) - }}, - {Header: "Öneri", Kind: "text", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelFirstNonEmpty(g.Recommendation, r.Recommendation) - }}, - {Header: "Son Satış", Kind: "text", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelFirstNonEmpty(g.LastSaleDate, r.LastSaleDate) - }}, - {Header: "Son Ref", Kind: "text", Value: func(r models.ProductPerformanceRow, g models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return productPerformanceExcelFirstNonEmpty(g.LastRefNumber, r.LastRefNumber) - }}, - {Header: "Güncelleme", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { - return r.UpdatedAt - }}, + numberExcelColumn(label+" Toplam Adet", "int", func(v *productPerformanceExcelVariant) any { + qty, _, _, _ := values(v) + return qty + }), + numberExcelColumn(label+" Toplam Ciro USD", "number", func(v *productPerformanceExcelVariant) any { + _, usd, _, _ := values(v) + return usd + }), + numberExcelColumn(label+" Ciro/Adet USD", "number", func(v *productPerformanceExcelVariant) any { + qty, usd, _, _ := values(v) + return productPerformanceExcelAvgPrice(usd, qty) + }), + numberExcelColumn(label+" Satış/Stok Oranı", "number", func(v *productPerformanceExcelVariant) any { + qty, _, _, _ := values(v) + return productPerformanceExcelStockTurnover(qty, v.StockQty) + }), + numberExcelColumn(label+" Taban Maliyet USD", "number", func(v *productPerformanceExcelVariant) any { return v.BasePriceUSD }), + numberExcelColumn(label+" Çıplak Maliyet USD", "number", func(v *productPerformanceExcelVariant) any { return v.CostPriceUSD }), + numberExcelColumn(label+" Taban Toplam K/Z USD", "number", func(v *productPerformanceExcelVariant) any { + qty, usd, _, _ := values(v) + return productPerformanceExcelGrossProfit(usd, qty, v.BasePriceUSD) + }), + numberExcelColumn(label+" Çıplak Toplam K/Z USD", "number", func(v *productPerformanceExcelVariant) any { + qty, usd, _, _ := values(v) + return productPerformanceExcelGrossProfit(usd, qty, v.CostPriceUSD) + }), + numberExcelColumn(label+" Taban Marj", "percent", func(v *productPerformanceExcelVariant) any { + qty, usd, _, _ := values(v) + return productPerformanceExcelMargin(usd, qty, v.BasePriceUSD) + }), + numberExcelColumn(label+" Çıplak Marj", "percent", func(v *productPerformanceExcelVariant) any { + qty, usd, _, _ := values(v) + return productPerformanceExcelMargin(usd, qty, v.CostPriceUSD) + }), + numberExcelColumn(label+" Tekil Piyasa", "int", func(v *productPerformanceExcelVariant) any { + _, _, markets, _ := values(v) + return markets + }), + numberExcelColumn(label+" Tekil Müşteri", "int", func(v *productPerformanceExcelVariant) any { + _, _, _, customers := values(v) + return customers + }), + numberExcelColumn(label+" Ürün Skor", "number", func(v *productPerformanceExcelVariant) any { + qty, usd, markets, customers := values(v) + return productPerformanceExcelProductScore(suffix, usd, productPerformanceExcelStockTurnover(qty, v.StockQty), float64(markets), float64(customers), productPerformanceExcelMargin(usd, qty, v.CostPriceUSD)) + }), } } -func productPerformanceExcelGeneralMap(rows []models.ProductPerformanceGeneralRow) map[string]models.ProductPerformanceGeneralRow { - out := make(map[string]models.ProductPerformanceGeneralRow, len(rows)) - for _, row := range rows { - out[productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)] = row - } - return out +func textExcelColumn(header string, value func(*productPerformanceExcelVariant) any) productPerformanceExcelColumn { + return productPerformanceExcelColumn{Header: header, Kind: "text", Value: func(v *productPerformanceExcelVariant, _ productPerformanceExcelStats) any { return value(v) }} } -func filterProductPerformanceExcelRows(rows []models.ProductPerformanceRow, generalByKey map[string]models.ProductPerformanceGeneralRow, filters map[string][]string) []models.ProductPerformanceRow { - if len(filters) == 0 { - return rows - } - out := make([]models.ProductPerformanceRow, 0, len(rows)) - for _, row := range rows { - general := generalByKey[productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)] - if productPerformanceExcelRowMatchesFilters(row, general, filters) { - out = append(out, row) - } - } - return out +func numberExcelColumn(header, kind string, value func(*productPerformanceExcelVariant) any) productPerformanceExcelColumn { + return productPerformanceExcelColumn{Header: header, Kind: kind, Value: func(v *productPerformanceExcelVariant, _ productPerformanceExcelStats) any { return value(v) }} } -func productPerformanceExcelRowMatchesFilters(row models.ProductPerformanceRow, general models.ProductPerformanceGeneralRow, filters map[string][]string) bool { - for field, values := range filters { - selected := productPerformanceExcelSelectedSet(values) - if len(selected) == 0 { +func productPerformanceExcelProductRowMatchesFilters(row models.ProductPerformanceRow, filters map[string][]string) bool { + for field, selected := range productPerformanceExcelFilterSets(filters) { + if field == "performance_bucket" { continue } - matched := false - for _, candidate := range productPerformanceExcelFilterCandidates(row, general, field) { - if selected[productPerformanceExcelNormalize(candidate)] { - matched = true - break - } - } - if !matched { + if !productPerformanceExcelMatchesAny(selected, productPerformanceExcelProductFilterCandidates(row, field)) { return false } } return true } -func productPerformanceExcelSelectedSet(values []string) map[string]bool { - out := make(map[string]bool, len(values)) - for _, value := range values { - value = productPerformanceExcelNormalize(value) - if value != "" { - out[value] = true +func productPerformanceExcelGeneralRowMatchesFilters(row models.ProductPerformanceGeneralRow, filters map[string][]string) bool { + for field, selected := range productPerformanceExcelFilterSets(filters) { + if field == "performance_bucket" { + continue + } + if !productPerformanceExcelMatchesAny(selected, productPerformanceExcelGeneralFilterCandidates(row, field)) { + return false + } + } + return true +} + +func productPerformanceExcelVariantMatchesPostFilters(row *productPerformanceExcelVariant, filters map[string][]string) bool { + selected := productPerformanceExcelFilterSets(filters)["performance_bucket"] + if len(selected) == 0 { + return true + } + return productPerformanceExcelMatchesAny(selected, []string{row.PerformanceBucket, productPerformanceExcelStatus(row)}) +} + +func productPerformanceExcelFilterSets(filters map[string][]string) map[string]map[string]bool { + out := map[string]map[string]bool{} + for field, values := range filters { + field = strings.TrimSpace(field) + for _, value := range values { + value = productPerformanceExcelNormalize(value) + if value == "" { + continue + } + if out[field] == nil { + out[field] = map[string]bool{} + } + out[field][value] = true } } return out } -func productPerformanceExcelFilterCandidates(row models.ProductPerformanceRow, general models.ProductPerformanceGeneralRow, field string) []string { +func productPerformanceExcelHasFilter(filters map[string][]string, field string) bool { + for _, value := range filters[strings.TrimSpace(field)] { + if strings.TrimSpace(value) != "" { + return true + } + } + return false +} + +func productPerformanceExcelProductFilterCandidates(row models.ProductPerformanceRow, field string) []string { switch strings.TrimSpace(field) { case "product_code": return []string{row.ProductCode} @@ -475,7 +521,7 @@ func productPerformanceExcelFilterCandidates(row models.ProductPerformanceRow, g case "yaka_kodu": return []string{row.YakaKodu} case "color_yaka": - return []string{productPerformanceExcelColorYaka(row), row.ColorCode + "/" + row.YakaKodu} + return []string{productPerformanceExcelColorYaka(row.ColorCode, row.ColorDescription, row.YakaKodu), row.ColorCode + "/" + row.YakaKodu} case "kategori": return []string{row.Kategori} case "askili_yan": @@ -483,33 +529,67 @@ func productPerformanceExcelFilterCandidates(row models.ProductPerformanceRow, g case "urun_ilk_grubu": return []string{productPerformanceExcelCleanOptional(row.UrunIlkGrubu)} case "urun_ana_grubu": - return []string{productPerformanceExcelFirstNonEmpty(general.UrunAnaGrubu, row.UrunAnaGrubu)} + return []string{row.UrunAnaGrubu} case "urun_alt_grubu": return []string{row.UrunAltGrubu} case "market_key": return []string{row.MarketKey, productPerformanceExcelMarketName(row.MarketKey)} - case "performance_bucket": - bucket := productPerformanceExcelFirstNonEmpty(general.PerformanceBucket, row.PerformanceBucket) - return []string{bucket, productPerformanceExcelBucketLabel(bucket)} default: return nil } } -func sortProductPerformanceExcelRows(rows []models.ProductPerformanceRow, generalByKey map[string]models.ProductPerformanceGeneralRow, sortBy string, desc bool) { +func productPerformanceExcelGeneralFilterCandidates(row models.ProductPerformanceGeneralRow, field string) []string { + switch strings.TrimSpace(field) { + case "product_code": + return []string{row.ProductCode} + case "item_description": + return []string{row.ItemDescription} + case "color_code": + return []string{row.ColorCode} + case "yaka_kodu": + return []string{row.YakaKodu} + case "color_yaka": + return []string{productPerformanceExcelColorYaka(row.ColorCode, row.ColorDescription, row.YakaKodu), row.ColorCode + "/" + row.YakaKodu} + case "kategori": + return []string{row.Kategori} + case "askili_yan": + return []string{productPerformanceExcelCleanOptional(row.AskiliYan)} + case "urun_ilk_grubu": + return []string{productPerformanceExcelCleanOptional(row.UrunIlkGrubu)} + case "urun_ana_grubu": + return []string{row.UrunAnaGrubu} + case "urun_alt_grubu": + return []string{row.UrunAltGrubu} + case "market_key": + return []string{row.MarketKey, productPerformanceExcelMarketName(row.MarketKey)} + default: + return nil + } +} + +func productPerformanceExcelMatchesAny(selected map[string]bool, candidates []string) bool { + if len(selected) == 0 { + return true + } + for _, candidate := range candidates { + if selected[productPerformanceExcelNormalize(candidate)] { + return true + } + } + return false +} + +func sortProductPerformanceExcelVariants(rows []*productPerformanceExcelVariant, sortBy string, desc bool) { sortBy = strings.TrimSpace(sortBy) if sortBy == "" || sortBy == "image" { sortBy = "product_code" desc = false } sort.SliceStable(rows, func(i, j int) bool { - left := rows[i] - right := rows[j] - leftGen := generalByKey[productPerformanceExcelVariantKey(left.ProductCode, left.ColorCode, left.YakaKodu)] - rightGen := generalByKey[productPerformanceExcelVariantKey(right.ProductCode, right.ColorCode, right.YakaKodu)] - cmp := productPerformanceExcelCompareSort(left, leftGen, right, rightGen, sortBy) + cmp := productPerformanceExcelCompare(rows[i], rows[j], sortBy) if cmp == 0 { - cmp = strings.Compare(productPerformanceExcelHierarchySortKey(left), productPerformanceExcelHierarchySortKey(right)) + cmp = strings.Compare(productPerformanceExcelHierarchySortKey(rows[i]), productPerformanceExcelHierarchySortKey(rows[j])) } if desc { return cmp > 0 @@ -518,20 +598,19 @@ func sortProductPerformanceExcelRows(rows []models.ProductPerformanceRow, genera }) } -func productPerformanceExcelCompareSort(left models.ProductPerformanceRow, leftGen models.ProductPerformanceGeneralRow, right models.ProductPerformanceRow, rightGen models.ProductPerformanceGeneralRow, sortBy string) int { - leftNum, rightNum, numeric := productPerformanceExcelSortNumeric(left, leftGen, sortBy), productPerformanceExcelSortNumeric(right, rightGen, sortBy), productPerformanceExcelIsNumericSort(sortBy) - if numeric { - if leftNum < rightNum { +func productPerformanceExcelCompare(a, b *productPerformanceExcelVariant, sortBy string) int { + if productPerformanceExcelIsNumericSort(sortBy) { + av := productPerformanceExcelSortNumber(a, sortBy) + bv := productPerformanceExcelSortNumber(b, sortBy) + if av < bv { return -1 } - if leftNum > rightNum { + if av > bv { return 1 } return 0 } - leftText := productPerformanceExcelSortText(left, leftGen, sortBy) - rightText := productPerformanceExcelSortText(right, rightGen, sortBy) - return strings.Compare(productPerformanceExcelNormalize(leftText), productPerformanceExcelNormalize(rightText)) + return strings.Compare(productPerformanceExcelNormalize(productPerformanceExcelSortText(a, sortBy)), productPerformanceExcelNormalize(productPerformanceExcelSortText(b, sortBy))) } func productPerformanceExcelIsNumericSort(sortBy string) bool { @@ -539,17 +618,16 @@ func productPerformanceExcelIsNumericSort(sortBy string) bool { case "stock_qty", "sales_qty_90d", "sales_qty_180d", "sales_qty_365d", "sales_qty_total", "sales_usd_90d", "sales_usd_180d", "sales_usd_365d", "sales_usd_total", "avg_price_usd_90d", "avg_price_usd_180d", "avg_price_usd_365d", "avg_price_usd_total", - "stock_days_90d", "stock_days_180d", "stock_days_365d", "stock_days_total", "stock_turnover_90d", "stock_turnover_180d", "stock_turnover_365d", "stock_turnover_total", "base_price_usd", "cost_price_usd", "market_count_90d", "customer_count_90d", "market_count_total", "customer_count_total", - "sales_index_90d", "sales_index_total", "performance_score", "performance_score_90d", "performance_score_total": + "performance_score", "performance_score_90d", "performance_score_total": return true default: return false } } -func productPerformanceExcelSortNumeric(row models.ProductPerformanceRow, general models.ProductPerformanceGeneralRow, sortBy string) float64 { +func productPerformanceExcelSortNumber(row *productPerformanceExcelVariant, sortBy string) float64 { switch sortBy { case "stock_qty": return row.StockQty @@ -560,7 +638,7 @@ func productPerformanceExcelSortNumeric(row models.ProductPerformanceRow, genera case "sales_qty_365d": return row.SalesQty365 case "sales_qty_total": - return productPerformanceExcelTotalSalesQty(row, general) + return row.totalQty() case "sales_usd_90d": return row.SalesUSD90 case "sales_usd_180d": @@ -568,7 +646,7 @@ func productPerformanceExcelSortNumeric(row models.ProductPerformanceRow, genera case "sales_usd_365d": return row.SalesUSD365 case "sales_usd_total": - return productPerformanceExcelTotalSalesUSD(row, general) + return row.totalUSD() case "avg_price_usd_90d": return productPerformanceExcelAvgPrice(row.SalesUSD90, row.SalesQty90) case "avg_price_usd_180d": @@ -576,54 +654,42 @@ func productPerformanceExcelSortNumeric(row models.ProductPerformanceRow, genera case "avg_price_usd_365d": return productPerformanceExcelAvgPrice(row.SalesUSD365, row.SalesQty365) case "avg_price_usd_total": - return productPerformanceExcelFirstNonZero(general.AvgPriceUSDTotal, productPerformanceExcelAvgPrice(productPerformanceExcelTotalSalesUSD(row, general), productPerformanceExcelTotalSalesQty(row, general))) - case "stock_days_90d": - return row.StockDays90 - case "stock_days_180d": - return row.StockDays180 - case "stock_days_365d": - return row.StockDays365 - case "stock_days_total": - return productPerformanceExcelFirstNonZero(general.StockDaysTotal, row.StockDaysTotal) + return productPerformanceExcelAvgPrice(row.totalUSD(), row.totalQty()) case "stock_turnover_90d": - return row.StockTurnover90 + return productPerformanceExcelStockTurnover(row.SalesQty90, row.StockQty) case "stock_turnover_180d": - return row.StockTurnover180 + return productPerformanceExcelStockTurnover(row.SalesQty180, row.StockQty) case "stock_turnover_365d": - return row.StockTurnover365 + return productPerformanceExcelStockTurnover(row.SalesQty365, row.StockQty) case "stock_turnover_total": - return row.StockTurnoverTotal + return productPerformanceExcelStockTurnover(row.totalQty(), row.StockQty) case "base_price_usd": - return productPerformanceExcelFirstNonZero(general.BasePriceUSD, row.BasePriceUSD) + return row.BasePriceUSD case "cost_price_usd": - return productPerformanceExcelFirstNonZero(general.CostPriceUSD, row.CostPriceUSD) + return row.CostPriceUSD case "market_count_90d": - return float64(row.MarketCount90) + return float64(row.marketCount90()) case "customer_count_90d": return float64(row.CustomerCount90) case "market_count_total": - return float64(general.MarketCountTotal) + return float64(row.marketCountTotal()) case "customer_count_total": - return float64(general.CustomerCountTotal) - case "sales_index_90d": - return row.SalesIndex90 - case "sales_index_total": - return general.SalesIndexTotal + return float64(row.CustomerCountTotal) case "performance_score", "performance_score_90d": - return row.PerformanceScore + return productPerformanceExcelProductScore("90d", row.SalesUSD90, productPerformanceExcelStockTurnover(row.SalesQty90, row.StockQty), float64(row.marketCount90()), float64(row.CustomerCount90), productPerformanceExcelMargin(row.SalesUSD90, row.SalesQty90, row.CostPriceUSD)) case "performance_score_total": - return general.PerformanceScore + return productPerformanceExcelProductScore("total", row.totalUSD(), productPerformanceExcelStockTurnover(row.totalQty(), row.StockQty), float64(row.marketCountTotal()), float64(row.CustomerCountTotal), productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD)) default: return 0 } } -func productPerformanceExcelSortText(row models.ProductPerformanceRow, general models.ProductPerformanceGeneralRow, sortBy string) string { +func productPerformanceExcelSortText(row *productPerformanceExcelVariant, sortBy string) string { switch sortBy { case "product_code": return row.ProductCode case "color_yaka": - return productPerformanceExcelColorYaka(row) + return productPerformanceExcelColorYaka(row.ColorCode, row.ColorDescription, row.YakaKodu) case "color_code": return row.ColorCode case "yaka_kodu": @@ -633,40 +699,26 @@ func productPerformanceExcelSortText(row models.ProductPerformanceRow, general m case "kategori": return row.Kategori case "askili_yan": - return productPerformanceExcelCleanOptional(row.AskiliYan) + return row.AskiliYan case "urun_ilk_grubu": - return productPerformanceExcelCleanOptional(row.UrunIlkGrubu) + return row.UrunIlkGrubu case "urun_ana_grubu": - return productPerformanceExcelFirstNonEmpty(general.UrunAnaGrubu, row.UrunAnaGrubu) + return row.UrunAnaGrubu case "urun_alt_grubu": return row.UrunAltGrubu case "market_key": - return productPerformanceExcelMarketName(row.MarketKey) + return row.marketsLabel() case "performance_bucket": - return productPerformanceExcelBucketLabel(productPerformanceExcelFirstNonEmpty(general.PerformanceBucket, row.PerformanceBucket)) + return productPerformanceExcelStatus(row) default: return productPerformanceExcelHierarchySortKey(row) } } -func productPerformanceExcelHierarchySortKey(row models.ProductPerformanceRow) string { - return strings.Join([]string{ - productPerformanceExcelCleanOptional(row.UrunIlkGrubu), - productPerformanceExcelCleanOptional(row.AskiliYan), - row.Kategori, - row.UrunAnaGrubu, - row.UrunAltGrubu, - row.ProductCode, - row.ColorCode, - row.YakaKodu, - }, "|") -} - -func productPerformanceExcelStatsFor(rows []models.ProductPerformanceRow, generalByKey map[string]models.ProductPerformanceGeneralRow) productPerformanceExcelStats { +func productPerformanceExcelStatsFor(rows []*productPerformanceExcelVariant) productPerformanceExcelStats { var stats productPerformanceExcelStats var c90, c180, c365, cTotal float64 for _, row := range rows { - general := generalByKey[productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)] if row.SalesUSD90 > 0 { stats.AvgSalesUSD90 += row.SalesUSD90 c90++ @@ -679,8 +731,8 @@ func productPerformanceExcelStatsFor(rows []models.ProductPerformanceRow, genera stats.AvgSalesUSD365 += row.SalesUSD365 c365++ } - if total := productPerformanceExcelTotalSalesUSD(row, general); total > 0 { - stats.AvgSalesUSDTotal += total + if row.totalUSD() > 0 { + stats.AvgSalesUSDTotal += row.totalUSD() cTotal++ } } @@ -703,12 +755,16 @@ func productPerformanceExcelVariantKey(productCode, colorCode, yakaKodu string) return strings.ToUpper(strings.TrimSpace(productCode)) + "|" + strings.ToUpper(strings.TrimSpace(colorCode)) + "|" + strings.ToUpper(strings.TrimSpace(yakaKodu)) } -func productPerformanceExcelColorYaka(row models.ProductPerformanceRow) string { - color := strings.TrimSpace(row.ColorCode) - if desc := strings.TrimSpace(row.ColorDescription); color != "" && desc != "" { +func productPerformanceExcelHierarchySortKey(row *productPerformanceExcelVariant) string { + return strings.Join([]string{row.UrunIlkGrubu, row.AskiliYan, row.Kategori, row.UrunAnaGrubu, row.UrunAltGrubu, row.ProductCode, row.ColorCode, row.YakaKodu}, "|") +} + +func productPerformanceExcelColorYaka(colorCode, colorDescription, yakaKodu string) string { + color := strings.TrimSpace(colorCode) + if desc := strings.TrimSpace(colorDescription); color != "" && desc != "" { color = color + "-" + strings.ToUpper(desc) } - yaka := strings.TrimSpace(row.YakaKodu) + yaka := strings.TrimSpace(yakaKodu) parts := make([]string, 0, 2) if color != "" && color != "-" { parts = append(parts, color) @@ -730,6 +786,34 @@ func productPerformanceExcelMarketName(value string) string { return "" } +func productPerformanceExcelStatus(row *productPerformanceExcelVariant) string { + switch { + case row.totalQty() > 0 && row.totalUSD() <= 0: + return "Ciro/Fiyat Kontrol" + case row.totalQty() <= 0 && row.StockQty > 0: + return "Stok Riski" + case productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD) < 0: + return "Fiyat Baskısı" + case productPerformanceExcelProductScore("total", row.totalUSD(), productPerformanceExcelStockTurnover(row.totalQty(), row.StockQty), float64(row.marketCountTotal()), float64(row.CustomerCountTotal), productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD)) >= 70: + return "Yıldız Ürün" + default: + return productPerformanceExcelBucketLabel(row.PerformanceBucket) + } +} + +func productPerformanceExcelRecommendation(row *productPerformanceExcelVariant) string { + switch { + case row.totalQty() > 0 && row.totalUSD() <= 0: + return "Satış adedi var ama USD ciro/fiyat yok. Satış tutarı aktarımı ve döviz dönüşümü kontrol edilmeli." + case row.totalQty() <= 0 && row.StockQty > 0: + return "Satış yok, stok duruyor. Piyasa/fiyat aksiyonu gerekli." + case row.Recommendation != "": + return row.Recommendation + default: + return "Düzenli takip." + } +} + func productPerformanceExcelBucketLabel(value string) string { switch strings.TrimSpace(value) { case "YILDIZ_URUN": @@ -782,16 +866,8 @@ func productPerformanceExcelFirstNonZero(values ...float64) float64 { return 0 } -func productPerformanceExcelTotalSalesQty(row models.ProductPerformanceRow, general models.ProductPerformanceGeneralRow) float64 { - return productPerformanceExcelFirstNonZero(general.SalesQtyTotal, row.SalesQtyTotal) -} - -func productPerformanceExcelTotalSalesUSD(row models.ProductPerformanceRow, general models.ProductPerformanceGeneralRow) float64 { - return productPerformanceExcelFirstNonZero(general.SalesUSDTotal, row.SalesUSDTotal) -} - func productPerformanceExcelAvgPrice(salesUSD, qty float64) float64 { - if qty == 0 { + if qty <= 0 { return 0 } return salesUSD / qty @@ -804,13 +880,6 @@ func productPerformanceExcelStockTurnover(salesQty, stockQty float64) float64 { return salesQty / stockQty } -func productPerformanceExcelUnitProfit(salesUSD, qty, unitCost float64) float64 { - if qty <= 0 { - return 0 - } - return (salesUSD / qty) - unitCost -} - func productPerformanceExcelGrossProfit(salesUSD, qty, unitCost float64) float64 { if qty <= 0 { return 0 @@ -819,27 +888,20 @@ func productPerformanceExcelGrossProfit(salesUSD, qty, unitCost float64) float64 } func productPerformanceExcelMargin(salesUSD, qty, unitCost float64) float64 { - if salesUSD == 0 { + if salesUSD <= 0 { return 0 } return productPerformanceExcelGrossProfit(salesUSD, qty, unitCost) / salesUSD } -func productPerformanceExcelRelativeIndex(value, average float64) float64 { - if value <= 0 || average <= 0 { - return 0 - } - return value / average -} - -func productPerformanceExcelProductScore(suffix string, salesUSD, salesIndex, margin, stockTurnover, marketCount, customerCount float64) float64 { - revenue := productPerformanceExcelRatioScore(salesIndex, 2) - if revenue == 0 { - revenue = productPerformanceExcelRatioScore(salesUSD, productPerformanceExcelProductRevenueTarget(suffix)) +func productPerformanceExcelProductScore(suffix string, salesUSD, stockTurnover, marketCount, customerCount, margin float64) float64 { + if salesUSD <= 0 { + return 1 } + revenueScore := productPerformanceExcelRatioScore(salesUSD, productPerformanceExcelProductRevenueTarget(suffix)) score := 0.30*productPerformanceExcelMarginScore(margin) + 0.20*productPerformanceExcelRatioScore(stockTurnover, 1.50) + - 0.20*revenue + + 0.20*revenueScore + 0.15*productPerformanceExcelRatioScore(marketCount, 8) + 0.15*productPerformanceExcelRatioScore(customerCount, 25) return productPerformanceExcelRoundScore(score) @@ -915,15 +977,36 @@ func productPerformanceExcelFloat(value any) float64 { } } +func productPerformanceExcelSortedSetLabel(values map[string]bool) string { + out := make([]string, 0, len(values)) + for value := range values { + if strings.TrimSpace(value) != "" { + out = append(out, value) + } + } + sort.Strings(out) + return strings.Join(out, ", ") +} + func productPerformanceExcelColumnWidth(header, kind string) float64 { if kind == "text" { + if strings.Contains(header, "Piyasalar") || strings.Contains(header, "Öneri") { + return 34 + } if len([]rune(header)) > 14 { return 24 } return 16 } - if strings.Contains(header, "Marj") || strings.Contains(header, "Skor") || strings.Contains(header, "Endeks") { + if strings.Contains(header, "Marj") || strings.Contains(header, "Skor") { return 14 } return 16 } + +func productPerformanceExcelMaxInt(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/ui/src/pages/ProductPerformanceProfitability.vue b/ui/src/pages/ProductPerformanceProfitability.vue index 54dc691..68b02f4 100644 --- a/ui/src/pages/ProductPerformanceProfitability.vue +++ b/ui/src/pages/ProductPerformanceProfitability.vue @@ -1670,6 +1670,7 @@ const productPerformanceExcelExportFilterFields = new Set([ 'color_yaka', 'color_code', 'yaka_kodu', + 'market_key', 'performance_bucket' ]) const performanceTabs = [ @@ -1815,16 +1816,16 @@ const columns = [ { name: 'stock_days_180d', label: '180G Stok Gün', field: row => formatNumber(row.stock_days_180d, 1), align: 'right', sortable: true }, { name: 'stock_days_365d', label: '360G Stok Gün', field: row => formatNumber(row.stock_days_365d, 1), align: 'right', sortable: true }, { name: 'stock_days_total', label: 'Genel Stok Gün', field: row => formatNumber(row.stock_days_total, 1), align: 'right', sortable: true }, - { name: 'stock_turnover_90d', label: '90G Stok Devir', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true }, - { name: 'stock_turnover_180d', label: '180G Stok Devir', field: row => formatNumber(row.stock_turnover_180d, 2), align: 'right', sortable: true }, - { name: 'stock_turnover_365d', label: '360G Stok Devir', field: row => formatNumber(row.stock_turnover_365d, 2), align: 'right', sortable: true }, - { name: 'stock_turnover_total', label: 'Genel Stok Devir', field: row => formatNumber(row.stock_turnover_total, 2), align: 'right', sortable: true }, - { name: 'avg_price_usd_90d', label: 'Ort. USD', field: row => formatMoney(row.avg_price_usd_90d, 'USD'), align: 'right' }, - { name: 'avg_price_usd_180d', label: '180G Ort. USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), align: 'right' }, - { name: 'avg_price_usd_365d', label: '360G Ort. USD', field: row => formatMoney(row.avg_price_usd_365d, 'USD'), align: 'right' }, - { name: 'avg_price_usd_total', label: 'Genel Ort. USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right' }, - { name: 'base_price_usd', label: 'Ort. USD Taban Maliyeti', field: row => formatMoney(row.base_price_usd, 'USD'), align: 'right', sortable: true }, - { name: 'cost_price_usd', label: 'Ort. USD Çıplak Maliyeti', field: row => formatMoney(row.cost_price_usd, 'USD'), align: 'right', sortable: true }, + { name: 'stock_turnover_90d', label: '90G Satış/Stok', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true }, + { name: 'stock_turnover_180d', label: '180G Satış/Stok', field: row => formatNumber(row.stock_turnover_180d, 2), align: 'right', sortable: true }, + { name: 'stock_turnover_365d', label: '360G Satış/Stok', field: row => formatNumber(row.stock_turnover_365d, 2), align: 'right', sortable: true }, + { name: 'stock_turnover_total', label: 'Genel Satış/Stok', field: row => formatNumber(row.stock_turnover_total, 2), align: 'right', sortable: true }, + { name: 'avg_price_usd_90d', label: '90G Ciro/Adet USD', field: row => formatMoney(row.avg_price_usd_90d, 'USD'), align: 'right' }, + { name: 'avg_price_usd_180d', label: '180G Ciro/Adet USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), align: 'right' }, + { name: 'avg_price_usd_365d', label: '360G Ciro/Adet USD', field: row => formatMoney(row.avg_price_usd_365d, 'USD'), align: 'right' }, + { name: 'avg_price_usd_total', label: 'Genel Ciro/Adet USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right' }, + { name: 'base_price_usd', label: 'USD Taban Maliyeti', field: row => formatMoney(row.base_price_usd, 'USD'), align: 'right', sortable: true }, + { name: 'cost_price_usd', label: 'USD Çıplak Maliyeti', field: row => formatMoney(row.cost_price_usd, 'USD'), align: 'right', sortable: true }, { name: 'unit_profit_base_90d', label: '90G P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_90d, 'USD'), align: 'right', sortable: true }, { name: 'unit_profit_cost_90d', label: '90G P.Başı Çıplak K/Z', field: row => formatMoney(row.unit_profit_cost_90d, 'USD'), align: 'right', sortable: true }, { name: 'unit_profit_base_180d', label: '180G P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_180d, 'USD'), align: 'right', sortable: true }, @@ -1834,6 +1835,8 @@ const columns = [ { name: 'gross_profit_usd_90d', label: '90G Toplam K/Z', field: row => formatMoney(row.gross_profit_usd_90d, 'USD'), align: 'right', sortable: true }, { name: 'gross_profit_usd_180d', label: '180G Toplam K/Z', field: row => formatMoney(row.gross_profit_usd_180d, 'USD'), align: 'right', sortable: true }, { name: 'gross_profit_usd_total', label: 'Genel Toplam K/Z', field: row => formatMoney(row.gross_profit_usd_total, 'USD'), align: 'right', sortable: true }, + { name: 'gross_profit_base_usd_total', label: 'Genel Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_total, 'USD'), align: 'right', sortable: true }, + { name: 'gross_profit_cost_usd_total', label: 'Genel Çıplak K/Z', field: row => formatMoney(row.gross_profit_cost_usd_total, 'USD'), align: 'right', sortable: true }, { name: 'gross_margin_base_90d', label: '90G Taban Marj', field: row => formatPercent(row.gross_margin_base_90d), align: 'right', sortable: true }, { name: 'gross_margin_cost_90d', label: '90G Çıplak Marj', field: row => formatPercent(row.gross_margin_cost_90d), align: 'right', sortable: true }, { name: 'gross_margin_base_180d', label: '180G Taban Marj', field: row => formatPercent(row.gross_margin_base_180d), align: 'right', sortable: true }, @@ -1865,7 +1868,23 @@ const periodOrder = { total: 4 } -const hiddenTableColumns = new Set(['item_description', 'performance_score']) +const hiddenTableColumns = new Set([ + 'item_description', + 'performance_score', + 'gross_profit_usd_total', + 'invoice_count_90d', + 'invoice_count_180d', + 'invoice_count_365d', + 'invoice_count_total', + 'sales_index_90d', + 'sales_index_180d', + 'sales_index_365d', + 'sales_index_total', + 'stock_days_90d', + 'stock_days_180d', + 'stock_days_365d', + 'stock_days_total' +]) const dimensionColumnNames = [ 'product_code', 'color_yaka', @@ -2022,16 +2041,18 @@ const generalColumns = [ { name: 'period_start', label: 'Başlangıç', field: 'period_start', align: 'left', sortable: true }, { name: 'period_end', label: 'Bitiş', field: 'period_end', align: 'left', sortable: true }, { name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true }, - { name: 'sales_qty_total', label: 'Toplam Satış', field: row => formatNumber(row.sales_qty_total, 0), align: 'right', sortable: true }, - { name: 'sales_usd_total', label: 'Toplam USD', field: row => formatMoney(row.sales_usd_total, 'USD'), align: 'right', sortable: true }, + { name: 'sales_qty_total', label: 'Toplam Adet', field: row => formatNumber(row.sales_qty_total, 0), align: 'right', sortable: true }, + { name: 'sales_usd_total', label: 'Toplam Ciro USD', field: row => formatMoney(row.sales_usd_total, 'USD'), align: 'right', sortable: true }, { name: 'avg_daily_sales_total', label: 'Günlük Ort.', field: row => formatNumber(row.avg_daily_sales_total, 3), align: 'right', sortable: true }, { name: 'stock_days_total', label: 'Genel Stok Gün', field: row => formatNumber(row.stock_days_total, 1), align: 'right', sortable: true }, - { name: 'avg_price_usd_total', label: 'Genel Ort. USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right', sortable: true }, + { name: 'avg_price_usd_total', label: 'Genel Ciro/Adet USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right', sortable: true }, { name: 'base_price_usd', label: 'USD Taban Maliyeti', field: row => formatMoney(row.base_price_usd, 'USD'), align: 'right', sortable: true }, { name: 'cost_price_usd', label: 'USD Çıplak Maliyeti', field: row => formatMoney(row.cost_price_usd, 'USD'), align: 'right', sortable: true }, { name: 'unit_profit_base_total', label: 'P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_total, 'USD'), align: 'right', sortable: true }, { name: 'unit_profit_cost_total', label: 'P.Başı Çıplak K/Z', field: row => formatMoney(row.unit_profit_cost_total, 'USD'), align: 'right', sortable: true }, { name: 'gross_profit_usd_total', label: 'Toplam K/Z', field: row => formatMoney(row.gross_profit_usd_total, 'USD'), align: 'right', sortable: true }, + { name: 'gross_profit_base_usd_total', label: 'Taban Toplam K/Z', field: row => formatMoney(row.gross_profit_base_usd_total, 'USD'), align: 'right', sortable: true }, + { name: 'gross_profit_cost_usd_total', label: 'Çıplak Toplam K/Z', field: row => formatMoney(row.gross_profit_cost_usd_total, 'USD'), align: 'right', sortable: true }, { name: 'gross_margin_base_total', label: 'Genel Taban Marj', field: row => formatPercent(row.gross_margin_base_total), align: 'right', sortable: true }, { name: 'gross_margin_cost_total', label: 'Genel Çıplak Marj', field: row => formatPercent(row.gross_margin_cost_total), align: 'right', sortable: true }, { name: 'market_count_total', label: 'Toplam Piyasa', field: row => formatNumber(row.market_count_total, 0), align: 'right', sortable: true }, @@ -2338,27 +2359,27 @@ const metricLabelOverrides = { sales_usd_180d: '180G Toplam Ciro USD', sales_usd_365d: '360G Toplam Ciro USD', sales_usd_total: 'Genel Toplam Ciro USD', - avg_price_usd: 'Ağ. Ort. Fiyat USD', - avg_price_usd_90d: '90G Ağ. Ort. Fiyat USD', - avg_price_usd_180d: '180G Ağ. Ort. Fiyat USD', - avg_price_usd_365d: '360G Ağ. Ort. Fiyat USD', - avg_price_usd_total: 'Genel Ağ. Ort. Fiyat USD', - base_price_usd: 'Ort. Taban Maliyet USD', - cost_price_usd: 'Ort. Çıplak Maliyet USD', - base_price_usd_90d: '90G Ort. Taban Maliyet USD', - cost_price_usd_90d: '90G Ort. Çıplak Maliyet USD', - base_price_usd_180d: '180G Ort. Taban Maliyet USD', - cost_price_usd_180d: '180G Ort. Çıplak Maliyet USD', - base_price_usd_365d: '360G Ort. Taban Maliyet USD', - cost_price_usd_365d: '360G Ort. Çıplak Maliyet USD', - base_price_usd_total: 'Genel Ort. Taban Maliyet USD', - cost_price_usd_total: 'Genel Ort. Çıplak Maliyet USD', - unit_profit_base_90d: '90G Ağ. Ort. Birim Taban K/Z USD', - unit_profit_cost_90d: '90G Ağ. Ort. Birim Çıplak K/Z USD', - unit_profit_base_180d: '180G Ağ. Ort. Birim Taban K/Z USD', - unit_profit_cost_180d: '180G Ağ. Ort. Birim Çıplak K/Z USD', - unit_profit_base_total: 'Genel Ağ. Ort. Birim Taban K/Z USD', - unit_profit_cost_total: 'Genel Ağ. Ort. Birim Çıplak K/Z USD', + avg_price_usd: 'Ciro/Adet USD', + avg_price_usd_90d: '90G Ciro/Adet USD', + avg_price_usd_180d: '180G Ciro/Adet USD', + avg_price_usd_365d: '360G Ciro/Adet USD', + avg_price_usd_total: 'Genel Ciro/Adet USD', + base_price_usd: 'Taban Maliyet USD', + cost_price_usd: 'Çıplak Maliyet USD', + base_price_usd_90d: '90G Taban Maliyet USD', + cost_price_usd_90d: '90G Çıplak Maliyet USD', + base_price_usd_180d: '180G Taban Maliyet USD', + cost_price_usd_180d: '180G Çıplak Maliyet USD', + base_price_usd_365d: '360G Taban Maliyet USD', + cost_price_usd_365d: '360G Çıplak Maliyet USD', + base_price_usd_total: 'Genel Taban Maliyet USD', + cost_price_usd_total: 'Genel Çıplak Maliyet USD', + unit_profit_base_90d: '90G Birim Taban K/Z USD', + unit_profit_cost_90d: '90G Birim Çıplak K/Z USD', + unit_profit_base_180d: '180G Birim Taban K/Z USD', + unit_profit_cost_180d: '180G Birim Çıplak K/Z USD', + unit_profit_base_total: 'Genel Birim Taban K/Z USD', + unit_profit_cost_total: 'Genel Birim Çıplak K/Z USD', gross_profit_usd_90d: '90G Toplam K/Z USD', gross_profit_usd_180d: '180G Toplam K/Z USD', gross_profit_usd_total: 'Genel Toplam K/Z USD', @@ -2370,33 +2391,37 @@ const metricLabelOverrides = { gross_profit_cost_usd_365d: '360G Çıplak Toplam K/Z USD', gross_profit_base_usd_total: 'Genel Taban Toplam K/Z USD', gross_profit_cost_usd_total: 'Genel Çıplak Toplam K/Z USD', - gross_margin_base_90d: '90G Ağ. Ort. Taban Marj', - gross_margin_cost_90d: '90G Ağ. Ort. Çıplak Marj', - gross_margin_base_180d: '180G Ağ. Ort. Taban Marj', - gross_margin_cost_180d: '180G Ağ. Ort. Çıplak Marj', - gross_margin_base_365d: '360G Ağ. Ort. Taban Marj', - gross_margin_cost_365d: '360G Ağ. Ort. Çıplak Marj', - gross_margin_base_total: 'Genel Ağ. Ort. Taban Marj', - gross_margin_cost_total: 'Genel Ağ. Ort. Çıplak Marj', - performance_score: 'Ağ. Ort. Skor', - performance_score_90d: '90G Ağ. Ort. Ürün Skor', - performance_score_180d: '180G Ağ. Ort. Ürün Skor', - performance_score_365d: '360G Ağ. Ort. Ürün Skor', - performance_score_total: 'Genel Ağ. Ort. Ürün Skor', - customer_score_90d: '90G Ağ. Ort. Müşteri Skor', - customer_score_180d: '180G Ağ. Ort. Müşteri Skor', - customer_score_365d: '360G Ağ. Ort. Müşteri Skor', - customer_score_total: 'Genel Ağ. Ort. Müşteri Skor', + gross_margin_base_90d: '90G Taban Marj', + gross_margin_cost_90d: '90G Çıplak Marj', + gross_margin_base_180d: '180G Taban Marj', + gross_margin_cost_180d: '180G Çıplak Marj', + gross_margin_base_365d: '360G Taban Marj', + 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', + performance_score_total: 'Genel Ürün Skor', + customer_score_90d: '90G Müşteri Skor', + customer_score_180d: '180G Müşteri Skor', + customer_score_365d: '360G Müşteri Skor', + customer_score_total: 'Genel Müşteri Skor', market_count_90d: '90G Tekil Piyasa', customer_count_90d: '90G Tekil Müşteri', market_count_total: 'Genel Tekil Piyasa', customer_count_total: 'Genel Tekil Müşteri', - sales_index_90d: '90G Ağ. Ort. Endeks', - sales_index_total: 'Genel Ağ. Ort. Endeks', - stock_days_90d: '90G Ağ. Ort. Stok Gün', - stock_days_180d: '180G Ağ. Ort. Stok Gün', - stock_days_365d: '360G Ağ. Ort. Stok Gün', - stock_days_total: 'Genel Ağ. Ort. Stok Gün', + stock_turnover_90d: '90G Satış/Stok', + stock_turnover_180d: '180G Satış/Stok', + stock_turnover_365d: '360G Satış/Stok', + stock_turnover_total: 'Genel Satış/Stok', + sales_index_90d: '90G Endeks', + sales_index_total: 'Genel Endeks', + stock_days_90d: '90G Stok Gün', + stock_days_180d: '180G Stok Gün', + stock_days_365d: '360G Stok Gün', + stock_days_total: 'Genel Stok Gün', avg_daily_sales_total: 'Genel Günlük Ort. Adet' } @@ -3365,10 +3390,10 @@ function makeGroupRow (key, level, groupDef, value, groupRows) { gross_profit_usd_90d: sumRows(groupRows, 'gross_profit_usd_90d'), gross_profit_usd_180d: sumRows(groupRows, 'gross_profit_usd_180d'), gross_profit_usd_total: sumRows(groupRows, 'gross_profit_usd_total'), - market_count_90d: distinctCount(groupRows, 'market_key'), - customer_count_90d: sumRows(groupRows, 'customer_count_90d'), - market_count_total: sumRows(groupRows, 'market_count_total'), - customer_count_total: sumRows(groupRows, 'customer_count_total'), + market_count_90d: distinctSpreadCount(groupRows, 'market_count_90d'), + customer_count_90d: distinctSpreadCount(groupRows, 'customer_count_90d'), + market_count_total: distinctSpreadCount(groupRows, 'market_count_total'), + customer_count_total: distinctSpreadCount(groupRows, 'customer_count_total'), avg_price_usd_90d: weightedAverage(groupRows, 'sales_usd_90d', 'sales_qty_90d'), avg_price_usd_180d: weightedAverage(groupRows, 'sales_usd_180d', 'sales_qty_180d'), avg_price_usd_365d: weightedAverage(groupRows, 'sales_usd_365d', 'sales_qty_365d'), @@ -3425,6 +3450,10 @@ function aggregateGroupFields (sourceRows, groupField = '') { out[field] = weightedAverageProductCost(sourceRows, field) continue } + if (isDistinctSpreadCountField(field)) { + out[field] = distinctSpreadCount(sourceRows, field) + continue + } if (shouldSumField(field)) { out[field] = sumRows(sourceRows, field) } else if (shouldAverageField(field)) { @@ -3576,8 +3605,9 @@ function periodMetricSource (row, suffix) { } } -function productScore100 ({ suffix = '90d', salesUSD = 0, salesIndex = 0, margin = 0, stockTurnover = 0, marketCount = 0, customerCount = 0 } = {}) { - const revenue = salesIndex > 0 ? ratioScore(salesIndex, 2) : ratioScore(salesUSD, productRevenueTarget(suffix)) +function productScore100 ({ suffix = '90d', salesUSD = 0, margin = 0, stockTurnover = 0, marketCount = 0, customerCount = 0 } = {}) { + if (Number(salesUSD || 0) <= 0) return 1 + const revenue = ratioScore(salesUSD, productRevenueTarget(suffix)) return clampScore( 0.30 * marginScore(margin) + 0.20 * ratioScore(stockTurnover, 1.5) + @@ -3588,6 +3618,7 @@ function productScore100 ({ suffix = '90d', salesUSD = 0, salesIndex = 0, margin } function customerScore100 ({ suffix = '90d', salesUSD = 0, margin = 0, productGroupCount = 0, salesQty = 0 } = {}) { + if (Number(salesUSD || 0) <= 0) return 1 return clampScore( 0.35 * ratioScore(salesUSD, customerRevenueTarget(suffix)) + 0.30 * marginScore(margin) + @@ -3708,6 +3739,56 @@ function distinctCount (sourceRows, field) { return values.size } +function isDistinctSpreadCountField (field) { + return /^market_count(_|$)/i.test(field) || /^customer_count(_|$)/i.test(field) +} + +function countPeriodSuffix (field) { + if (field.includes('_180d')) return '180d' + if (field.includes('_365d')) return '365d' + if (field.includes('_total')) return 'total' + return '90d' +} + +function salesUsdFieldForSuffix (suffix) { + return `sales_usd_${suffix}` +} + +function distinctSpreadCount (sourceRows, field) { + const suffix = countPeriodSuffix(field) + return /^market_count/i.test(field) + ? distinctRevenueMarketCount(sourceRows, suffix, field) + : distinctRevenueCustomerCount(sourceRows, suffix, field) +} + +function distinctRevenueMarketCount (sourceRows, suffix, fallbackField) { + const salesField = salesUsdFieldForSuffix(suffix) + const values = new Set() + for (const row of sourceRows) { + if (Number(row?.[salesField] || 0) <= 0) continue + const value = displayMarketName(row?.market_key) + if (value && value !== 'STOK' && value !== '-') values.add(value) + } + if (values.size > 0) return values.size + return sourceRows.reduce((sum, row) => { + return Number(row?.[salesField] || 0) > 0 ? sum + Number(row?.[fallbackField] || 0) : sum + }, 0) +} + +function distinctRevenueCustomerCount (sourceRows, suffix, fallbackField) { + const salesField = salesUsdFieldForSuffix(suffix) + const values = new Set() + for (const row of sourceRows) { + if (Number(row?.[salesField] || 0) <= 0) continue + const code = String(row?.customer_code || '').trim() + if (code && code !== '-') values.add(code) + } + if (values.size > 0) return values.size + return sourceRows.reduce((sum, row) => { + return Number(row?.[salesField] || 0) > 0 ? sum + Number(row?.[fallbackField] || 0) : sum + }, 0) +} + function distinctSummary (sourceRows, field, fallbackField) { const values = new Set() for (const row of sourceRows) {