Files
tehran_taj/apps/user-app/src/stores/cartStore.ts
T
2026-09-21 08:32:31 +03:30

138 lines
4.8 KiB
TypeScript

// Ported from legacy henza_pwa/src/store/cartStore.ts, adapted to our string
// ids and flat (non-paginated) product list. Persisted the same way (a single
// localStorage key), just via Pinia's own state instead of @vueuse's
// useStorage wrapping a hand-rolled class.
import { defineStore } from 'pinia'
import { api, type ProductSummary } from '../api/client'
export interface CartItem {
pid: string
vid: string
product: ProductSummary
qty: number
note?: string | null
}
const STORAGE_KEY = 'appCart'
function loadFromStorage(): CartItem[] {
try {
const raw = localStorage.getItem(STORAGE_KEY)
return raw ? JSON.parse(raw) : []
} catch {
return []
}
}
function persist(items: CartItem[]) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(items))
}
export const useCartStore = defineStore('cart', {
state: () => ({
items: loadFromStorage() as CartItem[],
}),
actions: {
// Re-syncs cart entries against fresh product data (e.g. price/weight
// changes since the item was added), dropping entries whose
// product/variant no longer exists — same intent as legacy's init().
async init() {
try {
const products = await api.listProducts()
const refreshed: CartItem[] = []
for (const item of this.items) {
const product = products.find((p) => p.id === item.pid)
const variant = product?.variants.find((v) => v.id === item.vid)
if (product && variant) {
refreshed.push({ ...item, product })
}
}
this.items = refreshed
persist(this.items)
} catch (e) {
console.log(e)
}
},
addToCart(data: CartItem) {
// Refuse a variant that cannot be ordered — out of stock and not flagged
// pre-order. The same guard the admin cart applies (stores/cart.ts), kept
// here so it holds even if a caller forgets to disable its button.
const target = data.product?.variants?.find((v) => v.id === data.vid)
if (!target) return false
if (!((target.qty ?? 0) > 0 || target.isPreOrder)) return false
const index = this.find_index(data)
if (index === -1) this.items.push({ ...data, qty: this.capQty(data.qty, target) })
else {
this.items[index].product = data.product
this.items[index].qty = this.capQty(data.qty, target)
}
persist(this.items)
return true
},
/**
* Stock ceiling for a cart line. A pre-order variant is orderable beyond
* what is on hand (that is the point of pre-ordering), so it is uncapped;
* anything else stops at its on-hand quantity.
*/
capQty(want: number, variant: { qty?: number; isPreOrder?: boolean }) {
const n = Math.max(1, Math.floor(want) || 1)
if (variant.isPreOrder) return n
const max = variant.qty ?? 0
return max > 0 ? Math.min(n, max) : n
},
/** The ceiling for an item already in the cart; null when uncapped. */
maxQty(item: Pick<CartItem, 'pid' | 'vid'>) {
const i = this.find_index(item)
if (i === -1) return null
const v = this.variant(this.items[i])
if (!v || v.isPreOrder) return null
return (v.qty ?? 0) > 0 ? (v.qty as number) : null
},
remove(item: CartItem) {
const index = this.find_index(item)
if (index > -1) this.items.splice(index, 1)
persist(this.items)
},
// Legacy's exact name (typo'd "Feild"), kept for consistency with the rest
// of this faithfully-ported store's method names.
updateFeild(value: unknown, index: number | null, item: Pick<CartItem, 'pid' | 'vid'>, field: keyof CartItem) {
if (!this.isInCart(item)) return false
const i = index ?? this.find_index(item)
// A quantity change is clamped to what is actually in stock (unless the
// variant is pre-orderable), so the ceiling holds no matter which screen
// drives the stepper.
if (field === 'qty') {
const v = this.variant(this.items[i])
value = v ? this.capQty(Number(value), v) : Math.max(1, Number(value) || 1)
}
;(this.items[i] as any)[field] = value
persist(this.items)
return true
},
find_index(item: Pick<CartItem, 'pid' | 'vid'>) {
return this.items.findIndex((cartItem) => cartItem.pid === item.pid && cartItem.vid === item.vid)
},
isInCart(item: Pick<CartItem, 'pid' | 'vid'>) {
return this.find_index(item) > -1
},
variant(item: CartItem) {
return item.product.variants.find((v) => v.id === item.vid)
},
total() {
return this.items
.reduce((sum, item) => {
const v = this.variant(item)
return sum + (v ? parseFloat(v.rashWeightGrams) * item.qty : 0)
}, 0)
.toFixed(2)
},
clear() {
this.items = []
persist(this.items)
},
},
})