product performance grouped kpi improvements

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