Fix product performance grouped JSON build
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user