Add shipping cost management and checkout shipping fees.
Business store shipping groups/static rates UI, and customer checkout that quotes and applies shipping on delivery and payment. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
95390933a7
commit
c59ca4444f
@@ -30,6 +30,54 @@ function totalQuantity(order: Order) {
|
||||
return order.items.reduce((sum, item) => sum + item.quantity, 0)
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null
|
||||
}
|
||||
|
||||
function parseOrderShipping(shippingAddress: Record<string, unknown>) {
|
||||
const root = asRecord(shippingAddress) ?? {}
|
||||
const deliveryMode =
|
||||
root.deliveryMode === 'pickup' || root.deliveryMode === 'delivery'
|
||||
? root.deliveryMode
|
||||
: root.province === 'Pickup'
|
||||
? 'pickup'
|
||||
: 'delivery'
|
||||
const shipping = asRecord(root.shipping)
|
||||
const groupsRaw = Array.isArray(shipping?.groups) ? shipping.groups : []
|
||||
const groups = groupsRaw
|
||||
.map((entry) => {
|
||||
const row = asRecord(entry)
|
||||
if (!row) return null
|
||||
const groupId = typeof row.groupId === 'string' ? row.groupId : ''
|
||||
if (!groupId) return null
|
||||
return {
|
||||
groupId,
|
||||
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,
|
||||
itemNames: Array.isArray(row.itemNames)
|
||||
? row.itemNames.filter((name): name is string => typeof name === 'string')
|
||||
: [],
|
||||
}
|
||||
})
|
||||
.filter((group): group is NonNullable<typeof group> => Boolean(group))
|
||||
|
||||
const addressParts = [
|
||||
typeof root.province === 'string' ? root.province : '',
|
||||
typeof root.city === 'string' ? root.city : '',
|
||||
typeof root.address === 'string' ? root.address : '',
|
||||
typeof root.postalCode === 'string' ? root.postalCode : '',
|
||||
].filter((part) => part && part !== 'Pickup' && part !== 'Store' && part !== 'In-store pickup')
|
||||
|
||||
return {
|
||||
deliveryMode,
|
||||
addressLine: addressParts.join(' · '),
|
||||
groups,
|
||||
}
|
||||
}
|
||||
|
||||
export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps) {
|
||||
const t = useT()
|
||||
const [mounted, setMounted] = useState(open)
|
||||
@@ -105,6 +153,47 @@ 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}
|
||||
|
||||
{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>
|
||||
)
|
||||
})()}
|
||||
|
||||
{order.items.length === 0 ? (
|
||||
<p className={styles.empty}>{t('orderItems.empty')}</p>
|
||||
) : (
|
||||
@@ -144,15 +233,35 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.summary}>
|
||||
<span className={styles.summaryLabel}>
|
||||
{t('orderItems.summary', {
|
||||
count: itemCount,
|
||||
itemsLabel:
|
||||
itemCount === 1 ? t('orderItems.item') : t('orderItems.itemsPlural'),
|
||||
})}
|
||||
</span>
|
||||
<span className={styles.summaryValue}>{formatIrtPrice(order.total)}</span>
|
||||
<div className={styles.totalsBlock}>
|
||||
<div className={styles.totalsRow}>
|
||||
<span>{t('orderItems.subtotal')}</span>
|
||||
<span className={styles.totalsValue}>{formatIrtPrice(order.subtotal)}</span>
|
||||
</div>
|
||||
<div className={styles.totalsRow}>
|
||||
<span>{t('orderItems.shipping')}</span>
|
||||
<span className={styles.totalsValue}>
|
||||
{order.shippingTotal > 0
|
||||
? formatIrtPrice(order.shippingTotal)
|
||||
: t('orderItems.freeShipping')}
|
||||
</span>
|
||||
</div>
|
||||
{order.discountTotal > 0 ? (
|
||||
<div className={styles.totalsRow}>
|
||||
<span>{t('orderItems.discount')}</span>
|
||||
<span className={styles.totalsValue}>{formatIrtPrice(order.discountTotal)}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={`${styles.totalsRow} ${styles.totalsRowGrand}`}>
|
||||
<span>
|
||||
{t('orderItems.summary', {
|
||||
count: itemCount,
|
||||
itemsLabel:
|
||||
itemCount === 1 ? t('orderItems.item') : t('orderItems.itemsPlural'),
|
||||
})}
|
||||
</span>
|
||||
<span className={styles.summaryValue}>{formatIrtPrice(order.total)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={modalStyles.actions}>
|
||||
|
||||
Reference in New Issue
Block a user