feat: catalog read cache (api + user apps) and clearer Zarrin accounting errors
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,39 +1,16 @@
|
||||
// The real GET /customers/:id/orders endpoint returns OrderItem rows with
|
||||
// only `productVariantId` (no nested product/variant title) — this joins
|
||||
// against the already-existing GET /catalog/products list client-side rather
|
||||
// against the shared catalogue (stores/catalogStore.ts) client-side rather
|
||||
// than touching apps/api to add an include.
|
||||
import { ref } from 'vue'
|
||||
import { api } from '../api/client'
|
||||
import { useCatalogStore, type VariantLookup } from '../stores/catalogStore'
|
||||
|
||||
export interface VariantLookup {
|
||||
productTitle: string
|
||||
variantTitle: string
|
||||
imageUrl?: string
|
||||
}
|
||||
|
||||
const cache = ref<Map<string, VariantLookup> | null>(null)
|
||||
|
||||
async function ensureLoaded() {
|
||||
if (cache.value) return cache.value
|
||||
const products = await api.listProducts()
|
||||
const map = new Map<string, VariantLookup>()
|
||||
for (const product of products) {
|
||||
for (const variant of product.variants) {
|
||||
map.set(variant.id, {
|
||||
productTitle: product.title,
|
||||
variantTitle: variant.title,
|
||||
imageUrl: product.images[0]?.url,
|
||||
})
|
||||
}
|
||||
}
|
||||
cache.value = map
|
||||
return map
|
||||
}
|
||||
export type { VariantLookup }
|
||||
|
||||
export function useOrderItemProduct() {
|
||||
const catalog = useCatalogStore()
|
||||
const lookup = async (productVariantId: string): Promise<VariantLookup | undefined> => {
|
||||
const map = await ensureLoaded()
|
||||
return map.get(productVariantId)
|
||||
if (!catalog.loaded) await catalog.load()
|
||||
return catalog.variantLookup.get(productVariantId)
|
||||
}
|
||||
return { lookup }
|
||||
}
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
// Adapted from legacy singleProduct.vue's own fetch (GET /product/:id).
|
||||
// The new API has no single-product-detail endpoint (only GET
|
||||
// /catalog/products, the flat list) — apps/api is out of scope for this port,
|
||||
// so this fetches the full list and finds the matching product client-side.
|
||||
// Wasteful for a large catalog, but honest: no fabricated endpoint, no silent
|
||||
// behavior change.
|
||||
// /catalog/products, the flat list), so the product is found in the shared
|
||||
// catalogue (stores/catalogStore.ts). When the catalogue is already cached,
|
||||
// opening a product costs no request at all.
|
||||
//
|
||||
// `initial` seeds the page with the ProductSummary the tapped card already
|
||||
// holds — the same shape, from the same endpoint — so the detail view renders
|
||||
// complete on its first frame instead of mounting blank and jumping when the
|
||||
// fetch lands. The fetch still runs, silently, and only replaces the product
|
||||
// if the server's copy actually differs (stock moved since the list loaded).
|
||||
// fetch lands. The lookup still runs, silently, and only replaces the product
|
||||
// if the catalogue's copy actually differs (stock moved since the list loaded).
|
||||
//
|
||||
// The silent refresh waits until the opening morph (ProductMorph.vue, ≤420ms)
|
||||
// is over: the response is the whole catalogue, and parsing it — or swapping
|
||||
// the product and re-rendering — mid-animation drops frames.
|
||||
// is over: if the catalogue has to come from the server, parsing it — or
|
||||
// swapping the product and re-rendering — mid-animation drops frames.
|
||||
import { ref } from 'vue'
|
||||
import { api, type ProductSummary } from '../api/client'
|
||||
import type { ProductSummary } from '../api/client'
|
||||
import { useCatalogStore } from '../stores/catalogStore'
|
||||
|
||||
const SEEDED_REFRESH_DELAY_MS = 600
|
||||
|
||||
export function useProductDetail(id: string, initial?: ProductSummary | null) {
|
||||
const catalog = useCatalogStore()
|
||||
const loading = ref(false)
|
||||
const flag = ref(Boolean(initial))
|
||||
const notFound = ref(false)
|
||||
@@ -30,7 +31,7 @@ export function useProductDetail(id: string, initial?: ProductSummary | null) {
|
||||
if (seeded) await new Promise((resolve) => setTimeout(resolve, SEEDED_REFRESH_DELAY_MS))
|
||||
else loading.value = true
|
||||
try {
|
||||
const products = await api.listProducts()
|
||||
const products = await catalog.load()
|
||||
const found = products.find((p) => p.id === id) ?? null
|
||||
if (seeded) {
|
||||
if (found && JSON.stringify(found) !== JSON.stringify(productRef.value)) productRef.value = found
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// Adapted from legacy henza_pwa/src/Mixins/getProducts.ts. Legacy paginated
|
||||
// against POST /get-products (page/limit/count). The new API's GET
|
||||
// /catalog/products returns the full active catalog in one flat response with
|
||||
// no pagination — so `pull()` (legacy's infinite-scroll "next page" trigger)
|
||||
// is a no-op after the first successful fetch here, and `totalPages` is
|
||||
// always 0. This keeps the ScrollFetchSearch infinite-scroll wiring intact
|
||||
// (it still calls fetch on scroll) without fabricating fake pages.
|
||||
import { reactive } from 'vue'
|
||||
import { api, type ProductSummary } from '../api/client'
|
||||
// no pagination — so `isPull` (legacy's infinite-scroll "next page" trigger)
|
||||
// is a no-op here, and `totalPages` is always 0. A caller that wants new
|
||||
// products at the end of the list passes `force` to refresh the cached
|
||||
// catalogue instead of fabricating fake pages.
|
||||
import { reactive, watch } from 'vue'
|
||||
import type { ProductSummary } from '../api/client'
|
||||
import { useCatalogStore } from '../stores/catalogStore'
|
||||
|
||||
export interface ProductsData {
|
||||
totalPages: number
|
||||
@@ -15,17 +16,27 @@ export interface ProductsData {
|
||||
}
|
||||
|
||||
export function useProducts() {
|
||||
const catalog = useCatalogStore()
|
||||
const productsData: ProductsData = reactive({
|
||||
totalPages: 0,
|
||||
productList: [],
|
||||
productList: catalog.products,
|
||||
flag: false,
|
||||
})
|
||||
|
||||
const getProducts = async (loading: { value: boolean }, isPull = false) => {
|
||||
// A refresh from another screen (pull-to-refresh) lands here too.
|
||||
watch(
|
||||
() => catalog.products,
|
||||
(list) => {
|
||||
productsData.productList = list
|
||||
productsData.flag = list.length < 1
|
||||
},
|
||||
)
|
||||
|
||||
const getProducts = async (loading: { value: boolean }, isPull = false, force = false) => {
|
||||
if (isPull) return // no further pages exist server-side yet
|
||||
loading.value = true
|
||||
loading.value = force || !catalog.loaded
|
||||
try {
|
||||
productsData.productList = await api.listProducts()
|
||||
productsData.productList = await catalog.load(force)
|
||||
productsData.flag = productsData.productList.length < 1
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
|
||||
Reference in New Issue
Block a user