From d1fbe60aeb279e261c28449092a5e3e42e651eb9 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Thu, 18 Jun 2026 17:49:30 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- svc/queries/product_pricing.go | 105 +++++++++++++++++++++- svc/queries/product_pricing_fx_publish.go | 41 +-------- svc/routes/product_pricing_save.go | 86 ++++++------------ ui/src/pages/ProductPricing.vue | 44 ++++++++- ui/src/pages/WholesaleCampaigns.vue | 45 +++++++++- 5 files changed, 216 insertions(+), 105 deletions(-) diff --git a/svc/queries/product_pricing.go b/svc/queries/product_pricing.go index b3d1faa..de6a756 100644 --- a/svc/queries/product_pricing.go +++ b/svc/queries/product_pricing.go @@ -672,6 +672,20 @@ func cleanProductPricingFilterValues(values []string) []string { return clean } +func isProductCodeOnlyPricingFilter(filters ProductPricingFilters) bool { + return len(cleanProductPricingFilterValues(filters.ProductCode)) > 0 && + strings.TrimSpace(filters.Search) == "" && + len(cleanProductPricingFilterValues(filters.BrandGroup)) == 0 && + len(cleanProductPricingFilterValues(filters.AskiliYan)) == 0 && + len(cleanProductPricingFilterValues(filters.Kategori)) == 0 && + len(cleanProductPricingFilterValues(filters.UrunIlkGrubu)) == 0 && + len(cleanProductPricingFilterValues(filters.UrunAnaGrubu)) == 0 && + len(cleanProductPricingFilterValues(filters.UrunAltGrubu)) == 0 && + len(cleanProductPricingFilterValues(filters.Icerik)) == 0 && + len(cleanProductPricingFilterValues(filters.Karisim)) == 0 && + len(cleanProductPricingFilterValues(filters.Marka)) == 0 +} + func GetProductPricingPage(ctx context.Context, page int, limit int, filters ProductPricingFilters, includeTotal bool, sortBy string, descending bool) (ProductPricingPage, error) { result := ProductPricingPage{ Rows: []models.ProductPricing{}, @@ -751,8 +765,28 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro }, " OR ")+")") } whereSQL := strings.Join(whereParts, " AND ") + cleanProductCodes := cleanProductPricingFilterValues(filters.ProductCode) + productCodeOnlyFilter := isProductCodeOnlyPricingFilter(filters) - if includeTotal { + if includeTotal && productCodeOnlyFilter { + result.TotalCount = len(cleanProductCodes) + if result.TotalCount == 0 { + result.TotalPages = 0 + result.Page = 1 + return result, nil + } + totalPages := int(math.Ceil(float64(result.TotalCount) / float64(limit))) + if totalPages <= 0 { + totalPages = 1 + } + if page > totalPages { + page = totalPages + offset = (page - 1) * limit + } + result.Page = page + result.Limit = limit + result.TotalPages = totalPages + } else if includeTotal { countQuery := ` SELECT COUNT(DISTINCT LTRIM(RTRIM(ProductCode))) FROM ProductFilterWithDescription('TR') @@ -789,7 +823,7 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro // 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 + productCodeFastPath := len(cleanProductCodes) > 0 sortBy = strings.TrimSpace(sortBy) orderDir := "DESC" if !descending { @@ -876,7 +910,72 @@ 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 { + if productCodeOnlyFilter { + valueRows := make([]string, 0, len(cleanProductCodes)) + directArgs := make([]any, 0, len(cleanProductCodes)) + for i, code := range cleanProductCodes { + paramName := "@p" + strconv.Itoa(i+1) + valueRows = append(valueRows, "("+paramName+")") + directArgs = append(directArgs, code) + } + args = directArgs + productQuery = ` + WITH req_codes AS ( + SELECT DISTINCT LTRIM(RTRIM(v.ProductCode)) AS ProductCode + FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(ProductCode) + WHERE LEN(LTRIM(RTRIM(v.ProductCode))) > 0 + ), + attr AS ( + SELECT + LTRIM(RTRIM(a.ItemCode)) AS ProductCode, + MAX(CASE WHEN a.AttributeTypeCode = 45 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS AskiliYan, + MAX(CASE WHEN a.AttributeTypeCode = 44 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Kategori, + MAX(CASE WHEN a.AttributeTypeCode = 42 THEN LTRIM(RTRIM(a.AttributeCode)) ELSE '' END) AS UrunIlkGrubuCode, + MAX(CASE WHEN a.AttributeTypeCode = 42 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS UrunIlkGrubu, + MAX(CASE WHEN a.AttributeTypeCode = 1 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS UrunAnaGrubu, + MAX(CASE WHEN a.AttributeTypeCode = 2 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS UrunAltGrubu, + MAX(CASE WHEN a.AttributeTypeCode = 41 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Icerik, + MAX(CASE WHEN a.AttributeTypeCode = 29 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Karisim, + MAX(CASE WHEN a.AttributeTypeCode = 10 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Marka, + MAX(CASE WHEN a.AttributeTypeCode = 10 THEN LTRIM(RTRIM(a.AttributeCode)) ELSE '' END) AS BrandCode + FROM dbo.prItemAttribute a WITH(NOLOCK) + INNER JOIN req_codes rc + ON rc.ProductCode = LTRIM(RTRIM(a.ItemCode)) + LEFT JOIN dbo.cdItemAttributeDesc d WITH(NOLOCK) + ON d.ItemTypeCode = a.ItemTypeCode + AND d.AttributeTypeCode = a.AttributeTypeCode + AND d.AttributeCode = a.AttributeCode + AND d.LangCode = 'TR' + WHERE a.ItemTypeCode = 1 + AND a.AttributeTypeCode IN (1,2,10,29,41,42,44,45) + GROUP BY LTRIM(RTRIM(a.ItemCode)) + ) + SELECT + rc.ProductCode, + CAST('' AS NVARCHAR(100)) AS BrandGroupSec, + COALESCE(attr.AskiliYan, '') AS AskiliYan, + COALESCE(attr.Kategori, '') AS Kategori, + COALESCE(attr.UrunIlkGrubu, '') AS UrunIlkGrubu, + COALESCE(attr.UrunAnaGrubu, '') AS UrunAnaGrubu, + COALESCE(attr.UrunAltGrubu, '') AS UrunAltGrubu, + COALESCE(attr.Icerik, '') AS Icerik, + COALESCE(attr.Karisim, '') AS Karisim, + COALESCE(attr.Marka, '') AS Marka, + COALESCE(attr.BrandCode, '') AS BrandCode + FROM req_codes rc + INNER JOIN dbo.cdItem ci WITH(NOLOCK) + ON ci.ItemTypeCode = 1 + AND LTRIM(RTRIM(ci.ItemCode)) = rc.ProductCode + LEFT JOIN attr + ON attr.ProductCode = rc.ProductCode + WHERE ISNULL(ci.IsBlocked, 0) = 0 + AND LEN(LTRIM(RTRIM(ci.ItemCode))) = 13 + AND COALESCE(attr.UrunIlkGrubuCode, '') IN ('SERI', 'AKSESUAR') + ORDER BY rc.ProductCode ASC + OFFSET ` + strconv.Itoa(offset) + ` ROWS + FETCH NEXT ` + strconv.Itoa(limit) + ` ROWS ONLY; + ` + } else if productCodeFastPath { productQuery = ` IF OBJECT_ID('tempdb..#req_codes') IS NOT NULL DROP TABLE #req_codes; diff --git a/svc/queries/product_pricing_fx_publish.go b/svc/queries/product_pricing_fx_publish.go index 0217fa1..cab81de 100644 --- a/svc/queries/product_pricing_fx_publish.go +++ b/svc/queries/product_pricing_fx_publish.go @@ -258,8 +258,9 @@ norm AS ( COALESCE(price, 0) AS price FROM input ), - -- Prefer PG's authoritative variant dimension table (mmitem_dim). Fall back to cache table if needed. - dims_mmitem_dim AS ( + -- Only PG's authoritative variant dimension table may drive delta writes. + -- Do not union sdprc/cache dimensions; stale rows there can re-create wrong variant keys. + dims AS ( SELECT norm.product_code AS product_code, md.val1::bigint AS dim1, @@ -275,41 +276,7 @@ norm AS ( AND COALESCE(md.is_active, TRUE) = TRUE WHERE md.val1 IS NOT NULL AND md.val1 > 0 - GROUP BY norm.product_code, md.val1, md.val2 - ), - dims_cache_table AS ( - SELECT - NULLIF(BTRIM(c.product_code), '') AS product_code, - c.dim1::bigint AS dim1, - c.dim3::bigint AS dim3 - FROM mk_mmitem_dim_combo c - JOIN norm - ON norm.product_code = c.product_code - WHERE c.dim1 IS NOT NULL - ), - dims_cache AS ( - SELECT product_code, dim1, dim3 FROM dims_mmitem_dim - UNION - SELECT product_code, dim1, dim3 FROM dims_cache_table - ), - dims_sdprc AS ( - SELECT - norm.product_code AS product_code, - s.dim1 AS dim1, - s.dim3 AS dim3 - FROM norm - JOIN mmitem mm - ON mm.code = norm.product_code - JOIN sdprc s - ON s.mmitem_id = mm.id - WHERE s.dim1 IS NOT NULL - AND s.dim1 > 0 - GROUP BY norm.product_code, s.dim1, s.dim3 - ), - dims AS ( - SELECT product_code, dim1, dim3 FROM dims_cache - UNION - SELECT product_code, dim1, dim3 FROM dims_sdprc + GROUP BY norm.product_code, md.val1, md.val3 ), mapped AS ( SELECT diff --git a/svc/routes/product_pricing_save.go b/svc/routes/product_pricing_save.go index 947d9ed..35483c9 100644 --- a/svc/routes/product_pricing_save.go +++ b/svc/routes/product_pricing_save.go @@ -431,37 +431,6 @@ WHERE is_active = TRUE return out } - loadDimCombosFromCache := func(productCode string) ([]dimCombo, error) { - productCode = strings.TrimSpace(productCode) - if productCode == "" { - return nil, nil - } - rows, err := pgTx.QueryContext(ctx, ` -SELECT dim1, dim3 -FROM mk_mmitem_dim_combo -WHERE product_code = $1 -ORDER BY dim1, dim3_key -`, productCode) - if err != nil { - return nil, err - } - defer rows.Close() - - out := make([]dimCombo, 0, 32) - for rows.Next() { - var d1 int64 - var d3 sql.NullInt64 - if err := rows.Scan(&d1, &d3); err != nil { - return nil, err - } - if d1 <= 0 { - continue - } - out = append(out, dimCombo{Dim1: d1, Dim3: d3}) - } - return out, rows.Err() - } - parseDimID := func(s string) (int64, bool) { s = strings.TrimSpace(s) if s == "" { @@ -812,7 +781,7 @@ filtered AS ( AND price > 0 ), grouped AS ( - -- Ensure one row per business key to avoid unique violations under strict constraints (e.g. uq_sdprc_3). + -- Ensure one row per business key to avoid unique violations under strict constraints. SELECT sdprcgrp_id, currency AS crn, @@ -822,16 +791,35 @@ grouped AS ( FROM filtered GROUP BY sdprcgrp_id, currency, dim1, dim3 ), -upserted AS ( +updated AS ( + UPDATE sdprc s + SET prc = g.prc, + zlins_dttm = now() + FROM grouped g + WHERE s.mmitem_id = $2::bigint + AND s.sdprcgrp_id = g.sdprcgrp_id + AND s.crn = g.crn + AND s.dim1 = g.dim1 + AND COALESCE(s.dim3, 0) = COALESCE(g.dim3, 0) + AND s.prc IS DISTINCT FROM g.prc + RETURNING 1 +), +inserted AS ( INSERT INTO sdprc (mmitem_id, sdprcgrp_id, crn, dim1, dim3, prc, zlins_dttm) SELECT $2::bigint, g.sdprcgrp_id, g.crn, g.dim1, g.dim3, g.prc, now() FROM grouped g - ON CONFLICT ON CONSTRAINT uq_sdprc_3 - DO UPDATE SET prc = EXCLUDED.prc, zlins_dttm = EXCLUDED.zlins_dttm - WHERE sdprc.prc IS DISTINCT FROM EXCLUDED.prc + WHERE NOT EXISTS ( + SELECT 1 + FROM sdprc s + WHERE s.mmitem_id = $2::bigint + AND s.sdprcgrp_id = g.sdprcgrp_id + AND s.crn = g.crn + AND s.dim1 = g.dim1 + AND COALESCE(s.dim3, 0) = COALESCE(g.dim3, 0) + ) RETURNING 1 ) -SELECT COUNT(*)::int FROM upserted; +SELECT ((SELECT COUNT(*) FROM updated) + (SELECT COUNT(*) FROM inserted))::int; ` var inserted int if err := pgTx.QueryRowContext(ctx, q, raw, mmItemID).Scan(&inserted); err != nil { @@ -1083,28 +1071,8 @@ VALUES ( _ = upsertDimCombosCache(code, dims) // best-effort cache fill } - // 2) Cache fallback (fast). - cacheStarted := time.Now() - if len(dims) == 0 { - cached, cacheErr := loadDimCombosFromCache(code) - if cacheErr == nil && len(cached) > 0 { - dims = cached - logger.Info("save:pg:dims:cache:hit", - "product_code", code, - "dims", len(dims), - "duration_ms", time.Since(cacheStarted).Milliseconds(), - ) - } else if cacheErr != nil { - logger.Error("save:pg:dims:cache-load:error", "product_code", code, "err", cacheErr) - } else { - logger.Info("save:pg:dims:cache:miss", - "product_code", code, - "duration_ms", time.Since(cacheStarted).Milliseconds(), - ) - } - } - - // 3) Last resort: MSSQL stock tokens (legacy). + // 2) Last resort: MSSQL stock tokens, then seed mmitem_dim. Do not use + // mk_mmitem_dim_combo as a write source; stale cache rows can create wrong keys. if len(dims) == 0 { d, err := loadDimsFromMssqlStock(code) if err != nil { diff --git a/ui/src/pages/ProductPricing.vue b/ui/src/pages/ProductPricing.vue index 329c51f..8850573 100644 --- a/ui/src/pages/ProductPricing.vue +++ b/ui/src/pages/ProductPricing.vue @@ -3,6 +3,18 @@ +
+ +
Yukleniyor...
+
Urun Fiyatlandirma
@@ -69,7 +81,7 @@ :disable="pageBusy" @click="leftDetailsExpanded = !leftDetailsExpanded" /> - + Tumunu Sec @@ -126,7 +138,7 @@
- + @@ -2503,7 +2515,12 @@ async function reloadData ({ page = 1, useCache = true } = {}) { await bindHorizontalScrollSync() // Let the table render before we re-enable actions (prevents double-submits while the UI is still updating). await nextTick() - await new Promise((resolve) => setTimeout(resolve, 0)) + const remainingBusyMs = Math.max(0, 350 - (Date.now() - startedAt)) + await new Promise((resolve) => setTimeout(resolve, remainingBusyMs)) + console.info('[product-pricing][ui] render:done', { + duration_ms: Date.now() - startedAt, + row_count: Array.isArray(store.rows) ? store.rows.length : 0 + }) isReloading.value = false } } @@ -2574,6 +2591,27 @@ onBeforeUnmount(() => { min-width: 170px; } +.page-busy-overlay { + position: fixed; + inset: 0; + z-index: 9000; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + background: rgba(255, 255, 255, 0.72); + backdrop-filter: blur(1px); + cursor: wait; + pointer-events: all; +} + +.page-busy-label { + color: #1f2937; + font-size: 13px; + font-weight: 700; +} + .top-actions { display: flex; flex-direction: column; diff --git a/ui/src/pages/WholesaleCampaigns.vue b/ui/src/pages/WholesaleCampaigns.vue index 0ab6f41..5a8685e 100644 --- a/ui/src/pages/WholesaleCampaigns.vue +++ b/ui/src/pages/WholesaleCampaigns.vue @@ -3,6 +3,18 @@ +
+ +
Yukleniyor...
+
Toptan Kampanya Yonetimi
@@ -69,7 +81,7 @@ :disable="pageBusy" @click="leftDetailsExpanded = !leftDetailsExpanded" /> - + Tumunu Sec @@ -130,7 +142,7 @@
- + @@ -2713,7 +2725,13 @@ async function reloadData ({ page = 1, useCache = true } = {}) { }) await bindHorizontalScrollSync() await nextTick() - await new Promise((resolve) => setTimeout(resolve, 0)) + const remainingBusyMs = Math.max(0, 350 - (Date.now() - startedAt)) + await new Promise((resolve) => setTimeout(resolve, remainingBusyMs)) + console.info('[product-pricing][ui] render:done', { + duration_ms: Date.now() - startedAt, + row_count: Array.isArray(store.rows) ? store.rows.length : 0, + variant_row_count: Array.isArray(variantRows.value) ? variantRows.value.length : 0 + }) isReloading.value = false } } @@ -2787,6 +2805,27 @@ onBeforeUnmount(() => { min-width: 170px; } +.page-busy-overlay { + position: fixed; + inset: 0; + z-index: 9000; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + background: rgba(255, 255, 255, 0.72); + backdrop-filter: blur(1px); + cursor: wait; + pointer-events: all; +} + +.page-busy-label { + color: #1f2937; + font-size: 13px; + font-weight: 700; +} + .top-actions { display: flex; flex-direction: column;