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:
dwindown
2025-11-10 16:01:24 +07:00
parent 2b48e60637
commit 3f6052f1de

View File

@@ -185,6 +185,35 @@ export default function OrderForm({
enabled: items.length > 0, 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 --- // --- Product search for Add Item ---
const [searchQ, setSearchQ] = React.useState(''); const [searchQ, setSearchQ] = React.useState('');
const [customerSearchQ, setCustomerSearchQ] = React.useState(''); const [customerSearchQ, setCustomerSearchQ] = React.useState('');
@@ -555,24 +584,38 @@ export default function OrderForm({
<div className="flex justify-between"> <div className="flex justify-between">
<span className="opacity-70">{__('Subtotal')}</span> <span className="opacity-70">{__('Subtotal')}</span>
<span> <span>
{itemsTotal ? money(itemsTotal) : '—'} {orderPreview ? money(orderPreview.subtotal) : itemsTotal ? money(itemsTotal) : '—'}
</span> </span>
</div> </div>
{shippingCost > 0 && ( {(orderPreview?.shipping_total > 0 || shippingCost > 0) && (
<div className="flex justify-between"> <div className="flex justify-between">
<span className="opacity-70">{__('Shipping')}</span> <span className="opacity-70">{__('Shipping')}</span>
<span>{money(shippingCost)}</span> <span>{orderPreview ? money(orderPreview.shipping_total) : money(shippingCost)}</span>
</div> </div>
)} )}
{couponDiscount > 0 && ( {(orderPreview?.discount_total > 0 || couponDiscount > 0) && (
<div className="flex justify-between text-green-700"> <div className="flex justify-between text-green-700">
<span>{__('Discount')}</span> <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>
)} )}
<div className="flex justify-between pt-1.5 border-t font-medium"> <div className="flex justify-between pt-1.5 border-t font-medium">
<span>{__('Total')}</span> <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> </div>
</div> </div>
@@ -913,6 +956,22 @@ export default function OrderForm({
{hasPhysicalProduct && ( {hasPhysicalProduct && (
<div> <div>
<Label>{__('Shipping method')}</Label> <Label>{__('Shipping method')}</Label>
{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}> <Select value={shippingMethod} onValueChange={setShippingMethod}>
<SelectTrigger className="w-full"><SelectValue placeholder={shippings.length ? __('Select shipping') : __('No methods')} /></SelectTrigger> <SelectTrigger className="w-full"><SelectValue placeholder={shippings.length ? __('Select shipping') : __('No methods')} /></SelectTrigger>
<SelectContent> <SelectContent>
@@ -921,6 +980,7 @@ export default function OrderForm({
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
)}
</div> </div>
)} )}
</div> </div>