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) } } })