Fix product performance grouped JSON build

This commit is contained in:
M_Kececi
2026-07-05 22:06:43 +03:00
parent fbdde54eee
commit 9e1ecb9b3f
5 changed files with 766 additions and 564 deletions
+58 -59
View File
@@ -1978,7 +1978,7 @@ SalesAgg AS (
MAX(last_ref_number) AS last_ref_number, MAX(last_ref_number) AS last_ref_number,
COALESCE(SUM(sales_qty),0) AS sales_qty_total, COALESCE(SUM(sales_qty),0) AS sales_qty_total,
COALESCE(SUM(sales_usd),0) AS sales_usd_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 COALESCE(SUM(invoice_count),0)::integer AS invoice_count_total
FROM mk_product_performance_sales_daily, Bounds FROM mk_product_performance_sales_daily, Bounds
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
@@ -2102,8 +2102,8 @@ Spread AS (
product_code, product_code,
color_code, color_code,
yaka_kodu, yaka_kodu,
COUNT(DISTINCT NULLIF(market_key, 'STOK'))::integer AS market_count_total, COUNT(DISTINCT NULLIF(market_key, 'STOK')) FILTER (WHERE COALESCE(sales_usd,0) > 0)::integer AS market_count_total,
COUNT(DISTINCT NULLIF(customer_code, '-'))::integer AS customer_count_total_all 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 FROM mk_product_performance_sales_daily, Bounds
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end 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') 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_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.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, 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, CASE WHEN b.market_key <> 'STOK' AND b.sales_usd_total > 0 THEN 1 ELSE 0 END AS market_count_total,
COALESCE(sp.customer_count_total_all, b.customer_count_total) AS customer_count_total, b.customer_count_total AS customer_count_total,
b.invoice_count_total, b.invoice_count_total,
CASE CASE
WHEN AVG(b.sales_qty_total) OVER (PARTITION BY b.market_key, b.kategori, b.urun_ana_grubu) <= 0 THEN 0 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) 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) / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)
END AS avg_price_usd_90d, 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(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_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 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 '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 >= 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 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 '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')::integer AS market_count_180d, 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')::integer AS market_count_365d, 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')::integer AS market_count_total, 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(stock_qty),0) AS stock_qty,
COALESCE(MAX(avg_stock_90d),0) AS avg_stock_90d, COALESCE(MAX(avg_stock_90d),0) AS avg_stock_90d,
COALESCE(MAX(avg_stock_180d),0) AS avg_stock_180d, COALESCE(MAX(avg_stock_180d),0) AS avg_stock_180d,
COALESCE(MAX(avg_stock_365d),0) AS avg_stock_365d, COALESCE(MAX(avg_stock_365d),0) AS avg_stock_365d,
COALESCE(MAX(avg_stock_total),0) AS avg_stock_total, 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(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_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, 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) 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) / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)
END AS cost_price_usd_90d, 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(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_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, 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, 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_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, 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, 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 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) 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) 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) / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)
END AS cost_price_usd_365d, 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(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_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, 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, CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) ELSE 0 END AS stock_turnover_total,
has_cost, has_cost,
sales_index_90d, 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(( ROUND((
LEAST(35, sales_usd_90d / 1000) LEAST(35, sales_usd_90d / 25000 * 35)
+ LEAST(25, GREATEST(gross_margin_cost_90d,0) * 55) + LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.40 * 30)
+ LEAST(20, invoice_count_90d * 2) + LEAST(20, product_group_count_90d / 8.0 * 20)
+ LEAST(10, sales_qty_90d / 10) + LEAST(15, sales_qty_90d / 500 * 15)
+ LEAST(10, product_count)
)::numeric, 4) )::numeric, 4)
END AS customer_score_90d, 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(( ROUND((
LEAST(35, sales_usd_180d / 1000) LEAST(35, sales_usd_180d / 50000 * 35)
+ LEAST(25, GREATEST(gross_margin_cost_180d,0) * 55) + LEAST(30, GREATEST(gross_margin_cost_180d,0) / 0.40 * 30)
+ LEAST(20, invoice_count_180d * 2) + LEAST(20, product_group_count_180d / 8.0 * 20)
+ LEAST(10, sales_qty_180d / 10) + LEAST(15, sales_qty_180d / 1000 * 15)
+ LEAST(10, product_count)
)::numeric, 4) )::numeric, 4)
END AS customer_score_180d, 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(( ROUND((
LEAST(35, sales_usd_365d / 1000) LEAST(35, sales_usd_365d / 100000 * 35)
+ LEAST(25, GREATEST(gross_margin_cost_365d,0) * 55) + LEAST(30, GREATEST(gross_margin_cost_365d,0) / 0.40 * 30)
+ LEAST(20, invoice_count_365d * 2) + LEAST(20, product_group_count_365d / 8.0 * 20)
+ LEAST(10, sales_qty_365d / 10) + LEAST(15, sales_qty_365d / 2000 * 15)
+ LEAST(10, product_count)
)::numeric, 4) )::numeric, 4)
END AS customer_score_365d, 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(( ROUND((
LEAST(35, sales_usd_total / 1000) LEAST(35, sales_usd_total / 300000 * 35)
+ LEAST(25, GREATEST(gross_margin_cost_total,0) * 55) + LEAST(30, GREATEST(gross_margin_cost_total,0) / 0.40 * 30)
+ LEAST(20, invoice_count_total * 2) + LEAST(20, product_group_count_total / 8.0 * 20)
+ LEAST(10, sales_qty_total / 10) + LEAST(15, sales_qty_total / 6000 * 15)
+ LEAST(10, product_count)
)::numeric, 4) )::numeric, 4)
END AS customer_score_total, 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( ROUND(
LEAST(35, sales_index_90d * 18) LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.40 * 30)
+ LEAST(30, GREATEST(gross_margin_cost_90d,0) * 60) + LEAST(20, (CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) ELSE 0 END) / 1.5 * 20)
+ LEAST(15, invoice_count_90d * 1.5) + LEAST(20, sales_usd_90d / 10000 * 20)
+ LEAST(10, sales_qty_90d / 10) + LEAST(15, market_count_90d / 8.0 * 15)
+ LEAST(10, (CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) ELSE 0 END) * 5), + LEAST(15, customer_count_90d / 25.0 * 15),
4 4
) )
END AS performance_score, 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_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_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, 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, COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_90d,0) > 0)::integer AS market_count_90d,
COALESCE(SUM(customer_count_90d),0)::integer AS customer_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(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(price_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS price_index_90d,
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(margin_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS margin_index_90d, CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(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) { func (n *productPerformanceGroupedSnapshotNode) addDistinctSpread(row map[string]any) {
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key")) market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
if market != "" && market != "STOK" { 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 { if n.Market90Seen == nil {
n.Market90Seen = map[string]bool{} n.Market90Seen = map[string]bool{}
} }
n.Market90Seen[market] = true 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 { if n.MarketTotalSeen == nil {
n.MarketTotalSeen = map[string]bool{} n.MarketTotalSeen = map[string]bool{}
} }
@@ -5311,13 +5307,13 @@ func (n *productPerformanceGroupedSnapshotNode) addDistinctSpread(row map[string
} }
customer := strings.TrimSpace(stringFromMap(row, "customer_code")) customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
if customer != "" && customer != "-" { 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 { if n.Customer90Seen == nil {
n.Customer90Seen = map[string]bool{} n.Customer90Seen = map[string]bool{}
} }
n.Customer90Seen[customer] = true 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 { if n.CustomerTotalSeen == nil {
n.CustomerTotalSeen = map[string]bool{} 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) { func addProductPerformanceDistinctSpread(row map[string]any, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen map[string]bool) {
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key")) market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
if market != "" && market != "STOK" { 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 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 marketTotalSeen[market] = true
} }
} }
customer := strings.TrimSpace(stringFromMap(row, "customer_code")) customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
if customer != "" && customer != "-" { 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 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 customerTotalSeen[customer] = true
} }
} }
@@ -5561,7 +5557,7 @@ func applyProductPerformanceDistinctSpread(row map[string]any, market90Seen, mar
if len(customerTotalSeen) > 0 { if len(customerTotalSeen) > 0 {
row["customer_count_total"] = len(customerTotalSeen) 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 { if intFromMap(row, "market_count_90d") == 0 {
row["market_count_90d"] = 1 row["market_count_90d"] = 1
} }
@@ -5569,7 +5565,7 @@ func applyProductPerformanceDistinctSpread(row map[string]any, market90Seen, mar
row["customer_count_90d"] = 1 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 { if intFromMap(row, "market_count_total") == 0 {
row["market_count_total"] = maxInt(1, intFromMap(row, "market_count_90d")) 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 { func productPerformanceProductScore(suffix string, salesUSD, salesIndex, margin, stockTurnover, marketCount, customerCount float64) float64 {
if salesUSD <= 0 {
return 1
}
revenueScore := productPerformanceRevenueScore(suffix, salesUSD, salesIndex, productPerformanceProductRevenueTarget(suffix)) revenueScore := productPerformanceRevenueScore(suffix, salesUSD, salesIndex, productPerformanceProductRevenueTarget(suffix))
score := 0.30*productPerformanceMarginComponentScore(margin) + score := 0.30*productPerformanceMarginComponentScore(margin) +
0.20*productPerformanceRatioScore(stockTurnover, 1.50) + 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 { func productPerformanceCustomerScore(suffix string, salesUSD, margin, productGroupCount, salesQty float64) float64 {
if salesUSD <= 0 {
return 1
}
score := 0.35*productPerformanceRatioScore(salesUSD, productPerformanceCustomerRevenueTarget(suffix)) + score := 0.35*productPerformanceRatioScore(salesUSD, productPerformanceCustomerRevenueTarget(suffix)) +
0.30*productPerformanceMarginComponentScore(margin) + 0.30*productPerformanceMarginComponentScore(margin) +
0.20*productPerformanceRatioScore(productGroupCount, 8) + 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 { func productPerformanceRevenueScore(suffix string, salesUSD, salesIndex, absoluteTarget float64) float64 {
if salesIndex > 0 {
return productPerformanceRatioScore(salesIndex, 2)
}
return productPerformanceRatioScore(salesUSD, absoluteTarget) return productPerformanceRatioScore(salesUSD, absoluteTarget)
} }
+58 -20
View File
@@ -76,16 +76,19 @@ func productPerformanceSalesSQL() string {
), SPACE(0))) ), SPACE(0)))
END, END,
Qty1 = ISNULL(I.Qty1, 0), Qty1 = ISNULL(I.Qty1, 0),
TLAmount = ISNULL(I.Doc_Amount, 0) * ISNULL(I.Loc_ExchangeRate, 0), TLAmount = CASE
USDAmount = (ISNULL(I.Doc_Amount, 0) * ISNULL(I.Loc_ExchangeRate, 0)) / ISNULL(( WHEN Amount.DocCurrencyCode = 'TRY' THEN Amount.DocNetAmount
SELECT TOP 1 Rate WHEN CurRate.Rate > 0 THEN Amount.DocNetAmount * CurRate.Rate
FROM AllExchangeRates WITH(NOLOCK) WHEN ISNULL(I.Loc_ExchangeRate, 0) > 0 THEN Amount.DocNetAmount * I.Loc_ExchangeRate
WHERE CurrencyCode = 'USD' ELSE 0
AND RelationCurrencyCode = 'TRY' END,
AND ExchangeTypeCode = 6 USDAmount = CASE
AND Rate > 1 WHEN Amount.DocCurrencyCode = 'USD' THEN Amount.DocNetAmount
ORDER BY ABS(DATEDIFF(DAY, AllExchangeRates.Date, I.InvoiceDate)) ASC WHEN Amount.DocCurrencyCode = 'TRY' AND UsdRate.Rate > 0 THEN Amount.DocNetAmount / UsdRate.Rate
), 1), 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 ( RN = ROW_NUMBER() OVER (
PARTITION BY I.InvoiceLineID PARTITION BY I.InvoiceLineID
ORDER BY I.InvoiceDate DESC, I.InvoiceNumber ORDER BY I.InvoiceDate DESC, I.InvoiceNumber
@@ -96,6 +99,39 @@ func productPerformanceSalesSQL() string {
AND CAF.CurrAccCode = I.CurrAccCode AND CAF.CurrAccCode = I.CurrAccCode
LEFT OUTER JOIN ProductAttributesFilter PAF WITH(NOLOCK) LEFT OUTER JOIN ProductAttributesFilter PAF WITH(NOLOCK)
ON PAF.ItemCode = I.ItemCode 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 WHERE I.InvoiceDate >= @p1
AND I.InvoiceDate < DATEADD(DAY, 1, @p2) AND I.InvoiceDate < DATEADD(DAY, 1, @p2)
AND I.ItemTypeCode = 1 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 '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 >= $1::date - INTERVAL '359 days') AS sales_usd_365d,
SUM(sales_usd) FILTER (WHERE sales_date >= DATE '2022-01-01') AS sales_usd_total, 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(sales_date) AS last_sale_date,
MAX(last_ref_number) AS last_ref_number MAX(last_ref_number) AS last_ref_number
FROM mk_product_performance_sales_daily FROM mk_product_performance_sales_daily
@@ -566,9 +602,9 @@ Spread AS (
product_code, product_code,
color_code, color_code,
yaka_kodu, yaka_kodu,
COUNT(DISTINCT market_key) AS market_count_90d COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) AS market_count_90d
FROM SalesAgg FROM SalesAgg
WHERE COALESCE(sales_qty_90d, 0) > 0 WHERE COALESCE(sales_usd_90d, 0) > 0
GROUP BY product_code, color_code, yaka_kodu GROUP BY product_code, color_code, yaka_kodu
), ),
Market AS ( 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(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(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), COALESCE(sales_index_90d, 0), COALESCE(price_index_90d, 0), COALESCE(margin_index_90d, 0),
ROUND( CASE WHEN COALESCE(sales_usd_90d, 0) <= 0 THEN 1 ELSE
LEAST(35, COALESCE(sales_index_90d, 0) * 18) ROUND(
+ LEAST(25, GREATEST(COALESCE(gross_margin_90d, 0), 0) * 50) LEAST(35, COALESCE(sales_index_90d, 0) * 18)
+ LEAST(20, COALESCE(customer_count_90d, 0) * 2) + LEAST(25, GREATEST(COALESCE(gross_margin_90d, 0), 0) * 50)
+ LEAST(10, COALESCE(market_count_90d, 0) * 3) + LEAST(20, COALESCE(customer_count_90d, 0) * 2)
+ 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 + LEAST(10, COALESCE(market_count_90d, 0) * 3)
, 4) AS performance_score, + 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 CASE
WHEN COALESCE(sales_index_90d,0) >= 1.25 AND COALESCE(gross_margin_90d,0) >= 0.25 THEN 'YILDIZ_URUN' 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' WHEN COALESCE(stock_qty,0) <= 0 AND COALESCE(sales_qty_90d,0) > 0 THEN 'STOKSUZ_TALEP'
+2 -1
View File
@@ -590,7 +590,8 @@ WHERE id = $1
return return
} }
if storedInDB && len(binData) > 0 { _ = storedInDB
if len(binData) > 0 {
w.Header().Set("Content-Type", http.DetectContentType(binData)) w.Header().Set("Content-Type", http.DetectContentType(binData))
w.Header().Set("Cache-Control", "public, max-age=3600") w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(binData) _, _ = w.Write(binData)
+503 -420
View File
@@ -26,7 +26,7 @@ type productPerformanceExcelExportRequest struct {
type productPerformanceExcelColumn struct { type productPerformanceExcelColumn struct {
Header string Header string
Kind string Kind string
Value func(models.ProductPerformanceRow, models.ProductPerformanceGeneralRow, productPerformanceExcelStats) any Value func(*productPerformanceExcelVariant, productPerformanceExcelStats) any
} }
type productPerformanceExcelStats struct { type productPerformanceExcelStats struct {
@@ -36,6 +36,52 @@ type productPerformanceExcelStats struct {
AvgSalesUSDTotal float64 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 { func ExportProductPerformanceExcelHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
traceID := utils.TraceIDFromRequest(r) 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, Limit: 50000,
Page: 1, Page: 1,
SortBy: "product_code", 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) http.Error(w, "urun performans genel excel listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
return return
} }
generalByKey := productPerformanceExcelGeneralMap(generalRows)
rows = filterProductPerformanceExcelRows(rows, generalByKey, req.Filters) rows := buildProductPerformanceExcelVariants(productRows, generalRows, req.Filters)
sortProductPerformanceExcelRows(rows, generalByKey, req.SortBy, req.Descending == nil || *req.Descending) sortProductPerformanceExcelVariants(rows, req.SortBy, req.Descending == nil || *req.Descending)
stats := productPerformanceExcelStatsFor(rows, generalByKey) stats := productPerformanceExcelStatsFor(rows)
file, err := buildProductPerformanceExcelFile(rows, generalByKey, stats) file, err := buildProductPerformanceExcelFile(rows, stats)
if err != nil { if err != nil {
http.Error(w, "urun performans excel dosyasi hazirlanamadi: "+err.Error(), http.StatusInternalServerError) http.Error(w, "urun performans excel dosyasi hazirlanamadi: "+err.Error(), http.StatusInternalServerError)
return return
@@ -85,7 +130,7 @@ func ExportProductPerformanceExcelHandler(pg *sql.DB) http.HandlerFunc {
return 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-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"") w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
w.Header().Set("Content-Length", fmt.Sprint(len(buf.Bytes()))) 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() f := excelize.NewFile()
sheet := "RenkYaka" sheet := "RenkYaka"
f.SetSheetName("Sheet1", sheet) f.SetSheetName("Sheet1", sheet)
@@ -121,350 +317,200 @@ func buildProductPerformanceExcelFile(rows []models.ProductPerformanceRow, gener
for rowIndex, row := range rows { for rowIndex, row := range rows {
excelRow := rowIndex + 2 excelRow := rowIndex + 2
general := generalByKey[productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
for colIndex, col := range columns { for colIndex, col := range columns {
cell, _ := excelize.CoordinatesToCellName(colIndex+1, excelRow) cell, _ := excelize.CoordinatesToCellName(colIndex+1, excelRow)
value := col.Value(row, general, stats) value := col.Value(row, stats)
switch col.Kind { if col.Kind == "text" {
case "text":
_ = f.SetCellStr(sheet, cell, strings.TrimSpace(fmt.Sprint(value))) _ = f.SetCellStr(sheet, cell, strings.TrimSpace(fmt.Sprint(value)))
default: } else {
_ = f.SetCellValue(sheet, cell, productPerformanceExcelFloat(value)) _ = f.SetCellValue(sheet, cell, productPerformanceExcelFloat(value))
} }
} }
} }
lastRow := len(rows) + 1 lastRow := productPerformanceExcelMaxInt(len(rows)+1, 2)
if len(columns) > 0 { if len(columns) > 0 {
filterLastRow := lastRow lastFilterCell, _ := excelize.CoordinatesToCellName(len(columns), lastRow)
if filterLastRow < 2 {
filterLastRow = 2
}
lastFilterCell, _ := excelize.CoordinatesToCellName(len(columns), filterLastRow)
_ = f.AutoFilter(sheet, "A1:"+lastFilterCell, []excelize.AutoFilterOptions{}) _ = f.AutoFilter(sheet, "A1:"+lastFilterCell, []excelize.AutoFilterOptions{})
} }
for i, col := range columns { for i, col := range columns {
colName, _ := excelize.ColumnNumberToName(i + 1) colName, _ := excelize.ColumnNumberToName(i + 1)
width := productPerformanceExcelColumnWidth(col.Header, col.Kind) _ = f.SetColWidth(sheet, colName, colName, productPerformanceExcelColumnWidth(col.Header, col.Kind))
_ = f.SetColWidth(sheet, colName, colName, width) style := decimalStyle
if lastRow >= 2 { switch col.Kind {
style := decimalStyle case "text":
switch col.Kind { style = textStyle
case "text": case "int":
style = textStyle style = intStyle
case "int": case "percent":
style = intStyle style = percentStyle
case "percent":
style = percentStyle
}
_ = f.SetCellStyle(sheet, fmt.Sprintf("%s2", colName), fmt.Sprintf("%s%d", colName, lastRow), style)
} }
_ = f.SetCellStyle(sheet, fmt.Sprintf("%s2", colName), fmt.Sprintf("%s%d", colName, lastRow), style)
} }
_ = f.SetPanes(sheet, &excelize.Panes{ _ = f.SetPanes(sheet, &excelize.Panes{Freeze: true, YSplit: 1, TopLeftCell: "A2", ActivePane: "bottomLeft"})
Freeze: true,
Split: false,
XSplit: 0,
YSplit: 1,
TopLeftCell: "A2",
ActivePane: "bottomLeft",
})
return f, nil return f, nil
} }
func productPerformanceExcelColumns() []productPerformanceExcelColumn { 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{ return []productPerformanceExcelColumn{
{Header: "KPI Tarihi", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { numberExcelColumn(label+" Toplam Adet", "int", func(v *productPerformanceExcelVariant) any {
return r.KpiDate qty, _, _, _ := values(v)
}}, return qty
{Header: "Ürün İlk Grubu", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { }),
return productPerformanceExcelCleanOptional(r.UrunIlkGrubu) numberExcelColumn(label+" Toplam Ciro USD", "number", func(v *productPerformanceExcelVariant) any {
}}, _, usd, _, _ := values(v)
{Header: "Askılı/Yan", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { return usd
return productPerformanceExcelCleanOptional(r.AskiliYan) }),
}}, numberExcelColumn(label+" Ciro/Adet USD", "number", func(v *productPerformanceExcelVariant) any {
{Header: "Kategori", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { qty, usd, _, _ := values(v)
return r.Kategori return productPerformanceExcelAvgPrice(usd, qty)
}}, }),
{Header: "Ürün Ana Grubu", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { numberExcelColumn(label+" Satış/Stok Oranı", "number", func(v *productPerformanceExcelVariant) any {
return r.UrunAnaGrubu qty, _, _, _ := values(v)
}}, return productPerformanceExcelStockTurnover(qty, v.StockQty)
{Header: "Ürün Alt Grubu", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { }),
return r.UrunAltGrubu 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 }),
{Header: "Ürün", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { numberExcelColumn(label+" Taban Toplam K/Z USD", "number", func(v *productPerformanceExcelVariant) any {
return r.ProductCode qty, usd, _, _ := values(v)
}}, return productPerformanceExcelGrossProfit(usd, qty, v.BasePriceUSD)
{Header: "Açıklama", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { }),
return r.ItemDescription numberExcelColumn(label+" Çıplak Toplam K/Z USD", "number", func(v *productPerformanceExcelVariant) any {
}}, qty, usd, _, _ := values(v)
{Header: "Renk", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { return productPerformanceExcelGrossProfit(usd, qty, v.CostPriceUSD)
return r.ColorCode }),
}}, numberExcelColumn(label+" Taban Marj", "percent", func(v *productPerformanceExcelVariant) any {
{Header: "Renk Açıklama", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { qty, usd, _, _ := values(v)
return r.ColorDescription return productPerformanceExcelMargin(usd, qty, v.BasePriceUSD)
}}, }),
{Header: "Yaka", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { numberExcelColumn(label+" Çıplak Marj", "percent", func(v *productPerformanceExcelVariant) any {
return r.YakaKodu qty, usd, _, _ := values(v)
}}, return productPerformanceExcelMargin(usd, qty, v.CostPriceUSD)
{Header: "Renk/Yaka", Kind: "text", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { }),
return productPerformanceExcelColorYaka(r) numberExcelColumn(label+" Tekil Piyasa", "int", func(v *productPerformanceExcelVariant) any {
}}, _, _, markets, _ := values(v)
{Header: "Toplam Stok", Kind: "int", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { return markets
return r.StockQty }),
}}, numberExcelColumn(label+" Tekil Müşteri", "int", func(v *productPerformanceExcelVariant) any {
_, _, _, customers := values(v)
{Header: "90G Toplam Adet", Kind: "int", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { return customers
return r.SalesQty90 }),
}}, numberExcelColumn(label+" Ürün Skor", "number", func(v *productPerformanceExcelVariant) any {
{Header: "90G Toplam Ciro USD", Kind: "number", Value: func(r models.ProductPerformanceRow, _ models.ProductPerformanceGeneralRow, _ productPerformanceExcelStats) any { qty, usd, markets, customers := values(v)
return r.SalesUSD90 return productPerformanceExcelProductScore(suffix, usd, productPerformanceExcelStockTurnover(qty, v.StockQty), float64(markets), float64(customers), productPerformanceExcelMargin(usd, qty, v.CostPriceUSD))
}}, }),
{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
}},
} }
} }
func productPerformanceExcelGeneralMap(rows []models.ProductPerformanceGeneralRow) map[string]models.ProductPerformanceGeneralRow { func textExcelColumn(header string, value func(*productPerformanceExcelVariant) any) productPerformanceExcelColumn {
out := make(map[string]models.ProductPerformanceGeneralRow, len(rows)) return productPerformanceExcelColumn{Header: header, Kind: "text", Value: func(v *productPerformanceExcelVariant, _ productPerformanceExcelStats) any { return value(v) }}
for _, row := range rows {
out[productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)] = row
}
return out
} }
func filterProductPerformanceExcelRows(rows []models.ProductPerformanceRow, generalByKey map[string]models.ProductPerformanceGeneralRow, filters map[string][]string) []models.ProductPerformanceRow { func numberExcelColumn(header, kind string, value func(*productPerformanceExcelVariant) any) productPerformanceExcelColumn {
if len(filters) == 0 { return productPerformanceExcelColumn{Header: header, Kind: kind, Value: func(v *productPerformanceExcelVariant, _ productPerformanceExcelStats) any { return value(v) }}
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 productPerformanceExcelRowMatchesFilters(row models.ProductPerformanceRow, general models.ProductPerformanceGeneralRow, filters map[string][]string) bool { func productPerformanceExcelProductRowMatchesFilters(row models.ProductPerformanceRow, filters map[string][]string) bool {
for field, values := range filters { for field, selected := range productPerformanceExcelFilterSets(filters) {
selected := productPerformanceExcelSelectedSet(values) if field == "performance_bucket" {
if len(selected) == 0 {
continue continue
} }
matched := false if !productPerformanceExcelMatchesAny(selected, productPerformanceExcelProductFilterCandidates(row, field)) {
for _, candidate := range productPerformanceExcelFilterCandidates(row, general, field) {
if selected[productPerformanceExcelNormalize(candidate)] {
matched = true
break
}
}
if !matched {
return false return false
} }
} }
return true return true
} }
func productPerformanceExcelSelectedSet(values []string) map[string]bool { func productPerformanceExcelGeneralRowMatchesFilters(row models.ProductPerformanceGeneralRow, filters map[string][]string) bool {
out := make(map[string]bool, len(values)) for field, selected := range productPerformanceExcelFilterSets(filters) {
for _, value := range values { if field == "performance_bucket" {
value = productPerformanceExcelNormalize(value) continue
if value != "" { }
out[value] = true 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 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) { switch strings.TrimSpace(field) {
case "product_code": case "product_code":
return []string{row.ProductCode} return []string{row.ProductCode}
@@ -475,7 +521,7 @@ func productPerformanceExcelFilterCandidates(row models.ProductPerformanceRow, g
case "yaka_kodu": case "yaka_kodu":
return []string{row.YakaKodu} return []string{row.YakaKodu}
case "color_yaka": 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": case "kategori":
return []string{row.Kategori} return []string{row.Kategori}
case "askili_yan": case "askili_yan":
@@ -483,33 +529,67 @@ func productPerformanceExcelFilterCandidates(row models.ProductPerformanceRow, g
case "urun_ilk_grubu": case "urun_ilk_grubu":
return []string{productPerformanceExcelCleanOptional(row.UrunIlkGrubu)} return []string{productPerformanceExcelCleanOptional(row.UrunIlkGrubu)}
case "urun_ana_grubu": case "urun_ana_grubu":
return []string{productPerformanceExcelFirstNonEmpty(general.UrunAnaGrubu, row.UrunAnaGrubu)} return []string{row.UrunAnaGrubu}
case "urun_alt_grubu": case "urun_alt_grubu":
return []string{row.UrunAltGrubu} return []string{row.UrunAltGrubu}
case "market_key": case "market_key":
return []string{row.MarketKey, productPerformanceExcelMarketName(row.MarketKey)} return []string{row.MarketKey, productPerformanceExcelMarketName(row.MarketKey)}
case "performance_bucket":
bucket := productPerformanceExcelFirstNonEmpty(general.PerformanceBucket, row.PerformanceBucket)
return []string{bucket, productPerformanceExcelBucketLabel(bucket)}
default: default:
return nil 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) sortBy = strings.TrimSpace(sortBy)
if sortBy == "" || sortBy == "image" { if sortBy == "" || sortBy == "image" {
sortBy = "product_code" sortBy = "product_code"
desc = false desc = false
} }
sort.SliceStable(rows, func(i, j int) bool { sort.SliceStable(rows, func(i, j int) bool {
left := rows[i] cmp := productPerformanceExcelCompare(rows[i], rows[j], sortBy)
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)
if cmp == 0 { if cmp == 0 {
cmp = strings.Compare(productPerformanceExcelHierarchySortKey(left), productPerformanceExcelHierarchySortKey(right)) cmp = strings.Compare(productPerformanceExcelHierarchySortKey(rows[i]), productPerformanceExcelHierarchySortKey(rows[j]))
} }
if desc { if desc {
return cmp > 0 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 { func productPerformanceExcelCompare(a, b *productPerformanceExcelVariant, sortBy string) int {
leftNum, rightNum, numeric := productPerformanceExcelSortNumeric(left, leftGen, sortBy), productPerformanceExcelSortNumeric(right, rightGen, sortBy), productPerformanceExcelIsNumericSort(sortBy) if productPerformanceExcelIsNumericSort(sortBy) {
if numeric { av := productPerformanceExcelSortNumber(a, sortBy)
if leftNum < rightNum { bv := productPerformanceExcelSortNumber(b, sortBy)
if av < bv {
return -1 return -1
} }
if leftNum > rightNum { if av > bv {
return 1 return 1
} }
return 0 return 0
} }
leftText := productPerformanceExcelSortText(left, leftGen, sortBy) return strings.Compare(productPerformanceExcelNormalize(productPerformanceExcelSortText(a, sortBy)), productPerformanceExcelNormalize(productPerformanceExcelSortText(b, sortBy)))
rightText := productPerformanceExcelSortText(right, rightGen, sortBy)
return strings.Compare(productPerformanceExcelNormalize(leftText), productPerformanceExcelNormalize(rightText))
} }
func productPerformanceExcelIsNumericSort(sortBy string) bool { 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", 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", "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", "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", "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", "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 return true
default: default:
return false return false
} }
} }
func productPerformanceExcelSortNumeric(row models.ProductPerformanceRow, general models.ProductPerformanceGeneralRow, sortBy string) float64 { func productPerformanceExcelSortNumber(row *productPerformanceExcelVariant, sortBy string) float64 {
switch sortBy { switch sortBy {
case "stock_qty": case "stock_qty":
return row.StockQty return row.StockQty
@@ -560,7 +638,7 @@ func productPerformanceExcelSortNumeric(row models.ProductPerformanceRow, genera
case "sales_qty_365d": case "sales_qty_365d":
return row.SalesQty365 return row.SalesQty365
case "sales_qty_total": case "sales_qty_total":
return productPerformanceExcelTotalSalesQty(row, general) return row.totalQty()
case "sales_usd_90d": case "sales_usd_90d":
return row.SalesUSD90 return row.SalesUSD90
case "sales_usd_180d": case "sales_usd_180d":
@@ -568,7 +646,7 @@ func productPerformanceExcelSortNumeric(row models.ProductPerformanceRow, genera
case "sales_usd_365d": case "sales_usd_365d":
return row.SalesUSD365 return row.SalesUSD365
case "sales_usd_total": case "sales_usd_total":
return productPerformanceExcelTotalSalesUSD(row, general) return row.totalUSD()
case "avg_price_usd_90d": case "avg_price_usd_90d":
return productPerformanceExcelAvgPrice(row.SalesUSD90, row.SalesQty90) return productPerformanceExcelAvgPrice(row.SalesUSD90, row.SalesQty90)
case "avg_price_usd_180d": case "avg_price_usd_180d":
@@ -576,54 +654,42 @@ func productPerformanceExcelSortNumeric(row models.ProductPerformanceRow, genera
case "avg_price_usd_365d": case "avg_price_usd_365d":
return productPerformanceExcelAvgPrice(row.SalesUSD365, row.SalesQty365) return productPerformanceExcelAvgPrice(row.SalesUSD365, row.SalesQty365)
case "avg_price_usd_total": case "avg_price_usd_total":
return productPerformanceExcelFirstNonZero(general.AvgPriceUSDTotal, productPerformanceExcelAvgPrice(productPerformanceExcelTotalSalesUSD(row, general), productPerformanceExcelTotalSalesQty(row, general))) return productPerformanceExcelAvgPrice(row.totalUSD(), row.totalQty())
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)
case "stock_turnover_90d": case "stock_turnover_90d":
return row.StockTurnover90 return productPerformanceExcelStockTurnover(row.SalesQty90, row.StockQty)
case "stock_turnover_180d": case "stock_turnover_180d":
return row.StockTurnover180 return productPerformanceExcelStockTurnover(row.SalesQty180, row.StockQty)
case "stock_turnover_365d": case "stock_turnover_365d":
return row.StockTurnover365 return productPerformanceExcelStockTurnover(row.SalesQty365, row.StockQty)
case "stock_turnover_total": case "stock_turnover_total":
return row.StockTurnoverTotal return productPerformanceExcelStockTurnover(row.totalQty(), row.StockQty)
case "base_price_usd": case "base_price_usd":
return productPerformanceExcelFirstNonZero(general.BasePriceUSD, row.BasePriceUSD) return row.BasePriceUSD
case "cost_price_usd": case "cost_price_usd":
return productPerformanceExcelFirstNonZero(general.CostPriceUSD, row.CostPriceUSD) return row.CostPriceUSD
case "market_count_90d": case "market_count_90d":
return float64(row.MarketCount90) return float64(row.marketCount90())
case "customer_count_90d": case "customer_count_90d":
return float64(row.CustomerCount90) return float64(row.CustomerCount90)
case "market_count_total": case "market_count_total":
return float64(general.MarketCountTotal) return float64(row.marketCountTotal())
case "customer_count_total": case "customer_count_total":
return float64(general.CustomerCountTotal) return float64(row.CustomerCountTotal)
case "sales_index_90d":
return row.SalesIndex90
case "sales_index_total":
return general.SalesIndexTotal
case "performance_score", "performance_score_90d": 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": 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: default:
return 0 return 0
} }
} }
func productPerformanceExcelSortText(row models.ProductPerformanceRow, general models.ProductPerformanceGeneralRow, sortBy string) string { func productPerformanceExcelSortText(row *productPerformanceExcelVariant, sortBy string) string {
switch sortBy { switch sortBy {
case "product_code": case "product_code":
return row.ProductCode return row.ProductCode
case "color_yaka": case "color_yaka":
return productPerformanceExcelColorYaka(row) return productPerformanceExcelColorYaka(row.ColorCode, row.ColorDescription, row.YakaKodu)
case "color_code": case "color_code":
return row.ColorCode return row.ColorCode
case "yaka_kodu": case "yaka_kodu":
@@ -633,40 +699,26 @@ func productPerformanceExcelSortText(row models.ProductPerformanceRow, general m
case "kategori": case "kategori":
return row.Kategori return row.Kategori
case "askili_yan": case "askili_yan":
return productPerformanceExcelCleanOptional(row.AskiliYan) return row.AskiliYan
case "urun_ilk_grubu": case "urun_ilk_grubu":
return productPerformanceExcelCleanOptional(row.UrunIlkGrubu) return row.UrunIlkGrubu
case "urun_ana_grubu": case "urun_ana_grubu":
return productPerformanceExcelFirstNonEmpty(general.UrunAnaGrubu, row.UrunAnaGrubu) return row.UrunAnaGrubu
case "urun_alt_grubu": case "urun_alt_grubu":
return row.UrunAltGrubu return row.UrunAltGrubu
case "market_key": case "market_key":
return productPerformanceExcelMarketName(row.MarketKey) return row.marketsLabel()
case "performance_bucket": case "performance_bucket":
return productPerformanceExcelBucketLabel(productPerformanceExcelFirstNonEmpty(general.PerformanceBucket, row.PerformanceBucket)) return productPerformanceExcelStatus(row)
default: default:
return productPerformanceExcelHierarchySortKey(row) return productPerformanceExcelHierarchySortKey(row)
} }
} }
func productPerformanceExcelHierarchySortKey(row models.ProductPerformanceRow) string { func productPerformanceExcelStatsFor(rows []*productPerformanceExcelVariant) productPerformanceExcelStats {
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 {
var stats productPerformanceExcelStats var stats productPerformanceExcelStats
var c90, c180, c365, cTotal float64 var c90, c180, c365, cTotal float64
for _, row := range rows { for _, row := range rows {
general := generalByKey[productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
if row.SalesUSD90 > 0 { if row.SalesUSD90 > 0 {
stats.AvgSalesUSD90 += row.SalesUSD90 stats.AvgSalesUSD90 += row.SalesUSD90
c90++ c90++
@@ -679,8 +731,8 @@ func productPerformanceExcelStatsFor(rows []models.ProductPerformanceRow, genera
stats.AvgSalesUSD365 += row.SalesUSD365 stats.AvgSalesUSD365 += row.SalesUSD365
c365++ c365++
} }
if total := productPerformanceExcelTotalSalesUSD(row, general); total > 0 { if row.totalUSD() > 0 {
stats.AvgSalesUSDTotal += total stats.AvgSalesUSDTotal += row.totalUSD()
cTotal++ 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)) return strings.ToUpper(strings.TrimSpace(productCode)) + "|" + strings.ToUpper(strings.TrimSpace(colorCode)) + "|" + strings.ToUpper(strings.TrimSpace(yakaKodu))
} }
func productPerformanceExcelColorYaka(row models.ProductPerformanceRow) string { func productPerformanceExcelHierarchySortKey(row *productPerformanceExcelVariant) string {
color := strings.TrimSpace(row.ColorCode) return strings.Join([]string{row.UrunIlkGrubu, row.AskiliYan, row.Kategori, row.UrunAnaGrubu, row.UrunAltGrubu, row.ProductCode, row.ColorCode, row.YakaKodu}, "|")
if desc := strings.TrimSpace(row.ColorDescription); color != "" && desc != "" { }
func productPerformanceExcelColorYaka(colorCode, colorDescription, yakaKodu string) string {
color := strings.TrimSpace(colorCode)
if desc := strings.TrimSpace(colorDescription); color != "" && desc != "" {
color = color + "-" + strings.ToUpper(desc) color = color + "-" + strings.ToUpper(desc)
} }
yaka := strings.TrimSpace(row.YakaKodu) yaka := strings.TrimSpace(yakaKodu)
parts := make([]string, 0, 2) parts := make([]string, 0, 2)
if color != "" && color != "-" { if color != "" && color != "-" {
parts = append(parts, color) parts = append(parts, color)
@@ -730,6 +786,34 @@ func productPerformanceExcelMarketName(value string) string {
return "" 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 { func productPerformanceExcelBucketLabel(value string) string {
switch strings.TrimSpace(value) { switch strings.TrimSpace(value) {
case "YILDIZ_URUN": case "YILDIZ_URUN":
@@ -782,16 +866,8 @@ func productPerformanceExcelFirstNonZero(values ...float64) float64 {
return 0 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 { func productPerformanceExcelAvgPrice(salesUSD, qty float64) float64 {
if qty == 0 { if qty <= 0 {
return 0 return 0
} }
return salesUSD / qty return salesUSD / qty
@@ -804,13 +880,6 @@ func productPerformanceExcelStockTurnover(salesQty, stockQty float64) float64 {
return salesQty / stockQty 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 { func productPerformanceExcelGrossProfit(salesUSD, qty, unitCost float64) float64 {
if qty <= 0 { if qty <= 0 {
return 0 return 0
@@ -819,27 +888,20 @@ func productPerformanceExcelGrossProfit(salesUSD, qty, unitCost float64) float64
} }
func productPerformanceExcelMargin(salesUSD, qty, unitCost float64) float64 { func productPerformanceExcelMargin(salesUSD, qty, unitCost float64) float64 {
if salesUSD == 0 { if salesUSD <= 0 {
return 0 return 0
} }
return productPerformanceExcelGrossProfit(salesUSD, qty, unitCost) / salesUSD return productPerformanceExcelGrossProfit(salesUSD, qty, unitCost) / salesUSD
} }
func productPerformanceExcelRelativeIndex(value, average float64) float64 { func productPerformanceExcelProductScore(suffix string, salesUSD, stockTurnover, marketCount, customerCount, margin float64) float64 {
if value <= 0 || average <= 0 { if salesUSD <= 0 {
return 0 return 1
}
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))
} }
revenueScore := productPerformanceExcelRatioScore(salesUSD, productPerformanceExcelProductRevenueTarget(suffix))
score := 0.30*productPerformanceExcelMarginScore(margin) + score := 0.30*productPerformanceExcelMarginScore(margin) +
0.20*productPerformanceExcelRatioScore(stockTurnover, 1.50) + 0.20*productPerformanceExcelRatioScore(stockTurnover, 1.50) +
0.20*revenue + 0.20*revenueScore +
0.15*productPerformanceExcelRatioScore(marketCount, 8) + 0.15*productPerformanceExcelRatioScore(marketCount, 8) +
0.15*productPerformanceExcelRatioScore(customerCount, 25) 0.15*productPerformanceExcelRatioScore(customerCount, 25)
return productPerformanceExcelRoundScore(score) 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 { func productPerformanceExcelColumnWidth(header, kind string) float64 {
if kind == "text" { if kind == "text" {
if strings.Contains(header, "Piyasalar") || strings.Contains(header, "Öneri") {
return 34
}
if len([]rune(header)) > 14 { if len([]rune(header)) > 14 {
return 24 return 24
} }
return 16 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 14
} }
return 16 return 16
} }
func productPerformanceExcelMaxInt(a, b int) int {
if a > b {
return a
}
return b
}
+145 -64
View File
@@ -1670,6 +1670,7 @@ const productPerformanceExcelExportFilterFields = new Set([
'color_yaka', 'color_yaka',
'color_code', 'color_code',
'yaka_kodu', 'yaka_kodu',
'market_key',
'performance_bucket' 'performance_bucket'
]) ])
const performanceTabs = [ 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_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_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_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_90d', label: '90G Satış/Stok', 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_180d', label: '180G Satış/Stok', 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_365d', label: '360G Satış/Stok', 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: '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: 'Ort. USD', field: row => formatMoney(row.avg_price_usd_90d, 'USD'), align: 'right' }, { 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 Ort. USD', field: row => formatMoney(row.avg_price_usd_180d, '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 Ort. USD', field: row => formatMoney(row.avg_price_usd_365d, '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 Ort. USD', field: row => formatMoney(row.avg_price_usd_total, '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: 'Ort. USD Taban Maliyeti', field: row => formatMoney(row.base_price_usd, '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: 'Ort. USD Çıplak Maliyeti', field: row => formatMoney(row.cost_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_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_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 }, { 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_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_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_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_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_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 }, { 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 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 = [ const dimensionColumnNames = [
'product_code', 'product_code',
'color_yaka', 'color_yaka',
@@ -2022,16 +2041,18 @@ const generalColumns = [
{ name: 'period_start', label: 'Başlangıç', field: 'period_start', align: 'left', sortable: true }, { 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: '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: '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_qty_total', label: 'Toplam Adet', 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_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: '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: '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: '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: '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_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: '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_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_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: '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 }, { 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_180d: '180G Toplam Ciro USD',
sales_usd_365d: '360G Toplam Ciro USD', sales_usd_365d: '360G Toplam Ciro USD',
sales_usd_total: 'Genel Toplam Ciro USD', sales_usd_total: 'Genel Toplam Ciro USD',
avg_price_usd: 'Ağ. Ort. Fiyat USD', avg_price_usd: 'Ciro/Adet USD',
avg_price_usd_90d: '90G Ağ. Ort. Fiyat USD', avg_price_usd_90d: '90G Ciro/Adet USD',
avg_price_usd_180d: '180G Ağ. Ort. Fiyat USD', avg_price_usd_180d: '180G Ciro/Adet USD',
avg_price_usd_365d: '360G Ağ. Ort. Fiyat USD', avg_price_usd_365d: '360G Ciro/Adet USD',
avg_price_usd_total: 'Genel Ağ. Ort. Fiyat USD', avg_price_usd_total: 'Genel Ciro/Adet USD',
base_price_usd: 'Ort. Taban Maliyet USD', base_price_usd: 'Taban Maliyet USD',
cost_price_usd: 'Ort. Çıplak Maliyet USD', cost_price_usd: 'Çıplak Maliyet USD',
base_price_usd_90d: '90G Ort. Taban Maliyet USD', base_price_usd_90d: '90G Taban Maliyet USD',
cost_price_usd_90d: '90G Ort. Çıplak Maliyet USD', cost_price_usd_90d: '90G Çıplak Maliyet USD',
base_price_usd_180d: '180G Ort. Taban Maliyet USD', base_price_usd_180d: '180G Taban Maliyet USD',
cost_price_usd_180d: '180G Ort. Çıplak Maliyet USD', cost_price_usd_180d: '180G Çıplak Maliyet USD',
base_price_usd_365d: '360G Ort. Taban Maliyet USD', base_price_usd_365d: '360G Taban Maliyet USD',
cost_price_usd_365d: '360G Ort. Çıplak Maliyet USD', cost_price_usd_365d: '360G Çıplak Maliyet USD',
base_price_usd_total: 'Genel Ort. Taban Maliyet USD', base_price_usd_total: 'Genel Taban Maliyet USD',
cost_price_usd_total: 'Genel Ort. Çıplak Maliyet USD', cost_price_usd_total: 'Genel Çıplak Maliyet USD',
unit_profit_base_90d: '90G Ağ. Ort. Birim Taban K/Z USD', unit_profit_base_90d: '90G Birim Taban K/Z USD',
unit_profit_cost_90d: '90G Ağ. Ort. Birim Çıplak K/Z USD', unit_profit_cost_90d: '90G Birim Çıplak K/Z USD',
unit_profit_base_180d: '180G Ağ. Ort. Birim Taban K/Z USD', unit_profit_base_180d: '180G Birim Taban K/Z USD',
unit_profit_cost_180d: '180G Ağ. Ort. Birim Çıplak K/Z USD', unit_profit_cost_180d: '180G Birim Çıplak K/Z USD',
unit_profit_base_total: 'Genel Ağ. Ort. Birim Taban K/Z USD', unit_profit_base_total: 'Genel Birim Taban K/Z USD',
unit_profit_cost_total: 'Genel Ağ. Ort. Birim Çıplak 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_90d: '90G Toplam K/Z USD',
gross_profit_usd_180d: '180G Toplam K/Z USD', gross_profit_usd_180d: '180G Toplam K/Z USD',
gross_profit_usd_total: 'Genel 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_cost_usd_365d: '360G Çıplak Toplam K/Z USD',
gross_profit_base_usd_total: 'Genel Taban 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_profit_cost_usd_total: 'Genel Çıplak Toplam K/Z USD',
gross_margin_base_90d: '90G Ağ. Ort. Taban Marj', gross_margin_base_90d: '90G Taban Marj',
gross_margin_cost_90d: '90G Ağ. Ort. Çıplak Marj', gross_margin_cost_90d: '90G Çıplak Marj',
gross_margin_base_180d: '180G Ağ. Ort. Taban Marj', gross_margin_base_180d: '180G Taban Marj',
gross_margin_cost_180d: '180G Ağ. Ort. Çıplak Marj', gross_margin_cost_180d: '180G Çıplak Marj',
gross_margin_base_365d: '360G Ağ. Ort. Taban Marj', gross_margin_base_365d: '360G Taban Marj',
gross_margin_cost_365d: '360G Ağ. Ort. Çıplak Marj', gross_margin_cost_365d: '360G Çıplak Marj',
gross_margin_base_total: 'Genel Ağ. Ort. Taban Marj', gross_margin_base_total: 'Genel Taban Marj',
gross_margin_cost_total: 'Genel Ağ. Ort. Çıplak Marj', gross_margin_cost_total: 'Genel Çıplak Marj',
performance_score: 'Ağ. Ort. Skor', performance_score: 'Skor',
performance_score_90d: '90G Ağ. Ort. Ürün Skor', performance_score_90d: '90G Ürün Skor',
performance_score_180d: '180G Ağ. Ort. Ürün Skor', performance_score_180d: '180G Ürün Skor',
performance_score_365d: '360G Ağ. Ort. Ürün Skor', performance_score_365d: '360G Ürün Skor',
performance_score_total: 'Genel Ağ. Ort. Ürün Skor', performance_score_total: 'Genel Ürün Skor',
customer_score_90d: '90G Ağ. Ort. Müşteri Skor', customer_score_90d: '90G Müşteri Skor',
customer_score_180d: '180G Ağ. Ort. Müşteri Skor', customer_score_180d: '180G Müşteri Skor',
customer_score_365d: '360G Ağ. Ort. Müşteri Skor', customer_score_365d: '360G Müşteri Skor',
customer_score_total: 'Genel Ağ. Ort. Müşteri Skor', customer_score_total: 'Genel Müşteri Skor',
market_count_90d: '90G Tekil Piyasa', market_count_90d: '90G Tekil Piyasa',
customer_count_90d: '90G Tekil Müşteri', customer_count_90d: '90G Tekil Müşteri',
market_count_total: 'Genel Tekil Piyasa', market_count_total: 'Genel Tekil Piyasa',
customer_count_total: 'Genel Tekil Müşteri', customer_count_total: 'Genel Tekil Müşteri',
sales_index_90d: '90G Ağ. Ort. Endeks', stock_turnover_90d: '90G Satış/Stok',
sales_index_total: 'Genel Ağ. Ort. Endeks', stock_turnover_180d: '180G Satış/Stok',
stock_days_90d: '90G Ağ. Ort. Stok Gün', stock_turnover_365d: '360G Satış/Stok',
stock_days_180d: '180G Ağ. Ort. Stok Gün', stock_turnover_total: 'Genel Satış/Stok',
stock_days_365d: '360G Ağ. Ort. Stok Gün', sales_index_90d: '90G Endeks',
stock_days_total: 'Genel Ağ. Ort. Stok Gün', 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' 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_90d: sumRows(groupRows, 'gross_profit_usd_90d'),
gross_profit_usd_180d: sumRows(groupRows, 'gross_profit_usd_180d'), gross_profit_usd_180d: sumRows(groupRows, 'gross_profit_usd_180d'),
gross_profit_usd_total: sumRows(groupRows, 'gross_profit_usd_total'), gross_profit_usd_total: sumRows(groupRows, 'gross_profit_usd_total'),
market_count_90d: distinctCount(groupRows, 'market_key'), market_count_90d: distinctSpreadCount(groupRows, 'market_count_90d'),
customer_count_90d: sumRows(groupRows, 'customer_count_90d'), customer_count_90d: distinctSpreadCount(groupRows, 'customer_count_90d'),
market_count_total: sumRows(groupRows, 'market_count_total'), market_count_total: distinctSpreadCount(groupRows, 'market_count_total'),
customer_count_total: sumRows(groupRows, 'customer_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_90d: weightedAverage(groupRows, 'sales_usd_90d', 'sales_qty_90d'),
avg_price_usd_180d: weightedAverage(groupRows, 'sales_usd_180d', 'sales_qty_180d'), avg_price_usd_180d: weightedAverage(groupRows, 'sales_usd_180d', 'sales_qty_180d'),
avg_price_usd_365d: weightedAverage(groupRows, 'sales_usd_365d', 'sales_qty_365d'), 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) out[field] = weightedAverageProductCost(sourceRows, field)
continue continue
} }
if (isDistinctSpreadCountField(field)) {
out[field] = distinctSpreadCount(sourceRows, field)
continue
}
if (shouldSumField(field)) { if (shouldSumField(field)) {
out[field] = sumRows(sourceRows, field) out[field] = sumRows(sourceRows, field)
} else if (shouldAverageField(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 } = {}) { function productScore100 ({ suffix = '90d', salesUSD = 0, margin = 0, stockTurnover = 0, marketCount = 0, customerCount = 0 } = {}) {
const revenue = salesIndex > 0 ? ratioScore(salesIndex, 2) : ratioScore(salesUSD, productRevenueTarget(suffix)) if (Number(salesUSD || 0) <= 0) return 1
const revenue = ratioScore(salesUSD, productRevenueTarget(suffix))
return clampScore( return clampScore(
0.30 * marginScore(margin) + 0.30 * marginScore(margin) +
0.20 * ratioScore(stockTurnover, 1.5) + 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 } = {}) { function customerScore100 ({ suffix = '90d', salesUSD = 0, margin = 0, productGroupCount = 0, salesQty = 0 } = {}) {
if (Number(salesUSD || 0) <= 0) return 1
return clampScore( return clampScore(
0.35 * ratioScore(salesUSD, customerRevenueTarget(suffix)) + 0.35 * ratioScore(salesUSD, customerRevenueTarget(suffix)) +
0.30 * marginScore(margin) + 0.30 * marginScore(margin) +
@@ -3708,6 +3739,56 @@ function distinctCount (sourceRows, field) {
return values.size 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) { function distinctSummary (sourceRows, field, fallbackField) {
const values = new Set() const values = new Set()
for (const row of sourceRows) { for (const row of sourceRows) {