377 lines
13 KiB
JavaScript
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)
|
|
}
|
|
}
|
|
})
|