diff options
Diffstat (limited to 'src-migrate/modules')
| -rw-r--r-- | src-migrate/modules/cart/components/ItemAction.tsx | 125 | ||||
| -rw-r--r-- | src-migrate/modules/cart/components/ItemSelect.tsx | 76 | ||||
| -rw-r--r-- | src-migrate/modules/cart/stores/useCartStore.ts | 198 |
3 files changed, 230 insertions, 169 deletions
diff --git a/src-migrate/modules/cart/components/ItemAction.tsx b/src-migrate/modules/cart/components/ItemAction.tsx index eea0cbe9..4dcebd9e 100644 --- a/src-migrate/modules/cart/components/ItemAction.tsx +++ b/src-migrate/modules/cart/components/ItemAction.tsx @@ -15,7 +15,11 @@ import { useProductCartContext } from '@/contexts/ProductCartContext'; import { removeSelectedItemsFromCookie, removeCartItemsFromCookie, + quantityUpdateState, + getCartDataFromCookie, + setCartDataToCookie, } from '~/utils/cart'; + import { toast } from 'react-hot-toast'; type Props = { @@ -30,25 +34,24 @@ const CartItemAction = ({ item }: Props) => { const [quantity, setQuantity] = useState<number>(item.quantity); - const { loadCart, cart, updateCartItem } = useCartStore(); // TAMBAHKAN cart dan updateCartItem + const { loadCart, cart, updateCartItem } = useCartStore(); const limitQty = item.limit_qty?.transaction || 0; - // PERBAIKI FUNCTION INI const handleDelete = async () => { if (typeof auth !== 'object') return; setIsLoadDelete(true); try { - // Step 1: Delete from server + // Delete from server await deleteUserCart(auth.id, [item.cart_id]); - // Step 2: Clean up cookies IMMEDIATELY + // Clean up cookies immediately removeSelectedItemsFromCookie([item.id]); removeCartItemsFromCookie([item.cart_id]); - // Step 3: Update local cart state optimistically + // Update local cart state optimistically if (cart) { const updatedProducts = cart.products.filter( (product) => product.id !== item.id @@ -61,7 +64,7 @@ const CartItemAction = ({ item }: Props) => { updateCartItem(updatedCart); } - // Step 4: Reload from server to ensure consistency + // Reload from server and refresh context await loadCart(auth.id); setRefreshCart(true); @@ -77,12 +80,47 @@ const CartItemAction = ({ item }: Props) => { } }; + const updateQuantityInCookie = (productId, cartId, newQuantity) => { + try { + const cartData = getCartDataFromCookie(); + let itemFound = false; + + // Find item by cart_id key or search within objects + if (cartData[cartId]) { + cartData[cartId].quantity = newQuantity; + itemFound = true; + } else { + // Search by product id or cart_id within objects + for (const key in cartData) { + const item = cartData[key]; + if (item.id === productId || item.cart_id === cartId) { + item.quantity = newQuantity; + itemFound = true; + break; + } + } + } + + if (itemFound) { + setCartDataToCookie(cartData); + return true; + } + + return false; + } catch (error) { + console.error('Error updating quantity in cookie:', error); + return false; + } + }; + const decreaseQty = () => { setQuantity((quantity) => (quantity -= 1)); }; + const increaseQty = () => { setQuantity((quantity) => (quantity += 1)); }; + const debounceQty = useDebounce(quantity, 1000); useEffect(() => { @@ -93,18 +131,73 @@ const CartItemAction = ({ item }: Props) => { useEffect(() => { const updateCart = async () => { if (typeof auth !== 'object' || isNaN(debounceQty)) return; + if (debounceQty === item.quantity) return; + quantityUpdateState.startUpdate(item.id); setIsLoadQuantity(true); - await upsertUserCart({ - userId: auth.id, - type: item.cart_type, - id: item.id, - qty: debounceQty, - selected: item.selected, - }); - await loadCart(auth.id); - setIsLoadQuantity(false); + + try { + // Update cookie immediately for responsive UI + updateQuantityInCookie(item.id, item.cart_id, debounceQty); + + // Update local cart state optimistically + if (cart) { + const updatedProducts = cart.products.map((product) => + product.id === item.id + ? { ...product, quantity: debounceQty } + : product + ); + const updatedCart = { + ...cart, + products: updatedProducts, + }; + updateCartItem(updatedCart); + } + + // Send update to server + await upsertUserCart({ + userId: auth.id, + type: item.cart_type, + id: item.id, + qty: debounceQty, + selected: item.selected, + }); + + // Reload from server to ensure consistency + await loadCart(auth.id); + + // Re-update cookie if server reload overwrote it + const currentCookieData = getCartDataFromCookie(); + let needsReUpdate = false; + + for (const key in currentCookieData) { + const cookieItem = currentCookieData[key]; + if ( + (cookieItem.id === item.id || + cookieItem.cart_id === item.cart_id) && + cookieItem.quantity !== debounceQty + ) { + needsReUpdate = true; + break; + } + } + + if (needsReUpdate) { + updateQuantityInCookie(item.id, item.cart_id, debounceQty); + } + } catch (error) { + console.error('Error updating quantity:', error); + toast.error('Gagal mengupdate quantity'); + + // Revert changes on error + updateQuantityInCookie(item.id, item.cart_id, item.quantity); + loadCart(auth.id); + } finally { + setIsLoadQuantity(false); + quantityUpdateState.endUpdate(item.id); + } }; + updateCart(); //eslint-disable-next-line react-hooks/exhaustive-deps }, [debounceQty]); @@ -156,4 +249,4 @@ const CartItemAction = ({ item }: Props) => { ); }; -export default CartItemAction; +export default CartItemAction;
\ No newline at end of file diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx index 00c7be43..8dbfe2bc 100644 --- a/src-migrate/modules/cart/components/ItemSelect.tsx +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -20,124 +20,98 @@ const CartItemSelect = ({ item }: Props) => { const { updateCartItem, cart, loadCart } = useCartStore(); const [isUpdating, setIsUpdating] = useState<boolean>(false); const [localSelected, setLocalSelected] = useState<boolean>(item.selected); - const [isGlobalUpdating, setIsGlobalUpdating] = useState<boolean>(false); // Subscribe to global checkbox update state useEffect(() => { const handleUpdateStateChange = (isUpdating) => { - setIsGlobalUpdating(isUpdating); + // This component doesn't need to react to global state changes + // Individual checkboxes are managed independently }; checkboxUpdateState.addListener(handleUpdateStateChange); - - return () => { - checkboxUpdateState.removeListener(handleUpdateStateChange); - }; + return () => checkboxUpdateState.removeListener(handleUpdateStateChange); }, []); - // Initialize local state from cookie or server + // Sync local state with cookie and server data useEffect(() => { - if (isUpdating) return; // Skip if we're currently updating + if (isUpdating) return; - // Check cookie first const selectedItems = getSelectedItemsFromCookie(); const storedState = selectedItems[item.id]; if (storedState !== undefined) { - // Only update local state if it differs from current state + // Update local state if cookie differs if (localSelected !== storedState) { setLocalSelected(storedState); } - // If cookie state differs from server state and we're not in the middle of an update, - // synchronize the item state with cookie - if (storedState !== item.selected) { - // Update cart item silently to match cookie - if (cart) { - const updatedCartItems = cart.products.map((cartItem) => - cartItem.id === item.id - ? { ...cartItem, selected: storedState } - : cartItem - ); - - const updatedCart = { ...cart, products: updatedCartItems }; - updateCartItem(updatedCart); - } + // Sync cart state with cookie if needed + if (storedState !== item.selected && cart) { + const updatedCartItems = cart.products.map((cartItem) => + cartItem.id === item.id + ? { ...cartItem, selected: storedState } + : cartItem + ); + updateCartItem({ ...cart, products: updatedCartItems }); } } else { - // Fall back to server state if no cookie exists + // Initialize cookie with server state setLocalSelected(item.selected); - - // Save state to cookie for future updateSelectedItemInCookie(item.id, item.selected, false); } }, [item.id, item.selected, localSelected, cart, updateCartItem, isUpdating]); const handleChange = useCallback( async (e: React.ChangeEvent<HTMLInputElement>) => { - if (typeof auth !== 'object' || !cart || isUpdating) { - return; - } + if (typeof auth !== 'object' || !cart || isUpdating) return; const newSelectedState = e.target.checked; - // Update local state immediately for responsiveness + // Update local state immediately setLocalSelected(newSelectedState); setIsUpdating(true); - - // Start the update - notify global state with this checkbox's ID checkboxUpdateState.startUpdate(item.id); try { - // The cookie update is now handled inside the function with notification - updateSelectedItemInCookie(item.id, newSelectedState, false); // We already started above + // Update cookie immediately for responsive UI + updateSelectedItemInCookie(item.id, newSelectedState, false); - // Update cart state immediately for UI responsiveness + // Update cart state optimistically const updatedCartItems = cart.products.map((cartItem) => cartItem.id === item.id ? { ...cartItem, selected: newSelectedState } : cartItem ); + updateCartItem({ ...cart, products: updatedCartItems }); - const updatedCart = { ...cart, products: updatedCartItems }; - updateCartItem(updatedCart); - - // Save to server + // Send update to server await upsertUserCart({ userId: auth.id, type: item.cart_type, id: item.id, qty: item.quantity, selected: newSelectedState, - // purchase_tax_id: item.purchase_tax_id || null, // Ensure null for numeric fields + purchase_tax_id: item.purchase_tax_id || null, }); - // Reload cart to ensure consistency + // Reload cart for consistency await loadCart(auth.id); } catch (error) { console.error('Failed to update item selection:', error); toast.error('Gagal memperbarui pilihan barang'); - // Revert local state on error + // Revert changes on error setLocalSelected(!newSelectedState); - - // Revert cookie change updateSelectedItemInCookie(item.id, !newSelectedState, false); - - // Reload cart to get server state loadCart(auth.id); } finally { setIsUpdating(false); - - // End the update - notify global state with this checkbox's ID checkboxUpdateState.endUpdate(item.id); } }, [auth, cart, item, isUpdating, updateCartItem, loadCart] ); - // Determine if THIS specific checkbox should be disabled - only disable - // if this specific checkbox is updating const isDisabled = isUpdating || checkboxUpdateState.isCheckboxUpdating(item.id); diff --git a/src-migrate/modules/cart/stores/useCartStore.ts b/src-migrate/modules/cart/stores/useCartStore.ts index d211304a..dc47b011 100644 --- a/src-migrate/modules/cart/stores/useCartStore.ts +++ b/src-migrate/modules/cart/stores/useCartStore.ts @@ -5,9 +5,6 @@ import { syncCartWithCookie, getCartDataFromCookie, getSelectedItemsFromCookie, - updateSelectedItemInCookie, - setAllSelectedInCookie, - removeCartItemsFromCookie, forceResetAllSelectedItems, } from '~/utils/cart'; @@ -26,8 +23,8 @@ type State = { type Action = { loadCart: (userId: number) => Promise<void>; updateCartItem: (updateCart: CartProps) => void; - syncCartWithCookieAndUpdate: (cart: CartProps) => { needsUpdate: boolean }; forceResetSelection: () => void; + clearCart: () => void; }; export const useCartStore = create<State & Action>((set, get) => ({ @@ -42,154 +39,151 @@ export const useCartStore = create<State & Action>((set, get) => ({ }, loadCart: async (userId) => { - if (get().isLoadCart === true) return; + if (get().isLoadCart) return; set({ isLoadCart: true }); + try { - // Fetch cart from API const cart: CartProps = (await getUserCart(userId)) as CartProps; - // Sync with cookie and get updated data if needed - const { needsUpdate } = get().syncCartWithCookieAndUpdate(cart); - - // If no update needed from cookie, just set the cart directly - if (!needsUpdate) { + // Sync with cookie data + const syncResult = syncCartWithCookie(cart); + + if (syncResult?.needsUpdate && cart.products) { + const selectedItems = getSelectedItemsFromCookie(); + + const updatedCart = { + ...cart, + products: cart.products.map((item) => ({ + ...item, + selected: + selectedItems[item.id] !== undefined + ? selectedItems[item.id] + : item.selected, + })), + }; + + set({ cart: updatedCart }); + } else { set({ cart }); } - // Update summary with current cart + // Update summary const summary = computeSummary(get().cart!); set({ summary }); } catch (error) { - console.error('Failed to load cart from API:', error); - - // Fallback to cookie if API fails - try { - const cartData = getCartDataFromCookie(); - if (Object.keys(cartData).length > 0) { - // Transform cart data from cookie to expected format - const products = Object.values(cartData).map((item) => ({ - cart_id: item.cart_id, - id: item.id, - cart_type: item.cart_type, - product_id: item.product?.id, - product_name: item.product?.name, - program_line_id: item.program_line?.id, - program_line_name: item.program_line?.name, - quantity: item.quantity, - selected: item.selected, - price: item.price, - package_price: item.package_price, - source: item.source, - })); - - const fallbackCart: CartProps = { - product_total: products.length, - products, - }; - - set({ cart: fallbackCart }); - const summary = computeSummary(fallbackCart); - set({ summary }); - } - } catch (cookieError) { - console.error('Failed to fallback to cookie:', cookieError); - } + console.error('Failed to load cart:', error); + + // Fallback to cookie data + await handleFallbackFromCookie(); } finally { set({ isLoadCart: false }); } }, updateCartItem: (updatedCart) => { - const cart = get().cart; - if (!cart) return; - set({ cart: updatedCart }); - - // Sync updated cart with cookie syncCartWithCookie(updatedCart); const summary = computeSummary(updatedCart); set({ summary }); }, - syncCartWithCookieAndUpdate: (cart) => { - if (!cart) return { needsUpdate: false }; - - // Sync cart with cookie - const result = syncCartWithCookie(cart); - - // If we need to update the cart based on cookie data - if (result.needsUpdate && cart.products) { - // Create updated cart with selections from cookie - const selectedItems = getSelectedItemsFromCookie(); - - const updatedCart = { - ...cart, - products: cart.products.map((item) => ({ - ...item, - selected: - selectedItems[item.id] !== undefined - ? selectedItems[item.id] - : item.selected, - })), - }; - - // Update the store - set({ cart: updatedCart }); - } - - return result; - }, - forceResetSelection: () => { const { cart } = get(); if (!cart) return; - // Reset all selections in cookie forceResetAllSelectedItems(); - // Update the cart in state const updatedCart = { ...cart, - products: cart.products.map((item) => ({ - ...item, - selected: false, - })), + products: cart.products.map((item) => ({ ...item, selected: false })), }; set({ cart: updatedCart }); - // Update summary const summary = computeSummary(updatedCart); set({ summary }); }, + + clearCart: () => { + set({ + cart: null, + summary: { + subtotal: 0, + discount: 0, + total: 0, + tax: 0, + grandTotal: 0, + }, + }); + }, })); +// Helper function for cookie fallback +const handleFallbackFromCookie = async () => { + try { + const cartData = getCartDataFromCookie(); + + if (Object.keys(cartData).length === 0) return; + + const products = Object.values(cartData).map(transformCookieItemToProduct); + + const fallbackCart: CartProps = { + product_total: products.length, + products, + }; + + useCartStore.setState({ cart: fallbackCart }); + + const summary = computeSummary(fallbackCart); + useCartStore.setState({ summary }); + } catch (error) { + console.error('Cookie fallback failed:', error); + } +}; + +// Helper function to transform cookie item to product format +const transformCookieItemToProduct = (item: any): CartItem => ({ + cart_id: item.cart_id, + id: item.id, + cart_type: item.cart_type, + product_id: item.product?.id, + product_name: item.product?.name, + program_line_id: item.program_line?.id, + program_line_name: item.program_line?.name, + quantity: item.quantity, + selected: item.selected, + price: item.price, + package_price: item.package_price, + source: item.source, +}); + +// Helper function to compute cart summary const computeSummary = (cart: CartProps) => { + if (!cart?.products) { + return { subtotal: 0, discount: 0, total: 0, grandTotal: 0, tax: 0 }; + } + + const PPN = parseFloat(process.env.NEXT_PUBLIC_PPN || '0'); let subtotal = 0; let discount = 0; - const PPN: number = process.env.NEXT_PUBLIC_PPN - ? parseFloat(process.env.NEXT_PUBLIC_PPN) - : 0; - - for (const item of cart?.products) { + for (const item of cart.products) { if (!item.selected) continue; - let price = 0; - if (item.cart_type === 'promotion') - price = (item?.package_price || 0) * item.quantity; - else if (item.cart_type === 'product') - price = item.price.price * item.quantity; + const price = + item.cart_type === 'promotion' + ? (item?.package_price || 0) * item.quantity + : item.price.price * item.quantity; subtotal += price; discount += price - item.price.price_discount * item.quantity; } - let total = subtotal - discount; - let grandTotal = total * PPN; - let tax = grandTotal - total; - // let grandTotal = total + tax; + + const total = subtotal - discount; + const grandTotal = total * (1 + PPN); + const tax = grandTotal - total; return { subtotal, discount, total, grandTotal, tax }; -}; +};
\ No newline at end of file |
