feat: initial commit

This commit is contained in:
s.esmaeili
2026-09-09 06:46:27 +03:30
commit 0ed6bed028
214 changed files with 20177 additions and 0 deletions
@@ -0,0 +1,82 @@
<!--
Ported from legacy henza_pwa/src/components/product/cartItem.vue.
Dropped: the "اجرت ریالی" (Rial wage) block, which displayed
`selectedVariant.rial_pay` — no such per-variant Rial field exists anywhere
in our domain model (not a stubbed-off feature like promotions, genuinely
nothing to adapt), so there's nothing honest to show here.
-->
<template>
<div class="border border-solid border-[#E5E7EB] bg-[#F9F9F9] block w-full rounded-[10px] p-[8px] flex">
<img v-lazy="image" class="w-[70px] h-[70px] m-auto" />
<div class="flex-1">
<h6 class="font-bold mb-2">{{ title }}</h6>
<div class="flex justify-between text-center mb-4">
<div class="flex-col justify-start">
<div class="mb-2 text-[11px]">وزن حدودی</div>
<div class="text-secondary text-[14px] font-bold">{{ selectedVariant?.rashWeightGrams }} گرم</div>
</div>
<div>
<div v-if="showDiscountWage" class="mb-2 text-[11px]">اجرت</div>
<div v-if="showDiscountWage" :class="{ 'text-[14px] font-bold text-secondary': true, 'line-through decoration-inherit ml-4': hasDiscountWage }">
{{ wage?.toFixed(2) }}% اجرت
</div>
<div class="text-[14px] font-bold text-danger" v-if="showDiscountWage">{{ hasDiscountWage?.toFixed(2) }}% اجرت</div>
</div>
</div>
<div class="flex justify-between">
<div class="mb-4">تعداد</div>
<div class="flex">
<Icon icon="vuesax-linear:add-square" class="cursor-pointer text-secondary" size="20" @click="increment" />
<span class="mx-[16px]">{{ value.qty }}</span>
<Icon icon="vuesax-linear:trash" @click="cart.remove(value)" v-if="value.qty === 1" class="cursor-pointer text-danger" size="20" />
<Icon icon="vuesax-linear:minus-square" @click="decrement" v-else class="cursor-pointer text-secondary" size="20" />
</div>
</div>
<Button class="w-full" type="primary" @click="emit('note', index)">
<template v-if="!value.note || value.note === ''">ثبت توضیحات</template>
<template v-else>ویرایش توضیحات</template>
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Button } from 'ant-design-vue'
import Icon from '../icon/Icon.vue'
import { useProduct } from '../../composables/useProduct'
import { useCartStore, type CartItem } from '../../stores/cartStore'
import { isUserLoggedIn } from '../../utils/checkAccess'
const props = withDefaults(defineProps<{ value: CartItem }>(), {})
const emit = defineEmits<{ (e: 'note', value: number): void }>()
const cart = useCartStore()
const index = cart.find_index(props.value)
const increment = () => {
cart.updateFeild(props.value.qty + 1, index, props.value, 'qty')
}
const decrement = () => {
cart.updateFeild(props.value.qty - 1, index, props.value, 'qty')
}
const { image, title, selectedVariant, wage, hasDiscountWage } = useProduct(props.value.product, cart.variant(props.value))
const showDiscountWage = computed(() => false && hasDiscountWage.value && isUserLoggedIn())
</script>
<style lang="less">
.description-item {
border-left: 1px solid @grey;
}
.description-item:last-child {
padding-left: 0;
border-left: 0;
}
.description-item:first-child {
padding-right: 0;
}
</style>
@@ -0,0 +1,45 @@
<!--
Ported from legacy henza_pwa/src/components/product/category.vue.
Not yet wired to real data: the new API has no categories-with-icon endpoint
(Prisma has a Category model, but no controller/route exposes it, and there
are no per-category icon images). HomeView passes an empty list for now, so
this renders nothing rather than fabricated placeholder tiles — see the
"existing issue found" note in the report about the legacy `id === 45`
special-case, preserved here for whenever real category data exists.
-->
<template>
<router-link :to="{ name: 'search' }" @click="setCats" class="cursor-pointer h-full block">
<div
class="h-full w-full text-center flex flex-col items-center justify-between rounded-[4px]"
:class="value.id === '45' ? 'bg-primary' : 'border border-1 border-secondary'"
>
<div class="flex justify-center mt-4 shrink-0">
<img v-lazy="value.icon?.image" alt="category" class="w-[32px] h-[32px] text-center" />
</div>
<div
class="mb-4 mt-4 py-1 px-1 font-semibold flex-1 flex items-center justify-center text-center min-h-[40px]"
:class="value.id === '45' ? 'text-white' : 'text-primary'"
>
{{ value.title }}
</div>
</div>
</router-link>
</template>
<script setup lang="ts">
import { useFiltersStore } from '../../stores/filtersStore'
interface CategoryValue {
id: string
title: string
icon?: { image: string }
}
const props = withDefaults(defineProps<{ value: CategoryValue }>(), {})
const filters = useFiltersStore()
const setCats = () => {
filters.query.cats = [props.value.id]
}
</script>
@@ -0,0 +1,15 @@
<!-- Ported from legacy henza_pwa/src/views/search/collection/components/CollectionCard.vue -->
<template>
<div class="flex flex-col items-center justify-center font-bold text-lg" style="color: #8c6a48">
<img class="w-[6rem] h-[6rem]" :src="image" />
<span>{{ minGram }} گرم</span>
</div>
</template>
<script setup lang="ts">
import { useProduct } from '../../composables/useProduct'
import type { ProductSummary } from '../../api/client'
const props = defineProps<{ item: ProductSummary }>()
const { image, min_gram: minGram } = useProduct(props.item)
</script>
@@ -0,0 +1,49 @@
<!-- Ported from legacy henza_pwa/src/components/product/order.vue -->
<template>
<div
@click="emit('view', value)"
class="w-full cursor-pointer relative rounded-[10px] mb-4 p-[10px] text-primary border border-solid border-[#E5E7EB] bg-[#fff]"
>
<div class="grid grid-cols-2 p-2">
<div>
<div class="text-[18px] font-sans font-semibold mb-2">
<span class="text-[#656f87]">سفارش :</span>
{{ value.orderNumber }}
</div>
<div>
<span class="text-[16px] font-bold pl-2">{{ value.items.length }}</span>
<span class="text-[14px] font-medium">آیتم</span>
</div>
</div>
<div class="grid justify-end">
<div class="rounded-[5px] px-[20px] py-[4px] w-fit text-[14px] mb-2" :style="{ background: color?.background, color: color?.font }">
{{ statusLabel }}
</div>
<div>
<span class="text-[16px] font-bold pl-2">{{ parseFloat(value.totalGoldWeightGrams).toFixed(2) }} </span>
<span class="text-[14px] font-medium">گرم</span>
</div>
</div>
</div>
<a-divider style="margin: 8px 0" />
<div class="flex justify-between p-2">
<div class="text-[16px] text-[#656f87]">{{ jdate(value.createdAt) }}</div>
<Icon icon="vuesax-linear:arrow-left-1" class="cursor-pointer text-left" size="24" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Divider as ADivider } from 'ant-design-vue'
import Icon from '../icon/Icon.vue'
import { ORDER_STATUS_COLORS, ORDER_STATUS_LABELS } from '../../utils/orderStatus'
import { jdate } from '../../utils/filters'
import type { OrderDetail } from '../../api/client'
const props = withDefaults(defineProps<{ value: OrderDetail }>(), {})
const emit = defineEmits<{ (e: 'view', value: OrderDetail): void }>()
const statusLabel = computed(() => ORDER_STATUS_LABELS[props.value.status] ?? props.value.status)
const color = computed(() => ORDER_STATUS_COLORS[props.value.status])
</script>
@@ -0,0 +1,46 @@
<!--
Ported from legacy henza_pwa/src/components/product/orderItem.vue.
Adapted: product/variant title comes from a client-side lookup (see
useOrderItemProduct.ts) since the real order-items response only carries
productVariantId. `note` always shows '-' — our OrderItem schema has no
note field (legacy's per-item note was a real column there; ours isn't).
-->
<template>
<div class="bg-white/10 rounded-[10px] p-[12px] flex items-center">
<img v-lazy="lookup?.imageUrl" class="ml-[8px] w-[120px]" />
<div class="flex-1" style="width: calc(100% - 82px)">
<div class="flex items-center">
<div class="font-semibold text-[16px] ml-2">{{ lookup?.productTitle ?? '—' }}</div>
<div class="text-[12px] text-secondary">({{ lookup?.variantTitle }})</div>
</div>
<div class="grid grid-cols-2">
<div class="mt-2">
<div>تعداد کالاها :</div>
<div class="pt-1">وزن :</div>
<div class="pt-1">اجرت :</div>
<div class="pt-1">توضیحات :</div>
</div>
<div class="text-left mt-2 text-black font-semibold">
<div>{{ value.quantity }} <span>عدد</span></div>
<div class="pt-1">{{ value.computedWeightGrams }} <span>گرم</span></div>
<div class="pt-1">{{ value.wagePercentSnapshot }} <span>درصد</span></div>
<div class="pt-1">-</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useOrderItemProduct, type VariantLookup } from '../../composables/useOrderItemProduct'
import type { OrderItemDetail } from '../../api/client'
const props = withDefaults(defineProps<{ value: OrderItemDetail }>(), {})
const { lookup: lookupVariant } = useOrderItemProduct()
const lookup = ref<VariantLookup | undefined>()
onMounted(async () => {
lookup.value = await lookupVariant(props.value.productVariantId)
})
</script>
@@ -0,0 +1,65 @@
<!--
Ported from legacy henza_pwa/src/components/product/productHorizontal.vue.
See useProduct.ts for the honest gaps vs. legacy (no promotions/warranty/
stock backend data yet).
-->
<template>
<div @click="openDrawer" class="block flex items-stretch rounded-[10px] bg-light px-[12px] py-[12px] cursor-pointer mb-4" v-bind="$attrs">
<img v-lazy="image" class="ml-[12px] w-[70px]" />
<div class="flex-1" style="width: calc(100% - 82px)">
<div class="w-full title flex justify-between">
<h5 class="font-bold h-[25px] text-black flex-1 truncate mb-4">{{ title }}</h5>
<div v-if="favorites.isFavorite(id)">
<Icon icon="vuesax-linear:heart-fill" class="cursor-pointer text-secondary z-[2]" @click.stop="favorites.toggle(id)" size="25" />
</div>
<div v-else>
<Icon icon="vuesax-linear:heart" class="cursor-pointer text-secondary z-[2]" @click.stop="favorites.toggle(id)" size="25" />
</div>
</div>
<div class="font-bold text-[15px] text-secondary text-align-start mb-4">{{ min_gram }} گرم</div>
<div>
<div class="flex justify-between items-center">
<span
v-if="showDiscountWage"
:class="{ 'font-bold text-[15px] text-secondary text-align-start': true, 'line-through decoration-inherit ml-8': hasDiscountWage }"
>{{ wage != null && wage % 1 === 0 ? wage : wage?.toFixed(2) }}% اجرت</span
>
</div>
<div v-if="has_warranty" class="flex mt-2 font-bold">
<Icon icon="vuesax-linear:shield_dollar" color="#B49474" />
<div class="self-center text-[#B49474] mr-2">تضمین بهترین قیمت</div>
</div>
</div>
</div>
</div>
<Drawer v-model:visible="visible" placement="bottom" :closable="false" class="rtl" height="100vh" :bodyStyle="{ padding: 0, position: 'relative' }">
<SingleProduct :id="id" @closeDrawer="visible = false" />
</Drawer>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Drawer } from 'ant-design-vue'
import Icon from '../icon/Icon.vue'
import SingleProduct from './SingleProduct.vue'
import { useProduct } from '../../composables/useProduct'
import { useFavoritesStore } from '../../stores/favoritesStore'
import { isUserLoggedIn } from '../../utils/checkAccess'
import type { ProductSummary } from '../../api/client'
const props = withDefaults(defineProps<{ value: ProductSummary }>(), {})
const favorites = useFavoritesStore()
const { image, id, title, min_gram, wage, hasDiscountWage, has_warranty } = useProduct(props.value)
const visible = ref(false)
const openDrawer = () => {
visible.value = true
}
// legacy: myStore.bankerPermission && hasDiscountWage && isUserLoggedIn() — no
// banker/login state exists yet, so this always evaluates false for now.
const showDiscountWage = computed(() => false && hasDiscountWage.value && isUserLoggedIn())
</script>
@@ -0,0 +1,44 @@
<!--
Ported from legacy henza_pwa/src/components/product/productList.vue.
The "ciny"/collection tile grid (only shown on the /search route) now
receives real collection data (see catalog.service.ts's `collections`
include, added specifically to power this).
-->
<template>
<ScrollFetchSearch class="h-screen" :loading="loading" :flag="flag" :padding="padding" :is-fav="isFav" @fetch="emit('pull')">
<div v-if="$route.name === 'search'" class="grid grid-cols-2 gap-2">
<div
v-for="item in ciny"
:key="item.id"
class="w-full bg-light flex justify-center rounded-[8px] border-gray-200 border-[1px] p-3 items-center gap-2 cursor-pointer"
@click="$router.push({ path: `/collection/${item.id}`, query: { page: item.title } })"
>
<Icon icon="vuesax-linear:money-recive" class="cursor-pointer" size="25" />
<div class="text-lg font-bold mt-1">{{ item.title }}</div>
</div>
</div>
<div class="mt-4">
<ProductVerticalFull :class="index > 0 ? 'mt-[16px]' : undefined" v-for="(item, index) in value" :key="item.id" :value="item" />
</div>
</ScrollFetchSearch>
</template>
<script setup lang="ts">
import Icon from '../icon/Icon.vue'
import ProductVerticalFull from './ProductVerticalFull.vue'
import ScrollFetchSearch from '../ui/ScrollFetchSearch.vue'
import type { ProductSummary } from '../../api/client'
withDefaults(
defineProps<{
value: ProductSummary[]
ciny?: Array<{ id: string; title: string }>
loading: boolean
flag: boolean
padding?: string
isFav?: boolean
}>(),
{ ciny: () => [] },
)
const emit = defineEmits<{ (e: 'pull'): void }>()
</script>
@@ -0,0 +1,75 @@
<!--
Ported from legacy henza_pwa/src/components/product/productVertical.vue.
See useProduct.ts for the honest gaps vs. legacy.
-->
<template>
<div @click="openDrawer" class="block relative w-[280px] rounded-[10px] p-[8px] bg-light" v-bind="$attrs">
<Icon
v-if="favorites.isFavorite(id)"
icon="vuesax-linear:heart-fill"
class="cursor-pointer text-secondary absolute top-[15.5px] left-[14px]"
@click.stop="favorites.toggle(id)"
size="25"
/>
<Icon
v-else
icon="vuesax-linear:heart"
class="cursor-pointer text-secondary absolute top-[15.5px] left-[14px]"
@click.stop="favorites.toggle(id)"
size="25"
/>
<span
class="absolute top-[15.5px] right-[14px] text-danger text-[12px] px-[8px] py-[4px] rounded-[5px] border-[1px] border-danger"
v-if="hasDiscountWage"
>تخفیف</span
>
<img v-lazy="image" class="w-[200px] h-[200px] mt-[35px] m-auto" />
<div class="bg-white rounded-[5px] p-[16px] mt-[8px]">
<div class="flex mb-[16px]">
<div class="font-bold text-[15px] pl-2 text-secondary border-solid border-grey" :class="isUserLoggedIn() ? 'border-l' : ''">{{ min_gram }} گرم</div>
<span
v-if="showDiscountWage"
:class="{ 'font-bold text-[15px] text-secondary mr-2 text-align-start': true, 'line-through decoration-inherit ml-2': hasDiscountWage }"
>{{ wage?.toFixed(2) }}% اجرت</span
>
</div>
<h5 class="font-bold text-[16px] text-black m-0">{{ title }}</h5>
<div v-if="has_warranty" class="flex mt-2">
<Icon icon="vuesax-linear:shield_dollar" color="#B49474" />
<div class="self-center text-[#B49474] text-xl mr-2">تضمین بهترین قیمت</div>
</div>
<div v-else>
<div>&nbsp;</div>
<div>&nbsp;</div>
</div>
</div>
<Drawer v-model:visible="visible" placement="bottom" :closable="false" class="rtl" height="100vh" :bodyStyle="{ padding: 0, position: 'relative' }">
<SingleProduct :id="id" @closeDrawer="visible = false" />
</Drawer>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Drawer } from 'ant-design-vue'
import Icon from '../icon/Icon.vue'
import SingleProduct from './SingleProduct.vue'
import { useProduct } from '../../composables/useProduct'
import { useFavoritesStore } from '../../stores/favoritesStore'
import { isUserLoggedIn } from '../../utils/checkAccess'
import type { ProductSummary } from '../../api/client'
const props = withDefaults(defineProps<{ value: ProductSummary }>(), {})
const favorites = useFavoritesStore()
const { image, id, title, min_gram, wage, hasDiscountWage, has_warranty } = useProduct(props.value)
const visible = ref(false)
const openDrawer = () => {
visible.value = true
}
const showDiscountWage = computed(() => false && hasDiscountWage.value && isUserLoggedIn())
</script>
@@ -0,0 +1,66 @@
<!--
Ported from legacy henza_pwa/src/components/product/productVerticalFull.vue.
See useProduct.ts for the honest gaps vs. legacy.
-->
<template>
<div @click="openDrawer" class="block w-full rounded-[10px] p-[8px] bg-light cursor-pointer relative mb-4" v-bind="$attrs">
<img v-lazy="image" class="w-[277px] h-[277px] m-auto" />
<span
class="absolute top-[15.5px] right-[14px] text-danger text-[12px] px-[8px] py-[4px] rounded-[5px] border-[1px] border-danger"
v-if="hasDiscountWage"
>تخفیف</span
>
<div class="bg-white rounded-[5px] p-[16px] mt-[8px] relative">
<div class="flex w-full gap-4">
<h5 class="font-bold text-[17px] text-black mb-[16px] flex-1 truncate">{{ title }}</h5>
<Icon
v-if="favorites.isFavorite(id)"
icon="vuesax-linear:heart-fill"
class="cursor-pointer text-secondary"
@click.stop="favorites.toggle(id)"
size="25"
/>
<Icon v-else icon="vuesax-linear:heart" class="cursor-pointer text-secondary" @click.stop="favorites.toggle(id)" size="25" />
</div>
<div class="flex">
<div class="font-bold text-[15px] pl-2 text-secondary border-solid border-grey" :class="showDiscountWage ? 'border-l' : ''">{{ min_gram }} گرم</div>
<span
v-if="showDiscountWage"
:class="{ 'font-bold text-[15px] text-secondary mr-2 text-align-start': true, 'line-through decoration-inherit ml-4': hasDiscountWage }"
>{{ wage?.toFixed(2) }}% اجرت</span
>
</div>
<div v-if="has_warranty" class="flex mt-2">
<Icon icon="vuesax-linear:shield_dollar" color="#B49474" />
<div class="self-center text-[#B49474] text-xl mr-2">تضمین بهترین قیمت</div>
</div>
</div>
</div>
<Drawer v-model:visible="visible" placement="bottom" :closable="false" class="rtl" height="100vh" :bodyStyle="{ padding: 0, position: 'relative' }">
<SingleProduct :id="id" @closeDrawer="visible = false" />
</Drawer>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Drawer } from 'ant-design-vue'
import Icon from '../icon/Icon.vue'
import SingleProduct from './SingleProduct.vue'
import { useProduct } from '../../composables/useProduct'
import { useFavoritesStore } from '../../stores/favoritesStore'
import { isUserLoggedIn } from '../../utils/checkAccess'
import type { ProductSummary } from '../../api/client'
const props = withDefaults(defineProps<{ value: ProductSummary }>(), {})
const favorites = useFavoritesStore()
const { image, id, title, min_gram, wage, hasDiscountWage, has_warranty } = useProduct(props.value)
const visible = ref(false)
const openDrawer = () => {
visible.value = true
}
const showDiscountWage = computed(() => false && hasDiscountWage.value && isUserLoggedIn())
</script>
@@ -0,0 +1,242 @@
<!--
Ported from legacy henza_pwa/src/views/singleProduct.vue.
CRITICAL EXISTING ISSUE — ported as-is, flagged prominently (not silently
fixed): the bottom of legacy's script has an unconditional global side
effect —
history.pushState(null, location.href);
window.onpopstate = function () { history.go(1); };
— that runs every time a SingleProduct instance is created (not just when
its drawer opens — ant-design-vue's Drawer does not lazily mount its slot
content by default). This permanently overrides the browser/hardware back
button for the rest of the session (forcing it to always go *forward*), and
fires immediately once the Home page renders its product cards, not only
when a product detail is actually opened. This looks like a broken attempt
to trap the user inside the drawer. I did not fix this — it's ported
verbatim below — but flagging it as the single most impactful bug found in
this port so far; recommend fixing in a dedicated bug-fix pass rather than
silently patching it here.
FIXED (not merely ported) — a genuine crash, not a stylistic difference:
legacy's image-gallery guard was `v-if="product.images[0].image"`, which
throws immediately for any product with zero images (TypeError: cannot read
property of undefined). Since 100% of our current seeded products have
`images: []`, porting this literally would make the view crash for every
real product today. Legacy's own `v-else` branch (a decorative placeholder
SVG) makes clear a "no images" case was already intended — the guard here
is `product.images && product.images.length > 0`, which is what's needed
for that already-intended fallback to actually be reachable. Flagging this
explicitly since it's a behavior change, not just documentation.
DROPPED as confirmed-dead code (not "possibly dead" — verified unreachable):
legacy also declared a second DialogVant (`loginStepTwoNotify`) gating order
placement on a customer verification step. The only code that ever set it
to `true` was in an explicicitly commented-out alternate click handler
(legacy's own comment: "کد قبلی (نیاز به تایید مدارک...)"); the actual live
click handler (`isUserLoggedIn() ? addToCart() : loginNotify = true`) never
references it. It cannot show under any reachable code path. Not ported.
-->
<template>
<Spinner :spinning="loading" tip="درحال دریافت اطلاعات" cls="overflow-hidden" v-if="flag && productDetail.productRef.value">
<container class="px-[32px] py-[16px] flex flex-col pb-[66px] bg-prim max-h-screen" style="padding-bottom: 20px">
<div class="w-screen col-span-6 mx-[-32px] max-w-screen-md">
<div class="fixed top-[20px] right-[16px] z-[100] p-4" @click="close">
<Icon icon="vuesax-linear:arrow-right-1" class="cursor-pointer text-white bg-secondary rounded-[5px] p-[2.5px]" size="24" />
</div>
<div v-if="favorites.isFavorite(id)">
<Icon icon="vuesax-linear:heart-fill" class="cursor-pointer text-secondary z-[100] fixed top-[20px] left-[16px] p-4" @click="toggleFav" size="25" />
</div>
<div v-else>
<Icon icon="vuesax-linear:heart" class="cursor-pointer text-secondary z-[100] fixed top-[20px] left-[16px] p-4" @click="toggleFav" size="25" />
</div>
<transition name="slideUp" mode="out-in">
<div v-if="hasImages">
<Slider :slides="slides" width="auto" height="55vh" />
</div>
<div v-else>
<svg height="55vh" width="auto" viewBox="0 0 12 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6 9C5.91177 5.77359 3.66177 4.75472 0 4.5C3.57353 4.37264 5.91177 3.18396 6 0C6.08824 3.22642 8.38235 4.4151 12 4.5C8.38235 4.75472 6.08824 5.77359 6 9Z"
fill="#8D6A46"
/>
</svg>
</div>
</transition>
</div>
<div class="flex-1">
<div class="overflow-y-auto max-h-[300px]">
<div class="w-full flex justify-between mb-4">
<h5 class="col-span-4 m-0 text-[19px] font-medium">{{ title }}</h5>
<Button type="primary" class="justify-self-end m-0 col-span-2" @click="showPopup = true">توضیحات</Button>
</div>
<div class="w-full flex justify-between mb-4">
<div>
<div v-if="showDiscountWage" class="flex mb-4">
<div class="ml-4">اجرت</div>
<div class="rounded-[5px] bg-danger text-white py-[4px] px-[8px]" v-if="promotion">-%{{ promotion.discount_percent }}</div>
</div>
<div v-if="showDiscountWage" class="flex">
<div :class="{ 'text-secondary': true, 'ml-4 text-[13px] line-through decoration-danger': hasDiscountWage }">
{{ wage != null && wage % 1 === 0 ? wage : wage?.toFixed(2) }}% اجرت
</div>
</div>
</div>
<div>
<h3 class="mb-0">{{ min_gram }}</h3>
<div class="text-secondary mb-0 text-left">گرم</div>
</div>
</div>
<div class="w-full flex justify-between">
<div class="w-full mb-[10px]">تنوع ها</div>
</div>
<div class="w-full flex-1 flex">
<Button
type="primary"
:ghost="selectedVariant?.id !== item.id"
class="last:ml-0 ml-[8px] whitespace-nowrap btn-small"
@click="selectedVariant = item"
v-for="item in variants"
:key="item.id"
>{{ item.title }}</Button
>
</div>
</div>
</div>
<div class="h-[55px]"></div>
<div class="w-full fixed bottom-0 z-20 left-0 right-0">
<div class="w-full bg-white flex justify-between p-4 border-t max-w-[768px] m-auto shadowset border-solid border-primary/10">
<Button type="primary" class="max-w-[768px] m-auto" size="large" block v-if="!cart.isInCart(storeProduct)" @click="isUserLoggedIn() ? addToCart() : (loginNotify = true)">
ثبت سفارش
</Button>
<router-link class="w-full max-w-[768px] m-auto" :to="{ name: 'cart' }" v-else>
<Button type="primary" size="large" block>مشاهده سبد خرید</Button>
</router-link>
</div>
</div>
<Drawer v-model:visible="showPopup" placement="bottom" :closable="false" height="60vh" :class="['henza-rounded-drawer rtl', 'bg-white', 'text-black']">
<div class="relative">
<div class="absolute top-0 z-10 w-full">
<Icon icon="vuesax-linear:arrow-right-1" @click="showPopup = false" class="cursor-pointer float-right ml-[30px] rounded-[100%] hover:bg-white/10" size="25" />
<h5>{{ title }}</h5>
</div>
</div>
<div class="h-[30px]"></div>
<div class="whitespace-pre-wrap text-justify leading-[2.5rem] max-h-[350px] overflow-y-auto">{{ body }}</div>
</Drawer>
</container>
<NotifyVant :timeOut="3000" v-model="notify" :color="'background-color: #22C55E'">
<div class="font-bold text-right mb-2">ثبت سفارش</div>
<div>محصول با موفقیت به سبد خرید افزوده شد</div>
</NotifyVant>
<DialogVant :timeOut="-1" v-model:show="loginNotify" :closeOnClickOverlay="true">
<template v-slot:img>
<img :src="loginDialogImage" alt="" class="opacity-80" />
</template>
<template v-slot:title>
<div class="p-8 text-justify">برای دسترسی به تمامی امکانات نرم افزار هنزاگلد، ابتدا گزینه ورود/ثبت نام را لمس کنید و در صورت نداشتن حساب کاربری، ثبت نام نمایید.</div>
</template>
<template v-slot:button>
<div class="text-center text-[#1C2D56]" @click="router.push({ name: 'login' })">ورود/ثبت نام</div>
<div class="text-center text-danger" @click="loginNotify = false">بیخیال</div>
</template>
</DialogVant>
</Spinner>
</template>
<script setup lang="ts">
import { computed, onBeforeMount, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Button, Drawer } from 'ant-design-vue'
import Icon from '../icon/Icon.vue'
import Slider from '../slider/Slider.vue'
import Spinner from '../Spinner.vue'
import NotifyVant from '../NotifyVant.vue'
import DialogVant from '../DialogVant.vue'
import { useProduct } from '../../composables/useProduct'
import { useProductDetail } from '../../composables/useProductDetail'
import { useFavoritesStore } from '../../stores/favoritesStore'
import { useCartStore, type CartItem } from '../../stores/cartStore'
import { isUserLoggedIn } from '../../utils/checkAccess'
import loginDialogImage from '../../assets/img/empty/dialogEmpty1.svg'
const props = defineProps<{ id: string }>()
const emit = defineEmits<{ (e: 'closeDrawer'): void }>()
const router = useRouter()
// This component is used two ways, exactly as in legacy: embedded in a bottom
// Drawer by the product cards, and (legacy route `singleProduct`, /product/:id)
// as a standalone page for deep links. In the drawer case the parent listens
// for closeDrawer; on the standalone route there is no parent, so fall back to
// history.back() — legacy declared a goBack() for this but never wired it,
// leaving the close button dead on the routed page.
const close = () => {
emit('closeDrawer')
if (router.currentRoute.value.name === 'singleProduct') router.back()
}
const favorites = useFavoritesStore()
const cart = useCartStore()
const showPopup = ref(false)
const notify = ref(false)
const loginNotify = ref(false)
const promotion = ref<{ discount_percent?: number } | null>(null) // no promotions backend yet
const productDetail = useProductDetail(props.id)
const loading = productDetail.loading
const flag = productDetail.flag
// useProduct needs a real ProductSummary; give it an empty placeholder until
// the detail fetch resolves, matching legacy's own pattern of an initial
// empty-shaped `product` ref before data arrives.
const emptyProduct = { id: props.id, title: '', description: null, variants: [], images: [], category: null, brand: null, collections: [] }
const productAdapter = computed(() => useProduct(productDetail.productRef.value ?? emptyProduct))
const id = computed(() => productAdapter.value.id.value)
const title = computed(() => productAdapter.value.title.value)
const variants = computed(() => productAdapter.value.variants.value)
const selectedVariant = computed({
get: () => productAdapter.value.selectedVariant.value,
set: (v) => (productAdapter.value.selectedVariant.value = v),
})
const min_gram = computed(() => productAdapter.value.min_gram.value)
const wage = computed(() => productAdapter.value.wage.value)
const hasDiscountWage = computed(() => productAdapter.value.hasDiscountWage.value)
const body = computed(() => productDetail.productRef.value?.description ?? '')
const hasImages = computed(() => (productDetail.productRef.value?.images.length ?? 0) > 0)
const slides = computed(() => productDetail.productRef.value?.images.map((i) => i.url) ?? [])
// legacy: myStore.bankerPermission && hasDiscountWage && isUserLoggedIn() — no
// banker/login state exists yet, so this always evaluates false for now.
const showDiscountWage = computed(() => false && hasDiscountWage.value && isUserLoggedIn())
const storeProduct = computed<CartItem>(() => ({
vid: selectedVariant.value?.id ?? '',
note: null,
pid: productDetail.productRef.value?.id ?? props.id,
product: productDetail.productRef.value ?? (emptyProduct as any),
qty: 1,
}))
onBeforeMount(async () => {
await productDetail.load()
})
const addToCart = () => {
cart.addToCart(storeProduct.value)
notify.value = true
}
const toggleFav = (e: Event) => {
e.stopPropagation()
favorites.toggle(props.id)
}
// CRITICAL EXISTING ISSUE, ported as-is — see the file-level comment above.
history.pushState(null, location.href)
window.onpopstate = function () {
history.go(1)
}
</script>