summaryrefslogtreecommitdiff
path: root/src-migrate
diff options
context:
space:
mode:
authorMiqdad <ahmadmiqdad27@gmail.com>2025-05-26 20:00:17 +0700
committerMiqdad <ahmadmiqdad27@gmail.com>2025-05-26 20:00:17 +0700
commit3feaad9127ff429b27f0eb69fa6ea539de2f2e8c (patch)
treed2b65790861531e08fd9eb3e1d1cd64eb5805e15 /src-migrate
parentcca6d803fc4db729865def23004ab1c4bd279e24 (diff)
<miqdad> Cleaning code
Diffstat (limited to 'src-migrate')
-rw-r--r--src-migrate/modules/cart/components/ItemAction.tsx125
-rw-r--r--src-migrate/modules/cart/components/ItemSelect.tsx76
-rw-r--r--src-migrate/modules/cart/stores/useCartStore.ts198
-rw-r--r--src-migrate/pages/shop/cart/index.tsx380
-rw-r--r--src-migrate/utils/cart.js40
-rw-r--r--src-migrate/utils/checkBoxState.js141
6 files changed, 477 insertions, 483 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
diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx
index 798ad318..03854d79 100644
--- a/src-migrate/pages/shop/cart/index.tsx
+++ b/src-migrate/pages/shop/cart/index.tsx
@@ -2,15 +2,7 @@ import style from './cart.module.css';
import React, { useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
-import {
- Button,
- Checkbox,
- Spinner,
- Tooltip,
- Text,
- Box,
- Flex,
-} from '@chakra-ui/react';
+import { Button, Checkbox, Spinner, Tooltip } from '@chakra-ui/react';
import { toast } from 'react-hot-toast';
import { useRouter } from 'next/router';
import { getAuth } from '~/libs/auth';
@@ -27,171 +19,65 @@ import { Trash2Icon } from 'lucide-react';
import { useProductCartContext } from '@/contexts/ProductCartContext';
import {
getSelectedItemsFromCookie,
- setSelectedItemsToCookie,
syncSelectedItemsWithCookie,
setAllSelectedInCookie,
removeSelectedItemsFromCookie,
- forceResetAllSelectedItems,
+ removeCartItemsFromCookie,
checkboxUpdateState,
+ quantityUpdateState,
} from '~/utils/cart';
-// Special ID for the "select all" checkbox
const SELECT_ALL_ID = 'select_all_checkbox';
const CartPage = () => {
const router = useRouter();
const auth = getAuth();
const [isStepApproval, setIsStepApproval] = useState(false);
- const [isLoad, setIsLoad] = useState<boolean>(false);
const [isLoadDelete, setIsLoadDelete] = useState<boolean>(false);
const { loadCart, cart, summary, updateCartItem } = useCartStore();
const device = useDevice();
const { setRefreshCart } = useProductCartContext();
const [isTop, setIsTop] = useState(true);
const [isUpdating, setIsUpdating] = useState(false);
- const [isStateMismatch, setIsStateMismatch] = useState(false);
const [isAnyCheckboxUpdating, setIsAnyCheckboxUpdating] = useState(false);
+ const [isAnyQuantityUpdating, setIsAnyQuantityUpdating] = useState(false);
- // Subscribe to checkbox update state changes
+ // Subscribe to update state changes
useEffect(() => {
- const handleUpdateStateChange = (isUpdating) => {
+ const handleCheckboxUpdate = (isUpdating) =>
setIsAnyCheckboxUpdating(isUpdating);
- };
+ const handleQuantityUpdate = (isUpdating) =>
+ setIsAnyQuantityUpdating(isUpdating);
- // Add listener for checkbox update state changes
- checkboxUpdateState.addListener(handleUpdateStateChange);
+ checkboxUpdateState.addListener(handleCheckboxUpdate);
+ quantityUpdateState.addListener(handleQuantityUpdate);
- // Cleanup listener on component unmount
return () => {
- checkboxUpdateState.removeListener(handleUpdateStateChange);
+ checkboxUpdateState.removeListener(handleCheckboxUpdate);
+ quantityUpdateState.removeListener(handleQuantityUpdate);
};
}, []);
- // Function to check if cart state is inconsistent
- const checkCartStateMismatch = () => {
- if (!cart || !cart.products || isUpdating) return false;
-
- try {
- // Ambil status selected dari cookie
- const selectedItems = getSelectedItemsFromCookie();
-
- // Periksa ketidaksesuaian antara UI dan cookie
- // 1. Periksa item yang selected di UI tapi tidak di cookie
- for (const product of cart.products) {
- const cookieState = selectedItems[product.id];
-
- // Jika ada di cookie tapi tidak sama dengan UI
- if (cookieState !== undefined && cookieState !== product.selected) {
- return true;
- }
-
- // Jika tidak ada di cookie tapi selected di UI
- if (cookieState === undefined && product.selected) {
- return true;
- }
- }
-
- // 2. Periksa item yang selected di cookie tapi tidak ada di cart
- for (const productId in selectedItems) {
- const isSelected = selectedItems[productId];
- if (isSelected) {
- // Cek apakah product id ini ada di cart
- const productExists = cart.products.some(
- (p) => p.id.toString() === productId.toString()
- );
- if (!productExists) {
- // Ada item selected di cookie yang tidak ada di cart
- return true;
- }
- }
- }
-
- return false;
- } catch (error) {
- console.error('Error checking cart state mismatch:', error);
- return false;
- }
- };
-
- // Function to reset all selected items when state is inconsistent
- const handleResetSelections = () => {
- if (!cart) return;
-
- setIsUpdating(true);
-
- try {
- // Use the forceResetSelection function from the store
- useCartStore.getState().forceResetSelection();
-
- // Set state back to normal
- setIsStateMismatch(false);
-
- // Give visual feedback
- toast.success('Semua pilihan telah direset');
-
- // Optional: Sync with server if needed
- if (typeof auth === 'object') {
- const updatePromises = cart.products.map((item) =>
- upsertUserCart({
- userId: auth.id,
- type: item.cart_type,
- id: item.id,
- qty: item.quantity,
- selected: false,
- purchase_tax_id: item.purchase_tax_id || null, // Fix integer field issue
- })
- );
-
- Promise.all(updatePromises)
- .then(() => loadCart(auth.id))
- .catch((error) => {
- console.error('Error updating selections to server:', error);
- })
- .finally(() => {
- setIsUpdating(false);
- });
- } else {
- setIsUpdating(false);
- }
- } catch (error) {
- console.error('Error resetting selections:', error);
- setIsUpdating(false);
- toast.error('Gagal mereset pilihan');
- }
- };
-
- // Check for state inconsistency
- useEffect(() => {
- if (!cart || !cart.products || isUpdating) return;
-
- const hasMismatch = checkCartStateMismatch();
- setIsStateMismatch(hasMismatch);
- }, [cart, isUpdating]);
-
+ // Handle scroll for sticky header styling
useEffect(() => {
- const handleScroll = () => {
- setIsTop(window.scrollY < 200);
- };
+ const handleScroll = () => setIsTop(window.scrollY < 200);
window.addEventListener('scroll', handleScroll);
- return () => {
- window.removeEventListener('scroll', handleScroll);
- };
+ return () => window.removeEventListener('scroll', handleScroll);
}, []);
+ // Initialize cart and sync with cookies
useEffect(() => {
- const loadCartWithStorage = async () => {
+ const initializeCart = async () => {
if (typeof auth === 'object' && !cart) {
await loadCart(auth.id);
setIsStepApproval(auth?.feature?.soApproval);
- // Sync selected items with server data using cookies
if (cart?.products) {
const { items, needsUpdate } = syncSelectedItemsWithCookie(
cart.products
);
- // If there's a mismatch between cookie and server data, update the UI
if (needsUpdate) {
const updatedCart = {
...cart,
@@ -207,37 +93,47 @@ const CartPage = () => {
}
};
- loadCartWithStorage();
- }, [auth, cart]);
+ initializeCart();
+ }, [auth, cart, loadCart, updateCartItem]);
+ // Computed values
const hasSelectedPromo = useMemo(() => {
- if (!cart) return false;
- return cart?.products?.some(
- (item) => item.cart_type === 'promotion' && item.selected
+ return (
+ cart?.products?.some(
+ (item) => item.cart_type === 'promotion' && item.selected
+ ) || false
);
}, [cart]);
const hasSelected = useMemo(() => {
- if (!cart) return false;
- return cart?.products?.some((item) => item.selected);
+ return cart?.products?.some((item) => item.selected) || false;
}, [cart]);
const hasSelectNoPrice = useMemo(() => {
- if (!cart) return false;
- return cart?.products?.some(
- (item) => item.selected && item.price.price_discount === 0
+ return (
+ cart?.products?.some(
+ (item) => item.selected && item.price.price_discount === 0
+ ) || false
);
}, [cart]);
const hasSelectedAll = useMemo(() => {
- if (!cart || !Array.isArray(cart.products)) return false;
- return (
- cart.products.length > 0 && cart.products.every((item) => item.selected)
- );
+ if (!cart?.products?.length) return false;
+ return cart.products.every((item) => item.selected);
}, [cart]);
+ // Button states
+ const areButtonsDisabled =
+ isUpdating ||
+ isLoadDelete ||
+ isAnyCheckboxUpdating ||
+ isAnyQuantityUpdating;
+ const isSelectAllDisabled =
+ isUpdating || checkboxUpdateState.isCheckboxUpdating(SELECT_ALL_ID);
+
+ // Handlers
const handleCheckout = () => {
- if (isUpdating || isLoadDelete || isAnyCheckboxUpdating) {
+ if (areButtonsDisabled) {
toast.error('Harap tunggu pembaruan selesai');
return;
}
@@ -245,7 +141,7 @@ const CartPage = () => {
};
const handleQuotation = () => {
- if (isUpdating || isLoadDelete || isAnyCheckboxUpdating) {
+ if (areButtonsDisabled) {
toast.error('Harap tunggu pembaruan selesai');
return;
}
@@ -256,16 +152,14 @@ const CartPage = () => {
}
};
- const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
- if (cart && !isUpdating && typeof auth === 'object') {
- const newSelectedState = !hasSelectedAll;
+ const handleSelectAll = async (e: React.ChangeEvent<HTMLInputElement>) => {
+ if (!cart || isUpdating || typeof auth !== 'object') return;
- // Set updating flag
- setIsUpdating(true);
-
- // Notify checkbox update state system with the special select all ID
- checkboxUpdateState.startUpdate(SELECT_ALL_ID);
+ const newSelectedState = !hasSelectedAll;
+ setIsUpdating(true);
+ checkboxUpdateState.startUpdate(SELECT_ALL_ID);
+ try {
// Update UI immediately
const updatedCart = {
...cart,
@@ -274,54 +168,47 @@ const CartPage = () => {
selected: newSelectedState,
})),
};
-
updateCartItem(updatedCart);
- // Get all product IDs in cart
+ // Update cookies
const productIds = cart.products.map((item) => item.id);
+ setAllSelectedInCookie(productIds, newSelectedState, false);
+
+ // Update server
+ const updatePromises = cart.products.map((item) =>
+ upsertUserCart({
+ userId: auth.id,
+ type: item.cart_type,
+ id: item.id,
+ qty: item.quantity,
+ selected: newSelectedState,
+ purchase_tax_id: item.purchase_tax_id || null,
+ })
+ );
- // Update cookies immediately for responsive UI
- setAllSelectedInCookie(productIds, newSelectedState, false); // We're already notifying
-
- try {
- // Update all items on server in background
- const updatePromises = cart.products.map((item) =>
- upsertUserCart({
- userId: auth.id,
- type: item.cart_type,
- id: item.id,
- qty: item.quantity,
- selected: newSelectedState,
- purchase_tax_id: item.purchase_tax_id || null, // Fix integer field issue
- })
- );
-
- await Promise.all(updatePromises);
- await loadCart(auth.id);
- } catch (error) {
- console.error('Error updating select all:', error);
-
- // Revert changes on error
- const revertedCart = {
- ...cart,
- products: cart.products.map((item) => ({
- ...item,
- selected: !newSelectedState,
- })),
- };
-
- updateCartItem(revertedCart);
-
- // Revert cookies
- setAllSelectedInCookie(productIds, !newSelectedState, false);
-
- toast.error('Gagal memperbarui pilihan');
- } finally {
- setIsUpdating(false);
+ await Promise.all(updatePromises);
+ await loadCart(auth.id);
+ } catch (error) {
+ console.error('Error updating select all:', error);
+ toast.error('Gagal memperbarui pilihan');
- // End update notification
- checkboxUpdateState.endUpdate(SELECT_ALL_ID);
- }
+ // Revert on error
+ const revertedCart = {
+ ...cart,
+ products: cart.products.map((item) => ({
+ ...item,
+ selected: !newSelectedState,
+ })),
+ };
+ updateCartItem(revertedCart);
+ setAllSelectedInCookie(
+ cart.products.map((item) => item.id),
+ !newSelectedState,
+ false
+ );
+ } finally {
+ setIsUpdating(false);
+ checkboxUpdateState.endUpdate(SELECT_ALL_ID);
}
};
@@ -336,12 +223,12 @@ const CartPage = () => {
const itemIdsToDelete = itemsToDelete.map((item) => item.id);
const cartIdsToDelete = itemsToDelete.map((item) => item.cart_id);
- // Step 1: Delete from server first
+ // Delete from server
for (const item of itemsToDelete) {
await deleteUserCart(auth.id, [item.cart_id]);
}
- // Step 2: Update local cart state immediately (optimistic update)
+ // Update local state optimistically
const updatedProducts = cart.products.filter((item) => !item.selected);
const updatedCart = {
...cart,
@@ -350,30 +237,20 @@ const CartPage = () => {
};
updateCartItem(updatedCart);
- // Step 3: Clean up cookies AFTER state update
+ // Clean up cookies
removeSelectedItemsFromCookie(itemIdsToDelete);
removeCartItemsFromCookie(cartIdsToDelete);
- // Step 4: Reload from server to ensure consistency (but don't wait for it to complete UI update)
- loadCart(auth.id)
- .then(() => {
- console.log('Cart reloaded from server');
- })
- .catch((error) => {
- console.error('Error reloading cart:', error);
- // If reload fails, at least we have the optimistic update
- });
+ // Reload from server
+ loadCart(auth.id).catch((error) =>
+ console.error('Error reloading cart:', error)
+ );
- // Step 5: Trigger context refresh
setRefreshCart(true);
-
- // Success feedback
toast.success('Item berhasil dihapus');
} catch (error) {
console.error('Failed to delete cart items:', error);
toast.error('Gagal menghapus item');
-
- // If deletion failed, reload cart to restore proper state
loadCart(auth.id);
} finally {
setIsLoadDelete(false);
@@ -381,16 +258,41 @@ const CartPage = () => {
}
};
- // Check if buttons should be disabled
- const areButtonsDisabled =
- isUpdating || isLoadDelete || isAnyCheckboxUpdating;
+ // Tooltip messages
+ const getTooltipMessage = () => {
+ if (isAnyQuantityUpdating) return 'Harap tunggu update quantity selesai';
+ if (isAnyCheckboxUpdating) return 'Harap tunggu pembaruan checkbox selesai';
+ if (isLoadDelete) return 'Harap tunggu penghapusan selesai';
+ if (isUpdating) return 'Harap tunggu pembaruan selesai';
+ return '';
+ };
- // Only disable the select all checkbox if it specifically is updating
- const isSelectAllDisabled =
- isUpdating || checkboxUpdateState.isCheckboxUpdating(SELECT_ALL_ID);
+ const getQuotationTooltip = () => {
+ const baseMessage = getTooltipMessage();
+ if (baseMessage) return baseMessage;
+ if (hasSelectedPromo) return 'Barang promo tidak dapat dibuat quotation';
+ if (!hasSelected) return 'Tidak ada item yang dipilih';
+ return '';
+ };
+
+ const getCheckoutTooltip = () => {
+ const baseMessage = getTooltipMessage();
+ if (baseMessage) return baseMessage;
+ if (!hasSelected) return 'Tidak ada item yang dipilih';
+ if (hasSelectNoPrice) return 'Terdapat item yang tidak ada harga';
+ return '';
+ };
+
+ const getDeleteTooltip = () => {
+ const baseMessage = getTooltipMessage();
+ if (baseMessage) return baseMessage;
+ if (!hasSelected) return 'Tidak ada item yang dipilih';
+ return '';
+ };
return (
<>
+ {/* Sticky Header */}
<div
className={`${
isTop ? 'border-b-[0px]' : 'border-b-[1px]'
@@ -408,7 +310,7 @@ const CartPage = () => {
colorScheme='red'
size='lg'
isChecked={hasSelectedAll}
- onChange={handleChange}
+ onChange={handleSelectAll}
isDisabled={isSelectAllDisabled}
opacity={isSelectAllDisabled ? 0.5 : 1}
cursor={isSelectAllDisabled ? 'not-allowed' : 'pointer'}
@@ -422,13 +324,9 @@ const CartPage = () => {
{hasSelectedAll ? 'Uncheck all' : 'Select all'}
</p>
</div>
+
<div className='flex items-center object-center'>
- <Tooltip
- label={clsxm({
- 'Tidak ada item yang dipilih': !hasSelected,
- 'Harap tunggu pembaruan selesai': areButtonsDisabled,
- })}
- >
+ <Tooltip label={getDeleteTooltip()}>
<Button
bg='#fadede'
variant='outline'
@@ -447,6 +345,7 @@ const CartPage = () => {
</div>
</div>
+ {/* Main Content */}
<div className={style.content}>
<div className={style['item-wrapper']}>
<div className={style['item-skeleton']}>
@@ -485,6 +384,8 @@ const CartPage = () => {
)}
</div>
</div>
+
+ {/* Cart Summary */}
<div
className={`${style['summary-wrapper']} ${
device.isMobile && (!cart || cart?.product_total === 0)
@@ -514,13 +415,7 @@ const CartPage = () => {
: style['summary-buttons']
}
>
- <Tooltip
- label={clsxm({
- 'Barang promo tidak dapat dibuat quotation': hasSelectedPromo,
- 'Harap tunggu pembaruan selesai': areButtonsDisabled,
- 'Tidak ada item yang dipilih': !hasSelected,
- })}
- >
+ <Tooltip label={getQuotationTooltip()}>
<Button
colorScheme='yellow'
w='full'
@@ -529,18 +424,13 @@ const CartPage = () => {
}
onClick={handleQuotation}
>
- {areButtonsDisabled ? <Spinner size='sm' mr={2} /> : null}
+ {areButtonsDisabled && <Spinner size='sm' mr={2} />}
Quotation
</Button>
</Tooltip>
+
{!isStepApproval && (
- <Tooltip
- label={clsxm({
- 'Tidak ada item yang dipilih': !hasSelected,
- 'Terdapat item yang tidak ada harga': hasSelectNoPrice,
- 'Harap tunggu pembaruan selesai': areButtonsDisabled,
- })}
- >
+ <Tooltip label={getCheckoutTooltip()}>
<Button
colorScheme='red'
w='full'
@@ -549,7 +439,7 @@ const CartPage = () => {
}
onClick={handleCheckout}
>
- {areButtonsDisabled ? <Spinner size='sm' mr={2} /> : null}
+ {areButtonsDisabled && <Spinner size='sm' mr={2} />}
Checkout
</Button>
</Tooltip>
@@ -562,4 +452,4 @@ const CartPage = () => {
);
};
-export default CartPage;
+export default CartPage; \ No newline at end of file
diff --git a/src-migrate/utils/cart.js b/src-migrate/utils/cart.js
index ebd771e5..4bdee49a 100644
--- a/src-migrate/utils/cart.js
+++ b/src-migrate/utils/cart.js
@@ -413,3 +413,43 @@ export const removeSelectedItemsFromCookie = (productIds) => {
return {};
}
};
+
+class QuantityUpdateState {
+ constructor() {
+ this.updateItems = new Set();
+ this.listeners = new Set();
+ }
+
+ startUpdate(itemId) {
+ this.updateItems.add(itemId);
+ this.notifyListeners();
+ }
+
+ endUpdate(itemId) {
+ this.updateItems.delete(itemId);
+ this.notifyListeners();
+ }
+
+ isAnyQuantityUpdating() {
+ return this.updateItems.size > 0;
+ }
+
+ isItemUpdating(itemId) {
+ return this.updateItems.has(itemId);
+ }
+
+ addListener(callback) {
+ this.listeners.add(callback);
+ }
+
+ removeListener(callback) {
+ this.listeners.delete(callback);
+ }
+
+ notifyListeners() {
+ const isUpdating = this.isAnyQuantityUpdating();
+ this.listeners.forEach(callback => callback(isUpdating));
+ }
+}
+
+export const quantityUpdateState = new QuantityUpdateState(); \ No newline at end of file
diff --git a/src-migrate/utils/checkBoxState.js b/src-migrate/utils/checkBoxState.js
index 8f7236c3..9568c321 100644
--- a/src-migrate/utils/checkBoxState.js
+++ b/src-migrate/utils/checkBoxState.js
@@ -1,87 +1,90 @@
/**
- * Enhanced state manager for checkbox updates
- * Tracks both global update state and individual checkbox update states
+ * State manager for checkbox updates
+ * Tracks global and individual checkbox update states
*/
+class CheckboxUpdateState {
+ constructor() {
+ this.updateCount = 0;
+ this.listeners = new Set();
+ this.updatingCheckboxIds = new Set();
+ }
+
+ // Global update state (for buttons quotation and checkout)
+ isUpdating() {
+ return this.updateCount > 0;
+ }
+
+ // Individual checkbox state
+ isCheckboxUpdating(itemId) {
+ return this.updatingCheckboxIds.has(String(itemId));
+ }
+
+ // Start update
+ startUpdate(itemId = null) {
+ this.updateCount++;
-// Track the number of ongoing updates
-let updateCount = 0;
-let listeners = [];
-
-// Track which checkboxes are currently updating by ID
-let updatingCheckboxIds = new Set();
-
-const checkboxUpdateState = {
- // Check if any checkboxes are currently updating (for buttons quotation and checkout)
- isUpdating: () => updateCount > 0,
-
- // Check if a specific checkbox is updating (for disabling just that checkbox)
- isCheckboxUpdating: (itemId) => updatingCheckboxIds.has(itemId.toString()),
-
- // Start an update for a specific checkbox
- startUpdate: (itemId = null) => {
- updateCount++;
-
- // If an item ID is provided, mark it as updating
if (itemId !== null) {
- updatingCheckboxIds.add(itemId.toString());
+ this.updatingCheckboxIds.add(String(itemId));
}
- notifyListeners();
- return updateCount;
- },
+ this.notifyListeners();
+ return this.updateCount;
+ }
- // End an update for a specific checkbox
- endUpdate: (itemId = null) => {
- updateCount = Math.max(0, updateCount - 1);
+ // End update
+ endUpdate(itemId = null) {
+ this.updateCount = Math.max(0, this.updateCount - 1);
- // If an item ID is provided, remove it from updating set
if (itemId !== null) {
- updatingCheckboxIds.delete(itemId.toString());
+ this.updatingCheckboxIds.delete(String(itemId));
}
- notifyListeners();
- return updateCount;
- },
-
- // Reset the update counter and clear all updating checkboxes
- reset: () => {
- updateCount = 0;
- updatingCheckboxIds.clear();
- notifyListeners();
- },
+ this.notifyListeners();
+ return this.updateCount;
+ }
- // Get IDs of all checkboxes currently updating (for debugging)
- // getUpdatingCheckboxIds: () => [...updatingCheckboxIds],
+ // Reset all states
+ reset() {
+ this.updateCount = 0;
+ this.updatingCheckboxIds.clear();
+ this.notifyListeners();
+ }
- // Add a listener function to be called when update state changes
- addListener: (callback) => {
+ // Listener management
+ addListener(callback) {
if (typeof callback === 'function') {
- listeners.push(callback);
-
- // Immediately call with current state
- callback(updateCount > 0);
- }
- },
-
- // Remove a listener
- removeListener: (callback) => {
- listeners = listeners.filter((listener) => listener !== callback);
- },
-
- // Get current counter (for debugging)
- getUpdateCount: () => updateCount,
-};
-
-// Private function to notify all listeners of state changes
-function notifyListeners() {
- const isUpdating = updateCount > 0;
- listeners.forEach((listener) => {
- try {
- listener(isUpdating);
- } catch (error) {
- console.error('Error in checkbox update state listener:', error);
+ this.listeners.add(callback);
+ // Immediate callback with current state
+ callback(this.isUpdating());
}
- });
+ }
+
+ removeListener(callback) {
+ this.listeners.delete(callback);
+ }
+
+ // Debug helpers
+ getUpdateCount() {
+ return this.updateCount;
+ }
+
+ getUpdatingCheckboxIds() {
+ return [...this.updatingCheckboxIds];
+ }
+
+ // Private method to notify listeners
+ notifyListeners() {
+ const isUpdating = this.isUpdating();
+
+ this.listeners.forEach((listener) => {
+ try {
+ listener(isUpdating);
+ } catch (error) {
+ console.error('Checkbox update state listener error:', error);
+ }
+ });
+ }
}
+const checkboxUpdateState = new CheckboxUpdateState();
export default checkboxUpdateState;