From 7d4445bb9bad3d6c945503086a07bd882536e5f6 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 19 May 2025 11:02:19 +0700 Subject: fix unresponsive cart select --- src-migrate/modules/cart/components/ItemSelect.tsx | 170 +++++++++++----- src-migrate/modules/cart/components/Summary.tsx | 219 +++++++++++++++++---- 2 files changed, 310 insertions(+), 79 deletions(-) (limited to 'src-migrate/modules/cart/components') diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx index d4a1b537..733ee64d 100644 --- a/src-migrate/modules/cart/components/ItemSelect.tsx +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -1,56 +1,138 @@ -import { Checkbox, Spinner } from '@chakra-ui/react' -import React, { useState } from 'react' - -import { getAuth } from '~/libs/auth' -import { CartItem } from '~/types/cart' -import { upsertUserCart } from '~/services/cart' - -import { useCartStore } from '../stores/useCartStore' +import { Checkbox } from '@chakra-ui/react'; +import React, { useState, useCallback, useEffect } from 'react'; +import { getAuth } from '~/libs/auth'; +import { CartItem } from '~/types/cart'; +import { upsertUserCart } from '~/services/cart'; +import { useCartStore } from '../stores/useCartStore'; +import { toast } from 'react-hot-toast'; +import { + getSelectedItemsFromCookie, + updateSelectedItemInCookie, +} from '~/utils/cart'; type Props = { - item: CartItem -} + item: CartItem; +}; const CartItemSelect = ({ item }: Props) => { - const auth = getAuth() - const { updateCartItem, cart } = useCartStore() + const auth = getAuth(); + const { updateCartItem, cart, loadCart } = useCartStore(); + const [isUpdating, setIsUpdating] = useState(false); + const [localSelected, setLocalSelected] = useState(item.selected); + + // Initialize local state from cookie or server + useEffect(() => { + if (isUpdating) return; // Skip if we're currently updating + + // Check cookie first + const selectedItems = getSelectedItemsFromCookie(); + const storedState = selectedItems[item.id]; + + if (storedState !== undefined) { + // Only update local state if it differs from current state + 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); + } + } + } else { + // Fall back to server state if no cookie exists + setLocalSelected(item.selected); + + // Save this state to cookie for future use + updateSelectedItemInCookie(item.id, item.selected); + } + }, [item.id, item.selected, localSelected, cart, updateCartItem, isUpdating]); + + const handleChange = useCallback( + async (e: React.ChangeEvent) => { + if (typeof auth !== 'object' || !cart || isUpdating) { + return; + } + + const newSelectedState = e.target.checked; + + // Update local state immediately for responsiveness + setLocalSelected(newSelectedState); + setIsUpdating(true); + + try { + // Update cookie immediately + updateSelectedItemInCookie(item.id, newSelectedState); + + // Update cart state immediately for UI responsiveness + const updatedCartItems = cart.products.map((cartItem) => + cartItem.id === item.id + ? { ...cartItem, selected: newSelectedState } + : cartItem + ); + + const updatedCart = { ...cart, products: updatedCartItems }; + updateCartItem(updatedCart); + + // Save to server + await upsertUserCart({ + userId: auth.id, + type: item.cart_type, + id: item.id, + qty: item.quantity, + selected: newSelectedState, + }); - const [isLoad, setIsLoad] = useState(false) + // Reload cart to ensure consistency + await loadCart(auth.id); + } catch (error) { + console.error('Failed to update item selection:', error); + toast.error('Gagal memperbarui pilihan barang'); - const handleChange = async (e: React.ChangeEvent) => { - if (typeof auth !== 'object' || !cart) return - - setIsLoad(true); - const updatedCartItems = cart.products.map(cartItem => - cartItem.id === item.id - ? { ...cartItem, selected: e.target.checked } - : cartItem - ); + // Revert local state on error + setLocalSelected(!newSelectedState); - // Update the entire cart - const updatedCart = { ...cart, products: updatedCartItems }; - updateCartItem(updatedCart); + // Update cookie back + updateSelectedItemInCookie(item.id, !newSelectedState); - setIsLoad(false); - } + // Reload cart to get server state + loadCart(auth.id); + } finally { + setIsUpdating(false); + } + }, + [auth, cart, item, isUpdating, updateCartItem, loadCart] + ); return ( -
- {isLoad && ( - - )} - - {!isLoad && ( - - )} +
+
- ) -} + ); +}; -export default CartItemSelect \ No newline at end of file +export default CartItemSelect; diff --git a/src-migrate/modules/cart/components/Summary.tsx b/src-migrate/modules/cart/components/Summary.tsx index 0af5ab18..b4fbab6b 100644 --- a/src-migrate/modules/cart/components/Summary.tsx +++ b/src-migrate/modules/cart/components/Summary.tsx @@ -1,20 +1,19 @@ -import style from '../styles/summary.module.css' - -import React from 'react' -import formatCurrency from '~/libs/formatCurrency' -import clsxm from '~/libs/clsxm' -import { Skeleton } from '@chakra-ui/react' -import _ from 'lodash' +import style from '../styles/summary.module.css'; +import React, { useEffect, useState, useMemo } from 'react'; +import formatCurrency from '~/libs/formatCurrency'; +import clsxm from '~/libs/clsxm'; +import { Skeleton, Box, useColorModeValue, Text } from '@chakra-ui/react'; type Props = { - total?: number - discount?: number - subtotal?: number - tax?: number - shipping?: number - grandTotal?: number - isLoaded: boolean -} + total?: number; + discount?: number; + subtotal?: number; + tax?: number; + shipping?: number; + grandTotal?: number; + isLoaded: boolean; + products?: any[]; // Added to detect changes in selected products +}; const CartSummary = ({ total, @@ -24,53 +23,203 @@ const CartSummary = ({ shipping, grandTotal, isLoaded = false, + products = [], }: Props) => { - const PPN : number = process.env.NEXT_PUBLIC_PPN ? parseFloat(process.env.NEXT_PUBLIC_PPN) : 0; - return ( - <> -
Ringkasan Pesanan
+ const PPN: number = process.env.NEXT_PUBLIC_PPN + ? parseFloat(process.env.NEXT_PUBLIC_PPN) + : 0; + const [isMounted, setIsMounted] = useState(false); + + // Local state to store calculated values + const [summaryValues, setSummaryValues] = useState({ + subtotal: 0, + discount: 0, + total: 0, + tax: 0, + shipping: 0, + grandTotal: 0, + }); + + const bgHighlight = useColorModeValue('red.50', 'red.900'); + + // This fixes hydration issues by ensuring the component only renders fully after mounting + useEffect(() => { + setIsMounted(true); + }, []); + + // Calculate summary based on products whenever products change + useMemo(() => { + if (!products || products.length === 0) return; + + // Only count selected products + const selectedProducts = products.filter((product) => product.selected); + + // Calculate values based on selected products + let calculatedSubtotal = 0; + let calculatedDiscount = 0; + + selectedProducts.forEach((product) => { + // Get raw price and discount from product + const productBasePrice = product.price?.price || 0; + const productQty = product.quantity || 1; + const productDiscountedPrice = + product.price?.price_discount || productBasePrice; + const productDiscount = productBasePrice - productDiscountedPrice; + + calculatedSubtotal += productBasePrice * productQty; + calculatedDiscount += productDiscount * productQty; + }); + + const calculatedTotal = calculatedSubtotal - calculatedDiscount; + const calculatedTax = calculatedTotal * (PPN - 1); + const calculatedShipping = shipping || 0; + const calculatedGrandTotal = + calculatedTotal + calculatedTax + calculatedShipping; + + // If calculated values are different from props, use calculated ones + const shouldUpdateValues = + Math.abs((subtotal || 0) - calculatedSubtotal) > 0.01 || + Math.abs((discount || 0) - calculatedDiscount) > 0.01 || + Math.abs((total || 0) - calculatedTotal) > 0.01 || + Math.abs((tax || 0) - calculatedTax) > 0.01 || + Math.abs((grandTotal || 0) - calculatedGrandTotal) > 0.01; -
+ if (shouldUpdateValues && isLoaded) { + setSummaryValues({ + subtotal: calculatedSubtotal, + discount: calculatedDiscount, + total: calculatedTotal, + tax: calculatedTax, + shipping: calculatedShipping, + grandTotal: calculatedGrandTotal, + }); + } else if (isLoaded) { + // Use values from props when available + setSummaryValues({ + subtotal: subtotal || 0, + discount: discount || 0, + total: total || 0, + tax: tax || 0, + shipping: shipping || 0, + grandTotal: grandTotal || 0, + }); + } + }, [ + products, + isLoaded, + subtotal, + discount, + total, + tax, + shipping, + grandTotal, + PPN, + ]); + + // Update local values whenever props change + useEffect(() => { + if (isLoaded) { + setSummaryValues({ + subtotal: subtotal || 0, + discount: discount || 0, + total: total || 0, + tax: tax || 0, + shipping: shipping || 0, + grandTotal: grandTotal || 0, + }); + } + }, [isLoaded, subtotal, discount, total, tax, shipping, grandTotal]); + + if (!isMounted) { + return ( + + + Ringkasan Pesanan + + {Array(6) + .fill(0) + .map((_, index) => ( + + ))} + + ); + } + + // Use local state for rendering to ensure responsiveness + const { + subtotal: displaySubtotal, + discount: displayDiscount, + total: displayTotal, + tax: displayTax, + shipping: displayShipping, + grandTotal: displayGrandTotal, + } = summaryValues; + + return ( + + + Ringkasan Pesanan +
Total Belanja - Rp {formatCurrency(subtotal || 0)} + + Rp {formatCurrency(displaySubtotal)} + Total Diskon - - Rp {formatCurrency(discount || 0)} + + - Rp {formatCurrency(displayDiscount)} +
Subtotal - Rp {formatCurrency(total || 0)} + Rp {formatCurrency(displayTotal)} - Tax {((PPN - 1) * 100).toFixed(0)}% - Rp {formatCurrency(tax || 0)} + + Tax {((PPN - 1) * 100).toFixed(0)}% + + Rp {formatCurrency(displayTax)} Biaya Kirim - Rp {formatCurrency(shipping || 0)} + + Rp {formatCurrency(displayShipping)} +
- - - Grand Total - - Rp {formatCurrency(grandTotal || 0)} + + + + Grand Total + + + Rp {formatCurrency(displayGrandTotal)} + +
- - ) -} + + ); +}; -export default CartSummary \ No newline at end of file +export default CartSummary; -- cgit v1.2.3 From 09cebc9020c4f1995a73305187bc1576e339d183 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 22 May 2025 10:05:09 +0700 Subject: disable button when updating checkboxes and change summary design --- src-migrate/modules/cart/components/ItemSelect.tsx | 43 ++++++++++++++++++---- src-migrate/modules/cart/components/Summary.tsx | 28 ++++---------- 2 files changed, 43 insertions(+), 28 deletions(-) (limited to 'src-migrate/modules/cart/components') diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx index 733ee64d..70b656ec 100644 --- a/src-migrate/modules/cart/components/ItemSelect.tsx +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -8,6 +8,7 @@ import { toast } from 'react-hot-toast'; import { getSelectedItemsFromCookie, updateSelectedItemInCookie, + checkboxUpdateState, } from '~/utils/cart'; type Props = { @@ -19,6 +20,20 @@ const CartItemSelect = ({ item }: Props) => { const { updateCartItem, cart, loadCart } = useCartStore(); const [isUpdating, setIsUpdating] = useState(false); const [localSelected, setLocalSelected] = useState(item.selected); + const [isGlobalUpdating, setIsGlobalUpdating] = useState(false); + + // Subscribe to global checkbox update state + useEffect(() => { + const handleUpdateStateChange = (isUpdating) => { + setIsGlobalUpdating(isUpdating); + }; + + checkboxUpdateState.addListener(handleUpdateStateChange); + + return () => { + checkboxUpdateState.removeListener(handleUpdateStateChange); + }; + }, []); // Initialize local state from cookie or server useEffect(() => { @@ -54,7 +69,7 @@ const CartItemSelect = ({ item }: Props) => { setLocalSelected(item.selected); // Save this state to cookie for future use - updateSelectedItemInCookie(item.id, item.selected); + updateSelectedItemInCookie(item.id, item.selected, false); // don't notify for initial sync } }, [item.id, item.selected, localSelected, cart, updateCartItem, isUpdating]); @@ -70,9 +85,12 @@ const CartItemSelect = ({ item }: Props) => { setLocalSelected(newSelectedState); setIsUpdating(true); + // Start the update - notify global state with this checkbox's ID + checkboxUpdateState.startUpdate(item.id); + try { - // Update cookie immediately - updateSelectedItemInCookie(item.id, newSelectedState); + // The cookie update is now handled inside the function with notification + updateSelectedItemInCookie(item.id, newSelectedState, false); // We already started above // Update cart state immediately for UI responsiveness const updatedCartItems = cart.products.map((cartItem) => @@ -91,6 +109,7 @@ const CartItemSelect = ({ item }: Props) => { id: item.id, qty: item.quantity, selected: newSelectedState, + purchase_tax_id: item.purchase_tax_id || null, // Ensure null for numeric fields }); // Reload cart to ensure consistency @@ -102,18 +121,26 @@ const CartItemSelect = ({ item }: Props) => { // Revert local state on error setLocalSelected(!newSelectedState); - // Update cookie back - updateSelectedItemInCookie(item.id, !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); + return (
{ size='lg' isChecked={localSelected} onChange={handleChange} - isDisabled={isUpdating} - opacity={isUpdating ? 0.5 : 1} - cursor={isUpdating ? 'not-allowed' : 'pointer'} + isDisabled={isDisabled} + opacity={isDisabled ? 0.5 : 1} + cursor={isDisabled ? 'not-allowed' : 'pointer'} _disabled={{ opacity: 0.5, cursor: 'not-allowed', diff --git a/src-migrate/modules/cart/components/Summary.tsx b/src-migrate/modules/cart/components/Summary.tsx index b4fbab6b..68db6323 100644 --- a/src-migrate/modules/cart/components/Summary.tsx +++ b/src-migrate/modules/cart/components/Summary.tsx @@ -40,8 +40,6 @@ const CartSummary = ({ grandTotal: 0, }); - const bgHighlight = useColorModeValue('red.50', 'red.900'); - // This fixes hydration issues by ensuring the component only renders fully after mounting useEffect(() => { setIsMounted(true); @@ -156,15 +154,7 @@ const CartSummary = ({ } = summaryValues; return ( - +
Ringkasan Pesanan @@ -208,17 +198,15 @@ const CartSummary = ({
- - - Grand Total - - - Rp {formatCurrency(displayGrandTotal)} - - + + Grand Total + + + Rp {formatCurrency(displayGrandTotal)} +
- +
); }; -- cgit v1.2.3 From 4904573845478e7e9648735d008153728870a123 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 23 May 2025 09:37:46 +0700 Subject: fix cookie not updating when delete an item --- src-migrate/modules/cart/components/ItemAction.tsx | 139 ++++++++++++++------- src-migrate/modules/cart/components/ItemSelect.tsx | 2 +- 2 files changed, 94 insertions(+), 47 deletions(-) (limited to 'src-migrate/modules/cart/components') diff --git a/src-migrate/modules/cart/components/ItemAction.tsx b/src-migrate/modules/cart/components/ItemAction.tsx index 7220e362..eea0cbe9 100644 --- a/src-migrate/modules/cart/components/ItemAction.tsx +++ b/src-migrate/modules/cart/components/ItemAction.tsx @@ -1,74 +1,121 @@ -import style from '../styles/item-action.module.css' +import style from '../styles/item-action.module.css'; -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react'; -import { Spinner, Tooltip } from '@chakra-ui/react' -import { MinusIcon, PlusIcon, Trash2Icon } from 'lucide-react' +import { Spinner, Tooltip } from '@chakra-ui/react'; +import { MinusIcon, PlusIcon, Trash2Icon } from 'lucide-react'; -import { CartItem } from '~/types/cart' -import { getAuth } from '~/libs/auth' -import { deleteUserCart, upsertUserCart } from '~/services/cart' +import { CartItem } from '~/types/cart'; +import { getAuth } from '~/libs/auth'; +import { deleteUserCart, upsertUserCart } from '~/services/cart'; -import { useDebounce } from 'usehooks-ts' -import { useCartStore } from '../stores/useCartStore' -import { useProductCartContext } from '@/contexts/ProductCartContext' +import { useDebounce } from 'usehooks-ts'; +import { useCartStore } from '../stores/useCartStore'; +import { useProductCartContext } from '@/contexts/ProductCartContext'; +import { + removeSelectedItemsFromCookie, + removeCartItemsFromCookie, +} from '~/utils/cart'; +import { toast } from 'react-hot-toast'; type Props = { - item: CartItem -} + item: CartItem; +}; const CartItemAction = ({ item }: Props) => { - const auth = getAuth() - const { setRefreshCart } = useProductCartContext() - const [isLoadDelete, setIsLoadDelete] = useState(false) - const [isLoadQuantity, setIsLoadQuantity] = useState(false) + const auth = getAuth(); + const { setRefreshCart } = useProductCartContext(); + const [isLoadDelete, setIsLoadDelete] = useState(false); + const [isLoadQuantity, setIsLoadQuantity] = useState(false); - const [quantity, setQuantity] = useState(item.quantity) + const [quantity, setQuantity] = useState(item.quantity); - const { loadCart } = useCartStore() + const { loadCart, cart, updateCartItem } = useCartStore(); // TAMBAHKAN cart dan updateCartItem - const limitQty = item.limit_qty?.transaction || 0 + const limitQty = item.limit_qty?.transaction || 0; + // PERBAIKI FUNCTION INI const handleDelete = async () => { - if (typeof auth !== 'object') return - - setIsLoadDelete(true) - await deleteUserCart(auth.id, [item.cart_id]) - await loadCart(auth.id) - setIsLoadDelete(false) - setRefreshCart(true) - } - - const decreaseQty = () => { setQuantity((quantity) => quantity -= 1) } - const increaseQty = () => { setQuantity((quantity) => quantity += 1) } - const debounceQty = useDebounce(quantity, 1000) + if (typeof auth !== 'object') return; + + setIsLoadDelete(true); + + try { + // Step 1: Delete from server + await deleteUserCart(auth.id, [item.cart_id]); + + // Step 2: Clean up cookies IMMEDIATELY + removeSelectedItemsFromCookie([item.id]); + removeCartItemsFromCookie([item.cart_id]); + + // Step 3: Update local cart state optimistically + if (cart) { + const updatedProducts = cart.products.filter( + (product) => product.id !== item.id + ); + const updatedCart = { + ...cart, + products: updatedProducts, + product_total: updatedProducts.length, + }; + updateCartItem(updatedCart); + } + + // Step 4: Reload from server to ensure consistency + await loadCart(auth.id); + setRefreshCart(true); + + toast.success('Item berhasil dihapus'); + } catch (error) { + console.error('Failed to delete cart item:', error); + toast.error('Gagal menghapus item'); + + // Reload on error + await loadCart(auth.id); + } finally { + setIsLoadDelete(false); + } + }; + + const decreaseQty = () => { + setQuantity((quantity) => (quantity -= 1)); + }; + const increaseQty = () => { + setQuantity((quantity) => (quantity += 1)); + }; + const debounceQty = useDebounce(quantity, 1000); + useEffect(() => { - if (isNaN(debounceQty)) setQuantity(1) - if (limitQty > 0 && debounceQty > limitQty) setQuantity(limitQty) - }, [debounceQty, limitQty]) + if (isNaN(debounceQty)) setQuantity(1); + if (limitQty > 0 && debounceQty > limitQty) setQuantity(limitQty); + }, [debounceQty, limitQty]); useEffect(() => { const updateCart = async () => { - if (typeof auth !== 'object' || isNaN(debounceQty)) return + if (typeof auth !== 'object' || isNaN(debounceQty)) return; - setIsLoadQuantity(true) + 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) - } - updateCart() + }); + await loadCart(auth.id); + setIsLoadQuantity(false); + }; + updateCart(); //eslint-disable-next-line react-hooks/exhaustive-deps - }, [debounceQty]) + }, [debounceQty]); return (
- @@ -106,7 +153,7 @@ const CartItemAction = ({ item }: Props) => {
- ) -} + ); +}; -export default CartItemAction \ No newline at end of file +export default CartItemAction; diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx index 70b656ec..f580f81d 100644 --- a/src-migrate/modules/cart/components/ItemSelect.tsx +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -109,7 +109,7 @@ const CartItemSelect = ({ item }: Props) => { 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, // Ensure null for numeric fields }); // Reload cart to ensure consistency -- cgit v1.2.3 From 1a247903bf7bb87e0a43b4e5e338ea67ec90e6de Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 26 May 2025 14:39:39 +0700 Subject: cleaning code --- src-migrate/modules/cart/components/ItemSelect.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src-migrate/modules/cart/components') diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx index f580f81d..00c7be43 100644 --- a/src-migrate/modules/cart/components/ItemSelect.tsx +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -68,8 +68,8 @@ const CartItemSelect = ({ item }: Props) => { // Fall back to server state if no cookie exists setLocalSelected(item.selected); - // Save this state to cookie for future use - updateSelectedItemInCookie(item.id, item.selected, false); // don't notify for initial sync + // Save state to cookie for future + updateSelectedItemInCookie(item.id, item.selected, false); } }, [item.id, item.selected, localSelected, cart, updateCartItem, isUpdating]); -- cgit v1.2.3 From 3feaad9127ff429b27f0eb69fa6ea539de2f2e8c Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 26 May 2025 20:00:17 +0700 Subject: Cleaning code --- src-migrate/modules/cart/components/ItemAction.tsx | 125 ++++++++++++++++++--- src-migrate/modules/cart/components/ItemSelect.tsx | 76 +++++-------- 2 files changed, 134 insertions(+), 67 deletions(-) (limited to 'src-migrate/modules/cart/components') 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(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(false); const [localSelected, setLocalSelected] = useState(item.selected); - const [isGlobalUpdating, setIsGlobalUpdating] = useState(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) => { - 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); -- cgit v1.2.3 From 2732c04b36f98a25895826b28003b1e2c56ad952 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 27 May 2025 09:05:10 +0700 Subject: remove purchase_tax_id and vendor_id --- src-migrate/modules/cart/components/ItemSelect.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src-migrate/modules/cart/components') diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx index 8dbfe2bc..72ab49aa 100644 --- a/src-migrate/modules/cart/components/ItemSelect.tsx +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -91,7 +91,8 @@ const CartItemSelect = ({ item }: Props) => { id: item.id, qty: item.quantity, selected: newSelectedState, - purchase_tax_id: item.purchase_tax_id || null, + // purchase_tax_id: item.purchase_tax_id, + // vendor_id: item.vendor_id }); // Reload cart for consistency -- cgit v1.2.3