From f7dc0ddd0a90e9be93b68fe5f92aeabf0b2ed11d Mon Sep 17 00:00:00 2001 From: Linc2427 Date: Tue, 15 Apr 2025 17:27:17 +0700 Subject: change invoice input to required --- src-migrate/validations/tempo.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'src-migrate') diff --git a/src-migrate/validations/tempo.ts b/src-migrate/validations/tempo.ts index cf5914b5..46ac1ef1 100644 --- a/src-migrate/validations/tempo.ts +++ b/src-migrate/validations/tempo.ts @@ -122,8 +122,12 @@ export const TempoSchemaPengiriman = z.object({ zipInvoice: z.string().min(1, { message: 'Kode pos harus diisi' }), isSameAddrees: z.string(), isSameAddreesStreet: z.string(), - tukarInvoiceInput: z.string().optional(), - tukarInvoiceInputPembayaran: z.string().optional(), + tukarInvoiceInput: z + .string() + .min(1, { message: 'Jadwal Penukaran Invoice Harus Diisi' }), + tukarInvoiceInputPembayaran: z + .string() + .min(1, { message: 'Jadwal Pembayaran Harus Diisi' }), dokumenPengiriman: z.string().optional(), dokumenPengirimanInput: z.string().optional(), dokumenKirimInput: z.string().optional(), -- cgit v1.2.3 From 81abbbabd11df17b5fe795e725f5841273fbf125 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 24 Apr 2025 14:31:48 +0700 Subject: Make Popup Banner show in product detail for non auth user --- src-migrate/modules/popup-information/index.tsx | 34 +++++++++++++++++----- .../product-detail/components/ProductDetail.tsx | 3 ++ 2 files changed, 29 insertions(+), 8 deletions(-) (limited to 'src-migrate') diff --git a/src-migrate/modules/popup-information/index.tsx b/src-migrate/modules/popup-information/index.tsx index d50711cc..5c3bc8fa 100644 --- a/src-migrate/modules/popup-information/index.tsx +++ b/src-migrate/modules/popup-information/index.tsx @@ -10,11 +10,21 @@ import dynamic from 'next/dynamic'; const PagePopupInformation = () => { const router = useRouter(); const isHomePage = router.pathname === '/'; + // Updated to match your URL structure with /shop/product/ + const isProductDetail = router.pathname.includes('/shop/product/'); const auth = getAuth(); const [active, setActive] = useState(false); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); + const [hasClosedPopup, setHasClosedPopup] = useState(false); + useEffect(() => { + // Check if user has closed the popup in this session + const popupClosed = sessionStorage.getItem('popupClosed'); + if (popupClosed) { + setHasClosedPopup(true); + } + }, []); useEffect(() => { const getData = async () => { @@ -26,25 +36,33 @@ const PagePopupInformation = () => { setLoading(false); }; - if (isHomePage && !auth) { + // Show popup if user is on homepage OR product detail page AND not authenticated AND hasn't closed popup + if ((isHomePage || isProductDetail) && !auth && !hasClosedPopup) { setActive(true); getData(); } - }, [isHomePage, auth]); + }, [isHomePage, isProductDetail, auth, hasClosedPopup]); + + const handleClose = () => { + setActive(false); + // Set session storage to remember user closed the popup + sessionStorage.setItem('popupClosed', 'true'); + }; + return (
{data && !loading && ( setActive(false)} + close={handleClose} mode='desktop' > -
setActive(false)} - > - +
+ {data[0]?.name} {
+
-- cgit v1.2.3 From d9dd7fd69d2f895f8e503f8d6becd4be3af18b15 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 24 Apr 2025 15:16:10 +0700 Subject: Remove session storage --- src-migrate/modules/popup-information/index.tsx | 29 ++++++------------------- 1 file changed, 7 insertions(+), 22 deletions(-) (limited to 'src-migrate') diff --git a/src-migrate/modules/popup-information/index.tsx b/src-migrate/modules/popup-information/index.tsx index 5c3bc8fa..68e0805b 100644 --- a/src-migrate/modules/popup-information/index.tsx +++ b/src-migrate/modules/popup-information/index.tsx @@ -10,21 +10,11 @@ import dynamic from 'next/dynamic'; const PagePopupInformation = () => { const router = useRouter(); const isHomePage = router.pathname === '/'; - // Updated to match your URL structure with /shop/product/ const isProductDetail = router.pathname.includes('/shop/product/'); const auth = getAuth(); const [active, setActive] = useState(false); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); - const [hasClosedPopup, setHasClosedPopup] = useState(false); - - useEffect(() => { - // Check if user has closed the popup in this session - const popupClosed = sessionStorage.getItem('popupClosed'); - if (popupClosed) { - setHasClosedPopup(true); - } - }, []); useEffect(() => { const getData = async () => { @@ -36,29 +26,24 @@ const PagePopupInformation = () => { setLoading(false); }; - // Show popup if user is on homepage OR product detail page AND not authenticated AND hasn't closed popup - if ((isHomePage || isProductDetail) && !auth && !hasClosedPopup) { + if ((isHomePage || isProductDetail) && !auth) { setActive(true); getData(); } - }, [isHomePage, isProductDetail, auth, hasClosedPopup]); - - const handleClose = () => { - setActive(false); - // Set session storage to remember user closed the popup - sessionStorage.setItem('popupClosed', 'true'); - }; - + }, [isHomePage, isProductDetail, auth]); return (
{data && !loading && ( setActive(false)} mode='desktop' > -
+
setActive(false)} + > Date: Tue, 6 May 2025 08:51:52 +0700 Subject: push --- .../product-detail/components/ProductDetail.tsx | 47 +++++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) (limited to 'src-migrate') diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index 4667e086..c26dafde 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -2,7 +2,7 @@ import style from '../styles/product-detail.module.css'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { Button } from '@chakra-ui/react'; import { MessageCircleIcon, Share2Icon } from 'lucide-react'; @@ -73,6 +73,19 @@ const ProductDetail = ({ product }: Props) => { // setSelectedVariant(product?.variants[0]) }, []); + + + // Gabungkan semua gambar produk (utama + tambahan) + const allImages = product.image_carousel ? [...product.image_carousel] : []; + + + if (product.image) { + allImages.unshift(product.image); // Tambahkan gambar utama di awal array + } + console.log(product); + + const [mainImage, setMainImage] = useState(allImages[0] || ''); + return ( <>
@@ -82,7 +95,37 @@ const ProductDetail = ({ product }: Props) => {
- + + + {/* Carousel horizontal */} + {allImages.length > 0 && ( +
+
+ {allImages.map((img, index) => ( +
setMainImage(img)} + > + {`Thumbnail { + e.target.src = '/path/to/fallback-image.jpg'; // Fallback jika gambar error + }} + /> +
+ ))} +
+
+ )} +
-- cgit v1.2.3 From 38666d79091b0cc95045a8d7e824e772d8b8cc12 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 6 May 2025 09:22:54 +0700 Subject: add type data image_carousel --- src-migrate/types/product.ts | 1 + 1 file changed, 1 insertion(+) (limited to 'src-migrate') diff --git a/src-migrate/types/product.ts b/src-migrate/types/product.ts index 85ea702a..14ba718f 100644 --- a/src-migrate/types/product.ts +++ b/src-migrate/types/product.ts @@ -4,6 +4,7 @@ export interface IProduct { id: number; image: string; image_mobile: string; + image_carousel: string[]; code: string; display_name: string; name: string; -- cgit v1.2.3 From 9e90f51952deee673c19f11c4498229e81ce29f2 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 6 May 2025 09:31:44 +0700 Subject: fix bug --- src-migrate/modules/product-detail/components/ProductDetail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src-migrate') diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index c26dafde..685c107d 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -117,7 +117,7 @@ const ProductDetail = ({ product }: Props) => { className="w-full h-full object-cover rounded-sm" loading="lazy" onError={(e) => { - e.target.src = '/path/to/fallback-image.jpg'; // Fallback jika gambar error + (e.target as HTMLImageElement).src = '/path/to/fallback-image.jpg'; }} />
-- cgit v1.2.3 From 166191e8f7335810cd0073b9aa2436a908a21d34 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 7 May 2025 09:20:55 +0700 Subject: voucher category --- src-migrate/modules/promo/components/Voucher.tsx | 1 + src-migrate/types/voucher.ts | 1 + 2 files changed, 2 insertions(+) (limited to 'src-migrate') diff --git a/src-migrate/modules/promo/components/Voucher.tsx b/src-migrate/modules/promo/components/Voucher.tsx index 034d13e9..0c225c74 100644 --- a/src-migrate/modules/promo/components/Voucher.tsx +++ b/src-migrate/modules/promo/components/Voucher.tsx @@ -18,6 +18,7 @@ interface Voucher { name: string; description: string; code: string; + voucher_category: []; } const VoucherComponent = () => { diff --git a/src-migrate/types/voucher.ts b/src-migrate/types/voucher.ts index 3e90f449..d3140372 100644 --- a/src-migrate/types/voucher.ts +++ b/src-migrate/types/voucher.ts @@ -3,6 +3,7 @@ export interface IVoucher { image: string; name: string; code: string; + voucher_category: []; description: string | false; remaining_time: string; } -- cgit v1.2.3 From 4ca48d5b2b8da512447d04bb7e6540ea2b76294b Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 7 May 2025 09:53:12 +0700 Subject: merge --- src-migrate/modules/popup-information/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'src-migrate') diff --git a/src-migrate/modules/popup-information/index.tsx b/src-migrate/modules/popup-information/index.tsx index 68e0805b..cae50abf 100644 --- a/src-migrate/modules/popup-information/index.tsx +++ b/src-migrate/modules/popup-information/index.tsx @@ -10,7 +10,6 @@ import dynamic from 'next/dynamic'; const PagePopupInformation = () => { const router = useRouter(); const isHomePage = router.pathname === '/'; - const isProductDetail = router.pathname.includes('/shop/product/'); const auth = getAuth(); const [active, setActive] = useState(false); const [data, setData] = useState(null); @@ -26,11 +25,11 @@ const PagePopupInformation = () => { setLoading(false); }; - if ((isHomePage || isProductDetail) && !auth) { + if (isHomePage && !auth) { setActive(true); getData(); } - }, [isHomePage, isProductDetail, auth]); + }, [isHomePage, auth]); return (
{data && !loading && ( -- cgit v1.2.3 From 29db3ca16589f1c6bad42545b56b1cb3a6039f4c Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 7 May 2025 11:47:08 +0700 Subject: remove unused function --- src-migrate/types/product.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'src-migrate') diff --git a/src-migrate/types/product.ts b/src-migrate/types/product.ts index 14ba718f..e2121dab 100644 --- a/src-migrate/types/product.ts +++ b/src-migrate/types/product.ts @@ -35,7 +35,21 @@ export interface IProduct { name: string; logo: string; }; - voucher_pasti_hemat : any; + voucher_pasti_hemat: any; + available_voucher?: IVoucher[]; +} + +export interface IVoucher { + id: number; + name: string; + code: string; + description: string; + image: string; + canApply: boolean; + discountVoucher: number; + termsConditions: string; + remaining_time: string; + apply_type: 'all' | 'shipping' | 'brand'; } export interface IProductDetail extends IProduct { -- cgit v1.2.3 From 11ea9426239b88181e5074a8e7246f8955346180 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 7 May 2025 11:51:15 +0700 Subject: Remove unused code --- src-migrate/types/product.ts | 14 -------------- 1 file changed, 14 deletions(-) (limited to 'src-migrate') diff --git a/src-migrate/types/product.ts b/src-migrate/types/product.ts index e2121dab..746cdd4a 100644 --- a/src-migrate/types/product.ts +++ b/src-migrate/types/product.ts @@ -36,20 +36,6 @@ export interface IProduct { logo: string; }; voucher_pasti_hemat: any; - available_voucher?: IVoucher[]; -} - -export interface IVoucher { - id: number; - name: string; - code: string; - description: string; - image: string; - canApply: boolean; - discountVoucher: number; - termsConditions: string; - remaining_time: string; - apply_type: 'all' | 'shipping' | 'brand'; } export interface IProductDetail extends IProduct { -- cgit v1.2.3 From 89a0e7f37d7537c2d0d3715817453279443d518b Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 7 May 2025 15:06:32 +0700 Subject: Fix conflict --- .../product-detail/components/ProductDetail.tsx | 30 +++++++++------------- 1 file changed, 12 insertions(+), 18 deletions(-) (limited to 'src-migrate') diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index bd2c895f..0660b9c0 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -23,8 +23,6 @@ import PriceAction from './PriceAction'; import SimilarBottom from './SimilarBottom'; import SimilarSide from './SimilarSide'; -import PagePopupInformation from '~/modules/popup-information'; - import { gtagProductDetail } from '@/core/utils/googleTag'; type Props = { @@ -75,12 +73,9 @@ const ProductDetail = ({ product }: Props) => { // setSelectedVariant(product?.variants[0]) }, []); - - // Gabungkan semua gambar produk (utama + tambahan) const allImages = product.image_carousel ? [...product.image_carousel] : []; - if (product.image) { allImages.unshift(product.image); // Tambahkan gambar utama di awal array } @@ -95,21 +90,20 @@ const ProductDetail = ({ product }: Props) => {
-
- - + + {/* Carousel horizontal */} {allImages.length > 0 && ( -
-
+
+
{allImages.map((img, index) => ( -
setMainImage(img)} @@ -117,10 +111,11 @@ const ProductDetail = ({ product }: Props) => { {`Thumbnail { - (e.target as HTMLImageElement).src = '/path/to/fallback-image.jpg'; + (e.target as HTMLImageElement).src = + '/path/to/fallback-image.jpg'; }} />
@@ -128,7 +123,6 @@ const ProductDetail = ({ product }: Props) => {
)} -
-- cgit v1.2.3 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 ++++++++++-- src-migrate/modules/cart/stores/useCartStore.ts | 135 ++++++- src-migrate/pages/shop/cart/index.tsx | 395 +++++++++++++++------ src-migrate/utils/cart.js | 290 +++++++++++++++ 5 files changed, 1015 insertions(+), 194 deletions(-) create mode 100644 src-migrate/utils/cart.js (limited to 'src-migrate') 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; diff --git a/src-migrate/modules/cart/stores/useCartStore.ts b/src-migrate/modules/cart/stores/useCartStore.ts index e7d2cdd3..d211304a 100644 --- a/src-migrate/modules/cart/stores/useCartStore.ts +++ b/src-migrate/modules/cart/stores/useCartStore.ts @@ -1,6 +1,15 @@ import { create } from 'zustand'; import { CartItem, CartProps } from '~/types/cart'; import { getUserCart } from '~/services/cart'; +import { + syncCartWithCookie, + getCartDataFromCookie, + getSelectedItemsFromCookie, + updateSelectedItemInCookie, + setAllSelectedInCookie, + removeCartItemsFromCookie, + forceResetAllSelectedItems, +} from '~/utils/cart'; type State = { cart: CartProps | null; @@ -17,6 +26,8 @@ type State = { type Action = { loadCart: (userId: number) => Promise; updateCartItem: (updateCart: CartProps) => void; + syncCartWithCookieAndUpdate: (cart: CartProps) => { needsUpdate: boolean }; + forceResetSelection: () => void; }; export const useCartStore = create((set, get) => ({ @@ -29,34 +40,140 @@ export const useCartStore = create((set, get) => ({ tax: 0, grandTotal: 0, }, + loadCart: async (userId) => { if (get().isLoadCart === true) return; set({ isLoadCart: true }); - const cart: CartProps = (await getUserCart(userId)) as CartProps; - set({ cart }); - set({ isLoadCart: false }); + try { + // Fetch cart from API + const cart: CartProps = (await getUserCart(userId)) as CartProps; - const summary = computeSummary(cart); - set({ summary }); + // 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) { + set({ cart }); + } + + // Update summary with current cart + 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); + } + } 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, + })), + }; + + set({ cart: updatedCart }); + + // Update summary + const summary = computeSummary(updatedCart); + set({ summary }); + }, })); const computeSummary = (cart: CartProps) => { let subtotal = 0; let discount = 0; - const PPN: number = process.env.NEXT_PUBLIC_PPN ? parseFloat(process.env.NEXT_PUBLIC_PPN) : 0; - + const PPN: number = process.env.NEXT_PUBLIC_PPN + ? parseFloat(process.env.NEXT_PUBLIC_PPN) + : 0; + for (const item of cart?.products) { if (!item.selected) continue; @@ -74,5 +191,5 @@ const computeSummary = (cart: CartProps) => { let tax = grandTotal - total; // let grandTotal = total + tax; - return { subtotal, discount, total, grandTotal, tax }; -}; \ No newline at end of file + return { subtotal, discount, total, grandTotal, tax }; +}; diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx index 24baa933..475a4259 100644 --- a/src-migrate/pages/shop/cart/index.tsx +++ b/src-migrate/pages/shop/cart/index.tsx @@ -1,8 +1,16 @@ import style from './cart.module.css'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; -import { Button, Checkbox, Spinner, Tooltip } from '@chakra-ui/react'; +import { + Button, + Checkbox, + Spinner, + Tooltip, + Text, + Box, + Flex, +} from '@chakra-ui/react'; import { toast } from 'react-hot-toast'; import { useRouter } from 'next/router'; import { getAuth } from '~/libs/auth'; @@ -14,26 +22,125 @@ import clsxm from '~/libs/clsxm'; import useDevice from '@/core/hooks/useDevice'; import CartSummaryMobile from '~/modules/cart/components/CartSummaryMobile'; import Image from '~/components/ui/image'; -import { CartItem } from '~/types/cart'; import { deleteUserCart, upsertUserCart } from '~/services/cart'; import { Trash2Icon } from 'lucide-react'; import { useProductCartContext } from '@/contexts/ProductCartContext'; +import { + getSelectedItemsFromCookie, + setSelectedItemsToCookie, + syncSelectedItemsWithCookie, + setAllSelectedInCookie, + removeSelectedItemsFromCookie, + forceResetAllSelectedItems, +} from '~/utils/cart'; const CartPage = () => { const router = useRouter(); const auth = getAuth(); const [isStepApproval, setIsStepApproval] = useState(false); - const [isSelectedAll, setIsSelectedAll] = useState(false); - const [isButtonChek, setIsButtonChek] = useState(false); - const [buttonSelectNow, setButtonSelectNow] = useState(true); const [isLoad, setIsLoad] = useState(false); const [isLoadDelete, setIsLoadDelete] = useState(false); const { loadCart, cart, summary, updateCartItem } = useCartStore(); - const useDivvice = useDevice(); + const device = useDevice(); const { setRefreshCart } = useProductCartContext(); const [isTop, setIsTop] = useState(true); - const [hasChanged, setHasChanged] = useState(false); - const prevCartRef = useRef(null); + const [isUpdating, setIsUpdating] = useState(false); + const [isStateMismatch, setIsStateMismatch] = useState(false); + + // 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, + }) + ); + + 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]); useEffect(() => { const handleScroll = () => { @@ -47,40 +154,35 @@ const CartPage = () => { }, []); useEffect(() => { - if (typeof auth === 'object' && !cart) { - loadCart(auth.id); - setIsStepApproval(auth?.feature?.soApproval); - } - }, [auth, loadCart, cart, isButtonChek]); + const loadCartWithStorage = async () => { + if (typeof auth === 'object' && !cart) { + await loadCart(auth.id); + setIsStepApproval(auth?.feature?.soApproval); - useEffect(() => { - if (typeof auth === 'object' && !cart) { - loadCart(auth.id); - setIsStepApproval(auth?.feature?.soApproval); - } - }, [auth, loadCart, cart, isButtonChek]); + // Sync selected items with server data using cookies + if (cart?.products) { + const { items, needsUpdate } = syncSelectedItemsWithCookie( + cart.products + ); - useEffect(() => { - const hasSelectedChanged = () => { - if (prevCartRef.current && cart) { - const prevCart = prevCartRef.current; - return cart.products.some( - (item, index) => - prevCart[index] && prevCart[index].selected !== item.selected - ); + // If there's a mismatch between cookie and server data, update the UI + if (needsUpdate) { + const updatedCart = { + ...cart, + products: cart.products.map((item) => ({ + ...item, + selected: + items[item.id] !== undefined ? items[item.id] : item.selected, + })), + }; + updateCartItem(updatedCart); + } + } } - return false; }; - if (hasSelectedChanged()) { - setHasChanged(true); - // Perform necessary actions here if selection has changed - } else { - setHasChanged(false); - } - - prevCartRef.current = cart ? [...cart.products] : null; - }, [cart]); + loadCartWithStorage(); + }, [auth, cart]); const hasSelectedPromo = useMemo(() => { if (!cart) return false; @@ -103,38 +205,24 @@ const CartPage = () => { const hasSelectedAll = useMemo(() => { if (!cart || !Array.isArray(cart.products)) return false; - return cart.products.every((item) => item.selected); + return ( + cart.products.length > 0 && cart.products.every((item) => item.selected) + ); }, [cart]); - useEffect(() => { - const updateCartItems = async () => { - if (typeof auth === 'object' && cart) { - const upsertPromises = cart.products.map((item) => - upsertUserCart({ - userId: auth.id, - type: item.cart_type, - id: item.id, - qty: item.quantity, - selected: item.selected, - }) - ); - try { - await Promise.all(upsertPromises); - await loadCart(auth.id); - } catch (error) { - console.error('Failed to update cart items:', error); - } - } - }; - - updateCartItems(); - }, [hasChanged]); - const handleCheckout = () => { + if (isUpdating || isLoadDelete) { + toast.error('Harap tunggu pembaruan selesai'); + return; + } router.push('/shop/checkout'); }; const handleQuotation = () => { + if (isUpdating || isLoadDelete) { + toast.error('Harap tunggu pembaruan selesai'); + return; + } if (hasSelectedPromo || !hasSelected) { toast.error('Maaf, Barang promo tidak dapat dibuat quotation'); } else { @@ -143,20 +231,62 @@ const CartPage = () => { }; const handleChange = async (e: React.ChangeEvent) => { - if (cart) { + if (cart && !isUpdating && typeof auth === 'object') { + const newSelectedState = !hasSelectedAll; + + // Update UI immediately const updatedCart = { ...cart, products: cart.products.map((item) => ({ ...item, - selected: !hasSelectedAll, + selected: newSelectedState, })), }; updateCartItem(updatedCart); - if (hasSelectedAll) { - setIsSelectedAll(false); - } else { - setIsSelectedAll(true); + + // Get all product IDs in cart + const productIds = cart.products.map((item) => item.id); + + // Update cookies immediately for responsive UI + setAllSelectedInCookie(productIds, newSelectedState); + + setIsUpdating(true); + + 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, + }) + ); + + 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); + + toast.error('Gagal memperbarui pilihan'); + } finally { + setIsUpdating(false); } } }; @@ -165,14 +295,25 @@ const CartPage = () => { if (typeof auth !== 'object' || !cart) return; setIsLoadDelete(true); - for (const item of cart.products) { - if (item.selected === true) { + try { + const itemsToDelete = cart.products.filter((item) => item.selected); + const itemIdsToDelete = itemsToDelete.map((item) => item.id); + + for (const item of itemsToDelete) { await deleteUserCart(auth.id, [item.cart_id]); - await loadCart(auth.id); } + + // Remove deleted items from cookie + removeSelectedItemsFromCookie(itemIdsToDelete); + + await loadCart(auth.id); + setRefreshCart(true); + } catch (error) { + console.error('Failed to delete cart items:', error); + toast.error('Gagal menghapus item'); + } finally { + setIsLoadDelete(false); } - setIsLoadDelete(false); - setRefreshCart(true); }; return ( @@ -180,27 +321,45 @@ const CartPage = () => {
-

Keranjang Belanja

+
+

Keranjang Belanja

+ {isStateMismatch && ( + + )} +
+
-
+
- {isLoad && } - {!isLoad && ( - - )} +

{hasSelectedAll ? 'Uncheck all' : 'Select all'}

-
+
{ bg='#fadede' variant='outline' colorScheme='red' - w='full' - isDisabled={!hasSelected} + w='auto' + size={device.isMobile ? 'sm' : 'md'} + isDisabled={!hasSelected || isUpdating} onClick={handleDelete} > {isLoadDelete && } @@ -223,19 +383,19 @@ const CartPage = () => {
-
+
{!cart && }
-
+
{cart?.products?.map((item) => ( ))} {cart?.products?.length === 0 && ( -
+
Empty Cart {
-
- {useDivvice.isMobile && ( - - )} - {!useDivvice.isMobile && ( - +
+ {device.isMobile ? ( + + ) : ( + )}
{ } > @@ -301,14 +477,21 @@ const CartPage = () => { label={clsxm({ 'Tidak ada item yang dipilih': !hasSelected, 'Terdapat item yang tidak ada harga': hasSelectNoPrice, + 'Harap tunggu pembaruan selesai': isUpdating, })} > diff --git a/src-migrate/utils/cart.js b/src-migrate/utils/cart.js new file mode 100644 index 00000000..431ff530 --- /dev/null +++ b/src-migrate/utils/cart.js @@ -0,0 +1,290 @@ +// cart-cookie-utils.js +import Cookies from 'js-cookie'; + +// Constants +const CART_ITEMS_COOKIE = 'cart_data'; +const SELECTED_ITEMS_COOKIE = 'cart_selected_items'; +const COOKIE_EXPIRY_DAYS = 7; // Cookie akan berlaku selama 7 hari + +/** + * Mengambil data cart lengkap dari cookie + * @returns {Object} Object dengan key cart_id dan value cart item data lengkap + */ +export const getCartDataFromCookie = () => { + try { + const storedData = Cookies.get(CART_ITEMS_COOKIE); + return storedData ? JSON.parse(storedData) : {}; + } catch (error) { + console.error('Error reading cart data from cookie:', error); + return {}; + } +}; + +/** + * Menyimpan data cart lengkap ke cookie + * @param {Object} cartData Object dengan key cart_id dan value cart item data lengkap + */ +export const setCartDataToCookie = (cartData) => { + try { + Cookies.set(CART_ITEMS_COOKIE, JSON.stringify(cartData), { + expires: COOKIE_EXPIRY_DAYS, + path: '/', + sameSite: 'strict', + }); + } catch (error) { + console.error('Error saving cart data to cookie:', error); + } +}; + +/** + * Mengambil state selected items dari cookie + * @returns {Object} Object dengan key product id dan value boolean selected status + */ +export const getSelectedItemsFromCookie = () => { + try { + const storedItems = Cookies.get(SELECTED_ITEMS_COOKIE); + return storedItems ? JSON.parse(storedItems) : {}; + } catch (error) { + console.error('Error reading selected items from cookie:', error); + return {}; + } +}; + +/** + * Menyimpan state selected items ke cookie + * @param {Object} items Object dengan key product id dan value boolean selected status + */ +export const setSelectedItemsToCookie = (items) => { + try { + Cookies.set(SELECTED_ITEMS_COOKIE, JSON.stringify(items), { + expires: COOKIE_EXPIRY_DAYS, + path: '/', + sameSite: 'strict', + }); + } catch (error) { + console.error('Error saving selected items to cookie:', error); + } +}; + +/** + * Transform cart items dari format API ke format yang lebih simpel untuk disimpan di cookie + * @param {Array} cartItems Array cart items dari API + * @returns {Object} Object dengan key cart_id dan value cart item data + */ +export const transformCartItemsForCookie = (cartItems) => { + if (!cartItems || !Array.isArray(cartItems)) return {}; + + const cartData = {}; + + cartItems.forEach((item) => { + // Skip items yang tidak memiliki cart_id + if (!item.cart_id) return; + + cartData[item.cart_id] = { + id: item.id, + cart_id: item.cart_id, + cart_type: item.cart_type, + product: item.product_id + ? { + id: item.product_id, + name: item.product_name || '', + } + : null, + program_line: item.program_line_id + ? { + id: item.program_line_id, + name: item.program_line_name || '', + } + : null, + quantity: item.quantity, + selected: item.selected, + price: item.price, + package_price: item.package_price, + source: item.source || 'add_to_cart', + }; + }); + + return cartData; +}; + +/** + * Sinkronisasi cart data dan selected items dari server dengan cookie + * @param {Object} cart Cart object dari API + * @returns {Object} Object yang berisi updated cartData dan selectedItems + */ +export const syncCartWithCookie = (cart) => { + try { + if (!cart || !cart.products) return { needsUpdate: false }; + + // Transform data dari API ke format cookie + const serverCartData = transformCartItemsForCookie(cart.products); + + // Ambil data lama dari cookie + const existingCartData = getCartDataFromCookie(); + + // Ambil selected status dari cookie + const selectedItems = getSelectedItemsFromCookie(); + + // Gabungkan data cart, prioritaskan data server + const mergedCartData = { ...existingCartData, ...serverCartData }; + + // Periksa apakah ada perbedaan status selected + let needsUpdate = false; + + // Update selected status berdasarkan cookie jika ada + for (const cartId in mergedCartData) { + const item = mergedCartData[cartId]; + if (item.id && selectedItems[item.id] !== undefined) { + // Jika status di cookie berbeda dengan di cart + if (item.selected !== selectedItems[item.id]) { + needsUpdate = true; + item.selected = selectedItems[item.id]; + } + } else if (item.id) { + // Jika tidak ada di cookie, tambahkan dari cart + selectedItems[item.id] = item.selected; + } + } + + // Simpan kembali ke cookie + setCartDataToCookie(mergedCartData); + setSelectedItemsToCookie(selectedItems); + + return { + cartData: mergedCartData, + selectedItems, + needsUpdate, + }; + } catch (error) { + console.error('Error syncing cart with cookie:', error); + return { needsUpdate: false }; + } +}; + +/** + * Update selected status item di cookie + * @param {number} productId ID produk + * @param {boolean} isSelected Status selected baru + */ +export const updateSelectedItemInCookie = (productId, isSelected) => { + try { + const selectedItems = getSelectedItemsFromCookie(); + selectedItems[productId] = isSelected; + setSelectedItemsToCookie(selectedItems); + + // Update juga di cart data + const cartData = getCartDataFromCookie(); + + for (const cartId in cartData) { + const item = cartData[cartId]; + if (item.id === productId) { + item.selected = isSelected; + } + } + + setCartDataToCookie(cartData); + + return { selectedItems, cartData }; + } catch (error) { + console.error('Error updating selected item in cookie:', error); + return {}; + } +}; + +/** + * Set semua item menjadi selected atau unselected di cookie + * @param {Array} productIds Array product IDs + * @param {boolean} isSelected Status selected baru + */ +export const setAllSelectedInCookie = (productIds, isSelected) => { + try { + const selectedItems = getSelectedItemsFromCookie(); + + productIds.forEach((id) => { + if (id) selectedItems[id] = isSelected; + }); + + setSelectedItemsToCookie(selectedItems); + + // Update juga di cart data + const cartData = getCartDataFromCookie(); + + for (const cartId in cartData) { + if (productIds.includes(cartData[cartId].id)) { + cartData[cartId].selected = isSelected; + } + } + + setCartDataToCookie(cartData); + + return { selectedItems, cartData }; + } catch (error) { + console.error('Error setting all selected in cookie:', error); + return {}; + } +}; + +/** + * Hapus item dari cookie + * @param {Array} cartIds Array cart IDs untuk dihapus + */ +export const removeCartItemsFromCookie = (cartIds) => { + try { + const cartData = getCartDataFromCookie(); + const selectedItems = getSelectedItemsFromCookie(); + const productIdsToRemove = []; + + // Hapus item dari cartData dan catat product IDs + cartIds.forEach((cartId) => { + if (cartData[cartId]) { + if (cartData[cartId].id) { + productIdsToRemove.push(cartData[cartId].id); + } + delete cartData[cartId]; + } + }); + + // Hapus dari selectedItems + productIdsToRemove.forEach((productId) => { + if (selectedItems[productId] !== undefined) { + delete selectedItems[productId]; + } + }); + + // Simpan kembali ke cookie + setCartDataToCookie(cartData); + setSelectedItemsToCookie(selectedItems); + + return { cartData, selectedItems }; + } catch (error) { + console.error('Error removing cart items from cookie:', error); + return {}; + } +}; + +/** + * Force reset semua selected items ke unselected state + */ +export const forceResetAllSelectedItems = () => { + try { + const cartData = getCartDataFromCookie(); + const selectedItems = {}; + + // Reset semua selected status di cartData + for (const cartId in cartData) { + cartData[cartId].selected = false; + if (cartData[cartId].id) { + selectedItems[cartData[cartId].id] = false; + } + } + + // Simpan kembali ke cookie + setCartDataToCookie(cartData); + setSelectedItemsToCookie(selectedItems); + + return { cartData, selectedItems }; + } catch (error) { + console.error('Error resetting all selected items:', error); + return {}; + } +}; -- 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 ++---- src-migrate/pages/shop/cart/index.tsx | 98 +++++++++++++------- src-migrate/utils/cart.js | 102 ++++++++++++++++++++- src-migrate/utils/checkBoxState.js | 89 ++++++++++++++++++ 5 files changed, 296 insertions(+), 64 deletions(-) create mode 100644 src-migrate/utils/checkBoxState.js (limited to 'src-migrate') 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)} +
- +
); }; 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 ( <>
{ >

Keranjang Belanja

- {isStateMismatch && ( - - )}
@@ -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 = () => { @@ -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, })} > 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(); + } } }; @@ -262,11 +293,38 @@ 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; -- 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 +- src-migrate/pages/shop/cart/index.tsx | 36 +++++- src-migrate/utils/cart.js | 68 +++++++--- 4 files changed, 174 insertions(+), 71 deletions(-) (limited to 'src-migrate') 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 diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx index eefe8d09..798ad318 100644 --- a/src-migrate/pages/shop/cart/index.tsx +++ b/src-migrate/pages/shop/cart/index.tsx @@ -329,24 +329,52 @@ const CartPage = () => { if (typeof auth !== 'object' || !cart) return; setIsLoadDelete(true); - checkboxUpdateState.startUpdate('delete_operation'); // Use special ID for delete + checkboxUpdateState.startUpdate('delete_operation'); try { const itemsToDelete = cart.products.filter((item) => item.selected); const itemIdsToDelete = itemsToDelete.map((item) => item.id); + const cartIdsToDelete = itemsToDelete.map((item) => item.cart_id); + // Step 1: Delete from server first for (const item of itemsToDelete) { await deleteUserCart(auth.id, [item.cart_id]); } - // Remove deleted items from cookie - removeSelectedItemsFromCookie(itemIdsToDelete); + // Step 2: Update local cart state immediately (optimistic update) + const updatedProducts = cart.products.filter((item) => !item.selected); + const updatedCart = { + ...cart, + products: updatedProducts, + product_total: updatedProducts.length, + }; + updateCartItem(updatedCart); - await loadCart(auth.id); + // Step 3: Clean up cookies AFTER state update + 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 + }); + + // 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); checkboxUpdateState.endUpdate('delete_operation'); diff --git a/src-migrate/utils/cart.js b/src-migrate/utils/cart.js index f474cbde..1ddc5446 100644 --- a/src-migrate/utils/cart.js +++ b/src-migrate/utils/cart.js @@ -297,26 +297,10 @@ 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 {}; - } -}; +/** + * Hapus item selected dari cookie berdasarkan product IDs dan juga hapus dari cart data + * @param {Array} productIds Array product IDs untuk dihapus + */ /** * Force reset semua selected items ke unselected state @@ -386,3 +370,47 @@ export const syncSelectedItemsWithCookie = (cartProducts) => { // Export the checkbox update state for use in components export { checkboxUpdateState }; + +/** + * Hapus item selected dari cookie berdasarkan product IDs dan juga hapus dari cart data + * @param {Array} productIds Array product IDs untuk dihapus + */ +/** + * Hapus item selected dari cookie berdasarkan product IDs dan juga hapus dari cart data + * @param {Array} productIds Array product IDs untuk dihapus + */ +export const removeSelectedItemsFromCookie = (productIds) => { + try { + const selectedItems = getSelectedItemsFromCookie(); + const cartData = getCartDataFromCookie(); + const cartIdsToRemove = []; + + // Find cart IDs that match the product IDs + for (const cartId in cartData) { + if (productIds.includes(cartData[cartId].id)) { + cartIdsToRemove.push(cartId); + } + } + + // Remove from selectedItems + productIds.forEach((productId) => { + if (selectedItems[productId] !== undefined) { + delete selectedItems[productId]; + } + }); + + // Remove from cartData + cartIdsToRemove.forEach((cartId) => { + delete cartData[cartId]; + }); + + // Save both cookies + setSelectedItemsToCookie(selectedItems); + setCartDataToCookie(cartData); + + return { selectedItems, cartData }; + } catch (error) { + console.error('Error removing selected items from cookie:', error); + return {}; + } +}; -- cgit v1.2.3 From 582f00294ba924b105c789b43e6e92beaf99260f Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 23 May 2025 10:31:38 +0700 Subject: remove checkboxstate utils --- src-migrate/utils/checkBoxState.js | 89 -------------------------------------- 1 file changed, 89 deletions(-) delete mode 100644 src-migrate/utils/checkBoxState.js (limited to 'src-migrate') diff --git a/src-migrate/utils/checkBoxState.js b/src-migrate/utils/checkBoxState.js deleted file mode 100644 index 0c58321f..00000000 --- a/src-migrate/utils/checkBoxState.js +++ /dev/null @@ -1,89 +0,0 @@ -// ~/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; -- cgit v1.2.3 From 31e27d92a1965f02b644a7d905366d7180d33c36 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 26 May 2025 10:38:39 +0700 Subject: add checkboxstate --- src-migrate/utils/checkBoxState.js | 89 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src-migrate/utils/checkBoxState.js (limited to 'src-migrate') 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; -- cgit v1.2.3 From 8ef5d44ff4aaf3f8826ffbb28e4466451a750af1 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 26 May 2025 11:52:18 +0700 Subject: push --- src-migrate/utils/checkBoxState.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src-migrate') diff --git a/src-migrate/utils/checkBoxState.js b/src-migrate/utils/checkBoxState.js index 0c58321f..8e65ea66 100644 --- a/src-migrate/utils/checkBoxState.js +++ b/src-migrate/utils/checkBoxState.js @@ -13,7 +13,7 @@ let listeners = []; let updatingCheckboxIds = new Set(); const checkboxUpdateState = { - // Check if any checkboxes are currently updating (for buttons) + // 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) -- 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 ++-- src-migrate/utils/cart.js | 7 +++---- src-migrate/utils/checkBoxState.js | 20 +++++++++----------- 3 files changed, 14 insertions(+), 17 deletions(-) (limited to 'src-migrate') 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]); diff --git a/src-migrate/utils/cart.js b/src-migrate/utils/cart.js index 1ddc5446..ebd771e5 100644 --- a/src-migrate/utils/cart.js +++ b/src-migrate/utils/cart.js @@ -117,7 +117,7 @@ export const syncCartWithCookie = (cart) => { try { if (!cart || !cart.products) return { needsUpdate: false }; - // Transform data dari API ke format cookie + // Transform data API ke cookie const serverCartData = transformCartItemsForCookie(cart.products); // Ambil data lama dari cookie @@ -126,7 +126,7 @@ export const syncCartWithCookie = (cart) => { // Ambil selected status dari cookie const selectedItems = getSelectedItemsFromCookie(); - // Gabungkan data cart, prioritaskan data server + // Gabungkan data cart, (prioritize data server) const mergedCartData = { ...existingCartData, ...serverCartData }; // Periksa apakah ada perbedaan status selected @@ -142,12 +142,11 @@ export const syncCartWithCookie = (cart) => { item.selected = selectedItems[item.id]; } } else if (item.id) { - // Jika tidak ada di cookie, tambahkan dari cart selectedItems[item.id] = item.selected; } } - // Simpan kembali ke cookie + // Simpan ke cookie setCartDataToCookie(mergedCartData); setSelectedItemsToCookie(selectedItems); diff --git a/src-migrate/utils/checkBoxState.js b/src-migrate/utils/checkBoxState.js index 8e65ea66..2b527f36 100644 --- a/src-migrate/utils/checkBoxState.js +++ b/src-migrate/utils/checkBoxState.js @@ -1,5 +1,3 @@ -// ~/modules/cart/utils/checkboxUpdateState.js - /** * Enhanced state manager for checkbox updates * Tracks both global update state and individual checkbox update states @@ -53,17 +51,17 @@ const checkboxUpdateState = { }, // Get IDs of all checkboxes currently updating (for debugging) - getUpdatingCheckboxIds: () => [...updatingCheckboxIds], + // getUpdatingCheckboxIds: () => [...updatingCheckboxIds], - // Add a listener function to be called when update state changes - addListener: (callback) => { - if (typeof callback === 'function') { - listeners.push(callback); + // // 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); - } - }, + // // Immediately call with current state + // callback(updateCount > 0); + // } + // }, // Remove a listener removeListener: (callback) => { -- cgit v1.2.3 From cca6d803fc4db729865def23004ab1c4bd279e24 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 26 May 2025 15:10:41 +0700 Subject: error checkboxstate --- src-migrate/utils/checkBoxState.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'src-migrate') diff --git a/src-migrate/utils/checkBoxState.js b/src-migrate/utils/checkBoxState.js index 2b527f36..8f7236c3 100644 --- a/src-migrate/utils/checkBoxState.js +++ b/src-migrate/utils/checkBoxState.js @@ -53,15 +53,15 @@ const checkboxUpdateState = { // 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); - // } - // }, + // 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) => { -- 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 ++--- src-migrate/modules/cart/stores/useCartStore.ts | 198 ++++++----- src-migrate/pages/shop/cart/index.tsx | 380 ++++++++------------- src-migrate/utils/cart.js | 40 +++ src-migrate/utils/checkBoxState.js | 141 ++++---- 6 files changed, 477 insertions(+), 483 deletions(-) (limited to 'src-migrate') 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); 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; updateCartItem: (updateCart: CartProps) => void; - syncCartWithCookieAndUpdate: (cart: CartProps) => { needsUpdate: boolean }; forceResetSelection: () => void; + clearCart: () => void; }; export const useCartStore = create((set, get) => ({ @@ -42,154 +39,151 @@ export const useCartStore = create((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(false); const [isLoadDelete, setIsLoadDelete] = useState(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) => { - if (cart && !isUpdating && typeof auth === 'object') { - const newSelectedState = !hasSelectedAll; + const handleSelectAll = async (e: React.ChangeEvent) => { + 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 */}
{ 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'}

+
- +
+ {/* Main Content */}
@@ -485,6 +384,8 @@ const CartPage = () => { )}
+ + {/* Cart Summary */}
{ : style['summary-buttons'] } > - + + {!isStepApproval && ( - + @@ -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; -- 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') 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