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