Fix product performance grouped JSON build

This commit is contained in:
M_Kececi
2026-07-13 22:43:51 +03:00
parent b80b5024f4
commit 3e0253f6be
2 changed files with 112 additions and 15 deletions
+71 -10
View File
@@ -4592,10 +4592,10 @@ SELECT (
'stock_days_180d', stock_days_180d, 'stock_days_180d', stock_days_180d,
'stock_days_365d', stock_days_365d, 'stock_days_365d', stock_days_365d,
'stock_days_total', stock_days_total, 'stock_days_total', stock_days_total,
'stock_turnover_90d', CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) * 4.0 ELSE 0 END, 'stock_turnover_90d', stock_turnover_90d,
'stock_turnover_180d', CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) * 2.0 ELSE 0 END, 'stock_turnover_180d', stock_turnover_180d,
'stock_turnover_365d', CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END, 'stock_turnover_365d', stock_turnover_365d,
'stock_turnover_total', CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) * 360.0 / GREATEST(1, ((SELECT kpi_date FROM LatestKPIDate) - DATE '2022-01-01') + 1) ELSE 0 END 'stock_turnover_total', stock_turnover_total
) )
) AS row_json ) AS row_json
FROM ( FROM (
@@ -4660,10 +4660,10 @@ FROM (
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_cost_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS unit_profit_cost_180d, CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_cost_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS unit_profit_cost_180d,
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(unit_profit_base_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS unit_profit_base_90d, CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(unit_profit_base_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS unit_profit_base_90d,
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_base_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS unit_profit_base_180d, CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_base_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS unit_profit_base_180d,
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_90d,0) > 0)::integer AS market_count_90d, COUNT(DISTINCT NULLIF(NULLIF(display_market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_90d,0) > 0)::integer AS market_count_90d,
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_180d,0) > 0)::integer AS market_count_180d, COUNT(DISTINCT NULLIF(NULLIF(display_market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_180d,0) > 0)::integer AS market_count_180d,
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_365d,0) > 0)::integer AS market_count_365d, COUNT(DISTINCT NULLIF(NULLIF(display_market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_365d,0) > 0)::integer AS market_count_365d,
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_total,0) > 0)::integer AS market_count_total, COUNT(DISTINCT NULLIF(NULLIF(display_market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_total,0) > 0)::integer AS market_count_total,
COALESCE(SUM(CASE WHEN COALESCE(sales_usd_90d,0) > 0 THEN customer_count_90d ELSE 0 END),0)::integer AS customer_count_90d, COALESCE(SUM(CASE WHEN COALESCE(sales_usd_90d,0) > 0 THEN customer_count_90d ELSE 0 END),0)::integer AS customer_count_90d,
COALESCE(SUM(CASE WHEN COALESCE(sales_usd_180d,0) > 0 THEN customer_count_180d ELSE 0 END),0)::integer AS customer_count_180d, COALESCE(SUM(CASE WHEN COALESCE(sales_usd_180d,0) > 0 THEN customer_count_180d ELSE 0 END),0)::integer AS customer_count_180d,
COALESCE(SUM(CASE WHEN COALESCE(sales_usd_365d,0) > 0 THEN customer_count_365d ELSE 0 END),0)::integer AS customer_count_365d, COALESCE(SUM(CASE WHEN COALESCE(sales_usd_365d,0) > 0 THEN customer_count_365d ELSE 0 END),0)::integer AS customer_count_365d,
@@ -4674,6 +4674,10 @@ FROM (
CASE WHEN SUM(sales_qty_total) > 0 THEN SUM(sales_index_total * sales_qty_total) / NULLIF(SUM(sales_qty_total),0) ELSE 0 END AS sales_index_total, CASE WHEN SUM(sales_qty_total) > 0 THEN SUM(sales_index_total * sales_qty_total) / NULLIF(SUM(sales_qty_total),0) ELSE 0 END AS sales_index_total,
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(price_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS price_index_90d, CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(price_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS price_index_90d,
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(margin_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS margin_index_90d, CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(margin_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS margin_index_90d,
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(stock_turnover_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS stock_turnover_90d,
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(stock_turnover_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS stock_turnover_180d,
CASE WHEN SUM(sales_qty_365d) > 0 THEN SUM(stock_turnover_365d * sales_qty_365d) / NULLIF(SUM(sales_qty_365d),0) ELSE 0 END AS stock_turnover_365d,
CASE WHEN SUM(sales_qty_total) > 0 THEN SUM(stock_turnover_total * sales_qty_total) / NULLIF(SUM(sales_qty_total),0) ELSE 0 END AS stock_turnover_total,
CASE CASE
WHEN SUM(CASE WHEN SUM(CASE
WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
@@ -4694,6 +4698,11 @@ FROM (
FROM ( FROM (
SELECT SELECT
Source.*, Source.*,
btrim(regexp_replace(COALESCE(Source.market_key,''), '^.*[|]', '')) AS display_market_key,
CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) * 4.0 ELSE 0 END AS stock_turnover_90d,
CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) * 2.0 ELSE 0 END AS stock_turnover_180d,
CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END AS stock_turnover_365d,
CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) * 360.0 / GREATEST(1, ((SELECT kpi_date FROM LatestKPIDate) - DATE '2022-01-01') + 1) ELSE 0 END AS stock_turnover_total,
ROW_NUMBER() OVER ( ROW_NUMBER() OVER (
PARTITION BY COALESCE(%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
@@ -4886,7 +4895,7 @@ func productPerformanceSQLGroupExpr(field string) (string, bool) {
case "color_yaka": case "color_yaka":
return "concat_ws('/', NULLIF(btrim(COALESCE(color_code,'')), ''), NULLIF(btrim(COALESCE(yaka_kodu,'')), ''))", true return "concat_ws('/', NULLIF(btrim(COALESCE(color_code,'')), ''), NULLIF(btrim(COALESCE(yaka_kodu,'')), ''))", true
case "market_key": case "market_key":
return "btrim(regexp_replace(COALESCE(market_key,''), '^.*\\|', ''))", true return "btrim(regexp_replace(COALESCE(market_key,''), '^.*[|]', ''))", true
default: default:
return "", false return "", false
} }
@@ -5300,6 +5309,26 @@ Spread AS (
s.color_code, s.color_code,
s.yaka_kodu, s.yaka_kodu,
s.market_key, s.market_key,
COALESCE(jsonb_agg(DISTINCT display_market_key) FILTER (
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '89 days' AND Latest.kpi_date
AND COALESCE(s.sales_usd,0) > 0
AND display_market_key NOT IN ('', 'STOK', '-')
), '[]'::jsonb) AS market_keys_90d,
COALESCE(jsonb_agg(DISTINCT display_market_key) FILTER (
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '179 days' AND Latest.kpi_date
AND COALESCE(s.sales_usd,0) > 0
AND display_market_key NOT IN ('', 'STOK', '-')
), '[]'::jsonb) AS market_keys_180d,
COALESCE(jsonb_agg(DISTINCT display_market_key) FILTER (
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '359 days' AND Latest.kpi_date
AND COALESCE(s.sales_usd,0) > 0
AND display_market_key NOT IN ('', 'STOK', '-')
), '[]'::jsonb) AS market_keys_365d,
COALESCE(jsonb_agg(DISTINCT display_market_key) FILTER (
WHERE s.sales_date BETWEEN DATE '2022-01-01' AND Latest.kpi_date
AND COALESCE(s.sales_usd,0) > 0
AND display_market_key NOT IN ('', 'STOK', '-')
), '[]'::jsonb) AS market_keys_total,
COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER ( COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '89 days' AND Latest.kpi_date WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '89 days' AND Latest.kpi_date
AND COALESCE(s.sales_usd,0) > 0 AND COALESCE(s.sales_usd,0) > 0
@@ -5320,7 +5349,12 @@ Spread AS (
AND COALESCE(s.sales_usd,0) > 0 AND COALESCE(s.sales_usd,0) > 0
AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-') AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
), '[]'::jsonb) AS customer_keys_total ), '[]'::jsonb) AS customer_keys_total
FROM (
SELECT
s.*,
btrim(regexp_replace(COALESCE(s.market_key,''), '^.*[|]', '')) AS display_market_key
FROM mk_product_performance_sales_daily s FROM mk_product_performance_sales_daily s
) s
CROSS JOIN Latest CROSS JOIN Latest
WHERE s.sales_date BETWEEN DATE '2022-01-01' AND Latest.kpi_date WHERE s.sales_date BETWEEN DATE '2022-01-01' AND Latest.kpi_date
AND upper(translate(btrim(COALESCE(s.urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER') AND upper(translate(btrim(COALESCE(s.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')
@@ -5331,6 +5365,10 @@ SELECT jsonb_build_object(
'color_code', color_code, 'color_code', color_code,
'yaka_kodu', yaka_kodu, 'yaka_kodu', yaka_kodu,
'market_key', market_key, 'market_key', market_key,
'__market_keys_90d', market_keys_90d,
'__market_keys_180d', market_keys_180d,
'__market_keys_365d', market_keys_365d,
'__market_keys_total', market_keys_total,
'__customer_keys_90d', customer_keys_90d, '__customer_keys_90d', customer_keys_90d,
'__customer_keys_180d', customer_keys_180d, '__customer_keys_180d', customer_keys_180d,
'__customer_keys_365d', customer_keys_365d, '__customer_keys_365d', customer_keys_365d,
@@ -5355,12 +5393,14 @@ FROM Spread`
continue continue
} }
for _, suffix := range productPerformancePeriodSuffixes() { for _, suffix := range productPerformancePeriodSuffixes() {
field := "__customer_keys_" + suffix for _, prefix := range []string{"__market_keys_", "__customer_keys_"} {
field := prefix + suffix
if value, ok := spread[field]; ok { if value, ok := spread[field]; ok {
row[field] = value row[field] = value
} }
} }
} }
}
return rows return rows
} }
@@ -5569,6 +5609,13 @@ func applyProductPerformanceGroupedChildScoreAverages(rows []map[string]any, mod
} }
row[field] = weightedAverageProductPerformanceScoreRows(childRows, field, suffix) row[field] = weightedAverageProductPerformanceScoreRows(childRows, field, suffix)
} }
for _, suffix := range productPerformancePeriodSuffixes() {
field := "stock_turnover_" + suffix
if !productPerformanceRowsHaveField(childRows, field) {
continue
}
row[field] = weightedAverageProductPerformanceRows(childRows, field, "sales_qty_"+suffix)
}
if score, ok := productPerformanceOptionalFloat(row, "performance_score_90d"); ok { if score, ok := productPerformanceOptionalFloat(row, "performance_score_90d"); ok {
row["performance_score"] = score row["performance_score"] = score
} }
@@ -5710,6 +5757,7 @@ func (n *productPerformanceGroupedSnapshotNode) addDistinctSpread(row map[string
} }
} }
for _, suffix := range productPerformancePeriodSuffixes() { for _, suffix := range productPerformancePeriodSuffixes() {
n.MarketSeen = productPerformanceAddSeenStrings(n.MarketSeen, suffix, productPerformanceStringSliceFromMap(row, "__market_keys_"+suffix))
n.CustomerSeen = productPerformanceAddSeenStrings(n.CustomerSeen, suffix, productPerformanceStringSliceFromMap(row, "__customer_keys_"+suffix)) n.CustomerSeen = productPerformanceAddSeenStrings(n.CustomerSeen, suffix, productPerformanceStringSliceFromMap(row, "__customer_keys_"+suffix))
} }
} }
@@ -5940,6 +5988,7 @@ func addProductPerformanceDistinctSpread(row map[string]any, marketSeen, custome
} }
} }
for _, suffix := range productPerformancePeriodSuffixes() { for _, suffix := range productPerformancePeriodSuffixes() {
marketSeen = productPerformanceAddSeenStrings(marketSeen, suffix, productPerformanceStringSliceFromMap(row, "__market_keys_"+suffix))
customerSeen = productPerformanceAddSeenStrings(customerSeen, suffix, productPerformanceStringSliceFromMap(row, "__customer_keys_"+suffix)) customerSeen = productPerformanceAddSeenStrings(customerSeen, suffix, productPerformanceStringSliceFromMap(row, "__customer_keys_"+suffix))
} }
} }
@@ -6135,6 +6184,7 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
} }
preservedCustomerScores := map[string]float64{} preservedCustomerScores := map[string]float64{}
preservedProductScores := map[string]float64{} preservedProductScores := map[string]float64{}
preservedStockTurnovers := map[string]float64{}
for _, suffix := range productPerformancePeriodSuffixes() { for _, suffix := range productPerformancePeriodSuffixes() {
if score, ok := productPerformanceOptionalFloat(out, "customer_score_"+suffix); ok { if score, ok := productPerformanceOptionalFloat(out, "customer_score_"+suffix); ok {
preservedCustomerScores[suffix] = score preservedCustomerScores[suffix] = score
@@ -6142,6 +6192,11 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
if score, ok := productPerformanceOptionalFloat(out, "performance_score_"+suffix); ok { if score, ok := productPerformanceOptionalFloat(out, "performance_score_"+suffix); ok {
preservedProductScores[suffix] = score preservedProductScores[suffix] = score
} }
if isGroup {
if turnover, ok := productPerformanceOptionalFloat(out, "stock_turnover_"+suffix); ok {
preservedStockTurnovers[suffix] = turnover
}
}
} }
if score, ok := productPerformanceOptionalFloat(out, "performance_score"); ok { if score, ok := productPerformanceOptionalFloat(out, "performance_score"); ok {
if _, exists := preservedProductScores["90d"]; !exists { if _, exists := preservedProductScores["90d"]; !exists {
@@ -6170,6 +6225,9 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
out["stock_days_"+suffix] = 0 out["stock_days_"+suffix] = 0
} }
out["stock_turnover_"+suffix] = productPerformanceAnnualizedStockTurnover(qty, turnoverBase, days) out["stock_turnover_"+suffix] = productPerformanceAnnualizedStockTurnover(qty, turnoverBase, days)
if turnover, ok := preservedStockTurnovers[suffix]; ok {
out["stock_turnover_"+suffix] = turnover
}
if qty > 0 { if qty > 0 {
out["avg_price_usd_"+suffix] = sales / qty out["avg_price_usd_"+suffix] = sales / qty
} }
@@ -6775,6 +6833,9 @@ func productPerformanceMetricWeightField(field string) string {
if productPerformanceUsesCostWeight(field) { if productPerformanceUsesCostWeight(field) {
return "__product_cost_weight" return "__product_cost_weight"
} }
if strings.HasPrefix(field, "stock_turnover") {
return "sales_qty_" + productPerformanceScoreSuffix(field)
}
if strings.Contains(field, "_180d") { if strings.Contains(field, "_180d") {
return "sales_qty_180d" return "sales_qty_180d"
} }
@@ -3525,6 +3525,7 @@ function shouldAverageField (field) {
} }
function weightFieldForMetric (field) { function weightFieldForMetric (field) {
if (field.startsWith('stock_turnover')) return `sales_qty_${countPeriodSuffix(field)}`
if (field.includes('_180d')) return 'sales_qty_180d' if (field.includes('_180d')) return 'sales_qty_180d'
if (field.includes('_365d')) return 'sales_qty_365d' if (field.includes('_365d')) return 'sales_qty_365d'
if (field.includes('_total')) return 'sales_qty_total' if (field.includes('_total')) return 'sales_qty_total'
@@ -3538,6 +3539,7 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
const isGroup = Boolean(String(groupField || '').trim()) const isGroup = Boolean(String(groupField || '').trim())
const preservedCustomerScores = {} const preservedCustomerScores = {}
const preservedProductScores = {} const preservedProductScores = {}
const preservedStockTurnovers = {}
for (const suffix of ['90d', '180d', '365d', 'total']) { for (const suffix of ['90d', '180d', '365d', 'total']) {
if (Object.prototype.hasOwnProperty.call(out, `customer_score_${suffix}`)) { if (Object.prototype.hasOwnProperty.call(out, `customer_score_${suffix}`)) {
preservedCustomerScores[suffix] = Number(out[`customer_score_${suffix}`] || 0) preservedCustomerScores[suffix] = Number(out[`customer_score_${suffix}`] || 0)
@@ -3545,6 +3547,9 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
if (Object.prototype.hasOwnProperty.call(out, `performance_score_${suffix}`)) { if (Object.prototype.hasOwnProperty.call(out, `performance_score_${suffix}`)) {
preservedProductScores[suffix] = Number(out[`performance_score_${suffix}`] || 0) preservedProductScores[suffix] = Number(out[`performance_score_${suffix}`] || 0)
} }
if (isGroup && Object.prototype.hasOwnProperty.call(out, `stock_turnover_${suffix}`)) {
preservedStockTurnovers[suffix] = Number(out[`stock_turnover_${suffix}`] || 0)
}
} }
if ( if (
Object.prototype.hasOwnProperty.call(out, 'performance_score') && Object.prototype.hasOwnProperty.call(out, 'performance_score') &&
@@ -3565,6 +3570,9 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
else if (qty <= 0 && stockQty > 0) out[`stock_days_${suffix}`] = 9999 else if (qty <= 0 && stockQty > 0) out[`stock_days_${suffix}`] = 9999
else if (!Object.prototype.hasOwnProperty.call(out, `stock_days_${suffix}`)) out[`stock_days_${suffix}`] = 0 else if (!Object.prototype.hasOwnProperty.call(out, `stock_days_${suffix}`)) out[`stock_days_${suffix}`] = 0
out[`stock_turnover_${suffix}`] = annualizedStockTurnover(qty, turnoverBase, days) out[`stock_turnover_${suffix}`] = annualizedStockTurnover(qty, turnoverBase, days)
if (Object.prototype.hasOwnProperty.call(preservedStockTurnovers, suffix)) {
out[`stock_turnover_${suffix}`] = preservedStockTurnovers[suffix]
}
if (qty > 0) out[`avg_price_usd_${suffix}`] = sales / qty if (qty > 0) out[`avg_price_usd_${suffix}`] = sales / qty
const { costPrice, basePrice } = periodCostPair(out, suffix) const { costPrice, basePrice } = periodCostPair(out, suffix)
out[`base_price_usd_${suffix}`] = basePrice out[`base_price_usd_${suffix}`] = basePrice
@@ -3917,6 +3925,12 @@ function distinctRevenueMarketCount (sourceRows, suffix, fallbackField) {
const values = new Set() const values = new Set()
for (const row of sourceRows) { for (const row of sourceRows) {
if (Number(row?.[salesField] || 0) <= 0) continue if (Number(row?.[salesField] || 0) <= 0) continue
const marketKeys = stringListFromRow(row, `__market_keys_${suffix}`)
for (const key of marketKeys) {
const value = displayMarketName(key)
if (value && value !== 'STOK' && value !== '-') values.add(value)
}
if (marketKeys.length > 0) continue
const value = displayMarketName(row?.market_key) const value = displayMarketName(row?.market_key)
if (value && value !== 'STOK' && value !== '-') values.add(value) if (value && value !== 'STOK' && value !== '-') values.add(value)
} }
@@ -3931,6 +3945,11 @@ function distinctRevenueCustomerCount (sourceRows, suffix, fallbackField) {
const values = new Set() const values = new Set()
for (const row of sourceRows) { for (const row of sourceRows) {
if (Number(row?.[salesField] || 0) <= 0) continue if (Number(row?.[salesField] || 0) <= 0) continue
const customerKeys = stringListFromRow(row, `__customer_keys_${suffix}`)
for (const code of customerKeys) {
if (code && code !== '-') values.add(code)
}
if (customerKeys.length > 0) continue
const code = String(row?.customer_code || '').trim() const code = String(row?.customer_code || '').trim()
if (code && code !== '-') values.add(code) if (code && code !== '-') values.add(code)
} }
@@ -3940,6 +3959,22 @@ function distinctRevenueCustomerCount (sourceRows, suffix, fallbackField) {
}, 0) }, 0)
} }
function stringListFromRow (row, field) {
const value = row?.[field]
if (Array.isArray(value)) return value.map(item => String(item || '').trim()).filter(Boolean)
const text = String(value || '').trim()
if (!text) return []
if (text.startsWith('[')) {
try {
const parsed = JSON.parse(text)
return Array.isArray(parsed) ? parsed.map(item => String(item || '').trim()).filter(Boolean) : []
} catch {
return []
}
}
return [text]
}
function distinctSummary (sourceRows, field, fallbackField) { function distinctSummary (sourceRows, field, fallbackField) {
const values = new Set() const values = new Set()
for (const row of sourceRows) { for (const row of sourceRows) {
@@ -4351,9 +4386,10 @@ function groupedOrExistingStockTurnover (row, suffix, periodDays) {
const avgStock = Number(row?.[`avg_stock_${suffix}`] || 0) const avgStock = Number(row?.[`avg_stock_${suffix}`] || 0)
const stockQty = Number(row?.stock_qty || 0) const stockQty = Number(row?.stock_qty || 0)
if (row?.__group) { if (row?.__group) {
if (avgStock > 0) return stockTurnover(salesQty, avgStock, periodDays)
const current = Number(row?.[`stock_turnover_${suffix}`]) const current = Number(row?.[`stock_turnover_${suffix}`])
return Number.isFinite(current) ? current : 0 if (Number.isFinite(current)) return current
if (avgStock > 0) return stockTurnover(salesQty, avgStock, periodDays)
return 0
} }
const stockBase = avgStock > 0 ? avgStock : stockQty const stockBase = avgStock > 0 ? avgStock : stockQty
return existingOrStockTurnover(row?.[`stock_turnover_${suffix}`], salesQty, stockBase, periodDays) return existingOrStockTurnover(row?.[`stock_turnover_${suffix}`], salesQty, stockBase, periodDays)