Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|||||||
Reference in New Issue
Block a user