Fix product performance grouped JSON build

This commit is contained in:
M_Kececi
2026-07-05 22:06:43 +03:00
parent fbdde54eee
commit 9e1ecb9b3f
5 changed files with 766 additions and 564 deletions
+145 -64
View File
@@ -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) {