Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-06-18 17:31:53 +03:00
parent f8c0fe338a
commit 149cea778e
8 changed files with 505 additions and 7 deletions

View File

@@ -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

View File

@@ -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 <name>" 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
}
}

View File

@@ -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 <name>" 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)
})
})

View File

@@ -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 <name>" 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()
})
})
}

View File

@@ -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 <name>" 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} }

View File

@@ -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,

View File

@@ -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 {
@@ -2630,6 +2675,7 @@ async function buildVariantRowsForProductPage (baseProductRows = []) {
}
}
variantRows.value = out
putVariantRowsCache(cacheKey, out)
} catch (err) {
console.error('[wholesale-campaigns][ui] variant-rows:error', {
status: err?.response?.status ?? null,

View File

@@ -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)
}