Fix product performance grouped JSON build

This commit is contained in:
M_Kececi
2026-07-05 00:38:44 +03:00
parent d0717900a6
commit 9f6e288d04
2 changed files with 227 additions and 90 deletions
+12 -12
View File
@@ -16,7 +16,7 @@ func GetProductPerformanceHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) 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() defer cancel()
f := queries.ProductPerformanceFilters{ f := queries.ProductPerformanceFilters{
@@ -90,7 +90,7 @@ func GetProductPerformanceSummaryHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) 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() defer cancel()
summary, err := queries.GetProductPerformanceSummary(ctx, pg) 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) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) 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() defer cancel()
rows, err := queries.ListProductPerformanceGeneral(ctx, pg, intQuery(r, "limit", 50000)) 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) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) 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() defer cancel()
rows, err := queries.ListProductPerformanceOrderAnalysis(ctx, pg, intQuery(r, "limit", 50000)) 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) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) 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() defer cancel()
rows, err := queries.ListProductPerformanceOrderGroups( rows, err := queries.ListProductPerformanceOrderGroups(
@@ -159,7 +159,7 @@ func GetProductPerformanceOrderProductCustomersHandler(pg *sql.DB) http.HandlerF
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) 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() defer cancel()
rows, err := queries.ListProductPerformanceOrderProductCustomers(ctx, pg, intQuery(r, "limit", 50000)) 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) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) 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() defer cancel()
rows, err := queries.ListProductPerformanceOrderMarketDetails(ctx, pg, intQuery(r, "limit", 50000)) 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) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) 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() defer cancel()
rows, err := queries.ListProductPerformanceMarkets(ctx, pg, intQuery(r, "limit", 50000)) 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) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) 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() defer cancel()
rows, err := queries.ListProductPerformanceCountries(ctx, pg, intQuery(r, "limit", 50000)) 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) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) 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() defer cancel()
rows, err := queries.ListProductPerformanceCustomers( rows, err := queries.ListProductPerformanceCustomers(
@@ -244,7 +244,7 @@ func GetProductPerformanceSalesBreakdownHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) 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() defer cancel()
rows, err := queries.ListProductPerformanceSalesBreakdown( rows, err := queries.ListProductPerformanceSalesBreakdown(
@@ -265,7 +265,7 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) 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() defer cancel()
mode := strings.TrimSpace(r.URL.Query().Get("mode")) mode := strings.TrimSpace(r.URL.Query().Get("mode"))
+212 -75
View File
@@ -14,7 +14,8 @@
@wheel.stop @wheel.stop
> >
<q-spinner-gears size="56px" color="primary" /> <q-spinner-gears size="56px" color="primary" />
<div class="page-busy-label">Yükleniyor...</div> <div class="page-busy-label">{{ loadingMessage }}</div>
<div v-if="loadingElapsedLabel" class="page-busy-time">{{ loadingElapsedLabel }}</div>
</div> </div>
</teleport> </teleport>
<div class="performance-top-bar row items-center justify-between q-col-gutter-xs q-mb-xs"> <div class="performance-top-bar row items-center justify-between q-col-gutter-xs q-mb-xs">
@@ -113,7 +114,7 @@
:rows="filteredGeneralRows" :rows="filteredGeneralRows"
:columns="generalColumns" :columns="generalColumns"
:loading="loading" :loading="loading"
:pagination="{ rowsPerPage: 0, sortBy: 'performance_score_total', descending: true }" v-model:pagination="tablePagination.general"
virtual-scroll virtual-scroll
:virtual-scroll-item-size="productThumbVirtualItemSize" :virtual-scroll-item-size="productThumbVirtualItemSize"
:virtual-scroll-slice-size="tableVirtualSliceSize" :virtual-scroll-slice-size="tableVirtualSliceSize"
@@ -224,8 +225,8 @@
class="performance-table sticky-dim-table sticky-dim-8 bg-white" class="performance-table sticky-dim-table sticky-dim-8 bg-white"
:rows="displayOrderProductCustomerTableRows" :rows="displayOrderProductCustomerTableRows"
:columns="orderProductCustomerColumns" :columns="orderProductCustomerColumns"
:loading="loading || backendGroupedLoading" :loading="loading"
:pagination="{ rowsPerPage: 0, sortBy: 'order_usd', descending: true }" v-model:pagination="tablePagination.order_product_customers"
:sort-method="sortProductGroupedTableRows" :sort-method="sortProductGroupedTableRows"
virtual-scroll virtual-scroll
:virtual-scroll-item-size="productThumbVirtualItemSize" :virtual-scroll-item-size="productThumbVirtualItemSize"
@@ -415,8 +416,8 @@
class="performance-table sticky-dim-table sticky-dim-10 bg-white" class="performance-table sticky-dim-table sticky-dim-10 bg-white"
:rows="displayOrderMarketDetailTableRows" :rows="displayOrderMarketDetailTableRows"
:columns="orderMarketDetailColumns" :columns="orderMarketDetailColumns"
:loading="loading || backendGroupedLoading" :loading="loading"
:pagination="{ rowsPerPage: 0, sortBy: 'order_date', descending: true }" v-model:pagination="tablePagination.order_market_details"
:sort-method="sortProductGroupedTableRows" :sort-method="sortProductGroupedTableRows"
virtual-scroll virtual-scroll
:virtual-scroll-item-size="64" :virtual-scroll-item-size="64"
@@ -558,8 +559,8 @@
]" ]"
:rows="displayProductKpiTableRows" :rows="displayProductKpiTableRows"
:columns="activeProductColumns" :columns="activeProductColumns"
:loading="loading || backendGroupedLoading" :loading="loading"
:pagination="pagination" v-model:pagination="activeProductPagination"
:sort-method="sortProductGroupedTableRows" :sort-method="sortProductGroupedTableRows"
virtual-scroll virtual-scroll
:virtual-scroll-item-size="productThumbVirtualItemSize" :virtual-scroll-item-size="productThumbVirtualItemSize"
@@ -714,8 +715,8 @@
class="performance-table sticky-dim-table sticky-dim-11 bg-white" class="performance-table sticky-dim-table sticky-dim-11 bg-white"
:rows="displayIdleTableRows" :rows="displayIdleTableRows"
:columns="idleColumns" :columns="idleColumns"
:loading="loading || backendGroupedLoading" :loading="loading"
:pagination="{ rowsPerPage: 0 }" v-model:pagination="tablePagination.idle"
:sort-method="sortProductGroupedTableRows" :sort-method="sortProductGroupedTableRows"
virtual-scroll virtual-scroll
:virtual-scroll-item-size="productThumbVirtualItemSize" :virtual-scroll-item-size="productThumbVirtualItemSize"
@@ -902,7 +903,7 @@
:rows="filteredMarketRows" :rows="filteredMarketRows"
:columns="marketColumns" :columns="marketColumns"
:loading="loading" :loading="loading"
:pagination="{ rowsPerPage: 0 }" v-model:pagination="tablePagination.markets"
virtual-scroll virtual-scroll
:virtual-scroll-item-size="64" :virtual-scroll-item-size="64"
:virtual-scroll-slice-size="tableVirtualSliceSize" :virtual-scroll-slice-size="tableVirtualSliceSize"
@@ -1008,8 +1009,8 @@
]" ]"
:rows="displayActiveSalesBreakdownTableRows" :rows="displayActiveSalesBreakdownTableRows"
:columns="visibleSalesBreakdownColumns" :columns="visibleSalesBreakdownColumns"
:loading="loading || backendGroupedLoading" :loading="loading"
:pagination="{ rowsPerPage: 0, sortBy: 'performance_score_total', descending: true }" v-model:pagination="activeSalesBreakdownPagination"
:sort-method="sortProductGroupedTableRows" :sort-method="sortProductGroupedTableRows"
virtual-scroll virtual-scroll
:virtual-scroll-item-size="productThumbVirtualItemSize" :virtual-scroll-item-size="productThumbVirtualItemSize"
@@ -1168,7 +1169,7 @@
:rows="filteredCustomerRows" :rows="filteredCustomerRows"
:columns="customerColumns" :columns="customerColumns"
:loading="loading" :loading="loading"
:pagination="{ rowsPerPage: 0 }" v-model:pagination="tablePagination.customers"
virtual-scroll virtual-scroll
:virtual-scroll-item-size="64" :virtual-scroll-item-size="64"
:virtual-scroll-slice-size="tableVirtualSliceSize" :virtual-scroll-slice-size="tableVirtualSliceSize"
@@ -1270,7 +1271,7 @@
:rows="filteredCountryRows" :rows="filteredCountryRows"
:columns="countryColumns" :columns="countryColumns"
:loading="loading" :loading="loading"
:pagination="{ rowsPerPage: 0 }" v-model:pagination="tablePagination.countries"
virtual-scroll virtual-scroll
:virtual-scroll-item-size="64" :virtual-scroll-item-size="64"
:virtual-scroll-slice-size="tableVirtualSliceSize" :virtual-scroll-slice-size="tableVirtualSliceSize"
@@ -1566,6 +1567,7 @@ const selectedDetailMainGroup = ref('')
const imageUrlByKey = ref({}) const imageUrlByKey = ref({})
const imageListByKey = ref({}) const imageListByKey = ref({})
const imageFailedKeys = ref(new Set()) const imageFailedKeys = ref(new Set())
const imageLoadingKeys = ref(new Set())
const imageDialog = ref(false) const imageDialog = ref(false)
const imageDialogUrls = ref([]) const imageDialogUrls = ref([])
const imageDialogTitle = ref('') const imageDialogTitle = ref('')
@@ -1584,6 +1586,9 @@ const backendGroupedLoading = ref(false)
const imagePrimeLoading = ref(false) const imagePrimeLoading = ref(false)
const filterBusy = ref(false) const filterBusy = ref(false)
const bulkExpandLoading = ref(false) const bulkExpandLoading = ref(false)
const loadingStartedAt = ref(0)
const loadingNow = ref(0)
const loadingStage = ref('')
const generalRowsLoaded = ref(false) const generalRowsLoaded = ref(false)
const detailLevelMenuOpen = ref(false) const detailLevelMenuOpen = ref(false)
const topScrollbarRef = ref(null) const topScrollbarRef = ref(null)
@@ -1593,6 +1598,7 @@ let imagePrimeActiveRequests = 0
let activeTableMiddleEl = null let activeTableMiddleEl = null
let topScrollbarResizeObserver = null let topScrollbarResizeObserver = null
let topScrollbarSyncing = false let topScrollbarSyncing = false
let loadingTimer = null
const columnFilters = reactive({}) const columnFilters = reactive({})
const columnFilterSearch = 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_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ş' } { name: 'order_market_details', icon: 'receipt_long', label: 'Piyasa > Müşteri > Ürün Sipariş' }
] ]
const pageBusy = computed(() => loading.value || filterBusy.value || bulkExpandLoading.value) const pageBusy = computed(() => loading.value)
const loadingElapsedLabel = computed(() => {
const pagination = ref({ if (!loadingStartedAt.value || !loadingNow.value) return ''
page: 1, const seconds = Math.max(0, Math.floor((loadingNow.value - loadingStartedAt.value) / 1000))
rowsPerPage: 0, const minutes = Math.floor(seconds / 60)
sortBy: 'performance_score_total', const rest = seconds % 60
descending: true 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 = [ const bucketOptions = [
{ label: 'Yıldız Ürün', value: 'YILDIZ_URUN' }, { label: 'Yıldız Ürün', value: 'YILDIZ_URUN' },
@@ -2739,8 +2785,7 @@ const productAutoExpandKeys = computed(() => {
}) })
const allProductGroupsExpanded = computed(() => { const allProductGroupsExpanded = computed(() => {
const keys = productAutoExpandKeys.value return selectedExpandThroughLevel.value >= autoExpandThroughLevel.value
return keys.length > 0 && keys.every(key => expandedGroups.value[key] === true)
}) })
function appendGroupRows (out, sourceRows, level, parentKeys, levels = groupLevels) { function appendGroupRows (out, sourceRows, level, parentKeys, levels = groupLevels) {
@@ -2784,22 +2829,13 @@ function buildGroupedTableRows (tableKey, sourceRows) {
} }
function backendRowsForTab (tabKey, fallbackRows) { function backendRowsForTab (tabKey, fallbackRows) {
const rows = backendGroupedRows.value[tabKey]
const backendRows = Array.isArray(rows) && rows.length ? rows : []
if (hasActiveTableFilters(tabKey)) {
return tabKey === 'product_detail' return tabKey === 'product_detail'
? filterRowsBySelectedDetailMainGroup(fallbackRows) ? filterRowsBySelectedDetailMainGroup(fallbackRows)
: fallbackRows : fallbackRows
} }
const sourceRows = tabKey === 'product_detail'
? filterRowsBySelectedDetailMainGroup(backendRows.length ? backendRows : fallbackRows)
: (backendRows.length ? backendRows : fallbackRows)
return sourceRows
}
function filterSourceRowsForTab (tabKey, fallbackRows) { function filterSourceRowsForTab (tabKey, fallbackRows) {
const rows = backendGroupedRows.value[tabKey] return fallbackRows
return Array.isArray(rows) && rows.length ? rows : fallbackRows
} }
function columnsForTableKey (tabKey) { function columnsForTableKey (tabKey) {
@@ -3387,7 +3423,10 @@ function shouldSkipGroupValue (field, value) {
} }
function isGroupExpanded (key) { 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) { function groupKeyDepth (key) {
@@ -3395,6 +3434,20 @@ function groupKeyDepth (key) {
return Math.max(-1, parts.length - 2) 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 () { function activeTabPrefix () {
return `tab:${activeTab.value}|` return `tab:${activeTab.value}|`
} }
@@ -3423,24 +3476,22 @@ function setSelectedExpandLevelsThrough (level) {
function applyActiveExpansionThroughLevel (level) { function applyActiveExpansionThroughLevel (level) {
setSelectedExpandLevelsThrough(level) setSelectedExpandLevelsThrough(level)
replaceActiveTabExpandedGroups(expandedLevelKeysThrough(level)) replaceActiveTabExpandedGroups([])
lastManualExpandedGroupKey.value = '' lastManualExpandedGroupKey.value = ''
} }
function toggleGroup (key) { function toggleGroup (key) {
const level = groupKeyDepth(key) const level = groupKeyDepth(key)
applyActiveExpansionThroughLevel(isGroupExpanded(key) ? level - 1 : level) applyActiveExpansionThroughLevel(isGroupExpanded(key) ? level - 1 : level)
scheduleLoadBackendGroupedRows()
} }
async function collapseAllProductGroups () { async function collapseAllProductGroups () {
if (bulkExpandLoading.value) return if (bulkExpandLoading.value) return
detailLevelMenuOpen.value = false detailLevelMenuOpen.value = false
applyActiveExpansionThroughLevel(-1) applyActiveExpansionThroughLevel(-1)
if (backendGroupedTimer) window.clearTimeout(backendGroupedTimer)
bulkExpandLoading.value = true bulkExpandLoading.value = true
try { try {
await loadBackendGroupedRows({ force: true }) await nextTick()
} finally { } finally {
bulkExpandLoading.value = false bulkExpandLoading.value = false
} }
@@ -3449,21 +3500,10 @@ async function collapseAllProductGroups () {
async function expandSelectedProductGroups () { async function expandSelectedProductGroups () {
if (bulkExpandLoading.value) return if (bulkExpandLoading.value) return
detailLevelMenuOpen.value = false detailLevelMenuOpen.value = false
const keys = productAutoExpandKeys.value applyActiveExpansionThroughLevel(autoExpandThroughLevel.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)
bulkExpandLoading.value = true bulkExpandLoading.value = true
try { try {
await loadBackendGroupedRows({ force: true }) await nextTick()
} finally { } finally {
bulkExpandLoading.value = false bulkExpandLoading.value = false
} }
@@ -3649,12 +3689,7 @@ function runWithFilterBusy (apply) {
function syncExpansionAfterFilterChange () { function syncExpansionAfterFilterChange () {
if (!backendGroupedSupportedTab(activeTab.value)) return if (!backendGroupedSupportedTab(activeTab.value)) return
const level = selectedExpandThroughLevel.value const level = selectedExpandThroughLevel.value
if (level < 0) { if (level >= 0) applyActiveExpansionThroughLevel(autoExpandThroughLevel.value)
scheduleLoadBackendGroupedRows()
return
}
applyActiveExpansionThroughLevel(autoExpandThroughLevel.value)
scheduleLoadBackendGroupedRows()
} }
function filterRowsForTable (tableKey, sourceRows, sourceColumns) { function filterRowsForTable (tableKey, sourceRows, sourceColumns) {
@@ -4166,7 +4201,21 @@ function getCachedProductImageUrl (row) {
if (!canShowProductImage(row)) return '' if (!canShowProductImage(row)) return ''
const key = productImageKey(row) const key = productImageKey(row)
if (imageFailedKeys.value.has(key)) return '' 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) { function markProductImageFailed (row) {
@@ -4510,30 +4559,112 @@ async function timedProductPerformanceGet (label, url, config = {}) {
async function reload () { async function reload () {
loading.value = true loading.value = true
startLoadingTimer('Rapor verileri hazırlanıyor...')
try { try {
generalRowsLoaded.value = false generalRowsLoaded.value = false
generalRows.value = [] generalRows.value = []
orderProductCustomersLoaded.value = false
orderProductCustomerRows.value = []
orderMarketDetailsLoaded.value = false
orderMarketDetailRows.value = []
orderAnalysisLoaded.value = false
orderAnalysisRows.value = []
orderMarketRows.value = []
orderCustomerRows.value = []
marketRowsLoaded.value = false marketRowsLoaded.value = false
marketRows.value = []
countryRowsLoaded.value = false countryRowsLoaded.value = false
countryRows.value = []
customerRowsLoaded.value = false 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([ 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', { timedProductPerformanceGet('product-kpi', '/pricing/product-performance', {
params: { params: {
page: 1, page: 1,
limit: productKpiFetchLimit limit: productKpiFetchLimit
}, },
timeout: 60000 timeout: 180000
}) })
]) ])
summary.value = summaryResp?.data || {} summary.value = summaryResp?.data || {}
rows.value = (Array.isArray(listResp?.data?.rows) ? listResp.data.rows : []).map(normalizeRow) rows.value = (Array.isArray(listResp?.data?.rows) ? listResp.data.rows : []).map(normalizeRow)
await ensureActiveTabData(false) loadingStage.value = 'Tüm rapor sekmeleri cacheleniyor...'
await loadBackendGroupedRows({ force: true }) await preloadAllReportTabs()
loadingStage.value = 'Rapor ekranı hazırlanıyor...'
await nextTick()
} catch (err) { } catch (err) {
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Ürün performans verisi alınamadı' }) Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Ürün performans verisi alınamadı' })
} finally { } finally {
loading.value = false 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 (generalRowsLoaded.value) return
if (showLoading) loading.value = true if (showLoading) loading.value = true
try { 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) const normalizedGeneralRows = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeGeneralRow)
generalRows.value = normalizedGeneralRows generalRows.value = normalizedGeneralRows
generalRowsLoaded.value = true generalRowsLoaded.value = true
@@ -4559,9 +4690,10 @@ async function loadGeneralRows (showLoading = true) {
} }
async function loadOrderProductCustomers (showLoading = true) { async function loadOrderProductCustomers (showLoading = true) {
if (orderProductCustomersLoaded.value) return
if (showLoading) loading.value = true if (showLoading) loading.value = true
try { 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) orderProductCustomerRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeOrderProductCustomerRow)
orderProductCustomersLoaded.value = true orderProductCustomersLoaded.value = true
} catch (err) { } catch (err) {
@@ -4572,9 +4704,10 @@ async function loadOrderProductCustomers (showLoading = true) {
} }
async function loadOrderMarketDetails (showLoading = true) { async function loadOrderMarketDetails (showLoading = true) {
if (orderMarketDetailsLoaded.value) return
if (showLoading) loading.value = true if (showLoading) loading.value = true
try { 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) orderMarketDetailRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeOrderMarketDetailRow)
orderMarketDetailsLoaded.value = true orderMarketDetailsLoaded.value = true
} catch (err) { } catch (err) {
@@ -4588,7 +4721,7 @@ async function loadMarketRows (showLoading = true) {
if (marketRowsLoaded.value) return if (marketRowsLoaded.value) return
if (showLoading) loading.value = true if (showLoading) loading.value = true
try { 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 : [] marketRows.value = Array.isArray(resp?.data) ? resp.data : []
marketRowsLoaded.value = true marketRowsLoaded.value = true
} catch (err) { } catch (err) {
@@ -4602,7 +4735,7 @@ async function loadCountryRows (showLoading = true) {
if (countryRowsLoaded.value) return if (countryRowsLoaded.value) return
if (showLoading) loading.value = true if (showLoading) loading.value = true
try { 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 => ({ countryRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(row => ({
...row, ...row,
country_key: `${row.country || '-'}|${row.customer_segment || '-'}|${row.market_key || '-'}` country_key: `${row.country || '-'}|${row.customer_segment || '-'}|${row.market_key || '-'}`
@@ -4621,7 +4754,7 @@ async function loadCustomerRows (showLoading = true) {
try { try {
const resp = await timedProductPerformanceGet('customers', '/pricing/product-performance/customers', { const resp = await timedProductPerformanceGet('customers', '/pricing/product-performance/customers', {
params: { limit: reportFetchLimit, breakdown: customerBreakdown.value }, params: { limit: reportFetchLimit, breakdown: customerBreakdown.value },
timeout: 60000 timeout: 180000
}) })
customerRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(row => ({ customerRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(row => ({
...row, ...row,
@@ -4641,6 +4774,9 @@ async function ensureActiveTabData (showLoading = true) {
if (tab === 'markets') return loadMarketRows(showLoading) if (tab === 'markets') return loadMarketRows(showLoading)
if (tab === 'countries') return loadCountryRows(showLoading) if (tab === 'countries') return loadCountryRows(showLoading)
if (tab === 'customers') return loadCustomerRows(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) { async function loadOrderAnalysis (showLoading = true) {
@@ -4680,7 +4816,6 @@ function bucketColor (value) {
watch(activeTab, () => { watch(activeTab, () => {
void ensureActiveTabData(false) void ensureActiveTabData(false)
scheduleLoadBackendGroupedRows()
setupTopScrollbarSync() setupTopScrollbarSync()
}) })
@@ -4704,14 +4839,9 @@ watch(detailMainGroupOptions, () => {
watch(selectedDetailMainGroup, () => { watch(selectedDetailMainGroup, () => {
if (activeTab.value === 'product_detail') { if (activeTab.value === 'product_detail') {
backendGroupedRows.value = {
...backendGroupedRows.value,
product_detail: []
}
void nextTick(() => { void nextTick(() => {
const level = selectedExpandThroughLevel.value const level = selectedExpandThroughLevel.value
applyActiveExpansionThroughLevel(level >= 0 ? autoExpandThroughLevel.value : -1) applyActiveExpansionThroughLevel(level >= 0 ? autoExpandThroughLevel.value : -1)
scheduleLoadBackendGroupedRows()
}) })
} }
}) })
@@ -4733,6 +4863,7 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
cleanupTopScrollbarSync() cleanupTopScrollbarSync()
if (backendGroupedTimer) window.clearTimeout(backendGroupedTimer) if (backendGroupedTimer) window.clearTimeout(backendGroupedTimer)
stopLoadingTimer()
}) })
</script> </script>
@@ -4762,6 +4893,12 @@ onBeforeUnmount(() => {
font-weight: 700; font-weight: 700;
} }
.page-busy-time {
color: #5f6b7a;
font-size: 13px;
font-weight: 600;
}
.performance-top-bar { .performance-top-bar {
min-height: 34px; min-height: 34px;
} }