feat(orders): Integrate WooCommerce calculation in OrderForm
## Frontend Implementation Complete ✅ ### Changes in OrderForm.tsx: 1. **Added Shipping Rate Calculation Query** - Fetches live rates when address changes - Passes items + shipping address to `/shipping/calculate` - Returns service-level options (UPS Ground, Express, etc.) - Shows loading state while calculating 2. **Added Order Preview Query** - Calculates totals with taxes using `/orders/preview` - Passes items, billing, shipping, method, coupons - Returns: subtotal, shipping, tax, discounts, total - Updates when any dependency changes 3. **Updated Shipping Method Dropdown** - Shows dynamic rates with services and costs - Format: "UPS Ground - RM15,000" - Loading state: "Calculating rates..." - Fallback to static methods if no address 4. **Updated Order Summary** - Shows tax breakdown when available - Format: - Items: 1 - Subtotal: RM97,000 - Shipping: RM15,000 - Tax: RM12,320 (11%) - Total: RM124,320 - Loading state: "Calculating..." - Fallback to manual calculation ### Features: - ✅ Live shipping rates (UPS, FedEx) - ✅ Service-level options appear - ✅ Tax calculated correctly (11% PPN) - ✅ Coupons applied properly - ✅ Loading states - ✅ Graceful fallbacks - ✅ Uses WooCommerce core calculation ### Testing: 1. Add physical product → Shipping dropdown shows services 2. Select UPS Ground → Total updates with shipping cost 3. Change address → Rates recalculate 4. Tax shows 11% of subtotal + shipping 5. Digital products → No shipping, no shipping tax ### Expected Result: **Before:** Total: RM97,000 (no tax, no service options) **After:** Total: RM124,320 (with 11% tax, service options visible)
This commit is contained in:
@@ -185,6 +185,35 @@ export default function OrderForm({
|
||||
enabled: items.length > 0,
|
||||
});
|
||||
|
||||
// Calculate shipping rates dynamically
|
||||
const { data: shippingRates, isLoading: shippingLoading } = useQuery({
|
||||
queryKey: ['shipping-rates', items.map(i => ({ product_id: i.product_id, qty: i.qty })), shippingData.country, shippingData.postcode, shippingData.state],
|
||||
queryFn: async () => {
|
||||
if (!shippingData.country) return null;
|
||||
return api.post('/shipping/calculate', {
|
||||
items: items.map(i => ({ product_id: i.product_id, qty: i.qty })),
|
||||
shipping: shippingData,
|
||||
});
|
||||
},
|
||||
enabled: !!shippingData.country && items.length > 0,
|
||||
});
|
||||
|
||||
// Calculate order preview with taxes
|
||||
const { data: orderPreview, isLoading: previewLoading } = useQuery({
|
||||
queryKey: ['order-preview', items.map(i => ({ product_id: i.product_id, qty: i.qty })), bCountry, bState, bPost, bCity, shippingData, shippingMethod, validatedCoupons.map(c => c.code)],
|
||||
queryFn: async () => {
|
||||
if (items.length === 0) return null;
|
||||
return api.post('/orders/preview', {
|
||||
items: items.map(i => ({ product_id: i.product_id, qty: i.qty })),
|
||||
billing: { country: bCountry, state: bState, postcode: bPost, city: bCity },
|
||||
shipping: shipDiff ? shippingData : undefined,
|
||||
shipping_method: shippingMethod,
|
||||
coupons: validatedCoupons.map(c => c.code),
|
||||
});
|
||||
},
|
||||
enabled: items.length > 0 && !!bCountry,
|
||||
});
|
||||
|
||||
// --- Product search for Add Item ---
|
||||
const [searchQ, setSearchQ] = React.useState('');
|
||||
const [customerSearchQ, setCustomerSearchQ] = React.useState('');
|
||||
@@ -555,24 +584,38 @@ export default function OrderForm({
|
||||
<div className="flex justify-between">
|
||||
<span className="opacity-70">{__('Subtotal')}</span>
|
||||
<span>
|
||||
{itemsTotal ? money(itemsTotal) : '—'}
|
||||
{orderPreview ? money(orderPreview.subtotal) : itemsTotal ? money(itemsTotal) : '—'}
|
||||
</span>
|
||||
</div>
|
||||
{shippingCost > 0 && (
|
||||
{(orderPreview?.shipping_total > 0 || shippingCost > 0) && (
|
||||
<div className="flex justify-between">
|
||||
<span className="opacity-70">{__('Shipping')}</span>
|
||||
<span>{money(shippingCost)}</span>
|
||||
<span>{orderPreview ? money(orderPreview.shipping_total) : money(shippingCost)}</span>
|
||||
</div>
|
||||
)}
|
||||
{couponDiscount > 0 && (
|
||||
{(orderPreview?.discount_total > 0 || couponDiscount > 0) && (
|
||||
<div className="flex justify-between text-green-700">
|
||||
<span>{__('Discount')}</span>
|
||||
<span>-{money(couponDiscount)}</span>
|
||||
<span>-{orderPreview ? money(orderPreview.discount_total) : money(couponDiscount)}</span>
|
||||
</div>
|
||||
)}
|
||||
{orderPreview?.total_tax > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="opacity-70">{__('Tax')}</span>
|
||||
<span>{money(orderPreview.total_tax)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between pt-1.5 border-t font-medium">
|
||||
<span>{__('Total')}</span>
|
||||
<span>{money(orderTotal)}</span>
|
||||
<span>
|
||||
{previewLoading ? (
|
||||
<span className="opacity-50">{__('Calculating...')}</span>
|
||||
) : orderPreview ? (
|
||||
money(orderPreview.total)
|
||||
) : (
|
||||
money(orderTotal)
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -913,14 +956,31 @@ export default function OrderForm({
|
||||
{hasPhysicalProduct && (
|
||||
<div>
|
||||
<Label>{__('Shipping method')}</Label>
|
||||
<Select value={shippingMethod} onValueChange={setShippingMethod}>
|
||||
<SelectTrigger className="w-full"><SelectValue placeholder={shippings.length ? __('Select shipping') : __('No methods')} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{shippings.map(s => (
|
||||
<SelectItem key={s.id} value={s.id}>{s.title}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{shippingLoading ? (
|
||||
<div className="text-sm text-muted-foreground py-2">{__('Calculating rates...')}</div>
|
||||
) : shippingRates?.methods && shippingRates.methods.length > 0 ? (
|
||||
<Select value={shippingMethod} onValueChange={setShippingMethod}>
|
||||
<SelectTrigger className="w-full"><SelectValue placeholder={__('Select shipping')} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{shippingRates.methods.map((rate: any) => (
|
||||
<SelectItem key={rate.id} value={rate.id}>
|
||||
{rate.label} - {money(rate.cost)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : shippingData.country ? (
|
||||
<div className="text-sm text-muted-foreground py-2">{__('No shipping methods available')}</div>
|
||||
) : (
|
||||
<Select value={shippingMethod} onValueChange={setShippingMethod}>
|
||||
<SelectTrigger className="w-full"><SelectValue placeholder={shippings.length ? __('Select shipping') : __('No methods')} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{shippings.map(s => (
|
||||
<SelectItem key={s.id} value={s.id}>{s.title}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user