From 9f45160648b4e80b833cfa5ffd01b731a1b84628 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Fri, 3 Jul 2026 02:55:49 +0300 Subject: [PATCH] product performance grouped kpi improvements --- .../pages/ProductPerformanceProfitability.vue | 278 +++++++++++++----- 1 file changed, 207 insertions(+), 71 deletions(-) diff --git a/ui/src/pages/ProductPerformanceProfitability.vue b/ui/src/pages/ProductPerformanceProfitability.vue index 51f40eb..e3e8bcd 100644 --- a/ui/src/pages/ProductPerformanceProfitability.vue +++ b/ui/src/pages/ProductPerformanceProfitability.vue @@ -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)),