product performance grouped kpi improvements

This commit is contained in:
M_Kececi
2026-07-03 01:53:19 +03:00
parent be6feb8aa8
commit 98e3587608
6 changed files with 794 additions and 126 deletions
+285 -75
View File
@@ -201,7 +201,7 @@
flat
bordered
row-key="row_key"
class="performance-table sticky-dim-table sticky-dim-10 bg-white"
class="performance-table sticky-dim-table sticky-dim-8 bg-white"
:rows="displayOrderProductCustomerTableRows"
:columns="orderProductCustomerColumns"
:loading="loading || backendGroupedLoading"
@@ -676,7 +676,7 @@
flat
bordered
row-key="row_key"
class="performance-table sticky-dim-table sticky-dim-6 bg-white"
class="performance-table sticky-dim-table sticky-dim-11 bg-white"
:rows="displayIdleTableRows"
:columns="idleColumns"
:loading="loading || backendGroupedLoading"
@@ -1515,14 +1515,14 @@ const columnFilters = reactive({})
const columnFilterSearch = reactive({})
const expandedGroups = ref({})
const selectedExpandLevelKeysByTab = reactive({
products: ['kategori', 'askili_yan', 'urun_ilk_grubu', 'urun_ana_grubu', 'urun_alt_grubu', 'market_key'],
sales_color_yaka_market_customer: ['color_code', 'yaka_kodu', 'market_key', 'customer_code'],
idle: ['kategori', 'askili_yan', 'urun_ilk_grubu', 'urun_ana_grubu', 'urun_alt_grubu', 'product_code'],
sales_product_country_segment_market_customer: ['product_code', 'country', 'customer_segment', 'market_key', 'customer_code'],
sales_market_customer_product: ['market_key', 'customer_code', 'product_code', 'color_code', 'yaka_kodu'],
sales_country_segment_market_customer_product: ['country', 'customer_segment', 'market_key', 'customer_code', 'product_code', 'color_code', 'yaka_kodu'],
order_product_customers: ['product_code', 'market_key', 'customer_code'],
order_market_details: ['market_key', 'customer_code', 'product_code', 'color_code', 'yaka_kodu']
products: ['kategori', 'askili_yan', 'urun_ilk_grubu', 'urun_ana_grubu', 'urun_alt_grubu', 'product_code', 'item_description', 'color_code', 'yaka_kodu', 'market_key'],
sales_color_yaka_market_customer: ['color_code', 'yaka_kodu', 'askili_yan', 'urun_ilk_grubu', 'urun_ana_grubu', 'urun_alt_grubu', 'product_code', 'item_description', 'country', 'market_key', 'customer_segment', 'customer_code', 'customer_name'],
idle: ['askili_yan', 'urun_ilk_grubu', 'urun_ana_grubu', 'urun_alt_grubu', 'product_code', 'item_description', 'color_code', 'yaka_kodu'],
sales_product_country_segment_market_customer: ['market_key', 'customer_code', 'customer_name', 'kategori', 'askili_yan', 'urun_ilk_grubu', 'urun_ana_grubu', 'urun_alt_grubu', 'product_code', 'item_description', 'color_code', 'yaka_kodu'],
sales_market_customer_product: ['market_key', 'customer_code', 'customer_name', 'kategori', 'askili_yan', 'urun_ilk_grubu', 'urun_ana_grubu', 'urun_alt_grubu', 'product_code', 'item_description', 'color_code', 'yaka_kodu'],
sales_country_segment_market_customer_product: ['market_key', 'customer_code', 'customer_name', 'kategori', 'askili_yan', 'urun_ilk_grubu', 'urun_ana_grubu', 'urun_alt_grubu', 'product_code', 'item_description', 'color_code', 'yaka_kodu'],
order_product_customers: ['market_key', 'customer_code', 'customer_name', 'kategori', 'askili_yan', 'urun_ilk_grubu', 'urun_ana_grubu', 'urun_alt_grubu', 'product_code', 'item_description', 'color_code', 'yaka_kodu'],
order_market_details: ['market_key', 'customer_code', 'customer_name', 'kategori', 'askili_yan', 'urun_ilk_grubu', 'urun_ana_grubu', 'urun_alt_grubu', 'product_code', 'item_description', 'color_code', 'yaka_kodu']
})
const productKpiFetchLimit = 50000
const performanceTabs = [
@@ -1530,7 +1530,6 @@ const performanceTabs = [
{ name: 'sales_color_yaka_market_customer', icon: 'palette', label: 'Renk > Yaka > Piyasa > Müşteri' },
{ name: 'idle', icon: 'warning', label: 'Atıl Stok / Maliyet' },
{ name: 'sales_product_country_segment_market_customer', icon: 'account_tree', label: 'Ürün > Ülke > Segment > Piyasa > Müşteri' },
{ name: 'sales_market_customer_product', icon: 'groups', label: 'Piyasa > Müşteri > Ürün Satış KPI' },
{ name: 'sales_country_segment_market_customer_product', icon: 'public', label: 'Ülke > Segment > Piyasa > Müşteri > Ürün Satış KPI' },
{ name: 'order_product_customers', icon: 'assignment_ind', label: 'Ürün > Piyasa > Müşteri Sipariş' },
{ name: 'order_market_details', icon: 'receipt_long', label: 'Piyasa > Müşteri > Ürün Sipariş' }
@@ -1571,7 +1570,6 @@ const customerBreakdownOptions = [
const salesBreakdownModes = {
sales_color_yaka_market_customer: 'color_yaka_market_customer',
sales_product_country_segment_market_customer: 'product_country_segment_market_customer',
sales_market_customer_product: 'market_customer_product',
sales_country_segment_market_customer_product: 'country_segment_market_customer_product'
}
@@ -1596,15 +1594,15 @@ const detailSalesColumns = [
const columns = [
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left' },
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left' },
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left' },
{ name: 'kategori', label: 'Kategori', field: 'kategori', align: 'left' },
{ name: 'askili_yan', label: 'Askılı/Yan', field: 'askili_yan', align: 'left' },
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left' },
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => row.urun_ana_grubu || row.seri || '', align: 'left' },
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left' },
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left' },
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left' },
{ 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: 'sales_qty_90d', label: '90G Satış', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
@@ -1773,14 +1771,19 @@ const orderGroupColumns = [
]
const orderProductCustomerColumns = [
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left', sortable: true },
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left', sortable: true },
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left', sortable: true },
{ name: 'customer_code', label: 'Müşteri Kodu', field: 'customer_code', align: 'left', sortable: true },
{ name: 'customer_name', label: 'Müşteri', field: 'customer_name', align: 'left', sortable: true },
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
{ name: 'kategori', label: 'Kategori', field: 'kategori', align: 'left', sortable: true },
{ name: 'askili_yan', label: 'Askılı/Yan', field: 'askili_yan', align: 'left', sortable: true },
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left', sortable: true },
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => row.urun_ana_grubu || row.seri || '', align: 'left', sortable: true },
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left', sortable: true },
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left', sortable: true },
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left', sortable: true },
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left', sortable: true },
{ name: 'order_qty', label: 'Açık Sipariş', field: row => formatNumber(row.order_qty, 0), align: 'right', sortable: true },
{ name: 'order_usd', label: 'Sipariş USD', field: row => formatMoney(row.order_usd, 'USD'), align: 'right', sortable: true },
{ name: 'avg_order_price_usd', label: 'Ort. USD', field: row => formatMoney(row.avg_order_price_usd, 'USD'), align: 'right', sortable: true },
@@ -1803,13 +1806,18 @@ const orderMarketDetailColumns = [
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left', sortable: true },
{ name: 'customer_code', label: 'Müşteri Kodu', field: 'customer_code', align: 'left', sortable: true },
{ name: 'customer_name', label: 'Müşteri', field: 'customer_name', align: 'left', sortable: true },
{ name: 'kategori', label: 'Kategori', field: 'kategori', align: 'left', sortable: true },
{ name: 'askili_yan', label: 'Askılı/Yan', field: 'askili_yan', align: 'left', sortable: true },
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left', sortable: true },
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => row.urun_ana_grubu || row.seri || '', align: 'left', sortable: true },
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left', sortable: true },
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left', sortable: true },
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left', sortable: true },
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left', sortable: true },
{ name: 'order_number', label: 'Sipariş No', field: 'order_number', align: 'left', sortable: true },
{ name: 'order_date', label: 'Sipariş Tarihi', field: 'order_date', align: 'left', sortable: true },
{ name: 'due_date', label: 'Termin', field: 'due_date', align: 'left', sortable: true },
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left', sortable: true },
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left', sortable: true },
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left', sortable: true },
{ name: 'order_qty', label: 'Adet', field: row => formatNumber(row.order_qty, 0), align: 'right', sortable: true },
{ name: 'order_usd', label: 'USD', field: row => formatMoney(row.order_usd, 'USD'), align: 'right', sortable: true },
{ name: 'avg_order_price_usd', label: 'Ort. USD', field: row => formatMoney(row.avg_order_price_usd, 'USD'), align: 'right', sortable: true },
@@ -1826,15 +1834,16 @@ const orderMarketDetailColumns = [
const idleColumns = [
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left' },
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left' },
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left' },
{ name: 'kategori', label: 'Kategori', field: 'kategori', align: 'left' },
{ name: 'askili_yan', label: 'Askılı/Yan', field: 'askili_yan', align: 'left' },
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left' },
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => row.urun_ana_grubu || row.seri || '', align: 'left' },
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left' },
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left' },
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left' },
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left' },
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left' },
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
{ name: 'cost_price_usd', label: 'Maliyet USD', field: row => formatMoney(row.cost_price_usd, 'USD'), align: 'right', sortable: true },
{ name: 'idle_cost_usd', label: 'Stok Maliyeti USD', field: 'idle_cost_usd', align: 'right', sortable: true },
@@ -1888,6 +1897,7 @@ const customerColumns = [
{ name: 'sales_qty_90d', label: '90G Adet', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_90d', label: '90G USD', field: 'sales_usd_90d', align: 'right', sortable: true },
{ name: 'avg_price_usd_90d', label: 'Ort. USD', field: 'avg_price_usd_90d', align: 'right', sortable: true },
{ name: 'customer_score_90d', label: '90G Müşteri Skor', field: row => formatNumber(row.customer_score_90d, 1), align: 'right', sortable: true },
{ name: 'base_price_usd_90d', label: '90G Taban', field: row => formatMoney(row.base_price_usd_90d, 'USD'), align: 'right', sortable: true },
{ name: 'cost_price_usd_90d', label: '90G Çıplak', field: row => formatMoney(row.cost_price_usd_90d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_base_usd_90d', label: '90G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_90d, 'USD'), align: 'right', sortable: true },
@@ -1901,52 +1911,65 @@ const customerColumns = [
]
const salesBreakdownColumns = [
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left', sortable: true },
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left', sortable: true },
{ name: 'askili_yan', label: 'Askılı/Yan', field: 'askili_yan', align: 'left', sortable: true },
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left', sortable: true },
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => row.urun_ana_grubu || row.seri || '', align: 'left', sortable: true },
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left', sortable: true },
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left', sortable: true },
{ name: 'country', label: 'Ülke', field: 'country', align: 'left', sortable: true },
{ name: 'customer_segment', label: 'Segment', field: 'customer_segment', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left', sortable: true },
{ name: 'customer_segment', label: 'Segment', field: 'customer_segment', align: 'left', sortable: true },
{ name: 'customer_code', label: 'Müşteri Kodu', field: 'customer_code', align: 'left', sortable: true },
{ name: 'customer_name', label: 'Müşteri', field: 'customer_name', align: 'left', sortable: true },
{ name: 'kategori', label: 'Kategori', field: 'kategori', align: 'left', sortable: true },
{ name: 'product_count', label: 'Ürün Sayısı', field: row => formatNumber(row.product_count, 0), align: 'right', sortable: true },
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
{ name: 'customer_count_90d', label: '90G Müşteri', field: row => formatNumber(row.customer_count_90d, 0), align: 'right', sortable: true },
{ name: 'invoice_count_90d', label: '90G Fatura', field: row => formatNumber(row.invoice_count_90d, 0), align: 'right', sortable: true },
{ name: 'sales_qty_90d', label: '90G Adet', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_90d', label: '90G USD', field: 'sales_usd_90d', align: 'right', sortable: true },
{ name: 'avg_price_usd_90d', label: 'Ort. USD', field: 'avg_price_usd_90d', 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: 'customer_count_180d', label: '180G Müşteri', field: row => formatNumber(row.customer_count_180d, 0), align: 'right', sortable: true },
{ name: 'invoice_count_180d', label: '180G Fatura', field: row => formatNumber(row.invoice_count_180d, 0), align: 'right', sortable: true },
{ name: 'sales_qty_180d', label: '180G Adet', field: row => formatNumber(row.sales_qty_180d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_180d', label: '180G USD', field: row => formatMoney(row.sales_usd_180d, 'USD'), align: 'right', sortable: true },
{ name: 'avg_price_usd_180d', label: '180G Ort. USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), 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: 'base_price_usd_180d', label: '180G Taban', field: row => formatMoney(row.base_price_usd_180d, 'USD'), align: 'right', sortable: true },
{ name: 'cost_price_usd_180d', label: '180G Çıplak', field: row => formatMoney(row.cost_price_usd_180d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_base_usd_180d', label: '180G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_180d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_cost_usd_180d', label: '180G Çıplak K/Z', field: row => formatMoney(row.gross_profit_cost_usd_180d, 'USD'), 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 },
{ name: 'gross_margin_cost_180d', label: '180G Çıplak Marj', field: row => formatPercent(row.gross_margin_cost_180d), 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: 'sales_qty_365d', label: '360G Adet', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_365d', label: '360G USD', field: row => formatMoney(row.sales_usd_365d, 'USD'), align: 'right', sortable: true },
{ name: 'avg_price_usd_365d', label: '360G Ort. USD', field: row => formatMoney(row.avg_price_usd_365d, 'USD'), 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: 'base_price_usd_365d', label: '360G Taban', field: row => formatMoney(row.base_price_usd_365d, 'USD'), align: 'right', sortable: true },
{ name: 'cost_price_usd_365d', label: '360G Çıplak', field: row => formatMoney(row.cost_price_usd_365d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_base_usd_365d', label: '360G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_365d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_cost_usd_365d', label: '360G Çıplak K/Z', field: row => formatMoney(row.gross_profit_cost_usd_365d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_margin_base_365d', label: '360G Taban Marj', field: row => formatPercent(row.gross_margin_base_365d), align: 'right', sortable: true },
{ name: 'gross_margin_cost_365d', label: '360G Çıplak Marj', field: row => formatPercent(row.gross_margin_cost_365d), 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_count_total', label: 'Genel Müşteri', field: row => formatNumber(row.customer_count_total, 0), align: 'right', sortable: true },
{ name: 'invoice_count_total', label: 'Genel Fatura', field: row => formatNumber(row.invoice_count_total, 0), align: 'right', sortable: true },
{ name: 'sales_qty_total', label: 'Genel Adet', field: row => formatNumber(row.sales_qty_total, 0), align: 'right', sortable: true },
{ name: 'sales_usd_total', label: 'Genel USD', field: row => formatMoney(row.sales_usd_total, 'USD'), 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: 'stock_turnover_total', label: 'Genel Stok Devir', field: row => formatNumber(row.stock_turnover_total, 2), align: 'right', sortable: true },
{ name: 'base_price_usd_total', label: 'Genel Taban', field: row => formatMoney(row.base_price_usd_total, 'USD'), align: 'right', sortable: true },
{ name: 'cost_price_usd_total', label: 'Genel Çıplak', field: row => formatMoney(row.cost_price_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_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: 'customer_score_total', label: 'Genel Müşteri Skor', field: row => formatNumber(row.customer_score_total, 1), align: 'right', sortable: true },
{ name: 'sales_index_90d', label: 'Endeks', field: row => formatNumber(row.sales_index_90d, 2), align: 'right', sortable: true },
{ name: 'performance_score', label: 'Skor', field: row => formatNumber(row.performance_score, 1), align: 'right', sortable: true },
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true },
@@ -2076,8 +2099,11 @@ const groupLevels = [
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', fallback: 'yas_grubu' },
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu', fallback: 'seri' },
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
{ key: 'market_key', label: 'Piyasa' },
{ key: 'product_code', label: 'Ürün Kodu' }
{ 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' }
]
const tabGroupLevels = {
@@ -2085,49 +2111,95 @@ const tabGroupLevels = {
sales_color_yaka_market_customer: [
{ key: 'color_code', label: 'Renk' },
{ key: 'yaka_kodu', label: 'Yaka' },
{ key: 'askili_yan', label: 'Askılı/Yan' },
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', fallback: 'yas_grubu' },
{ 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_code', label: 'Müşteri' }
{ key: 'customer_segment', label: 'Segment' },
{ key: 'customer_code', label: 'Müşteri Kodu' },
{ key: 'customer_name', label: 'Müşteri' }
],
idle: [
{ key: 'askili_yan', label: 'Askılı/Yan' },
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', fallback: 'yas_grubu' },
{ 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' }
],
sales_product_country_segment_market_customer: [
{ key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri Kodu' },
{ key: 'customer_name', label: 'Müşteri' },
{ key: 'kategori', label: 'Kategori' },
{ key: 'askili_yan', label: 'Askılı/Yan' },
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', fallback: 'yas_grubu' },
{ 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 Kodu' }
],
sales_product_country_segment_market_customer: [
{ key: 'product_code', label: 'Ürün Kodu' },
{ key: 'country', label: 'Ülke' },
{ key: 'customer_segment', label: 'Segment' },
{ key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri' }
{ key: 'product_code', label: 'Ürün' },
{ key: 'item_description', label: 'Açıklama' },
{ key: 'color_code', label: 'Renk' },
{ key: 'yaka_kodu', label: 'Yaka' }
],
sales_market_customer_product: [
{ key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri' },
{ key: 'product_code', label: 'Ürün Kodu' },
{ key: 'customer_code', label: 'Müşteri Kodu' },
{ key: 'customer_name', label: 'Müşteri' },
{ key: 'kategori', label: 'Kategori' },
{ key: 'askili_yan', label: 'Askılı/Yan' },
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', fallback: 'yas_grubu' },
{ 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' }
],
sales_country_segment_market_customer_product: [
{ key: 'country', label: 'Ülke' },
{ key: 'customer_segment', label: 'Segment' },
{ key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri' },
{ key: 'product_code', label: 'Ürün Kodu' },
{ key: 'customer_code', label: 'Müşteri Kodu' },
{ key: 'customer_name', label: 'Müşteri' },
{ key: 'kategori', label: 'Kategori' },
{ key: 'askili_yan', label: 'Askılı/Yan' },
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', fallback: 'yas_grubu' },
{ 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' }
],
order_product_customers: [
{ key: 'product_code', label: 'Ürün Kodu' },
{ key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri' }
{ key: 'customer_code', label: 'Müşteri Kodu' },
{ key: 'customer_name', label: 'Müşteri' },
{ key: 'kategori', label: 'Kategori' },
{ key: 'askili_yan', label: 'Askılı/Yan' },
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', fallback: 'yas_grubu' },
{ 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' }
],
order_market_details: [
{ key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri' },
{ key: 'product_code', label: 'Ürün Kodu' },
{ key: 'customer_code', label: 'Müşteri Kodu' },
{ key: 'customer_name', label: 'Müşteri' },
{ key: 'kategori', label: 'Kategori' },
{ key: 'askili_yan', label: 'Askılı/Yan' },
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', fallback: 'yas_grubu' },
{ 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' }
]
@@ -2321,6 +2393,7 @@ function sortProductLeafRows (sourceRows) {
function makeGroupRow (key, level, groupDef, value, groupRows) {
const imageSource = groupRows.find(row => row?.product_code) || {}
const aggregate = aggregateGroupFields(groupRows, groupDef.key)
return {
__group: true,
row_key: `group|${key}`,
@@ -2343,6 +2416,10 @@ function makeGroupRow (key, level, groupDef, value, groupRows) {
urun_ana_grubu: groupDef.key === 'urun_ana_grubu' ? value : distinctSummary(groupRows, 'urun_ana_grubu', 'seri'),
urun_alt_grubu: groupDef.key === 'urun_alt_grubu' ? value : distinctSummary(groupRows, 'urun_alt_grubu'),
market_key: groupDef.key === 'market_key' ? value : distinctSummary(groupRows, 'market_key'),
country: groupDef.key === 'country' ? value : distinctSummary(groupRows, 'country'),
customer_segment: groupDef.key === 'customer_segment' ? value : distinctSummary(groupRows, 'customer_segment'),
customer_code: groupDef.key === 'customer_code' ? value : distinctSummary(groupRows, 'customer_code'),
customer_name: groupDef.key === 'customer_name' ? value : distinctSummary(groupRows, 'customer_name'),
stock_qty: sumRows(groupRows, 'stock_qty'),
sales_qty_90d: sumRows(groupRows, 'sales_qty_90d'),
sales_qty_180d: sumRows(groupRows, 'sales_qty_180d'),
@@ -2384,14 +2461,14 @@ function makeGroupRow (key, level, groupDef, value, groupRows) {
stock_days_total: weightedAverageOrAverage(groupRows, 'stock_days_total', 'stock_qty'),
sales_index_90d: averageRows(groupRows, 'sales_index_90d'),
sales_index_total: averageRows(groupRows, 'sales_index_total'),
performance_score: weightedAverageOrAverage(groupRows, 'performance_score', 'sales_qty_90d'),
performance_bucket: dominantValue(groupRows, 'performance_bucket'),
...aggregateGroupFields(groupRows),
...aggregate,
performance_score: groupPerformanceScore(aggregate, groupDef.key),
recommendation: `${formatNumber(groupRows.length, 0)} satır`
}
}
function aggregateGroupFields (sourceRows) {
function aggregateGroupFields (sourceRows, groupField = '') {
const out = {}
const fields = new Set()
for (const row of sourceRows) {
@@ -2407,7 +2484,7 @@ function aggregateGroupFields (sourceRows) {
}
}
applyDerivedGroupMetrics(out, sourceRows)
applyDerivedGroupMetrics(out, sourceRows, groupField)
return out
}
@@ -2429,7 +2506,7 @@ function weightFieldForMetric (field) {
return 'sales_qty_90d'
}
function applyDerivedGroupMetrics (out, sourceRows) {
function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
for (const suffix of ['90d', '180d', '365d', 'total']) {
const sales = Number(out[`sales_usd_${suffix}`] || 0)
const qty = Number(out[`sales_qty_${suffix}`] || 0)
@@ -2459,6 +2536,82 @@ function applyDerivedGroupMetrics (out, sourceRows) {
if (sourceRows.some(row => row.performance_bucket)) {
out.performance_bucket = dominantValue(sourceRows, 'performance_bucket')
}
out.customer_score_90d = customerSalesPeriodScore(periodMetricSource(out, '90d'))
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 = groupPerformanceScore(out, groupField)
}
function groupPerformanceScore (row, groupField = '') {
if (Number(row?.order_qty || 0) > 0 || Number(row?.order_usd || 0) > 0) return orderGroupPerformanceScore(row)
if (['customer_code', 'customer_name'].includes(groupField)) return customerSalesGroupPerformanceScore(row)
return salesGroupPerformanceScore(row)
}
function salesGroupPerformanceScore (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)
const qty = Number(row?.sales_qty_90d || 0)
let stockTurnover = Number(row?.stock_turnover_90d || 0)
const stockQty = Number(row?.stock_qty || 0)
if (!stockTurnover && stockQty > 0) stockTurnover = qty / stockQty
return Math.min(35, Math.max(0, salesIndex) * 18) +
Math.min(30, Math.max(0, margin) * 60) +
Math.min(15, invoices * 1.5) +
Math.min(10, qty / 10) +
Math.min(10, Math.max(0, stockTurnover) * 5)
}
function customerSalesGroupPerformanceScore (row) {
const periodToScore = {
90: Number(row?.customer_score_90d || 0),
180: Number(row?.customer_score_180d || 0),
360: Number(row?.customer_score_365d || 0),
total: Number(row?.customer_score_total || 0)
}
const selected = selectedPeriods.value.length ? selectedPeriods.value : ['90', '180', '360', 'total']
const scores = selected.map(period => periodToScore[period]).filter(score => Number.isFinite(score) && score > 0)
if (!scores.length) return 0
return scores.reduce((sum, score) => sum + score, 0) / scores.length
}
function customerSalesPeriodScore (row) {
const salesUSD = Number(row?.sales_usd || 0)
const margin = Number(row?.gross_margin_cost ?? row?.gross_margin ?? 0)
const invoices = Number(row?.invoice_count || 0)
const qty = Number(row?.sales_qty || 0)
const productCount = Number(row?.product_count || 0)
return Math.min(35, salesUSD / 1000) +
Math.min(25, Math.max(0, margin) * 55) +
Math.min(20, invoices * 2) +
Math.min(10, qty / 10) +
Math.min(10, productCount)
}
function periodMetricSource (row, suffix) {
return {
sales_usd: Number(row?.[`sales_usd_${suffix}`] || 0),
gross_margin_cost: Number(row?.[`gross_margin_cost_${suffix}`] ?? 0),
gross_margin: Number(row?.[`gross_margin_${suffix}`] ?? 0),
invoice_count: Number(row?.[`invoice_count_${suffix}`] || 0),
sales_qty: Number(row?.[`sales_qty_${suffix}`] || 0),
product_count: Number(row?.product_count || 0)
}
}
function orderGroupPerformanceScore (row) {
const orderUSD = Number(row?.order_usd || 0)
const margin = Number(row?.expected_margin_cost || 0)
const orders = Number(row?.order_count || 0)
const qty = Number(row?.order_qty || 0)
const stockBonus = Number(row?.net_stock_after_order || 0) >= 0 ? 10 : 0
return Math.min(35, orderUSD / 1000) +
Math.min(25, Math.max(0, margin) * 60) +
Math.min(15, orders * 2) +
Math.min(15, qty / 10) +
stockBonus
}
function sumRows (sourceRows, field) {
@@ -2601,7 +2754,7 @@ 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', 'performance_score'].includes(name)
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)
? 'text-right'
: ''
}
@@ -2954,8 +3107,13 @@ function normalizeOrderMarketDetailRow (row) {
}
function normalizeSalesBreakdownRow (row) {
const next = { ...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')))
next.customer_score_total = Number(next.customer_score_total || customerSalesPeriodScore(periodMetricSource(next, 'total')))
return {
...row,
...next,
row_key: `sales-breakdown|${row?.breakdown || ''}|${row?.product_code || ''}|${row?.color_code || ''}|${row?.yaka_kodu || ''}|${row?.country || ''}|${row?.customer_segment || ''}|${row?.market_key || ''}|${row?.customer_code || ''}`
}
}
@@ -3165,9 +3323,13 @@ function backendGroupedSupportedTab (tabKey = activeTab.value) {
}
function activeExpandedGroupKeys () {
const tabPrefix = `tab:${activeTab.value}|`
const validFields = new Set(activeGroupLevels.value.map(level => level.key))
return Object.entries(expandedGroups.value)
.filter(([, value]) => value === true)
.map(([key]) => key)
.filter(key => key.startsWith(tabPrefix))
.filter(key => key.split('|').slice(1).every(part => validFields.has(String(part).split(':')[0])))
}
function scheduleLoadBackendGroupedRows () {
@@ -3195,11 +3357,17 @@ async function loadBackendGroupedRows () {
limit: tabKey === 'products' || tabKey === 'idle' ? productKpiFetchLimit : 50000
}
try {
const startedAt = performance.now()
const rows = await performanceStore.fetchGroupedRows(params)
backendGroupedRows.value = {
...backendGroupedRows.value,
[tabKey]: Array.isArray(rows) ? rows : []
}
console.info('[ProductPerformance][ui] grouped applied', {
tab: tabKey,
rows: Array.isArray(rows) ? rows.length : 0,
elapsedSec: Number(((performance.now() - startedAt) / 1000).toFixed(2))
})
} catch (err) {
console.warn('product performance grouped rows failed', err)
} finally {
@@ -3207,18 +3375,50 @@ async function loadBackendGroupedRows () {
}
}
function responseRowCount (data) {
if (Array.isArray(data)) return data.length
if (Array.isArray(data?.rows)) return data.rows.length
if (data && typeof data === 'object') return Object.keys(data).length
return 0
}
async function timedProductPerformanceGet (label, url, config = {}) {
const startedAt = performance.now()
console.info('[ProductPerformance][api] start', {
label,
url,
params: config?.params || {}
})
try {
const resp = await api.get(url, config)
console.info('[ProductPerformance][api] done', {
label,
rows: responseRowCount(resp?.data),
elapsedSec: Number(((performance.now() - startedAt) / 1000).toFixed(2))
})
return resp
} catch (err) {
console.warn('[ProductPerformance][api] failed', {
label,
elapsedSec: Number(((performance.now() - startedAt) / 1000).toFixed(2)),
message: err?.response?.data || err?.message || err
})
throw err
}
}
async function reload () {
loading.value = true
try {
const salesBreakdownRequests = salesBreakdownTabKeys.map(tabKey => {
return api.get('/pricing/product-performance/sales-breakdown', {
return timedProductPerformanceGet(`sales-breakdown:${tabKey}`, '/pricing/product-performance/sales-breakdown', {
params: { limit: 800, mode: salesBreakdownModes[tabKey] },
timeout: 60000
})
})
const [summaryResp, listResp, generalResp, marketResp, countryResp, customerResp, ...salesBreakdownResp] = await Promise.all([
api.get('/pricing/product-performance/summary', { timeout: 60000 }),
api.get('/pricing/product-performance', {
timedProductPerformanceGet('summary', '/pricing/product-performance/summary', { timeout: 60000 }),
timedProductPerformanceGet('product-kpi', '/pricing/product-performance', {
params: {
page: 1,
limit: productKpiFetchLimit,
@@ -3227,10 +3427,10 @@ async function reload () {
},
timeout: 60000
}),
api.get('/pricing/product-performance/general', { params: { limit: 500 }, timeout: 60000 }),
api.get('/pricing/product-performance/markets', { params: { limit: 500 }, timeout: 60000 }),
api.get('/pricing/product-performance/countries', { params: { limit: 500 }, timeout: 60000 }),
api.get('/pricing/product-performance/customers', {
timedProductPerformanceGet('general', '/pricing/product-performance/general', { params: { limit: 500 }, timeout: 60000 }),
timedProductPerformanceGet('markets', '/pricing/product-performance/markets', { params: { limit: 500 }, timeout: 60000 }),
timedProductPerformanceGet('countries', '/pricing/product-performance/countries', { params: { limit: 500 }, timeout: 60000 }),
timedProductPerformanceGet('customers', '/pricing/product-performance/customers', {
params: { limit: 500, breakdown: customerBreakdown.value },
timeout: 60000
}),
@@ -3275,7 +3475,7 @@ async function reload () {
async function loadOrderProductCustomers (showLoading = true) {
if (showLoading) loading.value = true
try {
const resp = await api.get('/pricing/product-performance/orders/product-customers', { params: { limit: 500 }, timeout: 90000 })
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)
@@ -3289,7 +3489,7 @@ async function loadOrderProductCustomers (showLoading = true) {
async function loadOrderMarketDetails (showLoading = true) {
if (showLoading) loading.value = true
try {
const resp = await api.get('/pricing/product-performance/orders/market-details', { params: { limit: 800 }, timeout: 90000 })
const resp = await timedProductPerformanceGet('orders:market-details', '/pricing/product-performance/orders/market-details', { params: { limit: 800 }, timeout: 90000 })
orderMarketDetailRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeOrderMarketDetailRow)
orderMarketDetailsLoaded.value = true
} catch (err) {
@@ -3303,9 +3503,9 @@ async function loadOrderAnalysis (showLoading = true) {
if (showLoading) loading.value = true
try {
const [productResp, marketResp, customerResp] = await Promise.all([
api.get('/pricing/product-performance/orders', { params: { limit: 500 }, timeout: 90000 }),
api.get('/pricing/product-performance/orders/groups', { params: { limit: 500, breakdown: 'market' }, timeout: 90000 }),
api.get('/pricing/product-performance/orders/groups', { params: { limit: 500, breakdown: 'customer' }, timeout: 90000 })
timedProductPerformanceGet('orders:products', '/pricing/product-performance/orders', { params: { limit: 500 }, timeout: 90000 }),
timedProductPerformanceGet('orders:groups:market', '/pricing/product-performance/orders/groups', { params: { limit: 500, breakdown: 'market' }, timeout: 90000 }),
timedProductPerformanceGet('orders:groups:customer', '/pricing/product-performance/orders/groups', { params: { limit: 500, breakdown: 'customer' }, timeout: 90000 })
])
orderAnalysisRows.value = (Array.isArray(productResp?.data) ? productResp.data : []).map(normalizeOrderAnalysisRow)
orderMarketRows.value = (Array.isArray(marketResp?.data) ? marketResp.data : []).map(normalizeOrderGroupRow)
@@ -3683,6 +3883,11 @@ onMounted(reload)
left: 1170px;
}
.sticky-dim-table :deep(.q-table th:nth-child(11)),
.sticky-dim-table :deep(.q-table td:nth-child(11)) {
left: 1300px;
}
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(-n+3)),
.sticky-dim-table.sticky-dim-3 :deep(.q-table td:nth-child(-n+3)),
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(-n+5)),
@@ -3694,7 +3899,9 @@ onMounted(reload)
.sticky-dim-table.sticky-dim-9 :deep(.q-table th:nth-child(-n+9)),
.sticky-dim-table.sticky-dim-9 :deep(.q-table td:nth-child(-n+9)),
.sticky-dim-table.sticky-dim-10 :deep(.q-table th:nth-child(-n+10)),
.sticky-dim-table.sticky-dim-10 :deep(.q-table td:nth-child(-n+10)) {
.sticky-dim-table.sticky-dim-10 :deep(.q-table td:nth-child(-n+10)),
.sticky-dim-table.sticky-dim-11 :deep(.q-table th:nth-child(-n+11)),
.sticky-dim-table.sticky-dim-11 :deep(.q-table td:nth-child(-n+11)) {
position: sticky;
z-index: 2;
}
@@ -3704,12 +3911,13 @@ onMounted(reload)
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(-n+6)),
.sticky-dim-table.sticky-dim-7 :deep(.q-table th:nth-child(-n+7)),
.sticky-dim-table.sticky-dim-9 :deep(.q-table th:nth-child(-n+9)),
.sticky-dim-table.sticky-dim-10 :deep(.q-table th:nth-child(-n+10)) {
.sticky-dim-table.sticky-dim-10 :deep(.q-table th:nth-child(-n+10)),
.sticky-dim-table.sticky-dim-11 :deep(.q-table th:nth-child(-n+11)) {
z-index: 4;
background: #f8fbff;
}
.sticky-dim-table :deep(.q-table tbody td:nth-child(-n+10)) {
.sticky-dim-table :deep(.q-table tbody td:nth-child(-n+11)) {
background: #fff;
}
@@ -3724,7 +3932,9 @@ onMounted(reload)
.sticky-dim-table.sticky-dim-9 :deep(.q-table th:nth-child(9)),
.sticky-dim-table.sticky-dim-9 :deep(.q-table td:nth-child(9)),
.sticky-dim-table.sticky-dim-10 :deep(.q-table th:nth-child(10)),
.sticky-dim-table.sticky-dim-10 :deep(.q-table td:nth-child(10)) {
.sticky-dim-table.sticky-dim-10 :deep(.q-table td:nth-child(10)),
.sticky-dim-table.sticky-dim-11 :deep(.q-table th:nth-child(11)),
.sticky-dim-table.sticky-dim-11 :deep(.q-table td:nth-child(11)) {
box-shadow: 8px 0 10px -10px rgba(17, 24, 39, 0.45);
}