Add پس کرایه checkbox beside shipping price and show it to customers.
Admin can mark a group as freight collect so the amount input is disabled and checkout shows پس کرایه at zero cost. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
41f21999d2
commit
55071af98c
@@ -58,6 +58,7 @@ function parseOrderShipping(shippingAddress: Record<string, unknown>) {
|
||||
title: typeof row.title === 'string' ? row.title : '',
|
||||
methodLabel: typeof row.methodLabel === 'string' ? row.methodLabel : null,
|
||||
amount: typeof row.amount === 'number' && Number.isFinite(row.amount) ? row.amount : 0,
|
||||
pasKraye: row.pasKraye === true,
|
||||
itemNames: Array.isArray(row.itemNames)
|
||||
? row.itemNames.filter((name): name is string => typeof name === 'string')
|
||||
: [],
|
||||
@@ -76,6 +77,7 @@ function parseOrderShipping(shippingAddress: Record<string, unknown>) {
|
||||
deliveryMode,
|
||||
addressLine: addressParts.join(' · '),
|
||||
groups,
|
||||
hasPasKraye: groups.some((group) => group.pasKraye),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +112,7 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
|
||||
if (!mounted || !order) return null
|
||||
|
||||
const itemCount = totalQuantity(order)
|
||||
const shippingInfo = parseOrderShipping(order.shippingAddress)
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
@@ -154,46 +157,43 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
const shippingInfo = parseOrderShipping(order.shippingAddress)
|
||||
return (
|
||||
<div className={styles.shippingBlock}>
|
||||
<div className={styles.shippingSectionTitle}>{t('orderItems.delivery')}</div>
|
||||
{shippingInfo.deliveryMode === 'pickup' ? (
|
||||
<p className={styles.addressText}>{t('orderItems.pickup')}</p>
|
||||
) : shippingInfo.addressLine ? (
|
||||
<p className={styles.addressText}>{shippingInfo.addressLine}</p>
|
||||
) : null}
|
||||
<div className={styles.shippingBlock}>
|
||||
<div className={styles.shippingSectionTitle}>{t('orderItems.delivery')}</div>
|
||||
{shippingInfo.deliveryMode === 'pickup' ? (
|
||||
<p className={styles.addressText}>{t('orderItems.pickup')}</p>
|
||||
) : shippingInfo.addressLine ? (
|
||||
<p className={styles.addressText}>{shippingInfo.addressLine}</p>
|
||||
) : null}
|
||||
|
||||
{shippingInfo.groups.length > 0 ? (
|
||||
<ul className={styles.shippingGroupList}>
|
||||
{shippingInfo.groups.map((group) => (
|
||||
<li key={group.groupId} className={styles.shippingGroupCard}>
|
||||
<div className={styles.shippingGroupHeader}>
|
||||
<span className={styles.shippingGroupTitle}>{group.title}</span>
|
||||
<span className={styles.shippingGroupAmount}>
|
||||
{group.amount > 0
|
||||
? formatIrtPrice(group.amount)
|
||||
: t('orderItems.freeShipping')}
|
||||
</span>
|
||||
</div>
|
||||
{group.methodLabel ? (
|
||||
<div className={styles.shippingMethod}>{group.methodLabel}</div>
|
||||
) : null}
|
||||
{group.itemNames.length > 0 ? (
|
||||
<ul className={styles.shippingItemNames}>
|
||||
{group.itemNames.map((name) => (
|
||||
<li key={`${group.groupId}-${name}`}>{name}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
{shippingInfo.groups.length > 0 ? (
|
||||
<ul className={styles.shippingGroupList}>
|
||||
{shippingInfo.groups.map((group) => (
|
||||
<li key={group.groupId} className={styles.shippingGroupCard}>
|
||||
<div className={styles.shippingGroupHeader}>
|
||||
<span className={styles.shippingGroupTitle}>{group.title}</span>
|
||||
<span className={styles.shippingGroupAmount}>
|
||||
{group.pasKraye
|
||||
? t('orderItems.pasKraye')
|
||||
: group.amount > 0
|
||||
? formatIrtPrice(group.amount)
|
||||
: t('orderItems.freeShipping')}
|
||||
</span>
|
||||
</div>
|
||||
{group.methodLabel ? (
|
||||
<div className={styles.shippingMethod}>{group.methodLabel}</div>
|
||||
) : null}
|
||||
{group.itemNames.length > 0 ? (
|
||||
<ul className={styles.shippingItemNames}>
|
||||
{group.itemNames.map((name) => (
|
||||
<li key={`${group.groupId}-${name}`}>{name}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{order.items.length === 0 ? (
|
||||
<EmptyState layout="inline" message={t('orderItems.empty')} />
|
||||
@@ -244,7 +244,9 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
|
||||
<span className={styles.totalsValue}>
|
||||
{order.shippingTotal > 0
|
||||
? formatIrtPrice(order.shippingTotal)
|
||||
: t('orderItems.freeShipping')}
|
||||
: shippingInfo.hasPasKraye
|
||||
? t('orderItems.pasKraye')
|
||||
: t('orderItems.freeShipping')}
|
||||
</span>
|
||||
</div>
|
||||
{order.discountTotal > 0 ? (
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface CheckoutShippingQuote {
|
||||
methodId: string | null
|
||||
methodLabel: string | null
|
||||
amount: number
|
||||
pasKraye?: boolean
|
||||
itemNames: string[]
|
||||
}
|
||||
|
||||
@@ -72,6 +73,7 @@ function parseShippingQuotes(value: unknown): CheckoutShippingQuote[] {
|
||||
methodId: typeof row.methodId === 'string' ? row.methodId : null,
|
||||
methodLabel: typeof row.methodLabel === 'string' ? row.methodLabel : null,
|
||||
amount: typeof row.amount === 'number' && Number.isFinite(row.amount) ? row.amount : 0,
|
||||
pasKraye: row.pasKraye === true,
|
||||
itemNames: Array.isArray(row.itemNames)
|
||||
? row.itemNames.filter((name): name is string => typeof name === 'string')
|
||||
: [],
|
||||
|
||||
@@ -160,6 +160,7 @@ const en = {
|
||||
'orderItems.shipping': 'Shipping',
|
||||
'orderItems.discount': 'Discount',
|
||||
'orderItems.freeShipping': 'Free shipping',
|
||||
'orderItems.pasKraye': 'Freight collect',
|
||||
|
||||
'favorites.title': 'My Favorites',
|
||||
'favorites.subtitle': 'Products you have saved for later.',
|
||||
@@ -588,6 +589,7 @@ const fa: Record<MessageKey, string> = {
|
||||
'orderItems.shipping': 'هزینه ارسال',
|
||||
'orderItems.discount': 'تخفیف',
|
||||
'orderItems.freeShipping': 'ارسال رایگان',
|
||||
'orderItems.pasKraye': 'پس کرایه',
|
||||
|
||||
'favorites.title': 'علاقهمندیها',
|
||||
'favorites.subtitle': 'محصولاتی که برای بعد ذخیره کردهاید.',
|
||||
|
||||
@@ -33,9 +33,16 @@ interface ShippingGroupQuote {
|
||||
title: string
|
||||
methods: ShippingMethodId[]
|
||||
amount: number
|
||||
pasKraye: boolean
|
||||
items: CartItem[]
|
||||
}
|
||||
|
||||
function formatShippingAmountLabel(quote: { amount: number; pasKraye?: boolean }) {
|
||||
if (quote.pasKraye) return 'پس کرایه'
|
||||
if (quote.amount > 0) return formatIrtPrice(quote.amount)
|
||||
return 'ارسال رایگان'
|
||||
}
|
||||
|
||||
function resolveProvinceId(
|
||||
provinces: CityOption[],
|
||||
provinceName: string | null | undefined,
|
||||
@@ -162,6 +169,7 @@ export function CheckoutDeliveryStep() {
|
||||
title: STORE_SHIPPING_TITLE,
|
||||
methods: [],
|
||||
amount: 0,
|
||||
pasKraye: false,
|
||||
items: cartItems,
|
||||
},
|
||||
]
|
||||
@@ -178,6 +186,7 @@ export function CheckoutDeliveryStep() {
|
||||
shippingConfig.staticShippingProvinceExceptions,
|
||||
provinceId,
|
||||
),
|
||||
pasKraye: false,
|
||||
items: cartItems,
|
||||
},
|
||||
]
|
||||
@@ -203,15 +212,19 @@ export function CheckoutDeliveryStep() {
|
||||
if (existing) {
|
||||
existing.items.push(item)
|
||||
} else {
|
||||
const pasKraye = group.pasKraye === true
|
||||
buckets.set(group.id, {
|
||||
key: group.id,
|
||||
title: quoteTitleForGroup(group.isDefault, group.nameFa, group.nameEn),
|
||||
methods: group.methods,
|
||||
amount: resolveProvinceAmount(
|
||||
group.defaultAmount,
|
||||
group.provinceExceptions,
|
||||
provinceId,
|
||||
),
|
||||
amount: pasKraye
|
||||
? 0
|
||||
: resolveProvinceAmount(
|
||||
group.defaultAmount,
|
||||
group.provinceExceptions,
|
||||
provinceId,
|
||||
),
|
||||
pasKraye,
|
||||
items: [item],
|
||||
})
|
||||
}
|
||||
@@ -223,6 +236,7 @@ export function CheckoutDeliveryStep() {
|
||||
title: STORE_SHIPPING_TITLE,
|
||||
methods: [],
|
||||
amount: 0,
|
||||
pasKraye: false,
|
||||
items: ungrouped,
|
||||
})
|
||||
}
|
||||
@@ -276,6 +290,7 @@ export function CheckoutDeliveryStep() {
|
||||
? SHIPPING_METHOD_LABELS_FA[methodId as ShippingMethodId] ?? methodId
|
||||
: null,
|
||||
amount: quote.amount,
|
||||
pasKraye: quote.pasKraye,
|
||||
itemNames: quote.items.map(
|
||||
(item) => item.productNameFa || item.productTitle,
|
||||
),
|
||||
@@ -288,6 +303,10 @@ export function CheckoutDeliveryStep() {
|
||||
() => shippingQuotes.reduce((sum, quote) => sum + quote.amount, 0),
|
||||
[shippingQuotes],
|
||||
)
|
||||
const hasPasKraye = useMemo(
|
||||
() => shippingQuotes.some((quote) => quote.pasKraye),
|
||||
[shippingQuotes],
|
||||
)
|
||||
|
||||
const shippingReady =
|
||||
deliveryMode === 'pickup' ||
|
||||
@@ -441,14 +460,14 @@ export function CheckoutDeliveryStep() {
|
||||
<span
|
||||
className={[
|
||||
styles.shippingGroupAmount,
|
||||
quote.amount > 0 ? '' : styles.shippingGroupAmountFree,
|
||||
quote.pasKraye || quote.amount <= 0
|
||||
? styles.shippingGroupAmountFree
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{quote.amount > 0
|
||||
? formatIrtPrice(quote.amount)
|
||||
: 'ارسال رایگان'}
|
||||
{formatShippingAmountLabel(quote)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -495,7 +514,13 @@ export function CheckoutDeliveryStep() {
|
||||
{shippingQuotes.length > 1 ? (
|
||||
<div className={styles.shippingTotalRow}>
|
||||
<span>جمع هزینه ارسال</span>
|
||||
<span className={styles.priceEn}>{formatIrtPrice(shippingTotal)}</span>
|
||||
<span className={styles.priceEn}>
|
||||
{hasPasKraye && shippingTotal <= 0
|
||||
? 'پس کرایه'
|
||||
: shippingTotal <= 0
|
||||
? 'ارسال رایگان'
|
||||
: formatIrtPrice(shippingTotal)}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -379,9 +379,11 @@ export function CheckoutPaymentStep() {
|
||||
<span className={styles.priceEn}>
|
||||
{deliveryMode === 'pickup'
|
||||
? '—'
|
||||
: shippingTotal <= 0
|
||||
? 'ارسال رایگان'
|
||||
: formatIrtPrice(shippingTotal)}
|
||||
: shippingQuotes.some((quote) => quote.pasKraye) && shippingTotal <= 0
|
||||
? 'پس کرایه'
|
||||
: shippingTotal <= 0
|
||||
? 'ارسال رایگان'
|
||||
: formatIrtPrice(shippingTotal)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.summaryRow}>
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface PublicShippingCostGroup {
|
||||
description: string | null
|
||||
methods: ShippingMethodId[]
|
||||
defaultAmount: number
|
||||
pasKraye?: boolean
|
||||
isDefault: boolean
|
||||
categoryIds: string[]
|
||||
provinceExceptions?: Array<{ provinceId: string; amount: number }>
|
||||
|
||||
Reference in New Issue
Block a user