From 7ef12df93a9001602ff0832ce42482a525ef4418 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Mon, 20 Apr 2026 09:42:34 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- svc/queries/product_pricing.go | 362 ++++++++++++++++---------------- svc/routes/product_pricing.go | 3 +- svc/routes/translations.go | 14 ++ ui/src/pages/ProductPricing.vue | 13 +- 4 files changed, 212 insertions(+), 180 deletions(-) diff --git a/svc/queries/product_pricing.go b/svc/queries/product_pricing.go index f2cac04..8aa15b2 100644 --- a/svc/queries/product_pricing.go +++ b/svc/queries/product_pricing.go @@ -5,6 +5,7 @@ import ( "bssapp-backend/models" "context" "database/sql" + "fmt" "strconv" "strings" "time" @@ -16,176 +17,24 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri } afterProductCode = strings.TrimSpace(afterProductCode) - cursorFilter := "" - args := make([]any, 0, 1) - if afterProductCode != "" { - cursorFilter = "WHERE bp.ProductCode > @p1" - args = append(args, afterProductCode) - } - - query := ` - WITH base_products AS ( - SELECT - LTRIM(RTRIM(ProductCode)) AS ProductCode, - 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 - FROM ProductFilterWithDescription('TR') - WHERE ProductAtt42 IN ('SERI', 'AKSESUAR') - AND IsBlocked = 0 - AND LEN(LTRIM(RTRIM(ProductCode))) = 13 - ), - paged_products AS ( - SELECT TOP (` + strconv.Itoa(limit) + `) - bp.ProductCode - FROM base_products bp - ` + cursorFilter + ` - ORDER BY bp.ProductCode - ), - latest_base_price AS ( - SELECT - LTRIM(RTRIM(b.ItemCode)) AS ItemCode, - CAST(b.Price AS DECIMAL(18, 2)) AS CostPrice, - CONVERT(VARCHAR(10), b.PriceDate, 23) AS LastPricingDate, - ROW_NUMBER() OVER ( - PARTITION BY LTRIM(RTRIM(b.ItemCode)) - ORDER BY b.PriceDate DESC, b.LastUpdatedDate DESC - ) AS rn - FROM prItemBasePrice b - WHERE b.ItemTypeCode = 1 - AND b.BasePriceCode = 1 - AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD' - AND EXISTS ( - SELECT 1 - FROM paged_products pp - WHERE pp.ProductCode = LTRIM(RTRIM(b.ItemCode)) - ) - ), - stock_entry_dates AS ( - SELECT - LTRIM(RTRIM(s.ItemCode)) AS ItemCode, - CONVERT(VARCHAR(10), MAX(s.OperationDate), 23) AS StockEntryDate - FROM trStock s WITH(NOLOCK) - WHERE s.ItemTypeCode = 1 - AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13 - AND s.In_Qty1 > 0 - AND LTRIM(RTRIM(s.WarehouseCode)) IN ( - '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', - '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', - '1-0-33','101','1-014','1-0-49','1-0-36' - ) - AND EXISTS ( - SELECT 1 - FROM paged_products pp - WHERE pp.ProductCode = LTRIM(RTRIM(s.ItemCode)) - ) - GROUP BY LTRIM(RTRIM(s.ItemCode)) - ), - stock_base AS ( - SELECT - LTRIM(RTRIM(s.ItemCode)) AS ItemCode, - SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1 - FROM trStock s WITH(NOLOCK) - WHERE s.ItemTypeCode = 1 - AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13 - AND EXISTS ( - SELECT 1 - FROM paged_products pp - WHERE pp.ProductCode = LTRIM(RTRIM(s.ItemCode)) - ) - GROUP BY LTRIM(RTRIM(s.ItemCode)) - ), - pick_base AS ( - SELECT - LTRIM(RTRIM(p.ItemCode)) AS ItemCode, - SUM(p.Qty1) AS PickingQty1 - FROM PickingStates p - WHERE p.ItemTypeCode = 1 - AND LEN(LTRIM(RTRIM(p.ItemCode))) = 13 - AND EXISTS ( - SELECT 1 - FROM paged_products pp - WHERE pp.ProductCode = LTRIM(RTRIM(p.ItemCode)) - ) - GROUP BY LTRIM(RTRIM(p.ItemCode)) - ), - reserve_base AS ( - SELECT - LTRIM(RTRIM(r.ItemCode)) AS ItemCode, - SUM(r.Qty1) AS ReserveQty1 - FROM ReserveStates r - WHERE r.ItemTypeCode = 1 - AND LEN(LTRIM(RTRIM(r.ItemCode))) = 13 - AND EXISTS ( - SELECT 1 - FROM paged_products pp - WHERE pp.ProductCode = LTRIM(RTRIM(r.ItemCode)) - ) - GROUP BY LTRIM(RTRIM(r.ItemCode)) - ), - disp_base AS ( - SELECT - LTRIM(RTRIM(d.ItemCode)) AS ItemCode, - SUM(d.Qty1) AS DispOrderQty1 - FROM DispOrderStates d - WHERE d.ItemTypeCode = 1 - AND LEN(LTRIM(RTRIM(d.ItemCode))) = 13 - AND EXISTS ( - SELECT 1 - FROM paged_products pp - WHERE pp.ProductCode = LTRIM(RTRIM(d.ItemCode)) - ) - GROUP BY LTRIM(RTRIM(d.ItemCode)) - ), - stock_totals AS ( - SELECT - pp.ProductCode AS ItemCode, - CAST(ROUND( - ISNULL(sb.InventoryQty1, 0) - - ISNULL(pb.PickingQty1, 0) - - ISNULL(rb.ReserveQty1, 0) - - ISNULL(db.DispOrderQty1, 0) - , 2) AS DECIMAL(18, 2)) AS StockQty - FROM paged_products pp - LEFT JOIN stock_base sb - ON sb.ItemCode = pp.ProductCode - LEFT JOIN pick_base pb - ON pb.ItemCode = pp.ProductCode - LEFT JOIN reserve_base rb - ON rb.ItemCode = pp.ProductCode - LEFT JOIN disp_base db - ON db.ItemCode = pp.ProductCode - ) - SELECT - bp.ProductCode AS ProductCode, - COALESCE(lp.CostPrice, 0) AS CostPrice, - COALESCE(st.StockQty, 0) AS StockQty, - COALESCE(se.StockEntryDate, '') AS StockEntryDate, - COALESCE(lp.LastPricingDate, '') AS LastPricingDate, - bp.AskiliYan, - bp.Kategori, - bp.UrunIlkGrubu, - bp.UrunAnaGrubu, - bp.UrunAltGrubu, - bp.Icerik, - bp.Karisim, - bp.Marka - FROM paged_products pp - INNER JOIN base_products bp - ON bp.ProductCode = pp.ProductCode - LEFT JOIN latest_base_price lp - ON lp.ItemCode = bp.ProductCode - AND lp.rn = 1 - LEFT JOIN stock_entry_dates se - ON se.ItemCode = bp.ProductCode - LEFT JOIN stock_totals st - ON st.ItemCode = bp.ProductCode - ORDER BY bp.ProductCode; + // Stage 1: fetch only paged products first (fast path). + productQuery := ` + SELECT TOP (` + strconv.Itoa(limit) + `) + LTRIM(RTRIM(ProductCode)) AS ProductCode, + 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 + FROM ProductFilterWithDescription('TR') + WHERE ProductAtt42 IN ('SERI', 'AKSESUAR') + AND IsBlocked = 0 + AND LEN(LTRIM(RTRIM(ProductCode))) = 13 + AND (@p1 = '' OR LTRIM(RTRIM(ProductCode)) > @p1) + ORDER BY LTRIM(RTRIM(ProductCode)); ` var ( @@ -194,7 +43,7 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri ) for attempt := 1; attempt <= 3; attempt++ { var err error - rows, err = db.MssqlDB.QueryContext(ctx, query, args...) + rows, err = db.MssqlDB.QueryContext(ctx, productQuery, afterProductCode) if err == nil { rowsErr = nil break @@ -215,15 +64,11 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri } defer rows.Close() - var out []models.ProductPricing + out := make([]models.ProductPricing, 0, limit) for rows.Next() { var item models.ProductPricing if err := rows.Scan( &item.ProductCode, - &item.CostPrice, - &item.StockQty, - &item.StockEntryDate, - &item.LastPricingDate, &item.AskiliYan, &item.Kategori, &item.UrunIlkGrubu, @@ -237,6 +82,171 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri } out = append(out, item) } + if err := rows.Err(); err != nil { + return nil, err + } + if len(out) == 0 { + return out, nil + } + + // Stage 2: fetch metrics only for paged product codes. + codes := make([]string, 0, len(out)) + for _, item := range out { + codes = append(codes, strings.TrimSpace(item.ProductCode)) + } + codesCSV := strings.Join(codes, ",") + + metricsQuery := ` + WITH req_codes AS ( + SELECT DISTINCT LTRIM(RTRIM(value)) AS ProductCode + FROM STRING_SPLIT(@p1, ',') + WHERE LEN(LTRIM(RTRIM(value))) > 0 + ), + latest_base_price AS ( + SELECT + LTRIM(RTRIM(b.ItemCode)) AS ItemCode, + CAST(b.Price AS DECIMAL(18, 2)) AS CostPrice, + CONVERT(VARCHAR(10), b.PriceDate, 23) AS LastPricingDate, + ROW_NUMBER() OVER ( + PARTITION BY LTRIM(RTRIM(b.ItemCode)) + ORDER BY b.PriceDate DESC, b.LastUpdatedDate DESC + ) AS rn + FROM prItemBasePrice b + INNER JOIN req_codes rc + ON rc.ProductCode = LTRIM(RTRIM(b.ItemCode)) + WHERE b.ItemTypeCode = 1 + AND b.BasePriceCode = 1 + AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD' + ), + stock_entry_dates AS ( + SELECT + LTRIM(RTRIM(s.ItemCode)) AS ItemCode, + CONVERT(VARCHAR(10), MAX(s.OperationDate), 23) AS StockEntryDate + FROM trStock s WITH(NOLOCK) + INNER JOIN req_codes rc + ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode)) + WHERE s.ItemTypeCode = 1 + AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13 + AND s.In_Qty1 > 0 + AND LTRIM(RTRIM(s.WarehouseCode)) IN ( + '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', + '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', + '1-0-33','101','1-014','1-0-49','1-0-36' + ) + GROUP BY LTRIM(RTRIM(s.ItemCode)) + ), + stock_base AS ( + SELECT + LTRIM(RTRIM(s.ItemCode)) AS ItemCode, + SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1 + FROM trStock s WITH(NOLOCK) + INNER JOIN req_codes rc + ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode)) + WHERE s.ItemTypeCode = 1 + AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13 + GROUP BY LTRIM(RTRIM(s.ItemCode)) + ), + pick_base AS ( + SELECT + LTRIM(RTRIM(p.ItemCode)) AS ItemCode, + SUM(p.Qty1) AS PickingQty1 + FROM PickingStates p + INNER JOIN req_codes rc + ON rc.ProductCode = LTRIM(RTRIM(p.ItemCode)) + WHERE p.ItemTypeCode = 1 + AND LEN(LTRIM(RTRIM(p.ItemCode))) = 13 + GROUP BY LTRIM(RTRIM(p.ItemCode)) + ), + reserve_base AS ( + SELECT + LTRIM(RTRIM(r.ItemCode)) AS ItemCode, + SUM(r.Qty1) AS ReserveQty1 + FROM ReserveStates r + INNER JOIN req_codes rc + ON rc.ProductCode = LTRIM(RTRIM(r.ItemCode)) + WHERE r.ItemTypeCode = 1 + AND LEN(LTRIM(RTRIM(r.ItemCode))) = 13 + GROUP BY LTRIM(RTRIM(r.ItemCode)) + ), + disp_base AS ( + SELECT + LTRIM(RTRIM(d.ItemCode)) AS ItemCode, + SUM(d.Qty1) AS DispOrderQty1 + FROM DispOrderStates d + INNER JOIN req_codes rc + ON rc.ProductCode = LTRIM(RTRIM(d.ItemCode)) + WHERE d.ItemTypeCode = 1 + AND LEN(LTRIM(RTRIM(d.ItemCode))) = 13 + GROUP BY LTRIM(RTRIM(d.ItemCode)) + ) + SELECT + rc.ProductCode, + COALESCE(lp.CostPrice, 0) AS CostPrice, + CAST(ROUND( + ISNULL(sb.InventoryQty1, 0) + - ISNULL(pb.PickingQty1, 0) + - ISNULL(rb.ReserveQty1, 0) + - ISNULL(db.DispOrderQty1, 0) + , 2) AS DECIMAL(18, 2)) AS StockQty, + COALESCE(se.StockEntryDate, '') AS StockEntryDate, + COALESCE(lp.LastPricingDate, '') AS LastPricingDate + FROM req_codes rc + LEFT JOIN latest_base_price lp + ON lp.ItemCode = rc.ProductCode + AND lp.rn = 1 + LEFT JOIN stock_entry_dates se + ON se.ItemCode = rc.ProductCode + LEFT JOIN stock_base sb + ON sb.ItemCode = rc.ProductCode + LEFT JOIN pick_base pb + ON pb.ItemCode = rc.ProductCode + LEFT JOIN reserve_base rb + ON rb.ItemCode = rc.ProductCode + LEFT JOIN disp_base db + ON db.ItemCode = rc.ProductCode; + ` + + metricsRows, err := db.MssqlDB.QueryContext(ctx, metricsQuery, codesCSV) + if err != nil { + return nil, fmt.Errorf("metrics query failed: %w", err) + } + defer metricsRows.Close() + + type metrics struct { + CostPrice float64 + StockQty float64 + StockEntryDate string + LastPricingDate string + } + metricsByCode := make(map[string]metrics, len(out)) + for metricsRows.Next() { + var ( + code string + m metrics + ) + if err := metricsRows.Scan( + &code, + &m.CostPrice, + &m.StockQty, + &m.StockEntryDate, + &m.LastPricingDate, + ); err != nil { + return nil, err + } + metricsByCode[strings.TrimSpace(code)] = m + } + if err := metricsRows.Err(); err != nil { + return nil, err + } + + for i := range out { + if m, ok := metricsByCode[strings.TrimSpace(out[i].ProductCode)]; ok { + out[i].CostPrice = m.CostPrice + out[i].StockQty = m.StockQty + out[i].StockEntryDate = m.StockEntryDate + out[i].LastPricingDate = m.LastPricingDate + } + } return out, nil } diff --git a/svc/routes/product_pricing.go b/svc/routes/product_pricing.go index 46d20a4..12104ab 100644 --- a/svc/routes/product_pricing.go +++ b/svc/routes/product_pricing.go @@ -26,7 +26,8 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) { } log.Printf("[ProductPricing] trace=%s start user=%s id=%d", traceID, claims.Username, claims.ID) - ctx, cancel := context.WithTimeout(r.Context(), 180*time.Second) + // Cloudflare upstream timeout is lower than 180s; fail fast and return API 504 instead of CDN 524. + ctx, cancel := context.WithTimeout(r.Context(), 110*time.Second) defer cancel() limit := 500 diff --git a/svc/routes/translations.go b/svc/routes/translations.go index 5fb3a60..25c6a57 100644 --- a/svc/routes/translations.go +++ b/svc/routes/translations.go @@ -59,6 +59,7 @@ var ( reScriptLabelProp = regexp.MustCompile(`\blabel\s*:\s*['"]([^'"]{2,180})['"]`) reScriptUIProp = regexp.MustCompile(`\b(?:label|message|title|placeholder|hint)\s*:\s*['"]([^'"]{2,180})['"]`) reTemplateDynamic = regexp.MustCompile(`[{][{]|[}][}]`) + reCodeLikeText = regexp.MustCompile(`(?i)(\bconst\b|\blet\b|\bvar\b|\breturn\b|\bfunction\b|=>|\|\||&&|\?\?|//|/\*|\*/|\.trim\(|\.replace\(|\.map\(|\.filter\()`) ) var translationNoiseTokens = map[string]struct{}{ @@ -1800,9 +1801,22 @@ func isCandidateText(s string) bool { if strings.Contains(s, "/api/") { return false } + if reCodeLikeText.MatchString(s) { + return false + } if strings.ContainsAny(s, "{}[];`") { return false } + symbolCount := 0 + for _, r := range s { + switch r { + case '(', ')', '=', ':', '/', '\\', '|', '&', '*', '<', '>', '_': + symbolCount++ + } + } + if symbolCount >= 4 { + return false + } return true } diff --git a/ui/src/pages/ProductPricing.vue b/ui/src/pages/ProductPricing.vue index 4b4cefb..ed80c05 100644 --- a/ui/src/pages/ProductPricing.vue +++ b/ui/src/pages/ProductPricing.vue @@ -894,9 +894,16 @@ async function reloadData () { console.info('[product-pricing][ui] reload:start', { at: new Date(startedAt).toISOString() }) - nextCursor.value = '' - await fetchChunk({ reset: true }) - await ensureEnoughVisibleRows(120, 6) + try { + nextCursor.value = '' + await fetchChunk({ reset: true }) + await ensureEnoughVisibleRows(120, 6) + } catch (err) { + console.error('[product-pricing][ui] reload:error', { + duration_ms: Date.now() - startedAt, + message: String(err?.message || err || 'reload failed') + }) + } console.info('[product-pricing][ui] reload:done', { duration_ms: Date.now() - startedAt, row_count: Array.isArray(store.rows) ? store.rows.length : 0,