Files
bssapp/ui/src/stores/ProductPricingStore.js
2026-06-18 17:32:03 +03:00

377 lines
13 KiB
JavaScript

import { defineStore } from 'pinia'
import api from 'src/services/api'
function toText (value) {
return String(value ?? '').trim()
}
function toNumber (value) {
const n = parseFlexibleNumber(value)
return Number.isFinite(n) ? Number(n.toFixed(2)) : 0
}
function parseFlexibleNumber (value) {
if (typeof value === 'number') return value
const text = String(value ?? '').trim().replace(/\s/g, '')
if (!text) return 0
const lastComma = text.lastIndexOf(',')
const lastDot = text.lastIndexOf('.')
let normalized = text
if (lastComma >= 0 && lastDot >= 0) {
// Keep the last separator as decimal, remove the other as thousand.
if (lastComma > lastDot) {
normalized = text.replace(/\./g, '').replace(',', '.')
} else {
normalized = text.replace(/,/g, '')
}
} else if (lastComma >= 0) {
normalized = text.replace(/\./g, '').replace(',', '.')
} else {
normalized = text.replace(/,/g, '')
}
const n = Number(normalized)
return Number.isFinite(n) ? n : 0
}
function mapRow (raw, index, baseIndex = 0) {
const row = {
id: baseIndex + index + 1,
productCode: toText(raw?.ProductCode),
stockQty: toNumber(raw?.StockQty),
stockEntryDate: toText(raw?.StockEntryDate),
lastCostingDate: toText(raw?.LastCostingDate),
lastPricingDate: toText(raw?.LastPricingDate),
askiliYan: toText(raw?.AskiliYan),
kategori: toText(raw?.Kategori),
urunIlkGrubu: toText(raw?.UrunIlkGrubu),
urunAnaGrubu: toText(raw?.UrunAnaGrubu),
urunAltGrubu: toText(raw?.UrunAltGrubu),
icerik: toText(raw?.Icerik),
karisim: toText(raw?.Karisim),
marka: toText(raw?.Marka),
brandGroupSelection: toText(raw?.BrandGroupSec),
costPrice: toNumber(raw?.CostPrice),
expenseForBasePrice: 0,
basePriceUsd: toNumber(raw?.BasePriceUsd),
basePriceTry: toNumber(raw?.BasePriceTry),
usd1: toNumber(raw?.USD1),
usd2: toNumber(raw?.USD2),
usd3: toNumber(raw?.USD3),
usd4: toNumber(raw?.USD4),
usd5: toNumber(raw?.USD5),
usd6: toNumber(raw?.USD6),
eur1: toNumber(raw?.EUR1),
eur2: toNumber(raw?.EUR2),
eur3: toNumber(raw?.EUR3),
eur4: toNumber(raw?.EUR4),
eur5: toNumber(raw?.EUR5),
eur6: toNumber(raw?.EUR6),
try1: toNumber(raw?.TRY1),
try2: toNumber(raw?.TRY2),
try3: toNumber(raw?.TRY3),
try4: toNumber(raw?.TRY4),
try5: toNumber(raw?.TRY5),
try6: toNumber(raw?.TRY6)
}
const originalFields = [
'costPrice',
'basePriceUsd',
'basePriceTry',
'usd1', 'usd2', 'usd3', 'usd4', 'usd5', 'usd6',
'eur1', 'eur2', 'eur3', 'eur4', 'eur5', 'eur6',
'try1', 'try2', 'try3', 'try4', 'try5', 'try6'
]
originalFields.forEach((field) => {
row[`__orig_${field}`] = row[field]
})
return row
}
function cloneRows (rows = []) {
return rows.map((r) => ({ ...r }))
}
function normalizeFilterList (list) {
if (!Array.isArray(list)) return []
return list.map((x) => toText(x)).filter(Boolean).sort()
}
function normalizeFilters (filters = {}) {
const keys = ['product_code', 'brand_group_selection', 'askili_yan', 'kategori', 'urun_ilk_grubu', 'urun_ana_grubu', 'urun_alt_grubu', 'icerik', 'karisim', 'marka']
const out = {}
for (const key of keys) out[key] = normalizeFilterList(filters[key])
const q = toText(filters.q)
if (q) out.q = q
return out
}
function hasPrimaryFilter (filters = {}) {
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)
}
function makeCacheKey (limit, page, filters) {
return JSON.stringify({
limit: Number(limit) || 500,
page: Number(page) || 1,
filters: normalizeFilters(filters),
sortBy: toText(filters?.__sortBy),
descending: Boolean(filters?.__descending)
})
}
export const useProductPricingStore = defineStore('product-pricing-store', {
state: () => ({
rows: [],
loading: false,
error: '',
hasMore: true,
page: 1,
totalPages: 1,
totalCount: 0,
pageCache: {},
cacheOrder: [],
prefetchInFlight: {}
}),
actions: {
cachePut (key, value) {
this.pageCache[key] = value
this.cacheOrder = this.cacheOrder.filter((x) => x !== key)
this.cacheOrder.push(key)
while (this.cacheOrder.length > 24) {
const oldest = this.cacheOrder.shift()
if (oldest) delete this.pageCache[oldest]
}
},
cacheGet (key) {
return this.pageCache[key] || null
},
applyPageResult (payload = {}, requestedPage = 1) {
const data = Array.isArray(payload?.rows) ? payload.rows : []
this.rows = cloneRows(data)
this.totalCount = Number.isFinite(payload?.totalCount) ? payload.totalCount : 0
this.totalPages = Math.max(1, Number(payload?.totalPages || 1))
this.page = Math.max(1, Number(payload?.page || requestedPage))
this.hasMore = this.page < this.totalPages
},
async prefetchPage (options = {}) {
const limit = Number(options?.limit) > 0 ? Number(options.limit) : 500
const page = Number(options?.page) > 0 ? Number(options.page) : 1
const filters = normalizeFilters(options?.filters || {})
const sortBy = toText(options?.sortBy)
const descending = Boolean(options?.descending)
const key = makeCacheKey(limit, page, filters)
if (this.pageCache[key]) return
if (this.prefetchInFlight[key]) {
await this.prefetchInFlight[key]
return
}
const run = async () => {
try {
const includeTotal = hasPrimaryFilter(filters) ? 1 : 0
const params = { limit, page, include_total: includeTotal }
if (sortBy) {
params.sort_by = sortBy
params.desc = descending ? 1 : 0
}
for (const k of Object.keys(filters)) {
if (k === 'q') {
params.q = filters.q
continue
}
if (Array.isArray(filters[k]) && filters[k].length > 0) {
params[k] = filters[k].join(',')
}
}
const res = await api.request({
method: 'GET',
url: '/pricing/products',
params,
timeout: 180000
})
const totalCount = Number(res?.headers?.['x-total-count'] || 0)
let totalPages = Math.max(1, Number(res?.headers?.['x-total-pages'] || 0))
const currentPage = Math.max(1, Number(res?.headers?.['x-page'] || page))
const data = Array.isArray(res?.data) ? res.data : []
const mapped = data.map((x, i) => mapRow(x, i, 0))
if (!Number.isFinite(totalPages) || totalPages <= 0) {
totalPages = mapped.length >= limit ? currentPage + 1 : currentPage
}
this.cachePut(key, {
rows: mapped,
totalCount: Number.isFinite(totalCount) ? totalCount : 0,
totalPages: Number.isFinite(totalPages) ? totalPages : 1,
page: currentPage
})
} catch {
}
}
this.prefetchInFlight[key] = run()
try {
await this.prefetchInFlight[key]
} finally {
delete this.prefetchInFlight[key]
}
},
async fetchRows (options = {}) {
const silent = Boolean(options?.silent)
if (!silent) {
this.loading = true
this.error = ''
}
const limit = Number(options?.limit) > 0 ? Number(options.limit) : 500
const page = Number(options?.page) > 0 ? Number(options.page) : 1
const append = Boolean(options?.append)
const baseIndex = append ? this.rows.length : 0
const filters = normalizeFilters(options?.filters || {})
const sortBy = toText(options?.sortBy)
const descending = Boolean(options?.descending)
const cacheKey = makeCacheKey(limit, page, filters)
const startedAt = Date.now()
console.info('[product-pricing][frontend] request:start', {
at: new Date(startedAt).toISOString(),
timeout_ms: 180000,
limit,
page,
append
})
try {
if (options?.useCache !== false) {
const inFlight = this.prefetchInFlight[cacheKey]
if (inFlight) {
await inFlight
}
const cached = this.cacheGet(cacheKey)
if (cached) {
this.applyPageResult(cached, page)
console.info('[product-pricing][frontend] request:cache-hit', {
page: this.page,
total_pages: this.totalPages,
row_count: this.rows.length,
duration_ms: Date.now() - startedAt
})
return {
traceId: null,
fetched: this.rows.length,
hasMore: this.hasMore,
page: this.page,
totalPages: this.totalPages,
totalCount: this.totalCount
}
}
}
const includeTotal = hasPrimaryFilter(filters) ? 1 : 0
const params = { limit, page, include_total: includeTotal }
if (sortBy) {
params.sort_by = sortBy
params.desc = descending ? 1 : 0
}
for (const key of Object.keys(filters)) {
if (key === 'q') {
params.q = filters.q
continue
}
const list = filters[key]
if (Array.isArray(list) && list.length > 0) params[key] = list.join(',')
}
const res = await api.request({
method: 'GET',
url: '/pricing/products',
params,
timeout: 180000
})
const traceId = res?.headers?.['x-trace-id'] || null
const totalCount = Number(res?.headers?.['x-total-count'] || 0)
let totalPages = Math.max(1, Number(res?.headers?.['x-total-pages'] || 0))
const currentPage = Math.max(1, Number(res?.headers?.['x-page'] || page))
const data = Array.isArray(res?.data) ? res.data : []
const mapped = data.map((x, i) => mapRow(x, i, baseIndex))
if (!Number.isFinite(totalPages) || totalPages <= 0) {
// When server skips count, infer "hasMore" from page size.
totalPages = mapped.length >= limit ? currentPage + 1 : currentPage
}
const payload = {
rows: mapped,
totalCount: Number.isFinite(totalCount) ? totalCount : 0,
totalPages: Number.isFinite(totalPages) ? totalPages : 1,
page: Number.isFinite(currentPage) ? currentPage : page
}
this.cachePut(cacheKey, payload)
if (append) {
this.rows = [...cloneRows(this.rows || []), ...mapped.map((r) => ({ ...r }))]
this.totalCount = Number.isFinite(payload.totalCount) ? payload.totalCount : this.totalCount
this.totalPages = Math.max(1, Number(payload.totalPages || this.totalPages || 1))
this.page = Math.max(1, Number(payload.page || page))
this.hasMore = this.page < this.totalPages
} else {
this.applyPageResult(payload, page)
}
// Background prefetch for next page to reduce perceived wait on page change.
if (this.page < this.totalPages) {
void this.prefetchPage({
limit,
page: this.page + 1,
filters
})
}
console.info('[product-pricing][frontend] request:success', {
trace_id: traceId,
duration_ms: Date.now() - startedAt,
row_count: this.rows.length,
fetched_count: mapped.length,
has_more: this.hasMore,
page: this.page,
total_pages: this.totalPages,
total_count: this.totalCount
})
return {
traceId,
fetched: mapped.length,
hasMore: this.hasMore,
page: this.page,
totalPages: this.totalPages,
totalCount: this.totalCount
}
} catch (err) {
this.rows = []
this.hasMore = false
const msg = err?.response?.data || err?.message || 'Urun fiyatlandirma listesi alinamadi'
this.error = toText(msg)
console.error('[product-pricing][frontend] request:error', {
trace_id: err?.response?.headers?.['x-trace-id'] || null,
duration_ms: Date.now() - startedAt,
timeout_ms: err?.config?.timeout ?? null,
status: err?.response?.status || null,
message: this.error
})
throw err
} finally {
if (!silent) this.loading = false
}
},
// fetchAllByGroups removed: keep paging server-side.
updateCell (row, field, val) {
if (!row || !field) return
row[field] = toNumber(val)
},
updateBrandGroupSelection (row, val) {
if (!row) return
row.brandGroupSelection = toText(val)
}
}
})