From 149cea778e0f7e9998c8fc347d2e2d934ad4cd0a Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Thu, 18 Jun 2026 17:31:53 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- svc/queries/product_pricing.go | 76 +++++++++- ui/.quasar/prod-spa/app.js | 75 ++++++++++ ui/.quasar/prod-spa/client-entry.js | 158 +++++++++++++++++++++ ui/.quasar/prod-spa/client-prefetch.js | 116 +++++++++++++++ ui/.quasar/prod-spa/quasar-user-options.js | 23 +++ ui/src/pages/ProductPricing.vue | 9 +- ui/src/pages/WholesaleCampaigns.vue | 52 ++++++- ui/src/stores/ProductPricingStore.js | 3 +- 8 files changed, 505 insertions(+), 7 deletions(-) create mode 100644 ui/.quasar/prod-spa/app.js create mode 100644 ui/.quasar/prod-spa/client-entry.js create mode 100644 ui/.quasar/prod-spa/client-prefetch.js create mode 100644 ui/.quasar/prod-spa/quasar-user-options.js diff --git a/svc/queries/product_pricing.go b/svc/queries/product_pricing.go index 54c4123..b3d1faa 100644 --- a/svc/queries/product_pricing.go +++ b/svc/queries/product_pricing.go @@ -655,6 +655,23 @@ func chunkStringSlice(values []string, size int) [][]string { return out } +func cleanProductPricingFilterValues(values []string) []string { + clean := make([]string, 0, len(values)) + seen := make(map[string]struct{}, len(values)) + for _, v := range values { + v = strings.TrimSpace(v) + if v == "" { + continue + } + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + clean = append(clean, v) + } + return clean +} + func GetProductPricingPage(ctx context.Context, page int, limit int, filters ProductPricingFilters, includeTotal bool, sortBy string, descending bool) (ProductPricingPage, error) { result := ProductPricingPage{ Rows: []models.ProductPricing{}, @@ -770,7 +787,9 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro result.Limit = limit } - // Stage 1: fetch only paged products first (fast path). + // Stage 1: fetch only paged products first. Exact product-code filters do not + // need the stock sort temp table here; detailed metrics are fetched below. + productCodeFastPath := len(cleanProductPricingFilterValues(filters.ProductCode)) > 0 sortBy = strings.TrimSpace(sortBy) orderDir := "DESC" if !descending { @@ -857,6 +876,61 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro OFFSET ` + strconv.Itoa(offset) + ` ROWS FETCH NEXT ` + strconv.Itoa(limit) + ` ROWS ONLY; ` + if productCodeFastPath { + productQuery = ` + IF OBJECT_ID('tempdb..#req_codes') IS NOT NULL DROP TABLE #req_codes; + + SELECT + f.ProductCode, + MAX(f.BrandGroupSec) AS BrandGroupSec, + MAX(f.AskiliYan) AS AskiliYan, + MAX(f.Kategori) AS Kategori, + MAX(f.UrunIlkGrubu) AS UrunIlkGrubu, + MAX(f.UrunAnaGrubu) AS UrunAnaGrubu, + MAX(f.UrunAltGrubu) AS UrunAltGrubu, + MAX(f.Icerik) AS Icerik, + MAX(f.Karisim) AS Karisim, + MAX(f.Marka) AS Marka, + MAX(f.BrandCode) AS BrandCode + INTO #req_codes + FROM ( + SELECT + LTRIM(RTRIM(ProductCode)) AS ProductCode, + ` + brandGroupExpr + ` AS BrandGroupSec, + COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') AS AskiliYan, + COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') AS Kategori, + COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') AS UrunIlkGrubu, + COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '') AS UrunAnaGrubu, + COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') AS UrunAltGrubu, + COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') AS Icerik, + COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') AS Karisim, + COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') AS Marka, + COALESCE(LTRIM(RTRIM(ProductAtt10)), '') AS BrandCode + FROM ProductFilterWithDescription('TR') + WHERE ` + whereSQL + ` + ) f + GROUP BY f.ProductCode; + + CREATE CLUSTERED INDEX IX_req_codes_ProductCode ON #req_codes(ProductCode); + + SELECT + rc.ProductCode, + rc.BrandGroupSec, + rc.AskiliYan, + rc.Kategori, + rc.UrunIlkGrubu, + rc.UrunAnaGrubu, + rc.UrunAltGrubu, + rc.Icerik, + rc.Karisim, + rc.Marka, + rc.BrandCode + FROM #req_codes rc + ORDER BY rc.ProductCode ASC + OFFSET ` + strconv.Itoa(offset) + ` ROWS + FETCH NEXT ` + strconv.Itoa(limit) + ` ROWS ONLY; + ` + } var ( rows *sql.Rows diff --git a/ui/.quasar/prod-spa/app.js b/ui/.quasar/prod-spa/app.js new file mode 100644 index 0000000..caeaac1 --- /dev/null +++ b/ui/.quasar/prod-spa/app.js @@ -0,0 +1,75 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + + + + + +import { Quasar } from 'quasar' +import { markRaw } from 'vue' +import RootComponent from 'app/src/App.vue' + +import createStore from 'app/src/stores/index' +import createRouter from 'app/src/router/index' + + + + + +export default async function (createAppFn, quasarUserOptions) { + + + // Create the app instance. + // Here we inject into it the Quasar UI, the router & possibly the store. + const app = createAppFn(RootComponent) + + + + app.use(Quasar, quasarUserOptions) + + + + + const store = typeof createStore === 'function' + ? await createStore({}) + : createStore + + + app.use(store) + + + + + + const router = markRaw( + typeof createRouter === 'function' + ? await createRouter({store}) + : createRouter + ) + + + // make router instance available in store + + store.use(({ store }) => { store.router = router }) + + + + // Expose the app, the router and the store. + // Note that we are not mounting the app here, since bootstrapping will be + // different depending on whether we are in a browser or on the server. + return { + app, + store, + router + } +} diff --git a/ui/.quasar/prod-spa/client-entry.js b/ui/.quasar/prod-spa/client-entry.js new file mode 100644 index 0000000..5223e2b --- /dev/null +++ b/ui/.quasar/prod-spa/client-entry.js @@ -0,0 +1,158 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + + +import { createApp } from 'vue' + + + + + + + +import '@quasar/extras/roboto-font/roboto-font.css' + +import '@quasar/extras/material-icons/material-icons.css' + + + + +// We load Quasar stylesheet file +import 'quasar/dist/quasar.sass' + + + + +import 'src/css/app.css' + + +import createQuasarApp from './app.js' +import quasarUserOptions from './quasar-user-options.js' + + + + + + + + +const publicPath = `/` + + +async function start ({ + app, + router + , store +}, bootFiles) { + + let hasRedirected = false + const getRedirectUrl = url => { + try { return router.resolve(url).href } + catch (err) {} + + return Object(url) === url + ? null + : url + } + const redirect = url => { + hasRedirected = true + + if (typeof url === 'string' && /^https?:\/\//.test(url)) { + window.location.href = url + return + } + + const href = getRedirectUrl(url) + + // continue if we didn't fail to resolve the url + if (href !== null) { + window.location.href = href + window.location.reload() + } + } + + const urlPath = window.location.href.replace(window.location.origin, '') + + for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) { + try { + await bootFiles[i]({ + app, + router, + store, + ssrContext: null, + redirect, + urlPath, + publicPath + }) + } + catch (err) { + if (err && err.url) { + redirect(err.url) + return + } + + console.error('[Quasar] boot error:', err) + return + } + } + + if (hasRedirected === true) return + + + app.use(router) + + + + + + + app.mount('#q-app') + + + +} + +createQuasarApp(createApp, quasarUserOptions) + + .then(app => { + // eventually remove this when Cordova/Capacitor/Electron support becomes old + const [ method, mapFn ] = Promise.allSettled !== void 0 + ? [ + 'allSettled', + bootFiles => bootFiles.map(result => { + if (result.status === 'rejected') { + console.error('[Quasar] boot error:', result.reason) + return + } + return result.value.default + }) + ] + : [ + 'all', + bootFiles => bootFiles.map(entry => entry.default) + ] + + return Promise[ method ]([ + + import(/* webpackMode: "eager" */ 'boot/dayjs'), + + import(/* webpackMode: "eager" */ 'boot/locale'), + + import(/* webpackMode: "eager" */ 'boot/resizeObserverGuard') + + ]).then(bootFiles => { + const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function') + start(app, boot) + }) + }) + diff --git a/ui/.quasar/prod-spa/client-prefetch.js b/ui/.quasar/prod-spa/client-prefetch.js new file mode 100644 index 0000000..9bbe3c5 --- /dev/null +++ b/ui/.quasar/prod-spa/client-prefetch.js @@ -0,0 +1,116 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + + + +import App from 'app/src/App.vue' +let appPrefetch = typeof App.preFetch === 'function' + ? App.preFetch + : ( + // Class components return the component options (and the preFetch hook) inside __c property + App.__c !== void 0 && typeof App.__c.preFetch === 'function' + ? App.__c.preFetch + : false + ) + + +function getMatchedComponents (to, router) { + const route = to + ? (to.matched ? to : router.resolve(to).route) + : router.currentRoute.value + + if (!route) { return [] } + + const matched = route.matched.filter(m => m.components !== void 0) + + if (matched.length === 0) { return [] } + + return Array.prototype.concat.apply([], matched.map(m => { + return Object.keys(m.components).map(key => { + const comp = m.components[key] + return { + path: m.path, + c: comp + } + }) + })) +} + +export function addPreFetchHooks ({ router, store, publicPath }) { + // Add router hook for handling preFetch. + // Doing it after initial route is resolved so that we don't double-fetch + // the data that we already have. Using router.beforeResolve() so that all + // async components are resolved. + router.beforeResolve((to, from, next) => { + const + urlPath = window.location.href.replace(window.location.origin, ''), + matched = getMatchedComponents(to, router), + prevMatched = getMatchedComponents(from, router) + + let diffed = false + const preFetchList = matched + .filter((m, i) => { + return diffed || (diffed = ( + !prevMatched[i] || + prevMatched[i].c !== m.c || + m.path.indexOf('/:') > -1 // does it has params? + )) + }) + .filter(m => m.c !== void 0 && ( + typeof m.c.preFetch === 'function' + // Class components return the component options (and the preFetch hook) inside __c property + || (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function') + )) + .map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch) + + + if (appPrefetch !== false) { + preFetchList.unshift(appPrefetch) + appPrefetch = false + } + + + if (preFetchList.length === 0) { + return next() + } + + let hasRedirected = false + const redirect = url => { + hasRedirected = true + next(url) + } + const proceed = () => { + + if (hasRedirected === false) { next() } + } + + + + preFetchList.reduce( + (promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({ + store, + currentRoute: to, + previousRoute: from, + redirect, + urlPath, + publicPath + })), + Promise.resolve() + ) + .then(proceed) + .catch(e => { + console.error(e) + proceed() + }) + }) +} diff --git a/ui/.quasar/prod-spa/quasar-user-options.js b/ui/.quasar/prod-spa/quasar-user-options.js new file mode 100644 index 0000000..ac1dae3 --- /dev/null +++ b/ui/.quasar/prod-spa/quasar-user-options.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + +import lang from 'quasar/lang/tr.js' + + + +import {Loading,Dialog,Notify} from 'quasar' + + + +export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} } + diff --git a/ui/src/pages/ProductPricing.vue b/ui/src/pages/ProductPricing.vue index 6c81de5..329c51f 100644 --- a/ui/src/pages/ProductPricing.vue +++ b/ui/src/pages/ProductPricing.vue @@ -2441,7 +2441,9 @@ function scheduleReload () { async function fetchChunk ({ page = 1, useCache = true } = {}) { const filters = buildServerFilters() const hasAnyFilter = Object.values(filters).some((v) => Array.isArray(v) && v.length > 0) + const productCodeCount = Array.isArray(filters.product_code) ? filters.product_code.length : 0 const hasPrimaryFilter = (filters.urun_ilk_grubu?.length || 0) > 0 || (filters.urun_ana_grubu?.length || 0) > 0 + const hasNarrowProductFilter = productCodeCount > 0 if (!hasAnyFilter) { // This endpoint is expensive without filters; require the user to scope down first. store.rows = [] @@ -2452,7 +2454,7 @@ async function fetchChunk ({ page = 1, useCache = true } = {}) { store.hasMore = false return 0 } - if (!hasPrimaryFilter) { + if (!hasPrimaryFilter && !hasNarrowProductFilter) { store.rows = [] store.error = GUIDANCE_MSG store.totalCount = 0 @@ -2461,8 +2463,11 @@ async function fetchChunk ({ page = 1, useCache = true } = {}) { store.hasMore = false return 0 } + const effectiveLimit = hasNarrowProductFilter + ? Math.max(productCodeCount, 1) + : PAGE_LIMIT const result = await store.fetchRows({ - limit: PAGE_LIMIT, + limit: effectiveLimit, page, append: false, silent: false, diff --git a/ui/src/pages/WholesaleCampaigns.vue b/ui/src/pages/WholesaleCampaigns.vue index 1948ae6..0ab6f41 100644 --- a/ui/src/pages/WholesaleCampaigns.vue +++ b/ui/src/pages/WholesaleCampaigns.vue @@ -869,6 +869,8 @@ const PAGE_LIMIT = 50 const currentPage = ref(1) let reloadTimer = null const variantRows = ref([]) +const variantRowsCache = new Map() +const VARIANT_ROWS_CACHE_LIMIT = 16 const GUIDANCE_MSG = "Calismak icin once Urun Ilk Grubu veya Urun Ana Grubu Secin ve GRUPLARI GETIR'e Basin." @@ -2145,6 +2147,7 @@ async function calculateRow (row) { }) Notify.create({ type: 'positive', message: 'Secilen kayitlar silindi.' }) selectedCampaignHistoryIds.value = [] + clearVariantRowsCache() await reloadCampaignHistory() await reloadData({ page: currentPage.value, useCache: false }) } @@ -2314,6 +2317,7 @@ async function saveSelectedRows () { // This avoids "Kaydet(1) but checkbox not ticked" confusion and ensures UI reflects DB. selectedMap.value = {} showSelectedOnly.value = false + clearVariantRowsCache() await reloadData({ page: currentPage.value, useCache: false }) } catch (err) { console.error('[wholesale-campaigns][ui] save:error', { @@ -2450,7 +2454,9 @@ function scheduleReload () { async function fetchChunk ({ page = 1, useCache = true } = {}) { const filters = buildServerFilters() const hasAnyFilter = Object.values(filters).some((v) => Array.isArray(v) && v.length > 0) + const productCodeCount = Array.isArray(filters.product_code) ? filters.product_code.length : 0 const hasPrimaryFilter = (filters.urun_ilk_grubu?.length || 0) > 0 || (filters.urun_ana_grubu?.length || 0) > 0 + const hasNarrowProductFilter = productCodeCount > 0 if (!hasAnyFilter) { // This endpoint is expensive without filters; require the user to scope down first. store.rows = [] @@ -2462,7 +2468,7 @@ async function fetchChunk ({ page = 1, useCache = true } = {}) { store.hasMore = false return 0 } - if (!hasPrimaryFilter) { + if (!hasPrimaryFilter && !hasNarrowProductFilter) { store.rows = [] variantRows.value = [] store.error = GUIDANCE_MSG @@ -2472,8 +2478,11 @@ async function fetchChunk ({ page = 1, useCache = true } = {}) { store.hasMore = false return 0 } + const effectiveLimit = hasNarrowProductFilter + ? Math.max(productCodeCount, 1) + : PAGE_LIMIT const result = await store.fetchRows({ - limit: PAGE_LIMIT, + limit: effectiveLimit, page, append: false, silent: false, @@ -2558,6 +2567,36 @@ function onRowCampaignChange (row, val) { toggleRowSelection(rowSelectionKey(row), true) } +function makeVariantRowsCacheKey (codes = []) { + return codes + .map((x) => String(x || '').trim()) + .filter(Boolean) + .sort() + .join(',') +} + +function getVariantRowsCache (key) { + if (!key || !variantRowsCache.has(key)) return null + const cached = variantRowsCache.get(key) + variantRowsCache.delete(key) + variantRowsCache.set(key, cached) + return Array.isArray(cached) ? cached.map((x) => ({ ...x })) : null +} + +function putVariantRowsCache (key, rows = []) { + if (!key) return + variantRowsCache.set(key, Array.isArray(rows) ? rows.map((x) => ({ ...x })) : []) + while (variantRowsCache.size > VARIANT_ROWS_CACHE_LIMIT) { + const oldest = variantRowsCache.keys().next().value + if (!oldest) break + variantRowsCache.delete(oldest) + } +} + +function clearVariantRowsCache () { + variantRowsCache.clear() +} + async function buildVariantRowsForProductPage (baseProductRows = []) { const base = Array.isArray(baseProductRows) ? baseProductRows : [] const codes = base.map((r) => String(r?.productCode || '').trim()).filter(Boolean) @@ -2565,6 +2604,12 @@ async function buildVariantRowsForProductPage (baseProductRows = []) { variantRows.value = [] return } + const cacheKey = makeVariantRowsCacheKey(codes) + const cached = getVariantRowsCache(cacheKey) + if (cached) { + variantRows.value = cached + return + } variantLoading.value = true try { @@ -2627,9 +2672,10 @@ async function buildVariantRowsForProductPage (baseProductRows = []) { } applyCampaignDerived(row) out.push(row) - } } + } variantRows.value = out + putVariantRowsCache(cacheKey, out) } catch (err) { console.error('[wholesale-campaigns][ui] variant-rows:error', { status: err?.response?.status ?? null, diff --git a/ui/src/stores/ProductPricingStore.js b/ui/src/stores/ProductPricingStore.js index ba57de0..d964ca6 100644 --- a/ui/src/stores/ProductPricingStore.js +++ b/ui/src/stores/ProductPricingStore.js @@ -109,7 +109,8 @@ function normalizeFilters (filters = {}) { } function hasPrimaryFilter (filters = {}) { - return (Array.isArray(filters.urun_ilk_grubu) && filters.urun_ilk_grubu.length > 0) || + return (Array.isArray(filters.product_code) && filters.product_code.length > 0) || + (Array.isArray(filters.urun_ilk_grubu) && filters.urun_ilk_grubu.length > 0) || (Array.isArray(filters.urun_ana_grubu) && filters.urun_ana_grubu.length > 0) }