summaryrefslogtreecommitdiff
path: root/src-migrate/pages
diff options
context:
space:
mode:
Diffstat (limited to 'src-migrate/pages')
-rw-r--r--src-migrate/pages/shop/cart/index.tsx380
1 files changed, 135 insertions, 245 deletions
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