Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-04-17 12:16:36 +03:00
parent f9728b8a4c
commit c6bdf83f05
4 changed files with 43 additions and 30 deletions

View File

@@ -5,17 +5,22 @@ import (
"bssapp-backend/models" "bssapp-backend/models"
"context" "context"
"database/sql" "database/sql"
"fmt" "strconv"
"strings" "strings"
"time" "time"
) )
func GetProductPricingList(ctx context.Context, limit int, offset int) ([]models.ProductPricing, error) { func GetProductPricingList(ctx context.Context, limit int, afterProductCode string) ([]models.ProductPricing, error) {
if limit <= 0 { if limit <= 0 {
limit = 500 limit = 500
} }
if offset < 0 { afterProductCode = strings.TrimSpace(afterProductCode)
offset = 0
cursorFilter := ""
args := make([]any, 0, 1)
if afterProductCode != "" {
cursorFilter = "WHERE bp.ProductCode > @p1"
args = append(args, afterProductCode)
} }
query := ` query := `
@@ -36,11 +41,11 @@ func GetProductPricingList(ctx context.Context, limit int, offset int) ([]models
AND LEN(LTRIM(RTRIM(ProductCode))) = 13 AND LEN(LTRIM(RTRIM(ProductCode))) = 13
), ),
paged_products AS ( paged_products AS (
SELECT SELECT TOP (` + strconv.Itoa(limit) + `)
bp.ProductCode bp.ProductCode
FROM base_products bp FROM base_products bp
` + cursorFilter + `
ORDER BY bp.ProductCode ORDER BY bp.ProductCode
OFFSET %d ROWS FETCH NEXT %d ROWS ONLY
), ),
latest_base_price AS ( latest_base_price AS (
SELECT SELECT
@@ -182,7 +187,6 @@ func GetProductPricingList(ctx context.Context, limit int, offset int) ([]models
ON st.ItemCode = bp.ProductCode ON st.ItemCode = bp.ProductCode
ORDER BY bp.ProductCode; ORDER BY bp.ProductCode;
` `
query = fmt.Sprintf(query, offset, limit)
var ( var (
rows *sql.Rows rows *sql.Rows
@@ -190,7 +194,7 @@ func GetProductPricingList(ctx context.Context, limit int, offset int) ([]models
) )
for attempt := 1; attempt <= 3; attempt++ { for attempt := 1; attempt <= 3; attempt++ {
var err error var err error
rows, err = db.MssqlDB.QueryContext(ctx, query) rows, err = db.MssqlDB.QueryContext(ctx, query, args...)
if err == nil { if err == nil {
rowsErr = nil rowsErr = nil
break break

View File

@@ -35,14 +35,9 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
limit = parsed limit = parsed
} }
} }
offset := 0 afterProductCode := strings.TrimSpace(r.URL.Query().Get("after_product_code"))
if raw := strings.TrimSpace(r.URL.Query().Get("offset")); raw != "" {
if parsed, err := strconv.Atoi(raw); err == nil && parsed >= 0 && parsed <= 1000000 {
offset = parsed
}
}
rows, err := queries.GetProductPricingList(ctx, limit+1, offset) rows, err := queries.GetProductPricingList(ctx, limit+1, afterProductCode)
if err != nil { if err != nil {
if isPricingTimeoutLike(err, ctx.Err()) { if isPricingTimeoutLike(err, ctx.Err()) {
log.Printf( log.Printf(
@@ -71,16 +66,21 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
if hasMore { if hasMore {
rows = rows[:limit] rows = rows[:limit]
} }
nextCursor := ""
if hasMore && len(rows) > 0 {
nextCursor = strings.TrimSpace(rows[len(rows)-1].ProductCode)
}
log.Printf( log.Printf(
"[ProductPricing] trace=%s success user=%s id=%d limit=%d offset=%d count=%d has_more=%t duration_ms=%d", "[ProductPricing] trace=%s success user=%s id=%d limit=%d after=%q count=%d has_more=%t next=%q duration_ms=%d",
traceID, traceID,
claims.Username, claims.Username,
claims.ID, claims.ID,
limit, limit,
offset, afterProductCode,
len(rows), len(rows),
hasMore, hasMore,
nextCursor,
time.Since(started).Milliseconds(), time.Since(started).Milliseconds(),
) )
@@ -90,6 +90,9 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
} else { } else {
w.Header().Set("X-Has-More", "false") w.Header().Set("X-Has-More", "false")
} }
if nextCursor != "" {
w.Header().Set("X-Next-Cursor", nextCursor)
}
_ = json.NewEncoder(w).Encode(rows) _ = json.NewEncoder(w).Encode(rows)
} }

View File

@@ -315,7 +315,7 @@ import { useProductPricingStore } from 'src/stores/ProductPricingStore'
const store = useProductPricingStore() const store = useProductPricingStore()
const FETCH_LIMIT = 500 const FETCH_LIMIT = 500
const currentOffset = ref(0) const nextCursor = ref('')
const loadingMore = ref(false) const loadingMore = ref(false)
const usdToTry = 38.25 const usdToTry = 38.25
@@ -852,14 +852,14 @@ function clearAllCurrencies () {
} }
async function fetchChunk ({ reset = false } = {}) { async function fetchChunk ({ reset = false } = {}) {
const offset = reset ? 0 : currentOffset.value const afterProductCode = reset ? '' : nextCursor.value
const result = await store.fetchRows({ const result = await store.fetchRows({
limit: FETCH_LIMIT, limit: FETCH_LIMIT,
offset, afterProductCode,
append: !reset append: !reset
}) })
const fetched = Number(result?.fetched) || 0 const fetched = Number(result?.fetched) || 0
currentOffset.value = offset + fetched nextCursor.value = String(result?.nextCursor || '')
return fetched return fetched
} }
@@ -894,7 +894,7 @@ async function reloadData () {
console.info('[product-pricing][ui] reload:start', { console.info('[product-pricing][ui] reload:start', {
at: new Date(startedAt).toISOString() at: new Date(startedAt).toISOString()
}) })
currentOffset.value = 0 nextCursor.value = ''
await fetchChunk({ reset: true }) await fetchChunk({ reset: true })
await ensureEnoughVisibleRows(120, 6) await ensureEnoughVisibleRows(120, 6)
console.info('[product-pricing][ui] reload:done', { console.info('[product-pricing][ui] reload:done', {

View File

@@ -10,9 +10,9 @@ function toNumber (value) {
return Number.isFinite(n) ? Number(n.toFixed(2)) : 0 return Number.isFinite(n) ? Number(n.toFixed(2)) : 0
} }
function mapRow (raw, index, offset = 0) { function mapRow (raw, index, baseIndex = 0) {
return { return {
id: offset + index + 1, id: baseIndex + index + 1,
productCode: toText(raw?.ProductCode), productCode: toText(raw?.ProductCode),
stockQty: toNumber(raw?.StockQty), stockQty: toNumber(raw?.StockQty),
stockEntryDate: toText(raw?.StockEntryDate), stockEntryDate: toText(raw?.StockEntryDate),
@@ -64,27 +64,31 @@ export const useProductPricingStore = defineStore('product-pricing-store', {
this.loading = true this.loading = true
this.error = '' this.error = ''
const limit = Number(options?.limit) > 0 ? Number(options.limit) : 500 const limit = Number(options?.limit) > 0 ? Number(options.limit) : 500
const offset = Number(options?.offset) >= 0 ? Number(options.offset) : 0 const afterProductCode = toText(options?.afterProductCode)
const append = Boolean(options?.append) const append = Boolean(options?.append)
const baseIndex = append ? this.rows.length : 0
const startedAt = Date.now() const startedAt = Date.now()
console.info('[product-pricing][frontend] request:start', { console.info('[product-pricing][frontend] request:start', {
at: new Date(startedAt).toISOString(), at: new Date(startedAt).toISOString(),
timeout_ms: 180000, timeout_ms: 180000,
limit, limit,
offset, after_product_code: afterProductCode || null,
append append
}) })
try { try {
const params = { limit }
if (afterProductCode) params.after_product_code = afterProductCode
const res = await api.request({ const res = await api.request({
method: 'GET', method: 'GET',
url: '/pricing/products', url: '/pricing/products',
params: { limit, offset }, params,
timeout: 180000 timeout: 180000
}) })
const traceId = res?.headers?.['x-trace-id'] || null const traceId = res?.headers?.['x-trace-id'] || null
const hasMoreHeader = String(res?.headers?.['x-has-more'] || '').toLowerCase() const hasMoreHeader = String(res?.headers?.['x-has-more'] || '').toLowerCase()
const nextCursorHeader = toText(res?.headers?.['x-next-cursor'])
const data = Array.isArray(res?.data) ? res.data : [] const data = Array.isArray(res?.data) ? res.data : []
const mapped = data.map((x, i) => mapRow(x, i, offset)) const mapped = data.map((x, i) => mapRow(x, i, baseIndex))
if (append) { if (append) {
const merged = [...this.rows] const merged = [...this.rows]
const seen = new Set(this.rows.map((x) => x?.productCode)) const seen = new Set(this.rows.map((x) => x?.productCode))
@@ -104,12 +108,14 @@ export const useProductPricingStore = defineStore('product-pricing-store', {
duration_ms: Date.now() - startedAt, duration_ms: Date.now() - startedAt,
row_count: this.rows.length, row_count: this.rows.length,
fetched_count: mapped.length, fetched_count: mapped.length,
has_more: this.hasMore has_more: this.hasMore,
next_cursor: nextCursorHeader || null
}) })
return { return {
traceId, traceId,
fetched: mapped.length, fetched: mapped.length,
hasMore: this.hasMore hasMore: this.hasMore,
nextCursor: nextCursorHeader
} }
} catch (err) { } catch (err) {
if (!append) this.rows = [] if (!append) this.rows = []