product performance grouped kpi improvements
This commit is contained in:
@@ -95,7 +95,7 @@
|
||||
:rows="filteredGeneralRows"
|
||||
:columns="generalColumns"
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 100, sortBy: 'performance_score', descending: true }"
|
||||
:pagination="{ rowsPerPage: 100, sortBy: 'performance_score_total', descending: true }"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="64"
|
||||
>
|
||||
@@ -971,7 +971,7 @@
|
||||
:rows="displayActiveSalesBreakdownTableRows"
|
||||
:columns="visibleSalesBreakdownColumns"
|
||||
:loading="loading || backendGroupedLoading"
|
||||
:pagination="{ rowsPerPage: 100, sortBy: 'performance_score', descending: true }"
|
||||
:pagination="{ rowsPerPage: 100, sortBy: 'performance_score_total', descending: true }"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="64"
|
||||
>
|
||||
@@ -1551,7 +1551,7 @@ const pagination = ref({
|
||||
page: 1,
|
||||
rowsPerPage: 100,
|
||||
rowsNumber: 0,
|
||||
sortBy: 'performance_score',
|
||||
sortBy: 'performance_score_total',
|
||||
descending: true
|
||||
})
|
||||
|
||||
@@ -1616,9 +1616,13 @@ const columns = [
|
||||
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left' },
|
||||
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left', sortable: true },
|
||||
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
|
||||
{ name: 'performance_score_90d', label: '90G Skor', field: row => formatNumber(row.performance_score_90d, 1), align: 'right', sortable: true },
|
||||
{ name: 'sales_qty_90d', label: '90G Satış', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
|
||||
{ name: 'performance_score_180d', label: '180G Skor', field: row => formatNumber(row.performance_score_180d, 1), align: 'right', sortable: true },
|
||||
{ name: 'sales_qty_180d', label: '180G Satış', field: row => formatNumber(row.sales_qty_180d, 0), align: 'right', sortable: true },
|
||||
{ name: 'performance_score_365d', label: '360G Skor', field: row => formatNumber(row.performance_score_365d, 1), align: 'right', sortable: true },
|
||||
{ name: 'sales_qty_365d', label: '360G Satış', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
|
||||
{ name: 'performance_score_total', label: 'Genel Skor', field: row => formatNumber(row.performance_score_total, 1), align: 'right', sortable: true },
|
||||
{ name: 'sales_qty_total', label: 'Genel Satış', field: row => formatNumber(row.sales_qty_total, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_usd_90d', label: '90G USD', field: 'sales_usd_90d', align: 'right', sortable: true },
|
||||
{ name: 'sales_usd_180d', label: '180G USD', field: 'sales_usd_180d', align: 'right', sortable: true },
|
||||
@@ -1677,6 +1681,24 @@ const periodOrder = {
|
||||
total: 4
|
||||
}
|
||||
|
||||
const hiddenTableColumns = new Set(['item_description', 'performance_score'])
|
||||
const dimensionColumnNames = [
|
||||
'product_code',
|
||||
'color_code',
|
||||
'yaka_kodu',
|
||||
'item_description',
|
||||
'kategori',
|
||||
'askili_yan',
|
||||
'urun_ilk_grubu',
|
||||
'urun_ana_grubu',
|
||||
'urun_alt_grubu',
|
||||
'market_key',
|
||||
'country',
|
||||
'customer_segment',
|
||||
'customer_code',
|
||||
'customer_name'
|
||||
]
|
||||
|
||||
const metricOrderFragments = [
|
||||
'customer_score',
|
||||
'performance_score',
|
||||
@@ -1739,14 +1761,23 @@ function isTrailingInfoColumn (name) {
|
||||
return ['performance_bucket', 'recommendation', 'last_sale_date', 'first_sale_date', 'is_overdue'].includes(name)
|
||||
}
|
||||
|
||||
function nonPeriodColumnOrder (name) {
|
||||
if (name === 'performance_score') return 2
|
||||
if (name === 'stock_qty' || name === 'product_count') return 1
|
||||
if (dimensionColumnNames.includes(name) || name === 'image') return 0
|
||||
return 3
|
||||
}
|
||||
|
||||
function orderedMetricColumns (sourceColumns) {
|
||||
return [...sourceColumns].sort((a, b) => {
|
||||
return sourceColumns.filter(col => !hiddenTableColumns.has(col.name)).sort((a, b) => {
|
||||
const aPeriod = columnPeriod(a.name)
|
||||
const bPeriod = columnPeriod(b.name)
|
||||
const aTrailing = isTrailingInfoColumn(a.name)
|
||||
const bTrailing = isTrailingInfoColumn(b.name)
|
||||
if (aTrailing !== bTrailing) return aTrailing ? 1 : -1
|
||||
if (!aPeriod && !bPeriod) return 0
|
||||
if (!aPeriod && !bPeriod) {
|
||||
return nonPeriodColumnOrder(a.name) - nonPeriodColumnOrder(b.name)
|
||||
}
|
||||
if (!aPeriod) return -1
|
||||
if (!bPeriod) return 1
|
||||
const periodDiff = periodOrder[aPeriod] - periodOrder[bPeriod]
|
||||
@@ -2068,6 +2099,65 @@ const salesBreakdownColumns = [
|
||||
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
|
||||
]
|
||||
|
||||
const productPeriodScoreColumns = [
|
||||
{ name: 'performance_score_90d', label: '90G Ürün Skor', field: row => formatNumber(row.performance_score_90d, 1), align: 'right', sortable: true },
|
||||
{ name: 'performance_score_180d', label: '180G Ürün Skor', field: row => formatNumber(row.performance_score_180d, 1), align: 'right', sortable: true },
|
||||
{ name: 'performance_score_365d', label: '360G Ürün Skor', field: row => formatNumber(row.performance_score_365d, 1), align: 'right', sortable: true },
|
||||
{ name: 'performance_score_total', label: 'Genel Ürün Skor', field: row => formatNumber(row.performance_score_total, 1), align: 'right', sortable: true }
|
||||
]
|
||||
|
||||
const customerPeriodScoreColumns = [
|
||||
{ name: 'customer_score_90d', label: '90G Müşteri Skor', field: row => formatNumber(row.customer_score_90d, 1), align: 'right', sortable: true },
|
||||
{ name: 'customer_score_180d', label: '180G Müşteri Skor', field: row => formatNumber(row.customer_score_180d, 1), align: 'right', sortable: true },
|
||||
{ name: 'customer_score_365d', label: '360G Müşteri Skor', field: row => formatNumber(row.customer_score_365d, 1), align: 'right', sortable: true },
|
||||
{ name: 'customer_score_total', label: 'Genel Müşteri Skor', field: row => formatNumber(row.customer_score_total, 1), align: 'right', sortable: true }
|
||||
]
|
||||
|
||||
function ensureColumns (targetColumns, columnsToEnsure) {
|
||||
const existing = new Set(targetColumns.map(col => col.name))
|
||||
for (const col of columnsToEnsure) {
|
||||
if (!existing.has(col.name)) targetColumns.push(col)
|
||||
}
|
||||
}
|
||||
|
||||
function removeColumnsInPlace (targetColumns, names) {
|
||||
const removeSet = new Set(names)
|
||||
const next = targetColumns.filter(col => !removeSet.has(col.name))
|
||||
targetColumns.splice(0, targetColumns.length, ...next)
|
||||
return targetColumns
|
||||
}
|
||||
|
||||
;[
|
||||
columns,
|
||||
generalColumns,
|
||||
orderAnalysisColumns,
|
||||
orderGroupColumns,
|
||||
orderProductCustomerColumns,
|
||||
orderMarketDetailColumns,
|
||||
idleColumns,
|
||||
marketColumns,
|
||||
countryColumns,
|
||||
salesBreakdownColumns
|
||||
].forEach(target => ensureColumns(target, productPeriodScoreColumns))
|
||||
|
||||
;[
|
||||
customerColumns,
|
||||
salesBreakdownColumns
|
||||
].forEach(target => ensureColumns(target, customerPeriodScoreColumns))
|
||||
|
||||
;[
|
||||
generalColumns,
|
||||
orderAnalysisColumns,
|
||||
orderGroupColumns,
|
||||
orderProductCustomerColumns,
|
||||
orderMarketDetailColumns,
|
||||
idleColumns,
|
||||
marketColumns,
|
||||
countryColumns,
|
||||
customerColumns,
|
||||
salesBreakdownColumns
|
||||
].forEach(target => removeColumnsInPlace(target, ['image']))
|
||||
|
||||
;[
|
||||
columns,
|
||||
generalColumns,
|
||||
@@ -2185,7 +2275,6 @@ const imageDialogInfoRows = computed(() => {
|
||||
{ key: 'product', label: 'Ürün', value: productCode || '-' },
|
||||
{ key: 'color', label: 'Renk', value: colorCode || '-' },
|
||||
{ key: 'yaka', label: 'Yaka', value: yakaCode || '-' },
|
||||
{ key: 'desc', label: 'Açıklama', value: row.item_description || '-' },
|
||||
{ key: 'category', label: 'Kategori', value: row.kategori || '-' },
|
||||
{ key: 'hanger', label: 'Askılı/Yan', value: row.askili_yan || '-' },
|
||||
{ key: 'first', label: 'İlk Grup', value: row.urun_ilk_grubu || row.yas_grubu || '-' },
|
||||
@@ -2205,7 +2294,6 @@ const groupLevels = [
|
||||
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu', fallback: 'seri' },
|
||||
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
|
||||
{ key: 'product_code', label: 'Ürün' },
|
||||
{ key: 'item_description', label: 'Açıklama' },
|
||||
{ key: 'color_code', label: 'Renk' },
|
||||
{ key: 'yaka_kodu', label: 'Yaka' },
|
||||
{ key: 'market_key', label: 'Piyasa' }
|
||||
@@ -2221,7 +2309,6 @@ const tabGroupLevels = {
|
||||
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu', fallback: 'seri' },
|
||||
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
|
||||
{ key: 'product_code', label: 'Ürün' },
|
||||
{ key: 'item_description', label: 'Açıklama' },
|
||||
{ key: 'country', label: 'Ülke' },
|
||||
{ key: 'market_key', label: 'Piyasa' },
|
||||
{ key: 'customer_segment', label: 'Segment' },
|
||||
@@ -2234,7 +2321,6 @@ const tabGroupLevels = {
|
||||
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu', fallback: 'seri' },
|
||||
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
|
||||
{ key: 'product_code', label: 'Ürün' },
|
||||
{ key: 'item_description', label: 'Açıklama' },
|
||||
{ key: 'color_code', label: 'Renk' },
|
||||
{ key: 'yaka_kodu', label: 'Yaka' }
|
||||
],
|
||||
@@ -2248,7 +2334,6 @@ const tabGroupLevels = {
|
||||
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu', fallback: 'seri' },
|
||||
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
|
||||
{ key: 'product_code', label: 'Ürün' },
|
||||
{ key: 'item_description', label: 'Açıklama' },
|
||||
{ key: 'color_code', label: 'Renk' },
|
||||
{ key: 'yaka_kodu', label: 'Yaka' }
|
||||
],
|
||||
@@ -2262,7 +2347,6 @@ const tabGroupLevels = {
|
||||
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu', fallback: 'seri' },
|
||||
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
|
||||
{ key: 'product_code', label: 'Ürün' },
|
||||
{ key: 'item_description', label: 'Açıklama' },
|
||||
{ key: 'color_code', label: 'Renk' },
|
||||
{ key: 'yaka_kodu', label: 'Yaka' }
|
||||
],
|
||||
@@ -2276,7 +2360,6 @@ const tabGroupLevels = {
|
||||
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu', fallback: 'seri' },
|
||||
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
|
||||
{ key: 'product_code', label: 'Ürün' },
|
||||
{ key: 'item_description', label: 'Açıklama' },
|
||||
{ key: 'color_code', label: 'Renk' },
|
||||
{ key: 'yaka_kodu', label: 'Yaka' }
|
||||
],
|
||||
@@ -2290,7 +2373,6 @@ const tabGroupLevels = {
|
||||
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu', fallback: 'seri' },
|
||||
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
|
||||
{ key: 'product_code', label: 'Ürün' },
|
||||
{ key: 'item_description', label: 'Açıklama' },
|
||||
{ key: 'color_code', label: 'Renk' },
|
||||
{ key: 'yaka_kodu', label: 'Yaka' }
|
||||
],
|
||||
@@ -2304,7 +2386,6 @@ const tabGroupLevels = {
|
||||
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu', fallback: 'seri' },
|
||||
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
|
||||
{ key: 'product_code', label: 'Ürün' },
|
||||
{ key: 'item_description', label: 'Açıklama' },
|
||||
{ key: 'color_code', label: 'Renk' },
|
||||
{ key: 'yaka_kodu', label: 'Yaka' }
|
||||
]
|
||||
@@ -2460,6 +2541,7 @@ function buildGroupedTableRows (tableKey, sourceRows) {
|
||||
}
|
||||
|
||||
function backendRowsForTab (tabKey, fallbackRows) {
|
||||
if (hasActiveTableFilters(tabKey)) return fallbackRows
|
||||
const rows = backendGroupedRows.value[tabKey]
|
||||
return Array.isArray(rows) && rows.length ? rows : fallbackRows
|
||||
}
|
||||
@@ -2658,6 +2740,10 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
|
||||
out.customer_score_180d = customerSalesPeriodScore(periodMetricSource(out, '180d'))
|
||||
out.customer_score_365d = customerSalesPeriodScore(periodMetricSource(out, '365d'))
|
||||
out.customer_score_total = customerSalesPeriodScore(periodMetricSource(out, 'total'))
|
||||
out.performance_score_90d = productSalesPeriodScore(productPeriodMetricSource(out, '90d'))
|
||||
out.performance_score_180d = productSalesPeriodScore(productPeriodMetricSource(out, '180d'))
|
||||
out.performance_score_365d = productSalesPeriodScore(productPeriodMetricSource(out, '365d'))
|
||||
out.performance_score_total = productSalesPeriodScore(productPeriodMetricSource(out, 'total'))
|
||||
out.performance_score = groupPerformanceScore(out, groupField)
|
||||
}
|
||||
|
||||
@@ -2668,6 +2754,10 @@ function groupPerformanceScore (row, groupField = '') {
|
||||
}
|
||||
|
||||
function salesGroupPerformanceScore (row) {
|
||||
return productSalesPeriodScore(productPeriodMetricSource(row, '90d'))
|
||||
}
|
||||
|
||||
function productSalesPeriodScore (row) {
|
||||
const salesIndex = Number(row?.sales_index_90d || 0)
|
||||
const margin = Number(row?.gross_margin_cost_90d ?? row?.gross_margin_90d ?? 0)
|
||||
const invoices = Number(row?.invoice_count_90d || 0)
|
||||
@@ -2682,6 +2772,22 @@ function salesGroupPerformanceScore (row) {
|
||||
Math.min(10, Math.max(0, stockTurnover) * 5)
|
||||
}
|
||||
|
||||
function productPeriodMetricSource (row, suffix) {
|
||||
const salesUSD = Number(row?.[`sales_usd_${suffix}`] || 0)
|
||||
const salesIndex = Number(row?.[`sales_index_${suffix}`] || 0) || (salesUSD > 0 ? salesUSD / 1000 : 0)
|
||||
const qty = Number(row?.[`sales_qty_${suffix}`] || 0)
|
||||
const stockQty = Number(row?.stock_qty || 0)
|
||||
return {
|
||||
sales_index_90d: salesIndex,
|
||||
gross_margin_cost_90d: Number(row?.[`gross_margin_cost_${suffix}`] ?? 0),
|
||||
gross_margin_90d: Number(row?.[`gross_margin_${suffix}`] ?? 0),
|
||||
invoice_count_90d: Number(row?.[`invoice_count_${suffix}`] || 0),
|
||||
sales_qty_90d: qty,
|
||||
stock_turnover_90d: Number(row?.[`stock_turnover_${suffix}`] || 0) || (stockQty > 0 ? qty / stockQty : 0),
|
||||
stock_qty: stockQty
|
||||
}
|
||||
}
|
||||
|
||||
function customerSalesGroupPerformanceScore (row) {
|
||||
const periodToScore = {
|
||||
90: Number(row?.customer_score_90d || 0),
|
||||
@@ -2922,32 +3028,27 @@ function groupIndentPx (row) {
|
||||
return '0px'
|
||||
}
|
||||
|
||||
function groupShowsImage (row) {
|
||||
return Number(row?.level ?? 0) < autoExpandThroughLevel.value
|
||||
function hasProductVariantImageKey (row) {
|
||||
const product = String(row?.image_product_code || row?.product_code || '').trim()
|
||||
const color = String(row?.image_color_code || row?.color_code || '').trim()
|
||||
return Boolean(product && color)
|
||||
}
|
||||
|
||||
const groupDimensionFields = new Set([
|
||||
'product_code',
|
||||
'color_code',
|
||||
'yaka_kodu',
|
||||
'item_description',
|
||||
'kategori',
|
||||
'askili_yan',
|
||||
'urun_ilk_grubu',
|
||||
'urun_ana_grubu',
|
||||
'urun_alt_grubu',
|
||||
'market_key',
|
||||
'country',
|
||||
'customer_segment',
|
||||
'customer_code',
|
||||
'customer_name'
|
||||
])
|
||||
function canShowProductImage (row) {
|
||||
return activeTab.value === 'products' && !row?.__group && hasProductVariantImageKey(row)
|
||||
}
|
||||
|
||||
function groupShowsImage () {
|
||||
return false
|
||||
}
|
||||
|
||||
const groupDimensionFields = new Set(dimensionColumnNames)
|
||||
|
||||
function groupCellClass (colOrName) {
|
||||
const name = colOrName?.name || colOrName
|
||||
if (name === 'image') return 'product-image-cell'
|
||||
if (colOrName?.align === 'right') return 'text-right'
|
||||
return ['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', 'stock_days_90d', 'stock_days_180d', 'stock_days_total', 'stock_turnover_90d', 'stock_turnover_180d', 'stock_turnover_365d', 'stock_turnover_total', 'avg_price_usd_90d', 'avg_price_usd_180d', 'avg_price_usd_365d', 'avg_price_usd_total', 'base_price_usd', 'cost_price_usd', 'unit_profit_base_90d', 'unit_profit_cost_90d', 'unit_profit_base_180d', 'unit_profit_cost_180d', 'unit_profit_base_total', 'unit_profit_cost_total', 'gross_profit_usd_90d', 'gross_profit_usd_180d', 'gross_profit_usd_total', 'gross_margin_base_90d', 'gross_margin_cost_90d', 'gross_margin_base_180d', 'gross_margin_cost_180d', 'gross_margin_base_total', 'gross_margin_cost_total', 'gross_margin_90d', 'gross_margin_180d', 'market_count_90d', 'customer_count_90d', 'market_count_total', 'customer_count_total', 'sales_index_90d', 'sales_index_total', 'customer_score_90d', 'customer_score_180d', 'customer_score_365d', 'customer_score_total', 'performance_score'].includes(name)
|
||||
return /(^|_)(score|qty|count|usd|days|turnover|index|margin)$/.test(name) || ['stock_qty', 'net_stock_after_order'].includes(name)
|
||||
? 'text-right'
|
||||
: ''
|
||||
}
|
||||
@@ -3017,6 +3118,11 @@ function selectedColumnFilters (tableKey, name) {
|
||||
return Array.isArray(selected) ? selected : []
|
||||
}
|
||||
|
||||
function hasActiveTableFilters (tableKey) {
|
||||
const prefix = `${tableKey}:`
|
||||
return Object.entries(columnFilters).some(([key, value]) => key.startsWith(prefix) && Array.isArray(value) && value.length > 0)
|
||||
}
|
||||
|
||||
function isColumnFilterValueSelected (tableKey, name, value) {
|
||||
return selectedColumnFilters(tableKey, name).includes(value)
|
||||
}
|
||||
@@ -3215,10 +3321,20 @@ function normalizeRow (row) {
|
||||
const color = String(row?.color_code || '').trim()
|
||||
const yaka = String(row?.yaka_kodu || '').trim()
|
||||
const market = String(row?.market_key || '').trim()
|
||||
return withProductMargins({
|
||||
return withPeriodScores(withProductMargins({
|
||||
...row,
|
||||
row_key: `${product}|${color}|${yaka}|${market}`
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
function withPeriodScores (row) {
|
||||
const next = { ...row }
|
||||
next.performance_score_90d = Number(next.performance_score_90d || productSalesPeriodScore(productPeriodMetricSource(next, '90d')))
|
||||
next.performance_score_180d = Number(next.performance_score_180d || productSalesPeriodScore(productPeriodMetricSource(next, '180d')))
|
||||
next.performance_score_365d = Number(next.performance_score_365d || productSalesPeriodScore(productPeriodMetricSource(next, '365d')))
|
||||
next.performance_score_total = Number(next.performance_score_total || productSalesPeriodScore(productPeriodMetricSource(next, 'total')))
|
||||
if (!Number(next.performance_score || 0)) next.performance_score = next.performance_score_90d
|
||||
return next
|
||||
}
|
||||
|
||||
function performanceVariantKey (row) {
|
||||
@@ -3255,10 +3371,10 @@ function normalizeGeneralRow (row) {
|
||||
const color = String(row?.color_code || '').trim()
|
||||
const yaka = String(row?.yaka_kodu || '').trim()
|
||||
const market = String(row?.market_key || '').trim()
|
||||
return withGeneralMargins({
|
||||
return withPeriodScores(withGeneralMargins({
|
||||
...row,
|
||||
row_key: `general|${product}|${color}|${yaka}|${market}`
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
function normalizeOrderAnalysisRow (row) {
|
||||
@@ -3266,19 +3382,19 @@ function normalizeOrderAnalysisRow (row) {
|
||||
const color = String(row?.color_code || '').trim()
|
||||
const yaka = String(row?.yaka_kodu || '').trim()
|
||||
const market = String(row?.market_key || '').trim()
|
||||
return {
|
||||
return withOrderPeriodScores({
|
||||
...row,
|
||||
row_key: `orders|${product}|${color}|${yaka}|${market}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeOrderGroupRow (row) {
|
||||
const key = String(row?.group_key || '').trim()
|
||||
const mode = String(row?.breakdown || '').trim()
|
||||
return {
|
||||
return withOrderPeriodScores({
|
||||
...row,
|
||||
row_key: `order-group|${mode}|${key}|${row?.market_key || ''}|${row?.customer_code || ''}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeOrderProductCustomerRow (row) {
|
||||
@@ -3287,21 +3403,32 @@ function normalizeOrderProductCustomerRow (row) {
|
||||
const yaka = String(row?.yaka_kodu || '').trim()
|
||||
const market = String(row?.market_key || '').trim()
|
||||
const customer = String(row?.customer_code || '').trim()
|
||||
return {
|
||||
return withOrderPeriodScores({
|
||||
...row,
|
||||
row_key: `order-product-customer|${product}|${color}|${yaka}|${market}|${customer}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeOrderMarketDetailRow (row) {
|
||||
return {
|
||||
return withOrderPeriodScores({
|
||||
...row,
|
||||
row_key: `order-market-detail|${row?.market_key || ''}|${row?.customer_code || ''}|${row?.order_number || ''}|${row?.product_code || ''}|${row?.color_code || ''}|${row?.yaka_kodu || ''}`
|
||||
})
|
||||
}
|
||||
|
||||
function withOrderPeriodScores (row) {
|
||||
const next = { ...row }
|
||||
const score = Number(next.performance_score || orderGroupPerformanceScore(next))
|
||||
next.performance_score_90d = Number(next.performance_score_90d || score)
|
||||
next.performance_score_180d = Number(next.performance_score_180d || score)
|
||||
next.performance_score_365d = Number(next.performance_score_365d || score)
|
||||
next.performance_score_total = Number(next.performance_score_total || score)
|
||||
next.performance_score = score
|
||||
return next
|
||||
}
|
||||
|
||||
function normalizeSalesBreakdownRow (row) {
|
||||
const next = { ...row }
|
||||
const next = withPeriodScores({ ...row })
|
||||
next.customer_score_90d = Number(next.customer_score_90d || customerSalesPeriodScore(periodMetricSource(next, '90d')))
|
||||
next.customer_score_180d = Number(next.customer_score_180d || customerSalesPeriodScore(periodMetricSource(next, '180d')))
|
||||
next.customer_score_365d = Number(next.customer_score_365d || customerSalesPeriodScore(periodMetricSource(next, '365d')))
|
||||
@@ -3320,6 +3447,7 @@ function orderGroupLabel (row) {
|
||||
}
|
||||
|
||||
function productImageKey (row) {
|
||||
if (!hasProductVariantImageKey(row)) return ''
|
||||
const product = String(row?.image_product_code || row?.product_code || '').trim()
|
||||
const color = String(row?.image_color_code || row?.color_code || '').trim()
|
||||
const yaka = String(row?.image_yaka_kodu || row?.yaka_kodu || '').trim()
|
||||
@@ -3365,6 +3493,7 @@ function resolveProductImageUrl (item) {
|
||||
}
|
||||
|
||||
async function fetchProductImagesForRow (row) {
|
||||
if (!hasProductVariantImageKey(row)) return []
|
||||
const key = productImageKey(row)
|
||||
if (Object.prototype.hasOwnProperty.call(imageListByKey.value, key)) {
|
||||
return imageListByKey.value[key]
|
||||
@@ -3406,6 +3535,7 @@ async function fetchProductImagesForRow (row) {
|
||||
}
|
||||
|
||||
function getCachedProductImageUrl (row) {
|
||||
if (!canShowProductImage(row)) return ''
|
||||
const key = productImageKey(row)
|
||||
return imageUrlByKey.value[key] || performanceStore.imageUrl(key) || ''
|
||||
}
|
||||
@@ -3419,12 +3549,14 @@ function markProductImageFailed (row) {
|
||||
}
|
||||
|
||||
async function primeProductImages (sourceRows) {
|
||||
if (activeTab.value !== 'products') return
|
||||
imagePrimeActiveRequests += 1
|
||||
imagePrimeLoading.value = true
|
||||
const items = []
|
||||
const seen = new Set()
|
||||
try {
|
||||
for (const row of sourceRows || []) {
|
||||
if (!canShowProductImage(row)) continue
|
||||
const key = productImageKey(row)
|
||||
const code = String(row?.image_product_code || row?.product_code || '').trim()
|
||||
if (!key || !code || seen.has(key) || Object.prototype.hasOwnProperty.call(imageListByKey.value, key) || performanceStore.hasImageKey(key)) continue
|
||||
@@ -3582,13 +3714,14 @@ async function loadBackendGroupedRows () {
|
||||
try {
|
||||
const startedAt = performance.now()
|
||||
const rows = await performanceStore.fetchGroupedRows(params)
|
||||
const normalizedRows = Array.isArray(rows) ? rows.map(row => normalizeGroupedDisplayRow(tabKey, row)) : []
|
||||
backendGroupedRows.value = {
|
||||
...backendGroupedRows.value,
|
||||
[tabKey]: Array.isArray(rows) ? rows : []
|
||||
[tabKey]: normalizedRows
|
||||
}
|
||||
console.info('[ProductPerformance][ui] grouped applied', {
|
||||
tab: tabKey,
|
||||
rows: Array.isArray(rows) ? rows.length : 0,
|
||||
rows: normalizedRows.length,
|
||||
elapsedSec: Number(((performance.now() - startedAt) / 1000).toFixed(2))
|
||||
})
|
||||
} catch (err) {
|
||||
@@ -3598,6 +3731,20 @@ async function loadBackendGroupedRows () {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeGroupedDisplayRow (tabKey, row) {
|
||||
if (tabKey === 'order_product_customers' || tabKey === 'order_market_details') {
|
||||
return withOrderPeriodScores({ ...row })
|
||||
}
|
||||
const next = withPeriodScores({ ...row })
|
||||
if (tabKey !== 'products' && tabKey !== 'idle') {
|
||||
next.customer_score_90d = Number(next.customer_score_90d || customerSalesPeriodScore(periodMetricSource(next, '90d')))
|
||||
next.customer_score_180d = Number(next.customer_score_180d || customerSalesPeriodScore(periodMetricSource(next, '180d')))
|
||||
next.customer_score_365d = Number(next.customer_score_365d || customerSalesPeriodScore(periodMetricSource(next, '365d')))
|
||||
next.customer_score_total = Number(next.customer_score_total || customerSalesPeriodScore(periodMetricSource(next, 'total')))
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
function responseRowCount (data) {
|
||||
if (Array.isArray(data)) return data.length
|
||||
if (Array.isArray(data?.rows)) return data.rows.length
|
||||
@@ -3687,7 +3834,6 @@ async function reload () {
|
||||
await loadOrderMarketDetails(false)
|
||||
}
|
||||
await loadBackendGroupedRows()
|
||||
await primeProductImages(rows.value)
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Ürün performans verisi alınamadı' })
|
||||
} finally {
|
||||
@@ -3701,7 +3847,6 @@ async function loadOrderProductCustomers (showLoading = true) {
|
||||
const resp = await timedProductPerformanceGet('orders:product-customers', '/pricing/product-performance/orders/product-customers', { params: { limit: 500 }, timeout: 90000 })
|
||||
orderProductCustomerRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeOrderProductCustomerRow)
|
||||
orderProductCustomersLoaded.value = true
|
||||
await primeProductImages(orderProductCustomerRows.value)
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Ürün müşteri sipariş analizi alınamadı' })
|
||||
} finally {
|
||||
@@ -3734,7 +3879,6 @@ async function loadOrderAnalysis (showLoading = true) {
|
||||
orderMarketRows.value = (Array.isArray(marketResp?.data) ? marketResp.data : []).map(normalizeOrderGroupRow)
|
||||
orderCustomerRows.value = (Array.isArray(customerResp?.data) ? customerResp.data : []).map(normalizeOrderGroupRow)
|
||||
orderAnalysisLoaded.value = true
|
||||
await primeProductImages(orderAnalysisRows.value)
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Sipariş analiz verisi alınamadı' })
|
||||
} finally {
|
||||
@@ -4051,18 +4195,18 @@ onMounted(reload)
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(-n+11)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(-n+11)) {
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(-n+10)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(-n+10)) {
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(-n+11)) {
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(-n+10)) {
|
||||
z-index: 4;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table tbody tr:not(.group-row) td:nth-child(-n+11)) {
|
||||
.product-breakdown-table :deep(.q-table tbody tr:not(.group-row) td:nth-child(-n+10)) {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@@ -4125,30 +4269,22 @@ onMounted(reload)
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(8)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(8)) {
|
||||
left: 922px;
|
||||
width: 180px;
|
||||
min-width: 180px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(9)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(9)) {
|
||||
left: 1102px;
|
||||
width: 90px;
|
||||
min-width: 90px;
|
||||
max-width: 90px;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(10)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(10)) {
|
||||
left: 1192px;
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(9)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(9)) {
|
||||
left: 1012px;
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
max-width: 80px;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(11)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(11)) {
|
||||
left: 1272px;
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(10)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(10)) {
|
||||
left: 1092px;
|
||||
width: 130px;
|
||||
min-width: 130px;
|
||||
max-width: 130px;
|
||||
@@ -4177,8 +4313,8 @@ onMounted(reload)
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(n+12):not(.text-right)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(n+12):not(.text-right)),
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(n+11):not(.text-right)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(n+11):not(.text-right)),
|
||||
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(n+4):not(.text-right)),
|
||||
.sticky-dim-table.sticky-dim-3 :deep(.q-table td:nth-child(n+4):not(.text-right)),
|
||||
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(n+6):not(.text-right)),
|
||||
|
||||
Reference in New Issue
Block a user