2026-09-18 12:54:45 +03:30
|
|
|
|
// Adapts our actual ProductSummary (apps/user-app/src/api/client.ts) into the
|
|
|
|
|
|
// same output shape legacy's Mixins/product/index.ts `useProduct()` produced.
|
|
|
|
|
|
// The legacy card components it was originally shaped for (ProductHorizontal/
|
|
|
|
|
|
// ProductVertical/ProductVerticalFull) are gone, replaced by the redesigned
|
|
|
|
|
|
// components/product/ProductCard.vue, components/home/ShowcaseCard.vue and
|
|
|
|
|
|
// components/product/CartItem.vue — all of which consume this same contract.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Honest gaps vs. legacy (no backend equivalent exists yet — apps/api was not
|
|
|
|
|
|
// touched to add these):
|
|
|
|
|
|
// - promotions/discounts: legacy matched a customer's order against admin
|
|
|
|
|
|
// promotion + ignore-list rules (getCandidPromotions/getPromotion). No such
|
|
|
|
|
|
// data exists in the new API, so `hasDiscountWage` is always null here —
|
|
|
|
|
|
// the discount badge and "before/after" price never render, which is
|
|
|
|
|
|
// correct given there is genuinely nothing to show, not a bug.
|
2026-10-08 11:26:55 +03:30
|
|
|
|
// - `wage` now comes from the server (wageQuoteStore), which adds the
|
|
|
|
|
|
// customer's own percent and the promotion exactly as legacy did.
|
2026-09-18 12:54:45 +03:30
|
|
|
|
// - `is_exist` (stock) and `has_warranty`: no such fields exist on our
|
|
|
|
|
|
// ProductVariant model yet — always true / false respectively.
|
|
|
|
|
|
import { computed, ref, unref, type Ref } from 'vue'
|
|
|
|
|
|
import type { ProductSummary, ProductVariantSummary } from '../api/client'
|
2026-10-08 11:26:55 +03:30
|
|
|
|
import { useSessionStore } from '../stores/session'
|
|
|
|
|
|
import { useWageQuoteStore } from '../stores/wageQuoteStore'
|
2026-09-18 12:54:45 +03:30
|
|
|
|
|
2026-09-22 18:35:33 +03:30
|
|
|
|
/** A variant can be ordered when it has stock, or when it is flagged pre-order. */
|
|
|
|
|
|
export function isVariantOrderable(v: ProductVariantSummary): boolean {
|
|
|
|
|
|
return (v.qty ?? 0) > 0 || Boolean(v.isPreOrder)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 12:54:45 +03:30
|
|
|
|
function getCheapestVariant(variants: ProductVariantSummary[] | undefined): ProductVariantSummary | null {
|
|
|
|
|
|
if (!variants || variants.length === 0) return null
|
2026-09-22 18:35:33 +03:30
|
|
|
|
// Prefer something the customer can actually order: defaulting to an
|
|
|
|
|
|
// out-of-stock variant means the page opens on a disabled button even though
|
|
|
|
|
|
// other weights are available. Falls back to the full list when nothing is
|
|
|
|
|
|
// orderable, so the page still shows a selection.
|
|
|
|
|
|
const pool = variants.filter(isVariantOrderable)
|
|
|
|
|
|
const candidates = pool.length ? pool : variants
|
|
|
|
|
|
return candidates.reduce((cheapest, v) =>
|
2026-09-18 12:54:45 +03:30
|
|
|
|
parseFloat(v.defaultWagePercent) < parseFloat(cheapest.defaultWagePercent) ? v : cheapest,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function useProduct(productItem: ProductSummary, initialVariant?: ProductVariantSummary) {
|
|
|
|
|
|
const product = ref<ProductSummary>(productItem)
|
|
|
|
|
|
const image = computed(() => product.value.images?.[0]?.url)
|
|
|
|
|
|
const id = computed(() => product.value.id)
|
|
|
|
|
|
const title = computed(() => product.value.title)
|
|
|
|
|
|
const variants = computed(() => product.value.variants)
|
|
|
|
|
|
const selectedVariant: Ref<ProductVariantSummary | null> = ref(initialVariant ?? getCheapestVariant(unref(variants)))
|
|
|
|
|
|
const min_gram = computed(() => selectedVariant.value ? parseFloat(selectedVariant.value.rashWeightGrams) : 0)
|
2026-10-08 11:26:55 +03:30
|
|
|
|
// The making charge the server will charge (MON-08): variant + the
|
|
|
|
|
|
// customer's own percent − the best active promotion, by the formula the
|
|
|
|
|
|
// admin «اجرت» screen controls. Signed in: the server's figure, hidden for
|
|
|
|
|
|
// the moment it loads rather than flashing a different number. Signed out
|
|
|
|
|
|
// (no customer, so no customer percent): the variant's own figure.
|
|
|
|
|
|
const session = useSessionStore()
|
|
|
|
|
|
const wageQuotes = useWageQuoteStore()
|
|
|
|
|
|
const wage = computed(() => {
|
|
|
|
|
|
const v = selectedVariant.value
|
|
|
|
|
|
if (!v) return null
|
|
|
|
|
|
if (!session.isLoggedIn) return parseFloat(v.defaultWagePercent)
|
|
|
|
|
|
wageQuotes.request(v.id)
|
|
|
|
|
|
return wageQuotes.finalPercent(v.id)
|
|
|
|
|
|
})
|
2026-09-18 12:54:45 +03:30
|
|
|
|
const hasDiscountWage = ref<number | null>(null) // no promotions backend yet — see file header
|
|
|
|
|
|
// REAL stock: ProductVariant.qty / isPreOrder exist and are returned by
|
|
|
|
|
|
// GET /catalog/products. A variant is orderable when it has stock, or when
|
|
|
|
|
|
// it is flagged pre-order — the same rule the admin cart uses.
|
|
|
|
|
|
const qty = computed(() => selectedVariant.value?.qty ?? 0)
|
|
|
|
|
|
const isPreOrder = computed(() => selectedVariant.value?.isPreOrder ?? false)
|
|
|
|
|
|
const is_exist = computed(() => qty.value > 0 || isPreOrder.value)
|
|
|
|
|
|
const productCode = computed(() => product.value.productCode ?? null)
|
|
|
|
|
|
const has_warranty = ref(false) // no warranty field on ProductVariant yet
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
product,
|
|
|
|
|
|
image,
|
|
|
|
|
|
id,
|
|
|
|
|
|
title,
|
|
|
|
|
|
productCode,
|
|
|
|
|
|
variants,
|
|
|
|
|
|
selectedVariant,
|
|
|
|
|
|
min_gram,
|
|
|
|
|
|
wage,
|
|
|
|
|
|
hasDiscountWage,
|
|
|
|
|
|
qty,
|
|
|
|
|
|
isPreOrder,
|
|
|
|
|
|
is_exist,
|
|
|
|
|
|
has_warranty,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|