feat: initial commit
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
// 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
|
||||
// than touching apps/api to add an include.
|
||||
import { ref } from 'vue'
|
||||
import { api } from '../api/client'
|
||||
|
||||
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 function useOrderItemProduct() {
|
||||
const lookup = async (productVariantId: string): Promise<VariantLookup | undefined> => {
|
||||
const map = await ensureLoaded()
|
||||
return map.get(productVariantId)
|
||||
}
|
||||
return { lookup }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Adapts our actual ProductSummary (apps/user-app/src/api/client.ts) into the
|
||||
// same output shape legacy's Mixins/product/index.ts `useProduct()` produced,
|
||||
// so the ported product-card components (ProductHorizontal/ProductVertical/
|
||||
// ProductVerticalFull) can render unchanged.
|
||||
//
|
||||
// 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.
|
||||
// - `wage`: legacy added the logged-in customer's own `percent` (a
|
||||
// commission/markup field on their account) on top of the variant's
|
||||
// making-charge %. No login exists in this port yet, so wage is just the
|
||||
// selected variant's defaultWagePercent on its own.
|
||||
// - `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'
|
||||
|
||||
function getCheapestVariant(variants: ProductVariantSummary[] | undefined): ProductVariantSummary | null {
|
||||
if (!variants || variants.length === 0) return null
|
||||
return variants.reduce((cheapest, v) =>
|
||||
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)
|
||||
const wage = computed(() => (selectedVariant.value ? parseFloat(selectedVariant.value.defaultWagePercent) : null))
|
||||
const hasDiscountWage = ref<number | null>(null) // no promotions backend yet — see file header
|
||||
const is_exist = ref(true) // no stock field on ProductVariant yet
|
||||
const has_warranty = ref(false) // no warranty field on ProductVariant yet
|
||||
|
||||
return {
|
||||
product,
|
||||
image,
|
||||
id,
|
||||
title,
|
||||
variants,
|
||||
selectedVariant,
|
||||
min_gram,
|
||||
wage,
|
||||
hasDiscountWage,
|
||||
is_exist,
|
||||
has_warranty,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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.
|
||||
import { ref } from 'vue'
|
||||
import { api, type ProductSummary } from '../api/client'
|
||||
|
||||
export function useProductDetail(id: string) {
|
||||
const loading = ref(false)
|
||||
const flag = ref(false)
|
||||
const notFound = ref(false)
|
||||
const productRef = ref<ProductSummary | null>(null)
|
||||
|
||||
const load = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const products = await api.listProducts()
|
||||
const found = products.find((p) => p.id === id) ?? null
|
||||
productRef.value = found
|
||||
notFound.value = !found
|
||||
flag.value = true
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { loading, flag, notFound, productRef, load }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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'
|
||||
|
||||
export interface ProductsData {
|
||||
totalPages: number
|
||||
productList: ProductSummary[]
|
||||
flag: boolean
|
||||
}
|
||||
|
||||
export function useProducts() {
|
||||
const productsData: ProductsData = reactive({
|
||||
totalPages: 0,
|
||||
productList: [],
|
||||
flag: false,
|
||||
})
|
||||
|
||||
const getProducts = async (loading: { value: boolean }, isPull = false) => {
|
||||
if (isPull) return // no further pages exist server-side yet
|
||||
loading.value = true
|
||||
try {
|
||||
productsData.productList = await api.listProducts()
|
||||
productsData.flag = productsData.productList.length < 1
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { productsData, getProducts }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Ported from legacy henza_pwa/src/Mixins/useRefresh.ts.
|
||||
// Auto-applies a waiting service worker as soon as one is available, so the
|
||||
// PWA self-updates instead of serving a stale precache indefinitely.
|
||||
import { unref, watchEffect } from 'vue'
|
||||
import { useRegisterSW } from 'virtual:pwa-register/vue'
|
||||
|
||||
export function useRefresh() {
|
||||
const { offlineReady, needRefresh, updateServiceWorker } = useRegisterSW()
|
||||
|
||||
const refreshApp = async () => {
|
||||
await updateServiceWorker()
|
||||
offlineReady.value = false
|
||||
needRefresh.value = false
|
||||
}
|
||||
|
||||
watchEffect(async () => (unref(offlineReady) || unref(needRefresh)) && (await refreshApp()))
|
||||
}
|
||||
Reference in New Issue
Block a user