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
@@ -57,6 +57,7 @@ function parseOrderShipping(shippingAddress: Record<string, unknown>) {
|
|||||||
title: typeof row.title === 'string' ? row.title : '',
|
title: typeof row.title === 'string' ? row.title : '',
|
||||||
methodLabel: typeof row.methodLabel === 'string' ? row.methodLabel : null,
|
methodLabel: typeof row.methodLabel === 'string' ? row.methodLabel : null,
|
||||||
amount: typeof row.amount === 'number' && Number.isFinite(row.amount) ? row.amount : 0,
|
amount: typeof row.amount === 'number' && Number.isFinite(row.amount) ? row.amount : 0,
|
||||||
|
pasKraye: row.pasKraye === true,
|
||||||
itemNames: Array.isArray(row.itemNames)
|
itemNames: Array.isArray(row.itemNames)
|
||||||
? row.itemNames.filter((name): name is string => typeof name === 'string')
|
? row.itemNames.filter((name): name is string => typeof name === 'string')
|
||||||
: [],
|
: [],
|
||||||
@@ -75,6 +76,7 @@ function parseOrderShipping(shippingAddress: Record<string, unknown>) {
|
|||||||
deliveryMode,
|
deliveryMode,
|
||||||
addressLine: addressParts.join(' · '),
|
addressLine: addressParts.join(' · '),
|
||||||
groups,
|
groups,
|
||||||
|
hasPasKraye: groups.some((group) => group.pasKraye),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +111,7 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
|
|||||||
if (!mounted || !order) return null
|
if (!mounted || !order) return null
|
||||||
|
|
||||||
const itemCount = totalQuantity(order)
|
const itemCount = totalQuantity(order)
|
||||||
|
const shippingInfo = parseOrderShipping(order.shippingAddress)
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
@@ -153,46 +156,43 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(() => {
|
<div className={styles.shippingBlock}>
|
||||||
const shippingInfo = parseOrderShipping(order.shippingAddress)
|
<div className={styles.shippingSectionTitle}>{t('orderItems.delivery')}</div>
|
||||||
return (
|
{shippingInfo.deliveryMode === 'pickup' ? (
|
||||||
<div className={styles.shippingBlock}>
|
<p className={styles.addressText}>{t('orderItems.pickup')}</p>
|
||||||
<div className={styles.shippingSectionTitle}>{t('orderItems.delivery')}</div>
|
) : shippingInfo.addressLine ? (
|
||||||
{shippingInfo.deliveryMode === 'pickup' ? (
|
<p className={styles.addressText}>{shippingInfo.addressLine}</p>
|
||||||
<p className={styles.addressText}>{t('orderItems.pickup')}</p>
|
) : null}
|
||||||
) : shippingInfo.addressLine ? (
|
|
||||||
<p className={styles.addressText}>{shippingInfo.addressLine}</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{shippingInfo.groups.length > 0 ? (
|
{shippingInfo.groups.length > 0 ? (
|
||||||
<ul className={styles.shippingGroupList}>
|
<ul className={styles.shippingGroupList}>
|
||||||
{shippingInfo.groups.map((group) => (
|
{shippingInfo.groups.map((group) => (
|
||||||
<li key={group.groupId} className={styles.shippingGroupCard}>
|
<li key={group.groupId} className={styles.shippingGroupCard}>
|
||||||
<div className={styles.shippingGroupHeader}>
|
<div className={styles.shippingGroupHeader}>
|
||||||
<span className={styles.shippingGroupTitle}>{group.title}</span>
|
<span className={styles.shippingGroupTitle}>{group.title}</span>
|
||||||
<span className={styles.shippingGroupAmount}>
|
<span className={styles.shippingGroupAmount}>
|
||||||
{group.amount > 0
|
{group.pasKraye
|
||||||
? formatIrtPrice(group.amount)
|
? t('orderItems.pasKraye')
|
||||||
: t('orderItems.freeShipping')}
|
: group.amount > 0
|
||||||
</span>
|
? formatIrtPrice(group.amount)
|
||||||
</div>
|
: t('orderItems.freeShipping')}
|
||||||
{group.methodLabel ? (
|
</span>
|
||||||
<div className={styles.shippingMethod}>{group.methodLabel}</div>
|
</div>
|
||||||
) : null}
|
{group.methodLabel ? (
|
||||||
{group.itemNames.length > 0 ? (
|
<div className={styles.shippingMethod}>{group.methodLabel}</div>
|
||||||
<ul className={styles.shippingItemNames}>
|
) : null}
|
||||||
{group.itemNames.map((name) => (
|
{group.itemNames.length > 0 ? (
|
||||||
<li key={`${group.groupId}-${name}`}>{name}</li>
|
<ul className={styles.shippingItemNames}>
|
||||||
))}
|
{group.itemNames.map((name) => (
|
||||||
</ul>
|
<li key={`${group.groupId}-${name}`}>{name}</li>
|
||||||
) : null}
|
))}
|
||||||
</li>
|
</ul>
|
||||||
))}
|
) : null}
|
||||||
</ul>
|
</li>
|
||||||
) : null}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
)
|
) : null}
|
||||||
})()}
|
</div>
|
||||||
|
|
||||||
{order.items.length === 0 ? (
|
{order.items.length === 0 ? (
|
||||||
<p className={styles.empty}>{t('orderItems.empty')}</p>
|
<p className={styles.empty}>{t('orderItems.empty')}</p>
|
||||||
@@ -243,7 +243,9 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
|
|||||||
<span className={styles.totalsValue}>
|
<span className={styles.totalsValue}>
|
||||||
{order.shippingTotal > 0
|
{order.shippingTotal > 0
|
||||||
? formatIrtPrice(order.shippingTotal)
|
? formatIrtPrice(order.shippingTotal)
|
||||||
: t('orderItems.freeShipping')}
|
: shippingInfo.hasPasKraye
|
||||||
|
? t('orderItems.pasKraye')
|
||||||
|
: t('orderItems.freeShipping')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{order.discountTotal > 0 ? (
|
{order.discountTotal > 0 ? (
|
||||||
|
|||||||
@@ -199,6 +199,37 @@
|
|||||||
direction: ltr;
|
direction: ltr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pasKrayeCheck {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-inline-start: 4px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||||
|
background: rgba(148, 163, 184, 0.08);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pasKrayeCheck input {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
accent-color: var(--primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amountRow input:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: rgba(148, 163, 184, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.checkRow {
|
.checkRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export function ShippingCostGroupModal({
|
|||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState('')
|
||||||
const [methods, setMethods] = useState<ShippingMethodId[]>([])
|
const [methods, setMethods] = useState<ShippingMethodId[]>([])
|
||||||
const [defaultAmount, setDefaultAmount] = useState('')
|
const [defaultAmount, setDefaultAmount] = useState('')
|
||||||
|
const [pasKraye, setPasKraye] = useState(false)
|
||||||
const [isDefault, setIsDefault] = useState(false)
|
const [isDefault, setIsDefault] = useState(false)
|
||||||
const [formError, setFormError] = useState('')
|
const [formError, setFormError] = useState('')
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ export function ShippingCostGroupModal({
|
|||||||
setNameEn(editingGroup?.nameEn ?? '')
|
setNameEn(editingGroup?.nameEn ?? '')
|
||||||
setDescription(editingGroup?.description ?? '')
|
setDescription(editingGroup?.description ?? '')
|
||||||
setMethods(editingGroup?.methods ?? [])
|
setMethods(editingGroup?.methods ?? [])
|
||||||
|
setPasKraye(editingGroup?.pasKraye ?? false)
|
||||||
setDefaultAmount(
|
setDefaultAmount(
|
||||||
editingGroup
|
editingGroup
|
||||||
? formatIrtInput(String(Math.round(editingGroup.defaultAmount)))
|
? formatIrtInput(String(Math.round(editingGroup.defaultAmount)))
|
||||||
@@ -98,7 +100,7 @@ export function ShippingCostGroupModal({
|
|||||||
|
|
||||||
const trimmedFa = nameFa.trim()
|
const trimmedFa = nameFa.trim()
|
||||||
const trimmedEn = nameEn.trim()
|
const trimmedEn = nameEn.trim()
|
||||||
const amount = parseIrtInput(defaultAmount)
|
const amount = pasKraye ? 0 : parseIrtInput(defaultAmount)
|
||||||
|
|
||||||
if (!trimmedFa || !trimmedEn) {
|
if (!trimmedFa || !trimmedEn) {
|
||||||
setFormError(t('store.shipping.form.errorRequired'))
|
setFormError(t('store.shipping.form.errorRequired'))
|
||||||
@@ -108,7 +110,7 @@ export function ShippingCostGroupModal({
|
|||||||
setFormError(t('store.shipping.form.errorMethods'))
|
setFormError(t('store.shipping.form.errorMethods'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (amount === null || amount < 0) {
|
if (!pasKraye && (amount === null || amount < 0)) {
|
||||||
setFormError(t('store.shipping.form.errorAmount'))
|
setFormError(t('store.shipping.form.errorAmount'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -118,7 +120,8 @@ export function ShippingCostGroupModal({
|
|||||||
nameEn: trimmedEn,
|
nameEn: trimmedEn,
|
||||||
description: description.trim() || undefined,
|
description: description.trim() || undefined,
|
||||||
methods,
|
methods,
|
||||||
defaultAmount: amount,
|
defaultAmount: amount ?? 0,
|
||||||
|
pasKraye,
|
||||||
isDefault,
|
isDefault,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -244,11 +247,28 @@ export function ShippingCostGroupModal({
|
|||||||
placeholder={t('store.shipping.form.defaultAmountPlaceholder')}
|
placeholder={t('store.shipping.form.defaultAmountPlaceholder')}
|
||||||
value={defaultAmount}
|
value={defaultAmount}
|
||||||
onChange={(e) => setDefaultAmount(formatIrtInput(e.target.value))}
|
onChange={(e) => setDefaultAmount(formatIrtInput(e.target.value))}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || pasKraye}
|
||||||
required
|
required={!pasKraye}
|
||||||
/>
|
/>
|
||||||
<span className={styles.amountUnit}>IRT</span>
|
<span className={styles.amountUnit}>IRT</span>
|
||||||
|
<label className={styles.pasKrayeCheck} htmlFor="shipping-pas-kraye">
|
||||||
|
<input
|
||||||
|
id="shipping-pas-kraye"
|
||||||
|
type="checkbox"
|
||||||
|
checked={pasKraye}
|
||||||
|
onChange={(e) => {
|
||||||
|
const checked = e.target.checked
|
||||||
|
setPasKraye(checked)
|
||||||
|
if (checked) setDefaultAmount('')
|
||||||
|
}}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<span>{t('store.shipping.form.pasKraye')}</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
{pasKraye ? (
|
||||||
|
<p className={styles.hint}>{t('store.shipping.form.pasKrayeHint')}</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className={styles.checkRow} htmlFor="shipping-is-default">
|
<label className={styles.checkRow} htmlFor="shipping-is-default">
|
||||||
|
|||||||
@@ -59,7 +59,11 @@ export function ShippingCostGroupRow({
|
|||||||
<div className={styles.chip}>
|
<div className={styles.chip}>
|
||||||
<span className={styles.chipLabel}>{t('store.shipping.row.defaultAmount')}</span>
|
<span className={styles.chipLabel}>{t('store.shipping.row.defaultAmount')}</span>
|
||||||
<span className={styles.chipValue}>
|
<span className={styles.chipValue}>
|
||||||
<span className={styles.chipNumber}>{formatIrtPrice(group.defaultAmount)}</span>
|
<span className={styles.chipNumber}>
|
||||||
|
{group.pasKraye
|
||||||
|
? t('store.shipping.form.pasKraye')
|
||||||
|
: formatIrtPrice(group.defaultAmount)}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1006,6 +1006,9 @@ const en = {
|
|||||||
'store.shipping.form.defaultAmountHint':
|
'store.shipping.form.defaultAmountHint':
|
||||||
'Applied to all provinces unless a city-specific amount is set later.',
|
'Applied to all provinces unless a city-specific amount is set later.',
|
||||||
'store.shipping.form.defaultAmountPlaceholder': 'e.g. 120,000',
|
'store.shipping.form.defaultAmountPlaceholder': 'e.g. 120,000',
|
||||||
|
'store.shipping.form.pasKraye': 'Freight collect',
|
||||||
|
'store.shipping.form.pasKrayeHint':
|
||||||
|
'Shipping is free at checkout, but customers see “پس کرایه” (pay on delivery).',
|
||||||
'store.shipping.form.isDefault': 'Make this the default group',
|
'store.shipping.form.isDefault': 'Make this the default group',
|
||||||
'store.shipping.form.isDefaultHint':
|
'store.shipping.form.isDefaultHint':
|
||||||
'Categories with no assigned shipping group will use this group.',
|
'Categories with no assigned shipping group will use this group.',
|
||||||
@@ -1303,6 +1306,7 @@ const en = {
|
|||||||
'orderItems.shipping': 'Shipping',
|
'orderItems.shipping': 'Shipping',
|
||||||
'orderItems.discount': 'Discount',
|
'orderItems.discount': 'Discount',
|
||||||
'orderItems.freeShipping': 'Free shipping',
|
'orderItems.freeShipping': 'Free shipping',
|
||||||
|
'orderItems.pasKraye': 'Freight collect',
|
||||||
|
|
||||||
'orderTransactions.title': 'Transaction details',
|
'orderTransactions.title': 'Transaction details',
|
||||||
'orderTransactions.empty': 'No payment transactions recorded for this order.',
|
'orderTransactions.empty': 'No payment transactions recorded for this order.',
|
||||||
@@ -3896,6 +3900,9 @@ const fa: Record<MessageKey, string> = {
|
|||||||
'store.shipping.form.defaultAmountHint':
|
'store.shipping.form.defaultAmountHint':
|
||||||
'برای همه استانها اعمال میشود مگر اینکه برای شهر خاصی مبلغ دیگری تعیین شود.',
|
'برای همه استانها اعمال میشود مگر اینکه برای شهر خاصی مبلغ دیگری تعیین شود.',
|
||||||
'store.shipping.form.defaultAmountPlaceholder': 'مثلاً ۱۲۰,۰۰۰',
|
'store.shipping.form.defaultAmountPlaceholder': 'مثلاً ۱۲۰,۰۰۰',
|
||||||
|
'store.shipping.form.pasKraye': 'پس کرایه',
|
||||||
|
'store.shipping.form.pasKrayeHint':
|
||||||
|
'هزینه ارسال در تسویهحساب رایگان محاسبه میشود، اما به مشتری «پس کرایه» نمایش داده میشود.',
|
||||||
'store.shipping.form.isDefault': 'این گروه را بهعنوان گروه پیشفرض تنظیم کن',
|
'store.shipping.form.isDefault': 'این گروه را بهعنوان گروه پیشفرض تنظیم کن',
|
||||||
'store.shipping.form.isDefaultHint':
|
'store.shipping.form.isDefaultHint':
|
||||||
'دستههایی که گروه ارسال اختصاصی ندارند از این گروه استفاده میکنند.',
|
'دستههایی که گروه ارسال اختصاصی ندارند از این گروه استفاده میکنند.',
|
||||||
@@ -4193,6 +4200,7 @@ const fa: Record<MessageKey, string> = {
|
|||||||
'orderItems.shipping': 'هزینه ارسال',
|
'orderItems.shipping': 'هزینه ارسال',
|
||||||
'orderItems.discount': 'تخفیف',
|
'orderItems.discount': 'تخفیف',
|
||||||
'orderItems.freeShipping': 'ارسال رایگان',
|
'orderItems.freeShipping': 'ارسال رایگان',
|
||||||
|
'orderItems.pasKraye': 'پس کرایه',
|
||||||
|
|
||||||
'orderTransactions.title': 'جزئیات تراکنش',
|
'orderTransactions.title': 'جزئیات تراکنش',
|
||||||
'orderTransactions.empty': 'تراکنش پرداختی برای این سفارش ثبت نشده است.',
|
'orderTransactions.empty': 'تراکنش پرداختی برای این سفارش ثبت نشده است.',
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface ShippingCostGroup {
|
|||||||
sortOrder: number
|
sortOrder: number
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
isDefault: boolean
|
isDefault: boolean
|
||||||
|
pasKraye: boolean
|
||||||
categoryIds: string[]
|
categoryIds: string[]
|
||||||
categories: ShippingCostGroupCategory[]
|
categories: ShippingCostGroupCategory[]
|
||||||
cityRates: ShippingCostGroupCityRate[]
|
cityRates: ShippingCostGroupCityRate[]
|
||||||
@@ -52,6 +53,7 @@ export interface ShippingCostGroupFormPayload {
|
|||||||
description?: string
|
description?: string
|
||||||
methods: ShippingMethodId[]
|
methods: ShippingMethodId[]
|
||||||
defaultAmount: number
|
defaultAmount: number
|
||||||
|
pasKraye?: boolean
|
||||||
isDefault?: boolean
|
isDefault?: boolean
|
||||||
categoryIds?: string[]
|
categoryIds?: string[]
|
||||||
provinceExceptions?: Array<{ provinceId: string; amount: number }>
|
provinceExceptions?: Array<{ provinceId: string; amount: number }>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ function parseOrderShipping(shippingAddress: Record<string, unknown>) {
|
|||||||
title: typeof row.title === 'string' ? row.title : '',
|
title: typeof row.title === 'string' ? row.title : '',
|
||||||
methodLabel: typeof row.methodLabel === 'string' ? row.methodLabel : null,
|
methodLabel: typeof row.methodLabel === 'string' ? row.methodLabel : null,
|
||||||
amount: typeof row.amount === 'number' && Number.isFinite(row.amount) ? row.amount : 0,
|
amount: typeof row.amount === 'number' && Number.isFinite(row.amount) ? row.amount : 0,
|
||||||
|
pasKraye: row.pasKraye === true,
|
||||||
itemNames: Array.isArray(row.itemNames)
|
itemNames: Array.isArray(row.itemNames)
|
||||||
? row.itemNames.filter((name): name is string => typeof name === 'string')
|
? row.itemNames.filter((name): name is string => typeof name === 'string')
|
||||||
: [],
|
: [],
|
||||||
@@ -76,6 +77,7 @@ function parseOrderShipping(shippingAddress: Record<string, unknown>) {
|
|||||||
deliveryMode,
|
deliveryMode,
|
||||||
addressLine: addressParts.join(' · '),
|
addressLine: addressParts.join(' · '),
|
||||||
groups,
|
groups,
|
||||||
|
hasPasKraye: groups.some((group) => group.pasKraye),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +112,7 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
|
|||||||
if (!mounted || !order) return null
|
if (!mounted || !order) return null
|
||||||
|
|
||||||
const itemCount = totalQuantity(order)
|
const itemCount = totalQuantity(order)
|
||||||
|
const shippingInfo = parseOrderShipping(order.shippingAddress)
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
@@ -154,46 +157,43 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(() => {
|
<div className={styles.shippingBlock}>
|
||||||
const shippingInfo = parseOrderShipping(order.shippingAddress)
|
<div className={styles.shippingSectionTitle}>{t('orderItems.delivery')}</div>
|
||||||
return (
|
{shippingInfo.deliveryMode === 'pickup' ? (
|
||||||
<div className={styles.shippingBlock}>
|
<p className={styles.addressText}>{t('orderItems.pickup')}</p>
|
||||||
<div className={styles.shippingSectionTitle}>{t('orderItems.delivery')}</div>
|
) : shippingInfo.addressLine ? (
|
||||||
{shippingInfo.deliveryMode === 'pickup' ? (
|
<p className={styles.addressText}>{shippingInfo.addressLine}</p>
|
||||||
<p className={styles.addressText}>{t('orderItems.pickup')}</p>
|
) : null}
|
||||||
) : shippingInfo.addressLine ? (
|
|
||||||
<p className={styles.addressText}>{shippingInfo.addressLine}</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{shippingInfo.groups.length > 0 ? (
|
{shippingInfo.groups.length > 0 ? (
|
||||||
<ul className={styles.shippingGroupList}>
|
<ul className={styles.shippingGroupList}>
|
||||||
{shippingInfo.groups.map((group) => (
|
{shippingInfo.groups.map((group) => (
|
||||||
<li key={group.groupId} className={styles.shippingGroupCard}>
|
<li key={group.groupId} className={styles.shippingGroupCard}>
|
||||||
<div className={styles.shippingGroupHeader}>
|
<div className={styles.shippingGroupHeader}>
|
||||||
<span className={styles.shippingGroupTitle}>{group.title}</span>
|
<span className={styles.shippingGroupTitle}>{group.title}</span>
|
||||||
<span className={styles.shippingGroupAmount}>
|
<span className={styles.shippingGroupAmount}>
|
||||||
{group.amount > 0
|
{group.pasKraye
|
||||||
? formatIrtPrice(group.amount)
|
? t('orderItems.pasKraye')
|
||||||
: t('orderItems.freeShipping')}
|
: group.amount > 0
|
||||||
</span>
|
? formatIrtPrice(group.amount)
|
||||||
</div>
|
: t('orderItems.freeShipping')}
|
||||||
{group.methodLabel ? (
|
</span>
|
||||||
<div className={styles.shippingMethod}>{group.methodLabel}</div>
|
</div>
|
||||||
) : null}
|
{group.methodLabel ? (
|
||||||
{group.itemNames.length > 0 ? (
|
<div className={styles.shippingMethod}>{group.methodLabel}</div>
|
||||||
<ul className={styles.shippingItemNames}>
|
) : null}
|
||||||
{group.itemNames.map((name) => (
|
{group.itemNames.length > 0 ? (
|
||||||
<li key={`${group.groupId}-${name}`}>{name}</li>
|
<ul className={styles.shippingItemNames}>
|
||||||
))}
|
{group.itemNames.map((name) => (
|
||||||
</ul>
|
<li key={`${group.groupId}-${name}`}>{name}</li>
|
||||||
) : null}
|
))}
|
||||||
</li>
|
</ul>
|
||||||
))}
|
) : null}
|
||||||
</ul>
|
</li>
|
||||||
) : null}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
)
|
) : null}
|
||||||
})()}
|
</div>
|
||||||
|
|
||||||
{order.items.length === 0 ? (
|
{order.items.length === 0 ? (
|
||||||
<EmptyState layout="inline" message={t('orderItems.empty')} />
|
<EmptyState layout="inline" message={t('orderItems.empty')} />
|
||||||
@@ -244,7 +244,9 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
|
|||||||
<span className={styles.totalsValue}>
|
<span className={styles.totalsValue}>
|
||||||
{order.shippingTotal > 0
|
{order.shippingTotal > 0
|
||||||
? formatIrtPrice(order.shippingTotal)
|
? formatIrtPrice(order.shippingTotal)
|
||||||
: t('orderItems.freeShipping')}
|
: shippingInfo.hasPasKraye
|
||||||
|
? t('orderItems.pasKraye')
|
||||||
|
: t('orderItems.freeShipping')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{order.discountTotal > 0 ? (
|
{order.discountTotal > 0 ? (
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface CheckoutShippingQuote {
|
|||||||
methodId: string | null
|
methodId: string | null
|
||||||
methodLabel: string | null
|
methodLabel: string | null
|
||||||
amount: number
|
amount: number
|
||||||
|
pasKraye?: boolean
|
||||||
itemNames: string[]
|
itemNames: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ function parseShippingQuotes(value: unknown): CheckoutShippingQuote[] {
|
|||||||
methodId: typeof row.methodId === 'string' ? row.methodId : null,
|
methodId: typeof row.methodId === 'string' ? row.methodId : null,
|
||||||
methodLabel: typeof row.methodLabel === 'string' ? row.methodLabel : null,
|
methodLabel: typeof row.methodLabel === 'string' ? row.methodLabel : null,
|
||||||
amount: typeof row.amount === 'number' && Number.isFinite(row.amount) ? row.amount : 0,
|
amount: typeof row.amount === 'number' && Number.isFinite(row.amount) ? row.amount : 0,
|
||||||
|
pasKraye: row.pasKraye === true,
|
||||||
itemNames: Array.isArray(row.itemNames)
|
itemNames: Array.isArray(row.itemNames)
|
||||||
? row.itemNames.filter((name): name is string => typeof name === 'string')
|
? row.itemNames.filter((name): name is string => typeof name === 'string')
|
||||||
: [],
|
: [],
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ const en = {
|
|||||||
'orderItems.shipping': 'Shipping',
|
'orderItems.shipping': 'Shipping',
|
||||||
'orderItems.discount': 'Discount',
|
'orderItems.discount': 'Discount',
|
||||||
'orderItems.freeShipping': 'Free shipping',
|
'orderItems.freeShipping': 'Free shipping',
|
||||||
|
'orderItems.pasKraye': 'Freight collect',
|
||||||
|
|
||||||
'favorites.title': 'My Favorites',
|
'favorites.title': 'My Favorites',
|
||||||
'favorites.subtitle': 'Products you have saved for later.',
|
'favorites.subtitle': 'Products you have saved for later.',
|
||||||
@@ -588,6 +589,7 @@ const fa: Record<MessageKey, string> = {
|
|||||||
'orderItems.shipping': 'هزینه ارسال',
|
'orderItems.shipping': 'هزینه ارسال',
|
||||||
'orderItems.discount': 'تخفیف',
|
'orderItems.discount': 'تخفیف',
|
||||||
'orderItems.freeShipping': 'ارسال رایگان',
|
'orderItems.freeShipping': 'ارسال رایگان',
|
||||||
|
'orderItems.pasKraye': 'پس کرایه',
|
||||||
|
|
||||||
'favorites.title': 'علاقهمندیها',
|
'favorites.title': 'علاقهمندیها',
|
||||||
'favorites.subtitle': 'محصولاتی که برای بعد ذخیره کردهاید.',
|
'favorites.subtitle': 'محصولاتی که برای بعد ذخیره کردهاید.',
|
||||||
|
|||||||
@@ -33,9 +33,16 @@ interface ShippingGroupQuote {
|
|||||||
title: string
|
title: string
|
||||||
methods: ShippingMethodId[]
|
methods: ShippingMethodId[]
|
||||||
amount: number
|
amount: number
|
||||||
|
pasKraye: boolean
|
||||||
items: CartItem[]
|
items: CartItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatShippingAmountLabel(quote: { amount: number; pasKraye?: boolean }) {
|
||||||
|
if (quote.pasKraye) return 'پس کرایه'
|
||||||
|
if (quote.amount > 0) return formatIrtPrice(quote.amount)
|
||||||
|
return 'ارسال رایگان'
|
||||||
|
}
|
||||||
|
|
||||||
function resolveProvinceId(
|
function resolveProvinceId(
|
||||||
provinces: CityOption[],
|
provinces: CityOption[],
|
||||||
provinceName: string | null | undefined,
|
provinceName: string | null | undefined,
|
||||||
@@ -162,6 +169,7 @@ export function CheckoutDeliveryStep() {
|
|||||||
title: STORE_SHIPPING_TITLE,
|
title: STORE_SHIPPING_TITLE,
|
||||||
methods: [],
|
methods: [],
|
||||||
amount: 0,
|
amount: 0,
|
||||||
|
pasKraye: false,
|
||||||
items: cartItems,
|
items: cartItems,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -178,6 +186,7 @@ export function CheckoutDeliveryStep() {
|
|||||||
shippingConfig.staticShippingProvinceExceptions,
|
shippingConfig.staticShippingProvinceExceptions,
|
||||||
provinceId,
|
provinceId,
|
||||||
),
|
),
|
||||||
|
pasKraye: false,
|
||||||
items: cartItems,
|
items: cartItems,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -203,15 +212,19 @@ export function CheckoutDeliveryStep() {
|
|||||||
if (existing) {
|
if (existing) {
|
||||||
existing.items.push(item)
|
existing.items.push(item)
|
||||||
} else {
|
} else {
|
||||||
|
const pasKraye = group.pasKraye === true
|
||||||
buckets.set(group.id, {
|
buckets.set(group.id, {
|
||||||
key: group.id,
|
key: group.id,
|
||||||
title: quoteTitleForGroup(group.isDefault, group.nameFa, group.nameEn),
|
title: quoteTitleForGroup(group.isDefault, group.nameFa, group.nameEn),
|
||||||
methods: group.methods,
|
methods: group.methods,
|
||||||
amount: resolveProvinceAmount(
|
amount: pasKraye
|
||||||
group.defaultAmount,
|
? 0
|
||||||
group.provinceExceptions,
|
: resolveProvinceAmount(
|
||||||
provinceId,
|
group.defaultAmount,
|
||||||
),
|
group.provinceExceptions,
|
||||||
|
provinceId,
|
||||||
|
),
|
||||||
|
pasKraye,
|
||||||
items: [item],
|
items: [item],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -223,6 +236,7 @@ export function CheckoutDeliveryStep() {
|
|||||||
title: STORE_SHIPPING_TITLE,
|
title: STORE_SHIPPING_TITLE,
|
||||||
methods: [],
|
methods: [],
|
||||||
amount: 0,
|
amount: 0,
|
||||||
|
pasKraye: false,
|
||||||
items: ungrouped,
|
items: ungrouped,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -276,6 +290,7 @@ export function CheckoutDeliveryStep() {
|
|||||||
? SHIPPING_METHOD_LABELS_FA[methodId as ShippingMethodId] ?? methodId
|
? SHIPPING_METHOD_LABELS_FA[methodId as ShippingMethodId] ?? methodId
|
||||||
: null,
|
: null,
|
||||||
amount: quote.amount,
|
amount: quote.amount,
|
||||||
|
pasKraye: quote.pasKraye,
|
||||||
itemNames: quote.items.map(
|
itemNames: quote.items.map(
|
||||||
(item) => item.productNameFa || item.productTitle,
|
(item) => item.productNameFa || item.productTitle,
|
||||||
),
|
),
|
||||||
@@ -288,6 +303,10 @@ export function CheckoutDeliveryStep() {
|
|||||||
() => shippingQuotes.reduce((sum, quote) => sum + quote.amount, 0),
|
() => shippingQuotes.reduce((sum, quote) => sum + quote.amount, 0),
|
||||||
[shippingQuotes],
|
[shippingQuotes],
|
||||||
)
|
)
|
||||||
|
const hasPasKraye = useMemo(
|
||||||
|
() => shippingQuotes.some((quote) => quote.pasKraye),
|
||||||
|
[shippingQuotes],
|
||||||
|
)
|
||||||
|
|
||||||
const shippingReady =
|
const shippingReady =
|
||||||
deliveryMode === 'pickup' ||
|
deliveryMode === 'pickup' ||
|
||||||
@@ -441,14 +460,14 @@ export function CheckoutDeliveryStep() {
|
|||||||
<span
|
<span
|
||||||
className={[
|
className={[
|
||||||
styles.shippingGroupAmount,
|
styles.shippingGroupAmount,
|
||||||
quote.amount > 0 ? '' : styles.shippingGroupAmountFree,
|
quote.pasKraye || quote.amount <= 0
|
||||||
|
? styles.shippingGroupAmountFree
|
||||||
|
: '',
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
>
|
>
|
||||||
{quote.amount > 0
|
{formatShippingAmountLabel(quote)}
|
||||||
? formatIrtPrice(quote.amount)
|
|
||||||
: 'ارسال رایگان'}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -495,7 +514,13 @@ export function CheckoutDeliveryStep() {
|
|||||||
{shippingQuotes.length > 1 ? (
|
{shippingQuotes.length > 1 ? (
|
||||||
<div className={styles.shippingTotalRow}>
|
<div className={styles.shippingTotalRow}>
|
||||||
<span>جمع هزینه ارسال</span>
|
<span>جمع هزینه ارسال</span>
|
||||||
<span className={styles.priceEn}>{formatIrtPrice(shippingTotal)}</span>
|
<span className={styles.priceEn}>
|
||||||
|
{hasPasKraye && shippingTotal <= 0
|
||||||
|
? 'پس کرایه'
|
||||||
|
: shippingTotal <= 0
|
||||||
|
? 'ارسال رایگان'
|
||||||
|
: formatIrtPrice(shippingTotal)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -379,9 +379,11 @@ export function CheckoutPaymentStep() {
|
|||||||
<span className={styles.priceEn}>
|
<span className={styles.priceEn}>
|
||||||
{deliveryMode === 'pickup'
|
{deliveryMode === 'pickup'
|
||||||
? '—'
|
? '—'
|
||||||
: shippingTotal <= 0
|
: shippingQuotes.some((quote) => quote.pasKraye) && shippingTotal <= 0
|
||||||
? 'ارسال رایگان'
|
? 'پس کرایه'
|
||||||
: formatIrtPrice(shippingTotal)}
|
: shippingTotal <= 0
|
||||||
|
? 'ارسال رایگان'
|
||||||
|
: formatIrtPrice(shippingTotal)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.summaryRow}>
|
<div className={styles.summaryRow}>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface PublicShippingCostGroup {
|
|||||||
description: string | null
|
description: string | null
|
||||||
methods: ShippingMethodId[]
|
methods: ShippingMethodId[]
|
||||||
defaultAmount: number
|
defaultAmount: number
|
||||||
|
pasKraye?: boolean
|
||||||
isDefault: boolean
|
isDefault: boolean
|
||||||
categoryIds: string[]
|
categoryIds: string[]
|
||||||
provinceExceptions?: Array<{ provinceId: string; amount: number }>
|
provinceExceptions?: Array<{ provinceId: string; amount: number }>
|
||||||
|
|||||||
Reference in New Issue
Block a user