From 71aa8d3940b01320e0735789f600e17b7ce83b66 Mon Sep 17 00:00:00 2001 From: dwindown Date: Mon, 10 Nov 2025 17:52:20 +0700 Subject: [PATCH] fix(orders): Fix shipping rate recalculation and auto-selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Issues Fixed: ### 1. Shipping rates not recalculating when address changes ✅ **Problem:** - Change province → Rates stay the same - Query was cached incorrectly **Root Cause:** Query key only tracked country, state, postcode: ```ts queryKey: [..., shippingData.country, shippingData.state, shippingData.postcode] ``` But Rajaongkir and other plugins also need: - City (different rates per city) - Address (for some plugins) **Solution:** ```ts queryKey: [ ..., shippingData.country, shippingData.state, shippingData.city, // Added shippingData.postcode, shippingData.address_1 // Added ], staleTime: 0, // Always refetch when key changes ``` ### 2. First rate auto-selected but dropdown shows placeholder ✅ **Problem:** - Rates calculated → First rate used in total - But dropdown shows "Select shipping" - Confusing UX **Solution:** Added useEffect to auto-select first rate: ```ts useEffect(() => { if (shippingRates?.methods?.length > 0) { const firstRateId = shippingRates.methods[0].id; const currentExists = shippingRates.methods.some(m => m.id === shippingMethod); // Auto-select if no selection or current not in new rates if (!shippingMethod || !currentExists) { setShippingMethod(firstRateId); } } }, [shippingRates?.methods]); ``` ## Benefits: - ✅ Change province → Rates recalculate immediately - ✅ First rate auto-selected in dropdown - ✅ Selection cleared if no rates available - ✅ Selection preserved if still valid after recalculation ## Testing: 1. Select Jakarta → Shows JNE rates 2. Change to Bali → Rates recalculate, first auto-selected 3. Change to remote area → Different rates, first auto-selected 4. Dropdown always shows current selection --- .../src/routes/Orders/partials/OrderForm.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/admin-spa/src/routes/Orders/partials/OrderForm.tsx b/admin-spa/src/routes/Orders/partials/OrderForm.tsx index f68ad62..72bc72f 100644 --- a/admin-spa/src/routes/Orders/partials/OrderForm.tsx +++ b/admin-spa/src/routes/Orders/partials/OrderForm.tsx @@ -187,7 +187,7 @@ export default function OrderForm({ // 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], + queryKey: ['shipping-rates', items.map(i => ({ product_id: i.product_id, qty: i.qty })), shippingData.country, shippingData.state, shippingData.city, shippingData.postcode, shippingData.address_1], queryFn: async () => { if (!shippingData.country) return null; return api.post('/shipping/calculate', { @@ -196,11 +196,12 @@ export default function OrderForm({ }); }, enabled: !!shippingData.country && items.length > 0, + staleTime: 0, // Always refetch when query key changes }); // 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)], + queryKey: ['order-preview', items.map(i => ({ product_id: i.product_id, qty: i.qty })), bCountry, bState, bPost, bCity, shippingData.country, shippingData.state, shippingData.city, shippingData.postcode, shippingMethod, validatedCoupons.map(c => c.code)], queryFn: async () => { if (items.length === 0) return null; return api.post('/orders/preview', { @@ -212,6 +213,7 @@ export default function OrderForm({ }); }, enabled: items.length > 0 && !!bCountry, + staleTime: 0, // Always refetch when query key changes }); // --- Product search for Add Item --- @@ -301,6 +303,21 @@ export default function OrderForm({ setValidatedCoupons(validatedCoupons.filter(c => c.code !== code)); }; + // Auto-select first shipping rate when rates change + React.useEffect(() => { + if (shippingRates?.methods && shippingRates.methods.length > 0) { + const firstRateId = shippingRates.methods[0].id; + // Only auto-select if no method selected or current method not in new rates + const currentMethodExists = shippingRates.methods.some((m: any) => m.id === shippingMethod); + if (!shippingMethod || !currentMethodExists) { + setShippingMethod(firstRateId); + } + } else if (shippingRates?.methods && shippingRates.methods.length === 0) { + // Clear selection if no rates available + setShippingMethod(''); + } + }, [shippingRates?.methods]); + // Check if cart has physical products const hasPhysicalProduct = React.useMemo( () => items.some(item => {