summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src-migrate/modules/cart/components/ItemSelect.tsx43
-rw-r--r--src-migrate/modules/cart/components/Summary.tsx28
-rw-r--r--src-migrate/pages/shop/cart/index.tsx98
-rw-r--r--src-migrate/utils/cart.js102
-rw-r--r--src-migrate/utils/checkBoxState.js89
-rw-r--r--src/lib/checkout/components/Checkout.jsx5
6 files changed, 297 insertions, 68 deletions
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<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);
+ };
+
+ 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 (
<div className='w-6 my-auto relative'>
<Checkbox
@@ -122,9 +149,9 @@ const CartItemSelect = ({ item }: Props) => {
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 (
- <Box
- className={style.summaryContainer}
- p={4}
- borderWidth='1px'
- borderRadius='lg'
- boxShadow='sm'
- position='sticky'
- top='170px'
- >
+ <div className={style.summaryContainer}>
<Text fontSize='lg' fontWeight='medium' mb={4}>
Ringkasan Pesanan
</Text>
@@ -208,17 +198,15 @@ const CartSummary = ({
<div className={style.divider} />
<Skeleton isLoaded={isLoaded}>
- <Box className={style.line} p={2} borderRadius='md' bg={bgHighlight}>
- <span className={clsxm(style.label, style.grandTotal)}>
- Grand Total
- </span>
- <span className={clsxm(style.value, style.grandTotalValue)}>
- Rp {formatCurrency(displayGrandTotal)}
- </span>
- </Box>
+ <span className={clsxm(style.label, style.grandTotal)}>
+ Grand Total
+ </span>
+ <span className={clsxm(style.value, style.grandTotalValue)}>
+ Rp {formatCurrency(displayGrandTotal)}
+ </span>
</Skeleton>
</div>
- </Box>
+ </div>
);
};
diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx
index 475a4259..eefe8d09 100644
--- a/src-migrate/pages/shop/cart/index.tsx
+++ b/src-migrate/pages/shop/cart/index.tsx
@@ -32,8 +32,12 @@ import {
setAllSelectedInCookie,
removeSelectedItemsFromCookie,
forceResetAllSelectedItems,
+ checkboxUpdateState,
} 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();
@@ -46,6 +50,22 @@ const CartPage = () => {
const [isTop, setIsTop] = useState(true);
const [isUpdating, setIsUpdating] = useState(false);
const [isStateMismatch, setIsStateMismatch] = useState(false);
+ const [isAnyCheckboxUpdating, setIsAnyCheckboxUpdating] = useState(false);
+
+ // Subscribe to checkbox update state changes
+ useEffect(() => {
+ const handleUpdateStateChange = (isUpdating) => {
+ setIsAnyCheckboxUpdating(isUpdating);
+ };
+
+ // Add listener for checkbox update state changes
+ checkboxUpdateState.addListener(handleUpdateStateChange);
+
+ // Cleanup listener on component unmount
+ return () => {
+ checkboxUpdateState.removeListener(handleUpdateStateChange);
+ };
+ }, []);
// Function to check if cart state is inconsistent
const checkCartStateMismatch = () => {
@@ -91,11 +111,14 @@ const CartPage = () => {
console.error('Error checking cart state mismatch:', error);
return false;
}
- }; // Function to reset all selected items when state is inconsistent
+ };
+
+ // 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();
@@ -115,6 +138,7 @@ const CartPage = () => {
id: item.id,
qty: item.quantity,
selected: false,
+ purchase_tax_id: item.purchase_tax_id || null, // Fix integer field issue
})
);
@@ -123,7 +147,9 @@ const CartPage = () => {
.catch((error) => {
console.error('Error updating selections to server:', error);
})
- .finally(() => setIsUpdating(false));
+ .finally(() => {
+ setIsUpdating(false);
+ });
} else {
setIsUpdating(false);
}
@@ -211,7 +237,7 @@ const CartPage = () => {
}, [cart]);
const handleCheckout = () => {
- if (isUpdating || isLoadDelete) {
+ if (isUpdating || isLoadDelete || isAnyCheckboxUpdating) {
toast.error('Harap tunggu pembaruan selesai');
return;
}
@@ -219,7 +245,7 @@ const CartPage = () => {
};
const handleQuotation = () => {
- if (isUpdating || isLoadDelete) {
+ if (isUpdating || isLoadDelete || isAnyCheckboxUpdating) {
toast.error('Harap tunggu pembaruan selesai');
return;
}
@@ -234,6 +260,12 @@ const CartPage = () => {
if (cart && !isUpdating && typeof auth === 'object') {
const newSelectedState = !hasSelectedAll;
+ // Set updating flag
+ setIsUpdating(true);
+
+ // Notify checkbox update state system with the special select all ID
+ checkboxUpdateState.startUpdate(SELECT_ALL_ID);
+
// Update UI immediately
const updatedCart = {
...cart,
@@ -249,9 +281,7 @@ const CartPage = () => {
const productIds = cart.products.map((item) => item.id);
// Update cookies immediately for responsive UI
- setAllSelectedInCookie(productIds, newSelectedState);
-
- setIsUpdating(true);
+ setAllSelectedInCookie(productIds, newSelectedState, false); // We're already notifying
try {
// Update all items on server in background
@@ -262,6 +292,7 @@ const CartPage = () => {
id: item.id,
qty: item.quantity,
selected: newSelectedState,
+ purchase_tax_id: item.purchase_tax_id || null, // Fix integer field issue
})
);
@@ -282,11 +313,14 @@ const CartPage = () => {
updateCartItem(revertedCart);
// Revert cookies
- setAllSelectedInCookie(productIds, !newSelectedState);
+ setAllSelectedInCookie(productIds, !newSelectedState, false);
toast.error('Gagal memperbarui pilihan');
} finally {
setIsUpdating(false);
+
+ // End update notification
+ checkboxUpdateState.endUpdate(SELECT_ALL_ID);
}
}
};
@@ -295,6 +329,8 @@ const CartPage = () => {
if (typeof auth !== 'object' || !cart) return;
setIsLoadDelete(true);
+ checkboxUpdateState.startUpdate('delete_operation'); // Use special ID for delete
+
try {
const itemsToDelete = cart.products.filter((item) => item.selected);
const itemIdsToDelete = itemsToDelete.map((item) => item.id);
@@ -313,9 +349,18 @@ const CartPage = () => {
toast.error('Gagal menghapus item');
} finally {
setIsLoadDelete(false);
+ checkboxUpdateState.endUpdate('delete_operation');
}
};
+ // Check if buttons should be disabled
+ const areButtonsDisabled =
+ isUpdating || isLoadDelete || isAnyCheckboxUpdating;
+
+ // Only disable the select all checkbox if it specifically is updating
+ const isSelectAllDisabled =
+ isUpdating || checkboxUpdateState.isCheckboxUpdating(SELECT_ALL_ID);
+
return (
<>
<div
@@ -325,16 +370,6 @@ const CartPage = () => {
>
<div className='flex items-center justify-between mb-2'>
<h1 className={style.title}>Keranjang Belanja</h1>
- {isStateMismatch && (
- <Button
- colorScheme='red'
- size='sm'
- isLoading={isUpdating}
- onClick={handleResetSelections}
- >
- Reset Pilihan
- </Button>
- )}
</div>
<div className='h-2' />
@@ -346,9 +381,9 @@ const CartPage = () => {
size='lg'
isChecked={hasSelectedAll}
onChange={handleChange}
- isDisabled={isUpdating || isLoadDelete}
- opacity={isUpdating ? 0.5 : 1}
- cursor={isUpdating ? 'not-allowed' : 'pointer'}
+ isDisabled={isSelectAllDisabled}
+ opacity={isSelectAllDisabled ? 0.5 : 1}
+ cursor={isSelectAllDisabled ? 'not-allowed' : 'pointer'}
_disabled={{
opacity: 0.5,
cursor: 'not-allowed',
@@ -363,6 +398,7 @@ const CartPage = () => {
<Tooltip
label={clsxm({
'Tidak ada item yang dipilih': !hasSelected,
+ 'Harap tunggu pembaruan selesai': areButtonsDisabled,
})}
>
<Button
@@ -371,7 +407,7 @@ const CartPage = () => {
colorScheme='red'
w='auto'
size={device.isMobile ? 'sm' : 'md'}
- isDisabled={!hasSelected || isUpdating}
+ isDisabled={!hasSelected || areButtonsDisabled}
onClick={handleDelete}
>
{isLoadDelete && <Spinner size='xs' />}
@@ -453,7 +489,7 @@ const CartPage = () => {
<Tooltip
label={clsxm({
'Barang promo tidak dapat dibuat quotation': hasSelectedPromo,
- 'Harap tunggu pembaruan selesai': isUpdating,
+ 'Harap tunggu pembaruan selesai': areButtonsDisabled,
'Tidak ada item yang dipilih': !hasSelected,
})}
>
@@ -461,14 +497,11 @@ const CartPage = () => {
colorScheme='yellow'
w='full'
isDisabled={
- hasSelectedPromo ||
- !hasSelected ||
- isUpdating ||
- isLoadDelete
+ hasSelectedPromo || !hasSelected || areButtonsDisabled
}
onClick={handleQuotation}
>
- {isUpdating ? <Spinner size='sm' mr={2} /> : null}
+ {areButtonsDisabled ? <Spinner size='sm' mr={2} /> : null}
Quotation
</Button>
</Tooltip>
@@ -477,21 +510,18 @@ const CartPage = () => {
label={clsxm({
'Tidak ada item yang dipilih': !hasSelected,
'Terdapat item yang tidak ada harga': hasSelectNoPrice,
- 'Harap tunggu pembaruan selesai': isUpdating,
+ 'Harap tunggu pembaruan selesai': areButtonsDisabled,
})}
>
<Button
colorScheme='red'
w='full'
isDisabled={
- !hasSelected ||
- hasSelectNoPrice ||
- isUpdating ||
- isLoadDelete
+ !hasSelected || hasSelectNoPrice || areButtonsDisabled
}
onClick={handleCheckout}
>
- {isUpdating ? <Spinner size='sm' mr={2} /> : null}
+ {areButtonsDisabled ? <Spinner size='sm' mr={2} /> : null}
Checkout
</Button>
</Tooltip>
diff --git a/src-migrate/utils/cart.js b/src-migrate/utils/cart.js
index 431ff530..f474cbde 100644
--- a/src-migrate/utils/cart.js
+++ b/src-migrate/utils/cart.js
@@ -1,5 +1,6 @@
// cart-cookie-utils.js
import Cookies from 'js-cookie';
+import checkboxUpdateState from './checkBoxState';
// Constants
const CART_ITEMS_COOKIE = 'cart_data';
@@ -165,9 +166,19 @@ export const syncCartWithCookie = (cart) => {
* Update selected status item di cookie
* @param {number} productId ID produk
* @param {boolean} isSelected Status selected baru
+ * @param {boolean} notifyUpdate Whether to notify checkbox update state (default: true)
*/
-export const updateSelectedItemInCookie = (productId, isSelected) => {
+export const updateSelectedItemInCookie = (
+ productId,
+ isSelected,
+ notifyUpdate = true
+) => {
try {
+ // Notify checkbox update state if requested
+ if (notifyUpdate) {
+ checkboxUpdateState.startUpdate();
+ }
+
const selectedItems = getSelectedItemsFromCookie();
selectedItems[productId] = isSelected;
setSelectedItemsToCookie(selectedItems);
@@ -188,6 +199,11 @@ export const updateSelectedItemInCookie = (productId, isSelected) => {
} catch (error) {
console.error('Error updating selected item in cookie:', error);
return {};
+ } finally {
+ // End update notification if requested
+ if (notifyUpdate) {
+ checkboxUpdateState.endUpdate();
+ }
}
};
@@ -195,9 +211,19 @@ export const updateSelectedItemInCookie = (productId, isSelected) => {
* Set semua item menjadi selected atau unselected di cookie
* @param {Array} productIds Array product IDs
* @param {boolean} isSelected Status selected baru
+ * @param {boolean} notifyUpdate Whether to notify checkbox update state (default: true)
*/
-export const setAllSelectedInCookie = (productIds, isSelected) => {
+export const setAllSelectedInCookie = (
+ productIds,
+ isSelected,
+ notifyUpdate = true
+) => {
try {
+ // Notify checkbox update state if requested
+ if (notifyUpdate) {
+ checkboxUpdateState.startUpdate();
+ }
+
const selectedItems = getSelectedItemsFromCookie();
productIds.forEach((id) => {
@@ -221,6 +247,11 @@ export const setAllSelectedInCookie = (productIds, isSelected) => {
} catch (error) {
console.error('Error setting all selected in cookie:', error);
return {};
+ } finally {
+ // End update notification if requested
+ if (notifyUpdate) {
+ checkboxUpdateState.endUpdate();
+ }
}
};
@@ -263,10 +294,37 @@ export const removeCartItemsFromCookie = (cartIds) => {
};
/**
+ * Hapus item selected dari cookie berdasarkan product IDs
+ * @param {Array} productIds Array product IDs untuk dihapus
+ */
+export const removeSelectedItemsFromCookie = (productIds) => {
+ try {
+ const selectedItems = getSelectedItemsFromCookie();
+
+ // Hapus dari selectedItems
+ productIds.forEach((productId) => {
+ if (selectedItems[productId] !== undefined) {
+ delete selectedItems[productId];
+ }
+ });
+
+ // Simpan kembali ke cookie
+ setSelectedItemsToCookie(selectedItems);
+
+ return { selectedItems };
+ } catch (error) {
+ console.error('Error removing selected items from cookie:', error);
+ return {};
+ }
+};
+
+/**
* Force reset semua selected items ke unselected state
*/
export const forceResetAllSelectedItems = () => {
try {
+ checkboxUpdateState.startUpdate();
+
const cartData = getCartDataFromCookie();
const selectedItems = {};
@@ -286,5 +344,45 @@ export const forceResetAllSelectedItems = () => {
} catch (error) {
console.error('Error resetting all selected items:', error);
return {};
+ } finally {
+ checkboxUpdateState.endUpdate();
}
};
+
+/**
+ * Sync selected items between cookie and cart data
+ * @param {Array} cartProducts Products array from cart
+ */
+export const syncSelectedItemsWithCookie = (cartProducts) => {
+ try {
+ if (!cartProducts || !Array.isArray(cartProducts)) {
+ return { items: {}, needsUpdate: false };
+ }
+
+ const selectedItems = getSelectedItemsFromCookie();
+ let needsUpdate = false;
+
+ // Check if we need to update any items based on cookie values
+ cartProducts.forEach((product) => {
+ if (product.id && selectedItems[product.id] !== undefined) {
+ if (product.selected !== selectedItems[product.id]) {
+ needsUpdate = true;
+ }
+ } else if (product.id) {
+ // If not in cookie, add with current value
+ selectedItems[product.id] = product.selected;
+ }
+ });
+
+ // Update the cookie with the latest values
+ setSelectedItemsToCookie(selectedItems);
+
+ return { items: selectedItems, needsUpdate };
+ } catch (error) {
+ console.error('Error syncing selected items with cookie:', error);
+ return { items: {}, needsUpdate: false };
+ }
+};
+
+// Export the checkbox update state for use in components
+export { checkboxUpdateState };
diff --git a/src-migrate/utils/checkBoxState.js b/src-migrate/utils/checkBoxState.js
new file mode 100644
index 00000000..0c58321f
--- /dev/null
+++ b/src-migrate/utils/checkBoxState.js
@@ -0,0 +1,89 @@
+// ~/modules/cart/utils/checkboxUpdateState.js
+
+/**
+ * Enhanced state manager for checkbox updates
+ * Tracks both global update state and individual checkbox update states
+ */
+
+// 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)
+ 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());
+ }
+
+ notifyListeners();
+ return updateCount;
+ },
+
+ // End an update for a specific checkbox
+ endUpdate: (itemId = null) => {
+ updateCount = Math.max(0, updateCount - 1);
+
+ // If an item ID is provided, remove it from updating set
+ if (itemId !== null) {
+ updatingCheckboxIds.delete(itemId.toString());
+ }
+
+ notifyListeners();
+ return updateCount;
+ },
+
+ // Reset the update counter and clear all updating checkboxes
+ reset: () => {
+ updateCount = 0;
+ updatingCheckboxIds.clear();
+ notifyListeners();
+ },
+
+ // Get IDs of all checkboxes currently updating (for debugging)
+ getUpdatingCheckboxIds: () => [...updatingCheckboxIds],
+
+ // Add a listener function to be called when update state changes
+ 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);
+ }
+ });
+}
+
+export default checkboxUpdateState;
diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx
index 4120df2c..5256a328 100644
--- a/src/lib/checkout/components/Checkout.jsx
+++ b/src/lib/checkout/components/Checkout.jsx
@@ -1106,10 +1106,7 @@ const Checkout = () => {
</div>
<span className='leading-5'>
Jika mengalami kesulitan dalam melakukan pembelian di website
- Indoteknik.{' '}
- <a href={whatsappUrl()} style={{ textDecoration: 'underline' }}>
- Hubungi kami disini
- </a>
+ Indoteknik. <a href={whatsappUrl()}>Hubungi kami disini</a>
</span>
</Alert>
</div>