Fix product performance grouped JSON build

This commit is contained in:
M_Kececi
2026-07-05 18:31:40 +03:00
parent 28e33d5e58
commit b2113a8d49
8 changed files with 1207 additions and 252 deletions
+235 -52
View File
@@ -1,4 +1,4 @@
<template>
<template>
<q-page class="product-performance-page q-pa-sm">
<q-inner-loading :showing="pageBusy">
<q-spinner-gears size="52px" color="primary" />
@@ -198,7 +198,7 @@
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
class="product-thumb"
@error="markProductImageFailed(props.row)"
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
loading="lazy"
decoding="async"
alt=""
@@ -311,7 +311,7 @@
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
class="product-thumb"
@error="markProductImageFailed(props.row)"
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
loading="lazy"
decoding="async"
alt=""
@@ -353,7 +353,7 @@
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
class="product-thumb"
@error="markProductImageFailed(props.row)"
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
loading="lazy"
decoding="async"
alt=""
@@ -374,7 +374,8 @@
dense
round
size="sm"
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
:icon="groupCanExpand(props.row) ? (isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right') : 'fiber_manual_record'"
:disable="!groupCanExpand(props.row)"
@click.stop="toggleGroup(props.row.key)"
/>
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
@@ -390,7 +391,7 @@
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
class="product-thumb"
@error="markProductImageFailed(props.row)"
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
loading="lazy"
decoding="async"
alt=""
@@ -529,7 +530,8 @@
dense
round
size="sm"
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
:icon="groupCanExpand(props.row) ? (isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right') : 'fiber_manual_record'"
:disable="!groupCanExpand(props.row)"
@click.stop="toggleGroup(props.row.key)"
/>
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
@@ -652,7 +654,7 @@
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
class="product-thumb"
@error="markProductImageFailed(props.row)"
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
loading="lazy"
decoding="async"
alt=""
@@ -673,7 +675,8 @@
dense
round
size="sm"
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
:icon="groupCanExpand(props.row) ? (isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right') : 'fiber_manual_record'"
:disable="!groupCanExpand(props.row)"
@click.stop="toggleGroup(props.row.key)"
/>
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
@@ -689,7 +692,7 @@
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
class="product-thumb"
@error="markProductImageFailed(props.row)"
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
loading="lazy"
decoding="async"
alt=""
@@ -801,7 +804,7 @@
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
class="product-thumb"
@error="markProductImageFailed(props.row)"
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
loading="lazy"
decoding="async"
alt=""
@@ -839,7 +842,7 @@
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
class="product-thumb"
@error="markProductImageFailed(props.row)"
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
loading="lazy"
decoding="async"
alt=""
@@ -860,7 +863,8 @@
dense
round
size="sm"
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
:icon="groupCanExpand(props.row) ? (isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right') : 'fiber_manual_record'"
:disable="!groupCanExpand(props.row)"
@click.stop="toggleGroup(props.row.key)"
/>
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
@@ -876,7 +880,7 @@
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
class="product-thumb"
@error="markProductImageFailed(props.row)"
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
loading="lazy"
decoding="async"
alt=""
@@ -1127,7 +1131,8 @@
dense
round
size="sm"
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
:icon="groupCanExpand(props.row) ? (isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right') : 'fiber_manual_record'"
:disable="!groupCanExpand(props.row)"
@click.stop="toggleGroup(props.row.key)"
/>
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
@@ -1586,6 +1591,7 @@ const detailLoading = ref(false)
const detailSalesRows = ref([])
const detailStockSizes = ref([])
const backendGroupedRows = ref({})
const backendGroupedFilterOptions = ref({})
const backendGroupedLoading = ref(false)
const imagePrimeLoading = ref(false)
const filterBusy = ref(false)
@@ -1644,9 +1650,6 @@ const backendGroupedFilterFields = new Set([
'urun_ana_grubu',
'urun_alt_grubu',
'product_code',
'item_description',
'color_code',
'yaka_kodu',
'color_yaka',
'market_key',
'country',
@@ -2725,8 +2728,22 @@ const columnFilterOptionMap = computed(() => {
const source = tableFilterSource(tableKey)
if (!source) return out
out[tableKey] = {}
const preparedOptionEntry = backendGroupedSupportedTab(tableKey)
? backendGroupedFilterOptions.value[tableKey]
: null
const preparedOptions = preparedOptionEntry?._key === backendGroupedFilterOptionsKey(tableKey)
? preparedOptionEntry.options || {}
: {}
for (const col of source.columns) {
if (!isColumnFilterable(col.name)) continue
if (backendGroupedSupportedTab(tableKey) && backendGroupedFilterFields.has(col.name)) {
out[tableKey][col.name] = preparedOptions[col.name] || []
continue
}
if (backendGroupedSupportedTab(tableKey) && !backendGroupedFilterFields.has(col.name)) {
out[tableKey][col.name] = []
continue
}
const seen = new Map()
for (const row of source.rows) {
const value = columnFilterValue(row, col)
@@ -2896,6 +2913,85 @@ function filterSourceRowsForTab (tabKey, fallbackRows) {
return fallbackRows
}
function backendGroupedFilterOptionParams (tabKey) {
const columns = columnsForTableKey(tabKey)
const fields = columns
.map(col => col?.name || '')
.filter(name => backendGroupedFilterFields.has(name))
const params = {
mode: tabKey,
groupLevels: (tabGroupLevels[tabKey] || groupLevels).map(level => level.key),
fields,
limit: 5000
}
if (tabKey === 'product_detail') params.urunAnaGrubu = selectedDetailMainGroup.value
return params
}
function backendGroupedFilterOptionsKey (tabKey, params = backendGroupedFilterOptionParams(tabKey)) {
return JSON.stringify({
tabKey,
mainGroup: params.urunAnaGrubu || '',
groupLevels: params.groupLevels || [],
fields: params.fields || []
})
}
async function ensureBackendGroupedFilterOptions (tabKey = activeTab.value, force = false) {
if (!backendGroupedSupportedTab(tabKey)) return
if (tabKey === 'product_detail' && !selectedDetailMainGroup.value) return
const params = backendGroupedFilterOptionParams(tabKey)
if (!params.fields.length) return
const key = backendGroupedFilterOptionsKey(tabKey, params)
if (!force && backendGroupedFilterOptions.value[tabKey]?._key === key) return
try {
const data = await performanceStore.fetchGroupedFilterOptions(params, { force })
if (backendGroupedFilterOptionsKey(tabKey) !== key) return
backendGroupedFilterOptions.value = {
...backendGroupedFilterOptions.value,
[tabKey]: {
_key: key,
options: normalizeBackendGroupedFilterOptions(data)
}
}
} catch (err) {
console.warn('product performance grouped filter options failed', err)
}
}
function normalizeBackendGroupedFilterOptions (data) {
const out = {}
const source = data && typeof data === 'object' ? data : {}
for (const [field, values] of Object.entries(source)) {
if (!Array.isArray(values)) continue
const seen = new Map()
for (const raw of values) {
const value = normalizeBackendFilterValue(field, raw)
if (!value || seen.has(value)) continue
seen.set(value, {
value,
label: backendFilterOptionLabel(field, value)
})
}
out[field] = Array.from(seen.values())
.sort((a, b) => String(a.label).localeCompare(String(b.label), 'tr', { numeric: true }))
}
return out
}
function normalizeBackendFilterValue (name, value) {
const text = String(value || '').trim()
if (name === 'performance_bucket') {
return bucketOptions.find(option => option.value === text || option.label === text)?.value || text
}
return text
}
function backendFilterOptionLabel (name, value) {
if (name === 'performance_bucket') return bucketLabel(value)
return String(value || '').trim()
}
function backendGroupedFilterState (tabKey) {
const out = {}
const columns = columnsForTableKey(tabKey)
@@ -2907,7 +3003,7 @@ function backendGroupedFilterState (tabKey) {
if (!backendGroupedFilterFields.has(name)) {
continue
}
out[name] = selected.map(value => String(value || '').trim()).filter(Boolean)
out[name] = selected.map(value => normalizeBackendFilterValue(name, value)).filter(Boolean)
}
return { filters: out, unsupported: false }
}
@@ -3298,18 +3394,16 @@ function salesGroupPerformanceScore (row) {
}
function productSalesPeriodScore (row) {
const salesIndex = Number(row?.sales_index_90d || 0)
const margin = Number(row?.gross_margin_cost_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)
const suffix = row?.suffix || '90d'
return productScore100({
suffix,
salesUSD: Number(row?.sales_usd_90d || 0),
salesIndex: Number(row?.sales_index_90d || 0),
margin: Number(row?.gross_margin_cost_90d ?? row?.gross_margin_90d ?? 0),
stockTurnover: Number(row?.stock_turnover_90d || 0),
marketCount: Number(row?.market_count_90d || 0),
customerCount: Number(row?.customer_count_90d || 0)
})
}
function productPeriodMetricSource (row, suffix) {
@@ -3319,12 +3413,15 @@ function productPeriodMetricSource (row, suffix) {
const turnoverKey = `stock_turnover_${suffix}`
const hasTurnover = Object.prototype.hasOwnProperty.call(row || {}, turnoverKey)
return {
suffix,
sales_index_90d: salesIndex,
sales_usd_90d: Number(row?.[`sales_usd_${suffix}`] || 0),
gross_margin_cost_90d: Number(row?.[`gross_margin_cost_${suffix}`] ?? 0),
gross_margin_90d: Number(row?.[`gross_margin_${suffix}`] ?? 0),
invoice_count_90d: Number(row?.[`invoice_count_${suffix}`] || 0),
sales_qty_90d: qty,
stock_turnover_90d: hasTurnover ? Number(row?.[turnoverKey] || 0) : (stockQty > 0 ? qty / stockQty : 0),
market_count_90d: periodCount(row, 'market_count', suffix),
customer_count_90d: periodCount(row, 'customer_count', suffix),
stock_qty: stockQty
}
}
@@ -3343,29 +3440,92 @@ function customerSalesGroupPerformanceScore (row) {
}
function customerSalesPeriodScore (row) {
const salesUSD = Number(row?.sales_usd || 0)
const margin = Number(row?.gross_margin_cost ?? 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)
return customerScore100({
suffix: row?.suffix || '90d',
salesUSD: Number(row?.sales_usd || 0),
margin: Number(row?.gross_margin_cost ?? row?.gross_margin ?? 0),
productGroupCount: Number(row?.product_group_count || row?.product_count || 0),
salesQty: Number(row?.sales_qty || 0)
})
}
function periodMetricSource (row, suffix) {
return {
suffix,
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_group_count: periodCount(row, 'product_group_count', suffix),
product_count: Number(row?.product_count || 0)
}
}
function productScore100 ({ suffix = '90d', salesUSD = 0, salesIndex = 0, margin = 0, stockTurnover = 0, marketCount = 0, customerCount = 0 } = {}) {
const revenue = salesIndex > 0 ? ratioScore(salesIndex, 2) : ratioScore(salesUSD, productRevenueTarget(suffix))
return clampScore(
0.30 * marginScore(margin) +
0.20 * ratioScore(stockTurnover, 1.5) +
0.20 * revenue +
0.15 * ratioScore(marketCount, 8) +
0.15 * ratioScore(customerCount, 25)
)
}
function customerScore100 ({ suffix = '90d', salesUSD = 0, margin = 0, productGroupCount = 0, salesQty = 0 } = {}) {
return clampScore(
0.35 * ratioScore(salesUSD, customerRevenueTarget(suffix)) +
0.30 * marginScore(margin) +
0.20 * ratioScore(productGroupCount, 8) +
0.15 * ratioScore(salesQty, customerQtyTarget(suffix))
)
}
function ratioScore (value, target) {
const n = Number(value || 0)
const t = Number(target || 0)
if (!Number.isFinite(n) || !Number.isFinite(t) || n <= 0 || t <= 0) return 0
return Math.min(100, Math.max(0, n) * 100 / t)
}
function marginScore (margin) {
return ratioScore(Math.max(0, Number(margin || 0)), 0.40)
}
function clampScore (score) {
const n = Number(score || 0)
if (!Number.isFinite(n) || n <= 0) return 0
return Math.round(Math.min(100, n) * 10000) / 10000
}
function productRevenueTarget (suffix) {
if (suffix === '180d') return 20000
if (suffix === '365d') return 40000
if (suffix === 'total') return 120000
return 10000
}
function customerRevenueTarget (suffix) {
if (suffix === '180d') return 50000
if (suffix === '365d') return 100000
if (suffix === 'total') return 300000
return 25000
}
function customerQtyTarget (suffix) {
if (suffix === '180d') return 1000
if (suffix === '365d') return 2000
if (suffix === 'total') return 6000
return 500
}
function periodCount (row, prefix, suffix) {
const keyed = Number(row?.[`${prefix}_${suffix}`] || 0)
if (keyed > 0) return keyed
if (suffix === 'total') return Number(row?.[`${prefix}_total`] || 0)
return Number(row?.[`${prefix}_90d`] || row?.[prefix] || 0)
}
function orderGroupPerformanceScore (row) {
const orderUSD = Number(row?.order_usd || 0)
const margin = Number(row?.expected_margin_cost || 0)
@@ -3549,7 +3709,16 @@ function shouldSkipGroupValue (field, value) {
return false
}
function groupCanExpand (rowOrKey) {
const key = typeof rowOrKey === 'string' ? rowOrKey : rowOrKey?.key
const depth = groupKeyDepth(key)
const tabKey = tabKeyFromGroupKey(key)
const levels = tabGroupLevels[tabKey] || groupLevels
return depth >= 0 && depth < levels.length - 1
}
function isGroupExpanded (key) {
if (!groupCanExpand(key)) return false
const tabKey = tabKeyFromGroupKey(key)
const depth = groupKeyDepth(key)
const level = selectedExpandThroughLevelForTab(tabKey)
@@ -3609,6 +3778,7 @@ function applyActiveExpansionThroughLevel (level) {
}
function toggleGroup (key) {
if (!groupCanExpand(key)) return
const level = groupKeyDepth(key)
applyActiveExpansionThroughLevel(isGroupExpanded(key) ? level - 1 : level)
}
@@ -3990,7 +4160,7 @@ function parseSortNumber (value) {
if (!text) return NaN
const normalized = text
.replace(/\s/g, '')
.replace(/[%$₺€£]/g, '')
.replace(/[%$\u20ba\u20ac\u00a3]/g, '')
.replace(/\.(?=\d{3}(\D|$))/g, '')
.replace(/,/g, '.')
.replace(/[^\d.-]/g, '')
@@ -4274,13 +4444,6 @@ function resolveProductImageUrls (item) {
if (url && !urls.includes(url)) urls.push(url)
}
const contentURL = String(item.content_url || item.ContentURL || '').trim()
if (contentURL.startsWith('/api/')) addURL(contentURL)
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
const imageId = Number(item.id || item.ID || 0)
if (Number.isFinite(imageId) && imageId > 0) addURL(`/api/product-images/${imageId}/content`)
const thumbURL = String(item.thumb_url || item.thumbUrl || '').trim()
addURL(thumbURL)
@@ -4292,6 +4455,13 @@ function resolveProductImageUrls (item) {
const fileName = String(item.file_name || item.FileName || '').trim()
if (fileName) addURL(`/uploads/image/${fileName}`)
const contentURL = String(item.content_url || item.ContentURL || '').trim()
if (contentURL.startsWith('/api/')) addURL(contentURL)
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
const imageId = Number(item.id || item.ID || 0)
if (Number.isFinite(imageId) && imageId > 0) addURL(`/api/product-images/${imageId}/content`)
return urls
}
@@ -4369,7 +4539,7 @@ function markProductImageFailed (row, failedUrl = '') {
if (!key) return
const currentUrl = String(failedUrl || imageUrlByKey.value[key] || performanceStore.imageUrl(key) || '').trim()
const list = imageListByKey.value[key] || performanceStore.imageList(key) || []
const nextList = Array.isArray(list) ? list.filter(url => url && url !== currentUrl) : []
const nextList = Array.isArray(list) ? list.filter(url => url && !sameImageUrl(url, currentUrl)) : []
if (nextList.length) {
performanceStore.setImageCache(key, nextList)
imageListByKey.value = { ...imageListByKey.value, [key]: nextList }
@@ -4382,6 +4552,18 @@ function markProductImageFailed (row, failedUrl = '') {
imageUrlByKey.value = { ...imageUrlByKey.value, [key]: '' }
}
function sameImageUrl (left, right) {
const a = String(left || '').trim()
const b = String(right || '').trim()
if (!a || !b) return false
if (a === b) return true
try {
return new URL(a, window.location.origin).href === new URL(b, window.location.origin).href
} catch {
return a.endsWith(b) || b.endsWith(a)
}
}
function markDialogImageFailed (url) {
const failedUrl = String(url || '').trim()
if (!failedUrl) return
@@ -4622,6 +4804,7 @@ async function loadBackendGroupedRows (options = {}) {
backendGroupedLoading.value = false
return
}
void ensureBackendGroupedFilterOptions(tabKey)
pruneActiveExpandedGroups()
backendGroupedLoading.value = true
const filtersKey = JSON.stringify(filterState.filters)
+49 -7
View File
@@ -26,13 +26,6 @@ function resolveProductImageUrls (item) {
if (url && !urls.includes(url)) urls.push(url)
}
const contentURL = String(item.content_url || item.ContentURL || '').trim()
if (contentURL.startsWith('/api/')) addURL(contentURL)
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
const imageId = Number(item.id || item.ID || 0)
if (Number.isFinite(imageId) && imageId > 0) addURL(`/api/product-images/${imageId}/content`)
const thumbURL = String(item.thumb_url || item.thumbUrl || '').trim()
addURL(thumbURL)
@@ -44,6 +37,13 @@ function resolveProductImageUrls (item) {
const fileName = String(item.file_name || item.FileName || '').trim()
if (fileName) addURL(`/uploads/image/${fileName}`)
const contentURL = String(item.content_url || item.ContentURL || '').trim()
if (contentURL.startsWith('/api/')) addURL(contentURL)
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
const imageId = Number(item.id || item.ID || 0)
if (Number.isFinite(imageId) && imageId > 0) addURL(`/api/product-images/${imageId}/content`)
return urls
}
@@ -69,6 +69,9 @@ export const useProductPerformanceStore = defineStore('product-performance-store
groupedLoadedAtByKey: {},
groupedLoadingByKey: {},
groupedInFlightByKey: {},
groupedFilterOptionsByKey: {},
groupedFilterOptionsLoadedAtByKey: {},
groupedFilterOptionsInFlightByKey: {},
imageUrlByKey: {},
imageListByKey: {},
imageLoadedAtByKey: {}
@@ -96,6 +99,16 @@ export const useProductPerformanceStore = defineStore('product-performance-store
})
},
groupedFilterOptionsCacheKey (params = {}) {
return JSON.stringify({
mode: String(params.mode || ''),
mainGroup: String(params.urunAnaGrubu || params.urun_ana_grubu || ''),
groupLevels: normalizedList(params.groupLevels),
fields: normalizedList(params.fields, true),
limit: Number(params.limit || 0)
})
},
async fetchGroupedRows (params = {}, options = {}) {
const key = this.groupedCacheKey(params)
const now = Date.now()
@@ -180,6 +193,35 @@ export const useProductPerformanceStore = defineStore('product-performance-store
return request
},
async fetchGroupedFilterOptions (params = {}, options = {}) {
const key = this.groupedFilterOptionsCacheKey(params)
const now = Date.now()
const loadedAt = Number(this.groupedFilterOptionsLoadedAtByKey[key] || 0)
const cached = this.groupedFilterOptionsByKey[key]
if (!options.force && cached && now - loadedAt < GROUPED_TTL_MS) return cached
if (this.groupedFilterOptionsInFlightByKey[key]) return this.groupedFilterOptionsInFlightByKey[key]
const request = api.post('/pricing/product-performance/grouped/filter-options', {
mode: params.mode,
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || '',
group_levels: Array.isArray(params.groupLevels) ? params.groupLevels : String(params.groupLevels || '').split(',').filter(Boolean),
fields: Array.isArray(params.fields) ? params.fields : String(params.fields || '').split(',').filter(Boolean),
limit: params.limit || 5000
}, { timeout: 60000 }).then(resp => {
const data = resp?.data && typeof resp.data === 'object' ? resp.data : {}
this.groupedFilterOptionsByKey = { ...this.groupedFilterOptionsByKey, [key]: data }
this.groupedFilterOptionsLoadedAtByKey = { ...this.groupedFilterOptionsLoadedAtByKey, [key]: Date.now() }
return data
}).finally(() => {
const nextInFlight = { ...this.groupedFilterOptionsInFlightByKey }
delete nextInFlight[key]
this.groupedFilterOptionsInFlightByKey = nextInFlight
})
this.groupedFilterOptionsInFlightByKey = { ...this.groupedFilterOptionsInFlightByKey, [key]: request }
return request
},
setImageCache (key, urls = []) {
const normalizedKey = String(key || '').trim()
if (!normalizedKey) return