diff --git a/svc/routes/product_performance.go b/svc/routes/product_performance.go
index f453379..47e2f07 100644
--- a/svc/routes/product_performance.go
+++ b/svc/routes/product_performance.go
@@ -16,7 +16,7 @@ func GetProductPerformanceHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
- ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 60*time.Second)
+ ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
f := queries.ProductPerformanceFilters{
@@ -90,7 +90,7 @@ func GetProductPerformanceSummaryHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
- ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 30*time.Second)
+ ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
summary, err := queries.GetProductPerformanceSummary(ctx, pg)
@@ -106,7 +106,7 @@ func GetProductPerformanceGeneralHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
- ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 60*time.Second)
+ ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceGeneral(ctx, pg, intQuery(r, "limit", 50000))
@@ -122,7 +122,7 @@ func GetProductPerformanceOrdersHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
- ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second)
+ ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceOrderAnalysis(ctx, pg, intQuery(r, "limit", 50000))
@@ -138,7 +138,7 @@ func GetProductPerformanceOrderGroupsHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
- ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second)
+ ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceOrderGroups(
@@ -159,7 +159,7 @@ func GetProductPerformanceOrderProductCustomersHandler(pg *sql.DB) http.HandlerF
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
- ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second)
+ ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceOrderProductCustomers(ctx, pg, intQuery(r, "limit", 50000))
@@ -175,7 +175,7 @@ func GetProductPerformanceOrderMarketDetailsHandler(pg *sql.DB) http.HandlerFunc
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
- ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second)
+ ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceOrderMarketDetails(ctx, pg, intQuery(r, "limit", 50000))
@@ -191,7 +191,7 @@ func GetProductPerformanceMarketsHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
- ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 30*time.Second)
+ ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceMarkets(ctx, pg, intQuery(r, "limit", 50000))
@@ -207,7 +207,7 @@ func GetProductPerformanceCountriesHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
- ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 30*time.Second)
+ ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceCountries(ctx, pg, intQuery(r, "limit", 50000))
@@ -223,7 +223,7 @@ func GetProductPerformanceCustomersHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
- ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 30*time.Second)
+ ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceCustomers(
@@ -244,7 +244,7 @@ func GetProductPerformanceSalesBreakdownHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
- ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 45*time.Second)
+ ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceSalesBreakdown(
@@ -265,7 +265,7 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
- ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second)
+ ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
mode := strings.TrimSpace(r.URL.Query().Get("mode"))
diff --git a/ui/src/pages/ProductPerformanceProfitability.vue b/ui/src/pages/ProductPerformanceProfitability.vue
index b5d7454..9e43bb7 100644
--- a/ui/src/pages/ProductPerformanceProfitability.vue
+++ b/ui/src/pages/ProductPerformanceProfitability.vue
@@ -14,7 +14,8 @@
@wheel.stop
>
-
Yükleniyor...
+ {{ loadingMessage }}
+ {{ loadingElapsedLabel }}
@@ -113,7 +114,7 @@
:rows="filteredGeneralRows"
:columns="generalColumns"
:loading="loading"
- :pagination="{ rowsPerPage: 0, sortBy: 'performance_score_total', descending: true }"
+ v-model:pagination="tablePagination.general"
virtual-scroll
:virtual-scroll-item-size="productThumbVirtualItemSize"
:virtual-scroll-slice-size="tableVirtualSliceSize"
@@ -224,8 +225,8 @@
class="performance-table sticky-dim-table sticky-dim-8 bg-white"
:rows="displayOrderProductCustomerTableRows"
:columns="orderProductCustomerColumns"
- :loading="loading || backendGroupedLoading"
- :pagination="{ rowsPerPage: 0, sortBy: 'order_usd', descending: true }"
+ :loading="loading"
+ v-model:pagination="tablePagination.order_product_customers"
:sort-method="sortProductGroupedTableRows"
virtual-scroll
:virtual-scroll-item-size="productThumbVirtualItemSize"
@@ -415,8 +416,8 @@
class="performance-table sticky-dim-table sticky-dim-10 bg-white"
:rows="displayOrderMarketDetailTableRows"
:columns="orderMarketDetailColumns"
- :loading="loading || backendGroupedLoading"
- :pagination="{ rowsPerPage: 0, sortBy: 'order_date', descending: true }"
+ :loading="loading"
+ v-model:pagination="tablePagination.order_market_details"
:sort-method="sortProductGroupedTableRows"
virtual-scroll
:virtual-scroll-item-size="64"
@@ -558,8 +559,8 @@
]"
:rows="displayProductKpiTableRows"
:columns="activeProductColumns"
- :loading="loading || backendGroupedLoading"
- :pagination="pagination"
+ :loading="loading"
+ v-model:pagination="activeProductPagination"
:sort-method="sortProductGroupedTableRows"
virtual-scroll
:virtual-scroll-item-size="productThumbVirtualItemSize"
@@ -714,8 +715,8 @@
class="performance-table sticky-dim-table sticky-dim-11 bg-white"
:rows="displayIdleTableRows"
:columns="idleColumns"
- :loading="loading || backendGroupedLoading"
- :pagination="{ rowsPerPage: 0 }"
+ :loading="loading"
+ v-model:pagination="tablePagination.idle"
:sort-method="sortProductGroupedTableRows"
virtual-scroll
:virtual-scroll-item-size="productThumbVirtualItemSize"
@@ -902,7 +903,7 @@
:rows="filteredMarketRows"
:columns="marketColumns"
:loading="loading"
- :pagination="{ rowsPerPage: 0 }"
+ v-model:pagination="tablePagination.markets"
virtual-scroll
:virtual-scroll-item-size="64"
:virtual-scroll-slice-size="tableVirtualSliceSize"
@@ -1008,8 +1009,8 @@
]"
:rows="displayActiveSalesBreakdownTableRows"
:columns="visibleSalesBreakdownColumns"
- :loading="loading || backendGroupedLoading"
- :pagination="{ rowsPerPage: 0, sortBy: 'performance_score_total', descending: true }"
+ :loading="loading"
+ v-model:pagination="activeSalesBreakdownPagination"
:sort-method="sortProductGroupedTableRows"
virtual-scroll
:virtual-scroll-item-size="productThumbVirtualItemSize"
@@ -1168,7 +1169,7 @@
:rows="filteredCustomerRows"
:columns="customerColumns"
:loading="loading"
- :pagination="{ rowsPerPage: 0 }"
+ v-model:pagination="tablePagination.customers"
virtual-scroll
:virtual-scroll-item-size="64"
:virtual-scroll-slice-size="tableVirtualSliceSize"
@@ -1270,7 +1271,7 @@
:rows="filteredCountryRows"
:columns="countryColumns"
:loading="loading"
- :pagination="{ rowsPerPage: 0 }"
+ v-model:pagination="tablePagination.countries"
virtual-scroll
:virtual-scroll-item-size="64"
:virtual-scroll-slice-size="tableVirtualSliceSize"
@@ -1566,6 +1567,7 @@ const selectedDetailMainGroup = ref('')
const imageUrlByKey = ref({})
const imageListByKey = ref({})
const imageFailedKeys = ref(new Set())
+const imageLoadingKeys = ref(new Set())
const imageDialog = ref(false)
const imageDialogUrls = ref([])
const imageDialogTitle = ref('')
@@ -1584,6 +1586,9 @@ const backendGroupedLoading = ref(false)
const imagePrimeLoading = ref(false)
const filterBusy = ref(false)
const bulkExpandLoading = ref(false)
+const loadingStartedAt = ref(0)
+const loadingNow = ref(0)
+const loadingStage = ref('')
const generalRowsLoaded = ref(false)
const detailLevelMenuOpen = ref(false)
const topScrollbarRef = ref(null)
@@ -1593,6 +1598,7 @@ let imagePrimeActiveRequests = 0
let activeTableMiddleEl = null
let topScrollbarResizeObserver = null
let topScrollbarSyncing = false
+let loadingTimer = null
const columnFilters = reactive({})
const columnFilterSearch = reactive({})
@@ -1631,14 +1637,54 @@ const performanceTabs = [
{ 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ş' }
]
-const pageBusy = computed(() => loading.value || filterBusy.value || bulkExpandLoading.value)
-
-const pagination = ref({
- page: 1,
- rowsPerPage: 0,
- sortBy: 'performance_score_total',
- descending: true
+const pageBusy = computed(() => loading.value)
+const loadingElapsedLabel = computed(() => {
+ if (!loadingStartedAt.value || !loadingNow.value) return ''
+ const seconds = Math.max(0, Math.floor((loadingNow.value - loadingStartedAt.value) / 1000))
+ const minutes = Math.floor(seconds / 60)
+ const rest = seconds % 60
+ return minutes > 0 ? `${minutes}:${String(rest).padStart(2, '0')}` : `${seconds} sn`
})
+const loadingMessage = computed(() => loadingStage.value || 'Rapor verileri yükleniyor...')
+
+const tablePagination = reactive({
+ general: { page: 1, rowsPerPage: 0, sortBy: 'performance_score_total', descending: true },
+ products: { page: 1, rowsPerPage: 0, sortBy: 'performance_score_total', descending: true },
+ product_detail: { page: 1, rowsPerPage: 0, sortBy: 'performance_score_total', descending: true },
+ order_product_customers: { page: 1, rowsPerPage: 0, sortBy: 'order_usd', descending: true },
+ order_market_details: { page: 1, rowsPerPage: 0, sortBy: 'order_date', descending: true },
+ idle: { page: 1, rowsPerPage: 0, sortBy: 'idle_cost_usd', descending: true },
+ markets: { page: 1, rowsPerPage: 0, sortBy: 'sales_usd_90d', descending: true },
+ countries: { page: 1, rowsPerPage: 0, sortBy: 'sales_usd_90d', descending: true },
+ customers: { page: 1, rowsPerPage: 0, sortBy: 'sales_usd_365d', descending: true },
+ sales_color_yaka_market_customer: { page: 1, rowsPerPage: 0, sortBy: 'performance_score_total', descending: true },
+ sales_product_country_segment_market_customer: { page: 1, rowsPerPage: 0, sortBy: 'performance_score_total', descending: true },
+ sales_market_customer_product: { page: 1, rowsPerPage: 0, sortBy: 'performance_score_total', descending: true },
+ sales_country_segment_market_customer_product: { page: 1, rowsPerPage: 0, sortBy: 'performance_score_total', descending: true }
+})
+
+const activeProductPagination = computed({
+ get: () => tablePagination[activeTab.value === 'product_detail' ? 'product_detail' : 'products'],
+ set: value => {
+ tablePagination[activeTab.value === 'product_detail' ? 'product_detail' : 'products'] = normalizeTablePagination(value)
+ }
+})
+
+const activeSalesBreakdownPagination = computed({
+ get: () => tablePagination[activeTab.value] || tablePagination.sales_color_yaka_market_customer,
+ set: value => {
+ tablePagination[activeTab.value] = normalizeTablePagination(value)
+ }
+})
+
+function normalizeTablePagination (value) {
+ return {
+ page: Number(value?.page || 1),
+ rowsPerPage: 0,
+ sortBy: value?.sortBy || '',
+ descending: value?.descending === true
+ }
+}
const bucketOptions = [
{ label: 'Yıldız Ürün', value: 'YILDIZ_URUN' },
@@ -2739,8 +2785,7 @@ const productAutoExpandKeys = computed(() => {
})
const allProductGroupsExpanded = computed(() => {
- const keys = productAutoExpandKeys.value
- return keys.length > 0 && keys.every(key => expandedGroups.value[key] === true)
+ return selectedExpandThroughLevel.value >= autoExpandThroughLevel.value
})
function appendGroupRows (out, sourceRows, level, parentKeys, levels = groupLevels) {
@@ -2784,22 +2829,13 @@ function buildGroupedTableRows (tableKey, sourceRows) {
}
function backendRowsForTab (tabKey, fallbackRows) {
- const rows = backendGroupedRows.value[tabKey]
- const backendRows = Array.isArray(rows) && rows.length ? rows : []
- if (hasActiveTableFilters(tabKey)) {
- return tabKey === 'product_detail'
- ? filterRowsBySelectedDetailMainGroup(fallbackRows)
- : fallbackRows
- }
- const sourceRows = tabKey === 'product_detail'
- ? filterRowsBySelectedDetailMainGroup(backendRows.length ? backendRows : fallbackRows)
- : (backendRows.length ? backendRows : fallbackRows)
- return sourceRows
+ return tabKey === 'product_detail'
+ ? filterRowsBySelectedDetailMainGroup(fallbackRows)
+ : fallbackRows
}
function filterSourceRowsForTab (tabKey, fallbackRows) {
- const rows = backendGroupedRows.value[tabKey]
- return Array.isArray(rows) && rows.length ? rows : fallbackRows
+ return fallbackRows
}
function columnsForTableKey (tabKey) {
@@ -3387,7 +3423,10 @@ function shouldSkipGroupValue (field, value) {
}
function isGroupExpanded (key) {
- return expandedGroups.value[key] === true
+ const tabKey = tabKeyFromGroupKey(key)
+ const depth = groupKeyDepth(key)
+ const level = selectedExpandThroughLevelForTab(tabKey)
+ return depth >= 0 && level >= depth
}
function groupKeyDepth (key) {
@@ -3395,6 +3434,20 @@ function groupKeyDepth (key) {
return Math.max(-1, parts.length - 2)
}
+function tabKeyFromGroupKey (key) {
+ const first = String(key || '').split('|')[0] || ''
+ return first.startsWith('tab:') ? first.slice(4) : activeTab.value
+}
+
+function selectedExpandThroughLevelForTab (tabKey) {
+ const levels = tabGroupLevels[tabKey] || groupLevels
+ const selected = new Set(selectedExpandLevelKeysByTab[tabKey] || [])
+ const indexes = levels
+ .map((level, index) => selected.has(level.key) ? index : -1)
+ .filter(index => index >= 0)
+ return indexes.length ? Math.max(...indexes) : -1
+}
+
function activeTabPrefix () {
return `tab:${activeTab.value}|`
}
@@ -3423,24 +3476,22 @@ function setSelectedExpandLevelsThrough (level) {
function applyActiveExpansionThroughLevel (level) {
setSelectedExpandLevelsThrough(level)
- replaceActiveTabExpandedGroups(expandedLevelKeysThrough(level))
+ replaceActiveTabExpandedGroups([])
lastManualExpandedGroupKey.value = ''
}
function toggleGroup (key) {
const level = groupKeyDepth(key)
applyActiveExpansionThroughLevel(isGroupExpanded(key) ? level - 1 : level)
- scheduleLoadBackendGroupedRows()
}
async function collapseAllProductGroups () {
if (bulkExpandLoading.value) return
detailLevelMenuOpen.value = false
applyActiveExpansionThroughLevel(-1)
- if (backendGroupedTimer) window.clearTimeout(backendGroupedTimer)
bulkExpandLoading.value = true
try {
- await loadBackendGroupedRows({ force: true })
+ await nextTick()
} finally {
bulkExpandLoading.value = false
}
@@ -3449,21 +3500,10 @@ async function collapseAllProductGroups () {
async function expandSelectedProductGroups () {
if (bulkExpandLoading.value) return
detailLevelMenuOpen.value = false
- const keys = productAutoExpandKeys.value
- const maxKeys = Math.min(maxBulkExpandKeys, maxBackendExpandedKeysForTab(activeTab.value))
- const expandableKeys = keys.slice(0, maxKeys)
- replaceActiveTabExpandedGroups(expandableKeys)
- lastManualExpandedGroupKey.value = ''
- if (keys.length > expandableKeys.length) {
- Notify.create({
- type: 'warning',
- message: `${formatNumber(expandableKeys.length, 0)} grup açıldı. Daha derin detaylar için satırları kademeli açın.`
- })
- }
- if (backendGroupedTimer) window.clearTimeout(backendGroupedTimer)
+ applyActiveExpansionThroughLevel(autoExpandThroughLevel.value)
bulkExpandLoading.value = true
try {
- await loadBackendGroupedRows({ force: true })
+ await nextTick()
} finally {
bulkExpandLoading.value = false
}
@@ -3649,12 +3689,7 @@ function runWithFilterBusy (apply) {
function syncExpansionAfterFilterChange () {
if (!backendGroupedSupportedTab(activeTab.value)) return
const level = selectedExpandThroughLevel.value
- if (level < 0) {
- scheduleLoadBackendGroupedRows()
- return
- }
- applyActiveExpansionThroughLevel(autoExpandThroughLevel.value)
- scheduleLoadBackendGroupedRows()
+ if (level >= 0) applyActiveExpansionThroughLevel(autoExpandThroughLevel.value)
}
function filterRowsForTable (tableKey, sourceRows, sourceColumns) {
@@ -4166,7 +4201,21 @@ function getCachedProductImageUrl (row) {
if (!canShowProductImage(row)) return ''
const key = productImageKey(row)
if (imageFailedKeys.value.has(key)) return ''
- return imageUrlByKey.value[key] || performanceStore.imageUrl(key) || ''
+ const url = imageUrlByKey.value[key] || performanceStore.imageUrl(key) || ''
+ if (!url) queueVisibleProductImage(row)
+ return url
+}
+
+function queueVisibleProductImage (row) {
+ const key = productImageKey(row)
+ if (!key || imageLoadingKeys.value.has(key) || imageFailedKeys.value.has(key)) return
+ if (Object.prototype.hasOwnProperty.call(imageListByKey.value, key) || performanceStore.hasImageKey(key)) return
+ imageLoadingKeys.value = new Set([...imageLoadingKeys.value, key])
+ void fetchProductImagesForRow(row).finally(() => {
+ const next = new Set(imageLoadingKeys.value)
+ next.delete(key)
+ imageLoadingKeys.value = next
+ })
}
function markProductImageFailed (row) {
@@ -4510,30 +4559,112 @@ async function timedProductPerformanceGet (label, url, config = {}) {
async function reload () {
loading.value = true
+ startLoadingTimer('Rapor verileri hazırlanıyor...')
try {
generalRowsLoaded.value = false
generalRows.value = []
+ orderProductCustomersLoaded.value = false
+ orderProductCustomerRows.value = []
+ orderMarketDetailsLoaded.value = false
+ orderMarketDetailRows.value = []
+ orderAnalysisLoaded.value = false
+ orderAnalysisRows.value = []
+ orderMarketRows.value = []
+ orderCustomerRows.value = []
marketRowsLoaded.value = false
+ marketRows.value = []
countryRowsLoaded.value = false
+ countryRows.value = []
customerRowsLoaded.value = false
+ customerRows.value = []
+ for (const key of salesBreakdownTabKeys) salesBreakdownRows[key] = []
+
+ loadingStage.value = 'Ana KPI ve özet yükleniyor...'
const [summaryResp, listResp] = await Promise.all([
- timedProductPerformanceGet('summary', '/pricing/product-performance/summary', { timeout: 60000 }),
+ timedProductPerformanceGet('summary', '/pricing/product-performance/summary', { timeout: 180000 }),
timedProductPerformanceGet('product-kpi', '/pricing/product-performance', {
params: {
page: 1,
limit: productKpiFetchLimit
},
- timeout: 60000
+ timeout: 180000
})
])
summary.value = summaryResp?.data || {}
rows.value = (Array.isArray(listResp?.data?.rows) ? listResp.data.rows : []).map(normalizeRow)
- await ensureActiveTabData(false)
- await loadBackendGroupedRows({ force: true })
+ loadingStage.value = 'Tüm rapor sekmeleri cacheleniyor...'
+ await preloadAllReportTabs()
+ loadingStage.value = 'Rapor ekranı hazırlanıyor...'
+ await nextTick()
} catch (err) {
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Ürün performans verisi alınamadı' })
} finally {
loading.value = false
+ stopLoadingTimer()
+ }
+}
+
+function startLoadingTimer (stage = '') {
+ loadingStage.value = stage
+ loadingStartedAt.value = Date.now()
+ loadingNow.value = Date.now()
+ if (loadingTimer) window.clearInterval(loadingTimer)
+ loadingTimer = window.setInterval(() => {
+ loadingNow.value = Date.now()
+ }, 1000)
+}
+
+function stopLoadingTimer () {
+ if (loadingTimer) {
+ window.clearInterval(loadingTimer)
+ loadingTimer = null
+ }
+ loadingStartedAt.value = 0
+ loadingNow.value = 0
+ loadingStage.value = ''
+}
+
+async function preloadAllReportTabs () {
+ const tasks = [
+ loadGeneralRows(false),
+ loadOrderProductCustomers(false),
+ loadOrderMarketDetails(false),
+ loadMarketRows(false),
+ loadCountryRows(false),
+ loadCustomerRows(false),
+ loadAllSalesBreakdownRows(false)
+ ]
+ const results = await Promise.allSettled(tasks)
+ const failed = results.filter(item => item.status === 'rejected')
+ if (failed.length) {
+ console.warn('[ProductPerformance][preload] partial failure', failed.map(item => item.reason?.message || item.reason))
+ }
+}
+
+async function loadAllSalesBreakdownRows (showLoading = true) {
+ if (showLoading) loading.value = true
+ try {
+ await Promise.all(salesBreakdownTabKeys.map(key => loadSalesBreakdownRows(key, false)))
+ } finally {
+ if (showLoading) loading.value = false
+ }
+}
+
+async function loadSalesBreakdownRows (tabKey, showLoading = true) {
+ if (!salesBreakdownTabKeys.includes(tabKey)) return
+ if (Array.isArray(salesBreakdownRows[tabKey]) && salesBreakdownRows[tabKey].length) return
+ if (showLoading) loading.value = true
+ try {
+ const mode = salesBreakdownModes[tabKey] || tabKey
+ const resp = await timedProductPerformanceGet(`sales:${tabKey}`, '/pricing/product-performance/sales-breakdown', {
+ params: { mode, limit: reportFetchLimit },
+ timeout: 180000
+ })
+ salesBreakdownRows[tabKey] = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeSalesBreakdownRow)
+ } catch (err) {
+ Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Satış kırılım verisi alınamadı' })
+ } finally {
+ if (showLoading) loading.value = false
}
}
@@ -4541,7 +4672,7 @@ async function loadGeneralRows (showLoading = true) {
if (generalRowsLoaded.value) return
if (showLoading) loading.value = true
try {
- const resp = await timedProductPerformanceGet('general', '/pricing/product-performance/general', { params: { limit: reportFetchLimit }, timeout: 90000 })
+ const resp = await timedProductPerformanceGet('general', '/pricing/product-performance/general', { params: { limit: reportFetchLimit }, timeout: 180000 })
const normalizedGeneralRows = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeGeneralRow)
generalRows.value = normalizedGeneralRows
generalRowsLoaded.value = true
@@ -4559,9 +4690,10 @@ async function loadGeneralRows (showLoading = true) {
}
async function loadOrderProductCustomers (showLoading = true) {
+ if (orderProductCustomersLoaded.value) return
if (showLoading) loading.value = true
try {
- const resp = await timedProductPerformanceGet('orders:product-customers', '/pricing/product-performance/orders/product-customers', { params: { limit: reportFetchLimit }, timeout: 90000 })
+ const resp = await timedProductPerformanceGet('orders:product-customers', '/pricing/product-performance/orders/product-customers', { params: { limit: reportFetchLimit }, timeout: 180000 })
orderProductCustomerRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeOrderProductCustomerRow)
orderProductCustomersLoaded.value = true
} catch (err) {
@@ -4572,9 +4704,10 @@ async function loadOrderProductCustomers (showLoading = true) {
}
async function loadOrderMarketDetails (showLoading = true) {
+ if (orderMarketDetailsLoaded.value) return
if (showLoading) loading.value = true
try {
- const resp = await timedProductPerformanceGet('orders:market-details', '/pricing/product-performance/orders/market-details', { params: { limit: reportFetchLimit }, timeout: 90000 })
+ const resp = await timedProductPerformanceGet('orders:market-details', '/pricing/product-performance/orders/market-details', { params: { limit: reportFetchLimit }, timeout: 180000 })
orderMarketDetailRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeOrderMarketDetailRow)
orderMarketDetailsLoaded.value = true
} catch (err) {
@@ -4588,7 +4721,7 @@ async function loadMarketRows (showLoading = true) {
if (marketRowsLoaded.value) return
if (showLoading) loading.value = true
try {
- const resp = await timedProductPerformanceGet('markets', '/pricing/product-performance/markets', { params: { limit: reportFetchLimit }, timeout: 60000 })
+ const resp = await timedProductPerformanceGet('markets', '/pricing/product-performance/markets', { params: { limit: reportFetchLimit }, timeout: 180000 })
marketRows.value = Array.isArray(resp?.data) ? resp.data : []
marketRowsLoaded.value = true
} catch (err) {
@@ -4602,7 +4735,7 @@ async function loadCountryRows (showLoading = true) {
if (countryRowsLoaded.value) return
if (showLoading) loading.value = true
try {
- const resp = await timedProductPerformanceGet('countries', '/pricing/product-performance/countries', { params: { limit: reportFetchLimit }, timeout: 60000 })
+ const resp = await timedProductPerformanceGet('countries', '/pricing/product-performance/countries', { params: { limit: reportFetchLimit }, timeout: 180000 })
countryRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(row => ({
...row,
country_key: `${row.country || '-'}|${row.customer_segment || '-'}|${row.market_key || '-'}`
@@ -4621,7 +4754,7 @@ async function loadCustomerRows (showLoading = true) {
try {
const resp = await timedProductPerformanceGet('customers', '/pricing/product-performance/customers', {
params: { limit: reportFetchLimit, breakdown: customerBreakdown.value },
- timeout: 60000
+ timeout: 180000
})
customerRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(row => ({
...row,
@@ -4641,6 +4774,9 @@ async function ensureActiveTabData (showLoading = true) {
if (tab === 'markets') return loadMarketRows(showLoading)
if (tab === 'countries') return loadCountryRows(showLoading)
if (tab === 'customers') return loadCustomerRows(showLoading)
+ if (tab === 'order_product_customers') return loadOrderProductCustomers(showLoading)
+ if (tab === 'order_market_details') return loadOrderMarketDetails(showLoading)
+ if (salesBreakdownTabKeys.includes(tab)) return loadSalesBreakdownRows(tab, showLoading)
}
async function loadOrderAnalysis (showLoading = true) {
@@ -4680,7 +4816,6 @@ function bucketColor (value) {
watch(activeTab, () => {
void ensureActiveTabData(false)
- scheduleLoadBackendGroupedRows()
setupTopScrollbarSync()
})
@@ -4704,14 +4839,9 @@ watch(detailMainGroupOptions, () => {
watch(selectedDetailMainGroup, () => {
if (activeTab.value === 'product_detail') {
- backendGroupedRows.value = {
- ...backendGroupedRows.value,
- product_detail: []
- }
void nextTick(() => {
const level = selectedExpandThroughLevel.value
applyActiveExpansionThroughLevel(level >= 0 ? autoExpandThroughLevel.value : -1)
- scheduleLoadBackendGroupedRows()
})
}
})
@@ -4733,6 +4863,7 @@ onMounted(() => {
onBeforeUnmount(() => {
cleanupTopScrollbarSync()
if (backendGroupedTimer) window.clearTimeout(backendGroupedTimer)
+ stopLoadingTimer()
})
@@ -4762,6 +4893,12 @@ onBeforeUnmount(() => {
font-weight: 700;
}
+.page-busy-time {
+ color: #5f6b7a;
+ font-size: 13px;
+ font-weight: 600;
+}
+
.performance-top-bar {
min-height: 34px;
}