diff options
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/cart/api/CartApi.js | 12 | ||||
| -rw-r--r-- | src/lib/cart/components/Cart.jsx | 691 | ||||
| -rw-r--r-- | src/lib/cart/components/CartOld.jsx | 497 | ||||
| -rw-r--r-- | src/lib/checkout/api/checkoutApi.js | 14 | ||||
| -rw-r--r-- | src/lib/checkout/api/getVoucher.js | 11 | ||||
| -rw-r--r-- | src/lib/checkout/components/Checkout.jsx | 1010 | ||||
| -rw-r--r-- | src/lib/checkout/components/CheckoutOld.jsx | 811 | ||||
| -rw-r--r-- | src/lib/product/components/Product/Product.jsx | 2 | ||||
| -rw-r--r-- | src/lib/product/components/Product/ProductDesktop.jsx | 129 | ||||
| -rw-r--r-- | src/lib/product/components/Product/ProductMobile.jsx | 22 | ||||
| -rw-r--r-- | src/lib/product/components/ProductCard.jsx | 2 | ||||
| -rw-r--r-- | src/lib/promotinProgram/api/homepageApi.js | 17 | ||||
| -rw-r--r-- | src/lib/promotinProgram/components/HomePage.jsx | 116 | ||||
| -rw-r--r-- | src/lib/promotinProgram/components/PromotionType.jsx | 304 | ||||
| -rw-r--r-- | src/lib/variant/components/VariantGroupCard.jsx | 43 |
15 files changed, 3288 insertions, 393 deletions
diff --git a/src/lib/cart/api/CartApi.js b/src/lib/cart/api/CartApi.js index 7683a935..038479f8 100644 --- a/src/lib/cart/api/CartApi.js +++ b/src/lib/cart/api/CartApi.js @@ -1,6 +1,7 @@ import odooApi from '@/core/api/odooApi' +import { getAuth } from '@/core/utils/auth' -const CartApi = async ({ variantIds }) => { +export const CartApi = async ({ variantIds }) => { if (variantIds) { const dataCart = await odooApi('GET', `/api/v2/product_variant/${variantIds}`) return dataCart @@ -8,4 +9,11 @@ const CartApi = async ({ variantIds }) => { return null } -export default CartApi +// export default CartApi + +export const getCartApi = async () => { + const id = getAuth()?.id + const cart = await odooApi('GET', `/api/v1/user/${id}/cart`) + + return cart +}
\ No newline at end of file diff --git a/src/lib/cart/components/Cart.jsx b/src/lib/cart/components/Cart.jsx index c02f976f..efbcf76b 100644 --- a/src/lib/cart/components/Cart.jsx +++ b/src/lib/cart/components/Cart.jsx @@ -4,7 +4,7 @@ import Image from '@/core/components/elements/Image/Image' import NextImage from 'next/image' import currencyFormat from '@/core/utils/currencyFormat' import { useEffect, useState } from 'react' -import { deleteItemCart, getItemCart, updateItemCart } from '@/core/utils/cart' +import { addCart, deleteItemCart, getCartApi, updateItemCart } from '@/core/utils/cart' import { CheckIcon, TrashIcon } from '@heroicons/react/24/outline' import { createSlug } from '@/core/utils/slug' import { useRouter } from 'next/router' @@ -17,12 +17,54 @@ import DesktopView from '@/core/components/views/DesktopView' import ProductCard from '@/lib/product/components/ProductCard' import productSearchApi from '@/lib/product/api/productSearchApi' import whatsappUrl from '@/core/utils/whatsappUrl' +import useAuth from '@/core/hooks/useAuth' +import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner' +import { getPromotionProgram } from '@/lib/promotinProgram/api/homepageApi' +import PromotionType from '@/lib/promotinProgram/components/PromotionType' import { gtagBeginCheckout } from '@/core/utils/googleTag' const Cart = () => { const router = useRouter() const [products, setProducts] = useState(null) - const { cart } = useCart({ enabled: !products }) + const [isLoading, setIsLoading] = useState(true) + const auth = useAuth() + + const [cart, setCart] = useState(null) + + useEffect(() => { + if (!auth) return + }, [auth]) + + const getCart = async () => { + const listCart = await getCartApi() + setCart(listCart) + } + + useEffect(() => { + getCart() + }, []) + + useEffect(() => { + if (cart) { + const processProducts = async () => { + const updatedProducts = await Promise.all( + cart.products.map(async (product) => { + const id = product.id + const program = await getPromotionProgram({ id }) + const isPromo = program.length > 0 ? 1 : 0 + return { + ...product, + isPromo + } + }) + ) + setProducts(updatedProducts) + } + // processProducts() + setProducts(cart.products) + setIsLoading(false) + } + }, [cart]) const [totalPriceBeforeTax, setTotalPriceBeforeTax] = useState(0) const [totalTaxAmount, setTotalTaxAmount] = useState(0) @@ -31,21 +73,10 @@ const Cart = () => { const [deleteConfirmation, setDeleteConfirmation] = useState(null) const [productRecomendation, setProductRecomendation] = useState(null) - - useEffect(() => { - if (cart.data && !products) { - const productsWithQuantity = cart.data.map((product) => { - const productInCart = getItemCart({ productId: product.id }) - if (!productInCart) return - return { - ...product, - quantity: productInCart.quantity, - selected: productInCart.selected - } - }) - setProducts(productsWithQuantity) - } - }, [cart, products]) + const [promotionType, setPromotionType] = useState(null) + const [variantId, setVariantId] = useState(null) + const [quantity, setQuantity] = useState(null) + const [promotionActiveId, setPromotionActiveId] = useState(null) useEffect(() => { if (!products) return @@ -55,13 +86,12 @@ const Cart = () => { let calculateTotalDiscountAmount = 0 for (const product of products) { if (product.quantity == '') continue - updateItemCart({ - productId: product.id, - quantity: product.quantity, - selected: product.selected - }) if (!product.selected) continue + + if (product.canBuy == false) { + toggleSelected(product.id) + } let priceBeforeTax = product.price.price / 1.11 calculateTotalPriceBeforeTax += priceBeforeTax * product.quantity calculateTotalTaxAmount += (product.price.price - priceBeforeTax) * product.quantity @@ -74,6 +104,24 @@ const Cart = () => { }, [products]) useEffect(() => { + const updateData = () => { + updateItemCart({ + productId: variantId, + quantity, + programLineId: promotionActiveId, + selected: true + }).then(() => { + getCart().then(() => { + if(promotionActiveId){ + setPromotionType(false) + } + }) + }) + } + updateData() + }, [promotionActiveId]) + + useEffect(() => { const LoadProductSimilar = async () => { const randProductIndex = Math.floor(Math.random() * products.length) const productLoad = await productSearchApi({ @@ -109,8 +157,16 @@ const Cart = () => { quantity = value != '' && value < 1 ? 1 : value break } - productsToUpdate[productIndex].quantity = quantity + let qty = quantity + productsToUpdate[productIndex].quantity = qty + setProducts([...productsToUpdate]) + addCart( + productId, + qty, + productsToUpdate[productIndex].selected, + productsToUpdate[productIndex].program?.id + ) } const toggleSelected = (productId) => { @@ -118,8 +174,16 @@ const Cart = () => { if (productIndex < 0) return let productsToUpdate = products - productsToUpdate[productIndex].selected = !productsToUpdate[productIndex].selected + let isSelected = !productsToUpdate[productIndex].selected + productsToUpdate[productIndex].selected = isSelected + setProducts([...productsToUpdate]) + addCart( + productId, + productsToUpdate[productIndex].quantity, + isSelected, + productsToUpdate[productIndex].program?.id + ) } const selectedProduct = () => { @@ -135,6 +199,13 @@ const Cart = () => { toast.success('Berhasil menghapus barang dari keranjang') } + const handlePopUpPromo = (variantId, quantity, promoId = null) => { + setPromotionType(true) + setVariantId(variantId) + setQuantity(quantity) + setPromotionActiveId(promoId) + } + const handleCheckout = () => { gtagBeginCheckout(products) router.push('/shop/checkout') @@ -173,6 +244,23 @@ const Cart = () => { </div> </BottomPopup> + <BottomPopup + className=' !h-[75%]' + title='Pakai Promo' + active={promotionType} + close={() => setPromotionType(false)} + > + <div className='flex mt-4'> + <PromotionType + isModal={true} + variantId={variantId} + setPromotionActiveId={setPromotionActiveId} + promotionActiveId={promotionActiveId} + quantity={quantity} + ></PromotionType> + </div> + </BottomPopup> + <MobileView> <div className='pt-4'> <div className='flex justify-between mb-4 px-4'> @@ -181,13 +269,13 @@ const Cart = () => { </div> <div className='flex flex-col gap-y-4 h-screen'> - {cart.isLoading && ( + {isLoading && ( <div className='flex justify-center my-4'> <Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' /> </div> )} - {!cart.isLoading && (!products || products?.length == 0) && ( + {!isLoading && (!products || products?.length == 0) && ( <div className='px-4'> <Alert className='text-center my-2' type='info'> Keranjang belanja anda masih kosong @@ -195,85 +283,204 @@ const Cart = () => { </div> )} - {products?.map((product) => ( - <div key={product?.id} className='flex mx-4'> - <input - type='checkbox' - onClick={() => toggleSelected(product.id)} - checked={product?.selected} - className='mr-2 accent-danger-500 w-4' - /> - - <Link - href={createSlug('/shop/product/', product?.parent.name, product?.parent.id)} - className='w-[30%] flex-shrink-0' - > - <Image - src={product?.parent?.image} - alt={product?.name} - className='object-contain object-center border border-gray_r-6 h-40 w-full rounded-md' - /> - </Link> - <div className='flex-1 px-2 text-caption-2'> - <Link - href={createSlug('/shop/product/', product?.parent.name, product?.parent.id)} - className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' - > - {product?.parent?.name} - </Link> - <div className='text-gray_r-11 mt-1'> - {product?.code}{' '} - {product?.attributes.length > 0 ? `| ${product?.attributes.join(', ')}` : ''} - </div> - {product?.price?.discountPercentage > 0 && ( - <div className='flex gap-x-1 items-center mt-3'> - <div className='text-gray_r-11 line-through text-caption-2'> - {currencyFormat(product?.price?.price)} + {products && + products?.map((product) => ( + <> + {product.hasProgram > 0 && ( + <div className='flex gap-x-2 bg-yellow-100 p-2 items-center'> + {product.program ? ( + <> + <div className='border border-solid border-red-600 rounded-md p-1 text-caption-2 whitespace-nowrap'> + <span className='text-red-600'>{product.program.type.label}</span> + </div> + <div className='text-caption-2'> + {product.program.type.value == 'merchandise' ? ( + <>Selamat anda mendapatkan merchandise indoteknik.com</> + ) : ( + <> + Selamat! Pembelian anda hemat + <span className='text-red-600 font-semibold text-caption-2'> + {' '} + {currencyFormat(product.program?.totalSavings)} + </span> + </> + )} + </div> + </> + ) : ( + <> + <div className='flex border border-solid border-red-600 rounded-md p-1'> + <span className='text-red-600'>Promo</span> + </div> + <div className='flex'>Pilih Promo Yang Tersedia Bisa lebih Hemat</div> + </> + )} + <div + onClick={() => + handlePopUpPromo(product.id, product.quantity, product.program?.id) + } + className='ml-auto text-red-500 flex gap-x-0 cursor-pointer' + > + <div className='font-semibold text-caption-2 whitespace-nowrap'> + {' '} + Cek Promo + </div> + <div> + <svg + aria-hidden='true' + fill='none' + stroke='currentColor' + stroke-width='1.5' + viewBox='0 0 20 24' + className='h-5 w-5 font-semibold' + > + <path + d='M8.25 4.5l7.5 7.5-7.5 7.5' + stroke-linecap='round' + stroke-linejoin='round' + ></path> + </svg> + </div> </div> - <div className='badge-solid-red'>{product?.price?.discountPercentage}%</div> </div> )} - <div className='font-normal mt-1'> - {currencyFormat(product?.price?.priceDiscount)} - </div> - <div className='flex justify-between items-center mt-1'> - <div className='text-danger-500 font-medium'> - {currencyFormat(product?.price?.priceDiscount * product?.quantity)} - </div> - <div className='flex gap-x-1'> - <button - type='button' - className='btn-light px-2 py-1' - onClick={() => updateQuantity(1, product?.id, 'MINUS')} - disabled={product?.quantity == 1} - > - - - </button> - <input - className='form-input w-6 border-0 border-b rounded-none py-1 px-0 text-center' - type='number' - value={product?.quantity} - onChange={(e) => updateQuantity(e.target.value, product?.id)} - onBlur={(e) => updateQuantity(e.target.value, product?.id, 'BLUR')} + <div key={product?.id} className='flex mx-4 relative'> + <ComponentCanBuy canBuy={product.canBuy} /> + <input + type='checkbox' + onClick={() => toggleSelected(product.id)} + checked={product?.selected} + className='mr-2 accent-danger-500 w-4' + /> + + <Link + href={createSlug('/shop/product/', product?.parent.name, product?.parent.id)} + className='w-[30%] flex-shrink-0' + > + <Image + src={product?.parent?.image} + alt={product?.name} + className='object-contain object-center border border-gray_r-6 h-40 w-full rounded-md' /> - <button - type='button' - className='btn-light px-2 py-1' - onClick={() => updateQuantity(1, product?.id, 'PLUS')} - > - + - </button> - <button - className='btn-red p-1 ml-1' - onClick={() => setDeleteConfirmation(product)} + </Link> + <div className='flex-1 px-2 text-caption-2'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' > - <TrashIcon className='w-4' /> - </button> + {product?.parent?.name} + </Link> + <div className='text-gray_r-11 mt-1'> + {product?.code}{' '} + {product?.attributes.length > 0 + ? `| ${product?.attributes.join(', ')}` + : ''} + </div> + {product?.price?.discountPercentage > 0 && ( + <div className='flex gap-x-1 items-center mt-3'> + <div className='text-gray_r-11 line-through text-caption-2'> + {currencyFormat(product?.price?.price)} + </div> + <div className='badge-solid-red'> + {product?.price?.discountPercentage}% + </div> + </div> + )} + <div className='font-normal mt-1'> + {currencyFormat(product?.price?.priceDiscount)} + </div> + <div className='flex justify-between items-center mt-1'> + <div className='text-danger-500 font-medium'> + {currencyFormat(product?.price?.priceDiscount * product?.quantity)} + </div> + <div className='flex gap-x-1'> + <button + type='button' + className='btn-light px-2 py-1' + onClick={() => updateQuantity(1, product?.id, 'MINUS')} + disabled={product?.quantity == 1} + > + - + </button> + <input + className='form-input w-6 border-0 border-b rounded-none py-1 px-0 text-center' + type='number' + value={product?.quantity} + onChange={(e) => updateQuantity(e.target.value, product?.id)} + onBlur={(e) => updateQuantity(e.target.value, product?.id, 'BLUR')} + /> + <button + type='button' + className='btn-light px-2 py-1' + onClick={() => updateQuantity(1, product?.id, 'PLUS')} + > + + + </button> + <button + className='btn-red p-1 ml-1' + onClick={() => setDeleteConfirmation(product)} + > + <TrashIcon className='w-4' /> + </button> + </div> + </div> </div> </div> - </div> - </div> - ))} + {product.program && + product.program.items && + product.program.items.map((item) => ( + <div key={item.id} className='flex mx-4 relative'> + <ComponentCanBuy canBuy={product.canBuy} /> + <input className='mr-2 accent-danger-500 w-4' /> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='w-[30%] flex-shrink-0' + > + <Image + src={item?.parent?.image} + alt={item?.name} + className='object-contain object-center border border-gray_r-6 h-40 w-full rounded-md' + /> + </Link> + <div className='flex-1 px-2 text-caption-2'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + <div className='px-2 text-left mt-2'> + <div className='mb-2'> + <span className='border border-solid border-red-600 rounded-md p-1 text-red-600'> + {product.program.type.label} + </span> + </div> + <div>{item.name}</div> + </div> + </Link> + {item.price?.discountPercentage > 0 && ( + <div className='flex gap-x-1 items-center mt-3 ml-3'> + <div className='text-gray_r-11 line-through text-caption-2'> + {currencyFormat(item?.price?.price)} + </div> + </div> + )} + <div className='text-danger-500 font-medium mt-1 ml-3'>Gratis</div> + </div> + </div> + ))} + </> + ))} <div className='sticky bottom-0 left-0 w-full p-4 mt-auto border-t border-gray_r-6 bg-white'> <div className='flex flex-col gap-y-3 mb-4'> @@ -330,105 +537,240 @@ const Cart = () => { </tr> </thead> <tbody> - {cart.isLoading && ( + {isLoading && ( <tr> <td colSpan={6}> - <div className='flex justify-center my-2'> - <Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' /> + <div className='container flex justify-center my-4'> + <LogoSpinner width={48} height={48} /> </div> </td> </tr> )} - {!cart.isLoading && (!products || products?.length == 0) && ( + {!isLoading && (!products || products?.length == 0) && ( <tr> <td colSpan={6}>Keranjang belanja anda masih kosong</td> </tr> )} {products && products?.map((product) => ( - <tr key={product.id}> - <td> - <input - type='checkbox' - onClick={() => toggleSelected(product.id)} - checked={product?.selected} - className='accent-danger-500 w-4' - /> - </td> - <td className='flex'> - <Link - href={createSlug( - '/shop/product/', - product?.parent.name, - product?.parent.id - )} - className='w-[20%] flex-shrink-0' - > - <Image - src={product?.parent?.image} - alt={product?.name} - className='object-contain object-center border border-gray_r-6 h-28 w-full rounded-md' + <> + {product.hasProgram && ( + <tr className='!border-b-0 pb-0'> + <td colSpan={6}> + <div className='flex gap-x-2 bg-yellow-100 p-2 items-center'> + {product.program ? ( + <> + <div className='flex border border-solid border-red-600 rounded-md p-1'> + <span className='text-red-600'> + {product.program.type.label} + </span> + </div> + <div className='flex'> + {product.program.type.value == 'merchandise' ? ( + <>Selamat anda mendapatkan merchandise indoteknik.com</> + ) : ( + <> + Selamat! Pembelian anda lebih hemat + <span className='text-red-600 font-semibold ml-2'> + {' '} + {currencyFormat(product.program?.totalSavings)} + </span> + </> + )} + </div> + </> + ) : ( + <> + <div className='flex border border-solid border-red-600 rounded-md p-1'> + <span className='text-red-600'>Promo</span> + </div> + <div className='flex'> + Pilih Promo Yang Tersedia Bisa lebih Hemat + </div> + </> + )} + <div + onClick={() => + handlePopUpPromo( + product.id, + product.quantity, + product.program?.id + ) + } + className='ml-auto text-red-500 flex gap-x-1 cursor-pointer' + > + <div className='font-semibold'> Cek Promo</div> + <div> + <svg + aria-hidden='true' + fill='none' + stroke='currentColor' + stroke-width='1.5' + viewBox='0 0 20 24' + className='h-5 w-5 font-semibold' + > + <path + d='M8.25 4.5l7.5 7.5-7.5 7.5' + stroke-linecap='round' + stroke-linejoin='round' + ></path> + </svg> + </div> + </div> + </div> + </td> + </tr> + )} + <tr + key={product.id} + className={`${product.hasProgram ? '!border-t-0 !border-b-0' : ''}`} + > + <td className='relative'> + <ComponentCanBuy canBuy={product.canBuy} /> + <input + type='checkbox' + onClick={() => toggleSelected(product.id)} + checked={product?.selected} + className='accent-danger-500 w-4' /> - </Link> - <div className='px-2 text-left'> + </td> + <td className='flex relative'> + <ComponentCanBuy canBuy={product.canBuy} /> <Link href={createSlug( '/shop/product/', product?.parent.name, product?.parent.id )} - className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + className='w-[20%] flex-shrink-0' > - {product?.parent?.name} + <Image + src={product?.parent?.image} + alt={product?.name} + className='object-contain object-center border border-gray_r-6 h-28 w-full rounded-md' + /> </Link> - <div className='text-gray_r-11 mt-2'> - {product?.code}{' '} - {product?.attributes.length > 0 - ? `| ${product?.attributes.join(', ')}` - : ''} - </div> - </div> - </td> - <td> - <input - className='form-input w-16 py-2 text-center bg-gray_r-1' - type='number' - value={product?.quantity} - onChange={(e) => updateQuantity(e.target.value, product?.id)} - onBlur={(e) => updateQuantity(e.target.value, product?.id, 'BLUR')} - /> - </td> - <td> - {product?.price?.discountPercentage > 0 && ( - <div className='flex gap-x-1 items-center justify-center mt-3'> - <div className='text-gray_r-11 line-through text-caption-1'> - {currencyFormat(product?.price?.price)} + <div className='px-2 text-left'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + {product?.parent?.name} + </Link> + <div className='text-gray_r-11 mt-2'> + {product?.code}{' '} + {product?.attributes.length > 0 + ? `| ${product?.attributes.join(', ')}` + : ''} </div> - <div className='badge-solid-red'> - {product?.price?.discountPercentage}% + </div> + </td> + <td className='relative'> + <ComponentCanBuy canBuy={product.canBuy} /> + <input + className='form-input w-16 py-2 text-center bg-gray_r-1' + type='number' + value={product?.quantity} + onChange={(e) => updateQuantity(e.target.value, product?.id)} + onBlur={(e) => updateQuantity(e.target.value, product?.id, 'BLUR')} + /> + </td> + <td className='relative'> + <ComponentCanBuy canBuy={product.canBuy} /> + {product?.price?.discountPercentage > 0 && ( + <div className='flex gap-x-1 items-center justify-center mt-3'> + <div className='text-gray_r-11 line-through text-caption-1'> + {currencyFormat(product?.price?.price)} + </div> + <div className='badge-solid-red'> + {product?.price?.discountPercentage}% + </div> </div> + )} + <div className='font-normal mt-1'> + {currencyFormat(product?.price?.priceDiscount)} </div> - )} - <div className='font-normal mt-1'> - {currencyFormat(product?.price?.priceDiscount)} - </div> - </td> - <td> - <div className='text-danger-500 font-medium'> - {currencyFormat(product?.price?.priceDiscount * product?.quantity)} - </div> - </td> - <td> - <div className='flex justify-center items-center h-full'> - <button - className='btn-red p-1 ml-1' - onClick={() => setDeleteConfirmation(product)} - > - <TrashIcon className='w-4' /> - </button> - </div> - </td> - </tr> + </td> + <td className='relative'> + <ComponentCanBuy canBuy={product.canBuy} /> + <div className='text-danger-500 font-medium'> + {currencyFormat(product?.price?.priceDiscount * product?.quantity)} + </div> + </td> + <td> + <div className='flex justify-center items-center h-full'> + <button + className='btn-red p-1 ml-1' + onClick={() => setDeleteConfirmation(product)} + > + <TrashIcon className='w-4' /> + </button> + </div> + </td> + </tr> + {product?.program?.items && ( + <tr key={product.program.id} className='!border-t-0'> + {product.program.items.map((item) => ( + <> + <td className='relative'> + <ComponentCanBuy canBuy={product.canBuy} /> + </td> + <td className='flex relative'> + <ComponentCanBuy canBuy={product.canBuy} /> + <div className='w-[20%] flex-shrink-0'> + <Image + src={item.parent.image} + alt={item.name} + className='object-contain object-center border border-gray_r-6 h-28 w-full rounded-md' + /> + </div> + <div className='px-2 text-left'> + <div className=''> + <span className='border border-solid border-red-600 rounded-md p-1 text-red-600'> + {product.program.type.label} + </span> + </div> + <div className='mt-2 line-clamp-2 leading-6'>{item.name}</div> + </div> + </td> + <td className='relative'> + <ComponentCanBuy canBuy={product.canBuy} /> + <input + className='form-input w-16 py-2 text-center bg-gray_r-1' + type='number' + value={1} + disabled + /> + </td> + <td className='relative'> + <ComponentCanBuy canBuy={product.canBuy} /> + {item?.price?.discountPercentage > 0 && ( + <div className='flex gap-x-1 items-center justify-center mt-3'> + <div className='text-gray_r-11 line-through text-caption-1'> + {currencyFormat(product?.price?.price)} + </div> + </div> + )} + <div className='font-normal mt-1'> + {item?.price.priceDiscount > 0 ? 'Gratis' : ''} + </div> + </td> + <td className='relative'> + <ComponentCanBuy canBuy={product.canBuy} /> + <div className='text-danger-500 font-medium'> + {item.price.priceDiscount > 0 ? 'Gratis' : ''} + </div> + </td> + <td></td> + </> + ))} + </tr> + )} + </> ))} </tbody> </table> @@ -506,4 +848,7 @@ const Cart = () => { ) } +const ComponentCanBuy = ({ canBuy }) => + !canBuy && <div className='absolute w-full h-full bg-gray_r-3/40 top-0 left-0' /> + export default Cart diff --git a/src/lib/cart/components/CartOld.jsx b/src/lib/cart/components/CartOld.jsx new file mode 100644 index 00000000..718541af --- /dev/null +++ b/src/lib/cart/components/CartOld.jsx @@ -0,0 +1,497 @@ +import Link from '@/core/components/elements/Link/Link' +import useCart from '../hooks/useCart' +import Image from '@/core/components/elements/Image/Image' +import NextImage from 'next/image' +import currencyFormat from '@/core/utils/currencyFormat' +import { useEffect, useState } from 'react' +import { deleteItemCart, getItemCart, updateItemCart } from '@/core/utils/cart' +import { CheckIcon, TrashIcon } from '@heroicons/react/24/outline' +import { createSlug } from '@/core/utils/slug' +import { useRouter } from 'next/router' +import BottomPopup from '@/core/components/elements/Popup/BottomPopup' +import { toast } from 'react-hot-toast' +import Spinner from '@/core/components/elements/Spinner/Spinner' +import Alert from '@/core/components/elements/Alert/Alert' +import MobileView from '@/core/components/views/MobileView' +import DesktopView from '@/core/components/views/DesktopView' +import ProductCard from '@/lib/product/components/ProductCard' +import productSearchApi from '@/lib/product/api/productSearchApi' +import whatsappUrl from '@/core/utils/whatsappUrl' +import useAuth from '@/core/hooks/useAuth' + +const Cart = () => { + const router = useRouter() + const [products, setProducts] = useState(null) + const auth = useAuth() + + useEffect(() => { + if (!auth) return + }, [auth]) + const { cart } = useCart({ enabled: !products }) + + const [totalPriceBeforeTax, setTotalPriceBeforeTax] = useState(0) + const [totalTaxAmount, setTotalTaxAmount] = useState(0) + const [totalDiscountAmount, setTotalDiscountAmount] = useState(0) + + const [deleteConfirmation, setDeleteConfirmation] = useState(null) + + const [productRecomendation, setProductRecomendation] = useState(null) + + useEffect(() => { + if (cart.data && !products) { + const productsWithQuantity = cart.data.map((product) => { + const productInCart = getItemCart({ productId: product.id }) + if (!productInCart) return + return { + ...product, + quantity: productInCart.quantity, + selected: productInCart.selected + } + }) + setProducts(productsWithQuantity) + } + }, [cart, products]) + + useEffect(() => { + if (!products) return + + let calculateTotalPriceBeforeTax = 0 + let calculateTotalTaxAmount = 0 + let calculateTotalDiscountAmount = 0 + for (const product of products) { + if (product.quantity == '') continue + updateItemCart({ + productId: product.id, + quantity: product.quantity, + selected: product.selected + }) + + if (!product.selected) continue + let priceBeforeTax = product.price.price / 1.11 + calculateTotalPriceBeforeTax += priceBeforeTax * product.quantity + calculateTotalTaxAmount += (product.price.price - priceBeforeTax) * product.quantity + calculateTotalDiscountAmount += + (product.price.price - product.price.priceDiscount) * product.quantity + } + setTotalPriceBeforeTax(calculateTotalPriceBeforeTax) + setTotalTaxAmount(calculateTotalTaxAmount) + setTotalDiscountAmount(calculateTotalDiscountAmount) + }, [products]) + + useEffect(() => { + const LoadProductSimilar = async () => { + const randProductIndex = Math.floor(Math.random() * products.length) + const productLoad = await productSearchApi({ + query: `q=${products?.[randProductIndex].parent.name}&limit=10&operation=OR` + }) + + setProductRecomendation(productLoad) + } + if (products?.length > 0 && !productRecomendation) LoadProductSimilar() + }, [products, productRecomendation]) + + const updateQuantity = (value, productId, operation = '') => { + let productIndex = products.findIndex((product) => product.id == productId) + if (productIndex < 0) return + + let productsToUpdate = products + let quantity = productsToUpdate[productIndex].quantity + if (value != '' && isNaN(parseInt(value))) return + value = value != '' ? parseInt(value) : '' + switch (operation) { + case 'PLUS': + quantity += value + break + case 'MINUS': + if (quantity - value < 1) return + quantity -= value + break + case 'BLUR': + if (value != '' && value > 0) return + quantity = 1 + break + default: + quantity = value != '' && value < 1 ? 1 : value + break + } + productsToUpdate[productIndex].quantity = quantity + setProducts([...productsToUpdate]) + } + + const toggleSelected = (productId) => { + let productIndex = products.findIndex((product) => product.id == productId) + if (productIndex < 0) return + + let productsToUpdate = products + productsToUpdate[productIndex].selected = !productsToUpdate[productIndex].selected + setProducts([...productsToUpdate]) + } + + const selectedProduct = () => { + if (!products) return [] + return products?.filter((product) => product?.selected == true) + } + + const deleteProduct = (productId) => { + const productsToUpdate = products.filter((product) => product.id != productId) + deleteItemCart({ productId }) + setDeleteConfirmation(null) + setProducts([...productsToUpdate]) + toast.success('Berhasil menghapus barang dari keranjang') + } + + return ( + <> + <BottomPopup + active={deleteConfirmation} + close={() => setDeleteConfirmation(null)} + title='Hapus dari Keranjang' + > + <div className='leading-7 text-gray_r-12/80'> + Apakah anda yakin menghapus barang{' '} + <span className='underline'>{deleteConfirmation?.name}</span> dari keranjang? + </div> + <div className='flex mt-6 gap-x-4 md:justify-end'> + <button + className='btn-solid-red flex-1 md:flex-none' + type='button' + onClick={() => deleteProduct(deleteConfirmation?.id)} + > + Ya, Hapus + </button> + <button + className='btn-light flex-1 md:flex-none' + type='button' + onClick={() => setDeleteConfirmation(null)} + > + Batal + </button> + </div> + </BottomPopup> + + <MobileView> + <div className='pt-4'> + <div className='flex justify-between mb-4 px-4'> + <h1 className='font-semibold'>Keranjang</h1> + <Link href='/'>Cari Produk Lain</Link> + </div> + + <div className='flex flex-col gap-y-4 h-screen'> + {cart.isLoading && ( + <div className='flex justify-center my-4'> + <Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' /> + </div> + )} + + {!cart.isLoading && (!products || products?.length == 0) && ( + <div className='px-4'> + <Alert className='text-center my-2' type='info'> + Keranjang belanja anda masih kosong + </Alert> + </div> + )} + + {products?.map((product) => ( + <div key={product?.id} className='flex mx-4'> + <input + type='checkbox' + onClick={() => toggleSelected(product.id)} + checked={product?.selected} + className='mr-2 accent-danger-500 w-4' + /> + + <Link + href={createSlug('/shop/product/', product?.parent.name, product?.parent.id)} + className='w-[30%] flex-shrink-0' + > + <Image + src={product?.parent?.image} + alt={product?.name} + className='object-contain object-center border border-gray_r-6 h-40 w-full rounded-md' + /> + </Link> + <div className='flex-1 px-2 text-caption-2'> + <Link + href={createSlug('/shop/product/', product?.parent.name, product?.parent.id)} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + {product?.parent?.name} + </Link> + <div className='text-gray_r-11 mt-1'> + {product?.code}{' '} + {product?.attributes.length > 0 ? `| ${product?.attributes.join(', ')}` : ''} + </div> + {product?.price?.discountPercentage > 0 && ( + <div className='flex gap-x-1 items-center mt-3'> + <div className='text-gray_r-11 line-through text-caption-2'> + {currencyFormat(product?.price?.price)} + </div> + <div className='badge-solid-red'>{product?.price?.discountPercentage}%</div> + </div> + )} + <div className='font-normal mt-1'> + {currencyFormat(product?.price?.priceDiscount)} + </div> + <div className='flex justify-between items-center mt-1'> + <div className='text-danger-500 font-medium'> + {currencyFormat(product?.price?.priceDiscount * product?.quantity)} + </div> + <div className='flex gap-x-1'> + <button + type='button' + className='btn-light px-2 py-1' + onClick={() => updateQuantity(1, product?.id, 'MINUS')} + disabled={product?.quantity == 1} + > + - + </button> + <input + className='form-input w-6 border-0 border-b rounded-none py-1 px-0 text-center' + type='number' + value={product?.quantity} + onChange={(e) => updateQuantity(e.target.value, product?.id)} + onBlur={(e) => updateQuantity(e.target.value, product?.id, 'BLUR')} + /> + <button + type='button' + className='btn-light px-2 py-1' + onClick={() => updateQuantity(1, product?.id, 'PLUS')} + > + + + </button> + <button + className='btn-red p-1 ml-1' + onClick={() => setDeleteConfirmation(product)} + > + <TrashIcon className='w-4' /> + </button> + </div> + </div> + </div> + </div> + ))} + + <div className='sticky bottom-0 left-0 w-full p-4 mt-auto border-t border-gray_r-6 bg-white'> + <div className='flex justify-between mb-4'> + <div className='text-gray_r-11'> + Total: + <span className='text-danger-500 font-semibold'> + + {selectedProduct().length > 0 + ? currencyFormat(totalPriceBeforeTax - totalDiscountAmount + totalTaxAmount) + : '-'} + </span> + </div> + </div> + <div className='flex gap-x-3'> + <button + type='button' + className='btn-yellow flex-1' + disabled={selectedProduct().length == 0} + onClick={() => router.push('/shop/quotation')} + > + Quotation + </button> + <button + type='button' + className='btn-solid-red flex-1' + disabled={selectedProduct().length == 0} + onClick={() => router.push('/shop/checkout')} + > + Checkout + </button> + </div> + </div> + </div> + </div> + </MobileView> + + <DesktopView> + <div className='container mx-auto py-10 grid grid-cols-12'> + <div className='col-span-9 border border-gray_r-6 rounded bg-white p-4 pt-6'> + <h1 className='text-title-sm font-semibold mb-6'>Keranjang</h1> + + <table className='table-cart'> + <thead> + <tr> + <th colSpan={2}>Nama Produk</th> + <th>Jumlah</th> + <th>Harga</th> + <th>Subtotal</th> + <th>Action</th> + </tr> + </thead> + <tbody> + {cart.isLoading && ( + <tr> + <td colSpan={6}> + <div className='flex justify-center my-2'> + <Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' /> + </div> + </td> + </tr> + )} + {!cart.isLoading && (!products || products?.length == 0) && ( + <tr> + <td colSpan={6}>Keranjang belanja anda masih kosong</td> + </tr> + )} + {products && + products?.map((product) => ( + <tr key={product.id}> + <td> + <input + type='checkbox' + onClick={() => toggleSelected(product.id)} + checked={product?.selected} + className='accent-danger-500 w-4' + /> + </td> + <td className='flex'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='w-[20%] flex-shrink-0' + > + <Image + src={product?.parent?.image} + alt={product?.name} + className='object-contain object-center border border-gray_r-6 h-28 w-full rounded-md' + /> + </Link> + <div className='px-2 text-left'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + {product?.parent?.name} + </Link> + <div className='text-gray_r-11 mt-2'> + {product?.code}{' '} + {product?.attributes.length > 0 + ? `| ${product?.attributes.join(', ')}` + : ''} + </div> + </div> + </td> + <td> + <input + className='form-input w-16 py-2 text-center bg-gray_r-1' + type='number' + value={product?.quantity} + onChange={(e) => updateQuantity(e.target.value, product?.id)} + onBlur={(e) => updateQuantity(e.target.value, product?.id, 'BLUR')} + /> + </td> + <td> + {product?.price?.discountPercentage > 0 && ( + <div className='flex gap-x-1 items-center justify-center mt-3'> + <div className='text-gray_r-11 line-through text-caption-1'> + {currencyFormat(product?.price?.price)} + </div> + <div className='badge-solid-red'> + {product?.price?.discountPercentage}% + </div> + </div> + )} + <div className='font-normal mt-1'> + {currencyFormat(product?.price?.priceDiscount)} + </div> + </td> + <td> + <div className='text-danger-500 font-medium'> + {currencyFormat(product?.price?.priceDiscount * product?.quantity)} + </div> + </td> + <td> + <div className='flex justify-center items-center h-full'> + <button + className='btn-red p-1 ml-1' + onClick={() => setDeleteConfirmation(product)} + > + <TrashIcon className='w-4' /> + </button> + </div> + </td> + </tr> + ))} + </tbody> + </table> + + <div className='pt-2 pb-6 flex items-center gap-x-3'> + <NextImage + src='/images/logo-question.png' + alt='Logo Question Indoteknik' + width={60} + height={60} + /> + <div className='text-gray_r-12/90'> + Tanya stock untuk pembelian anda sebelum melanjutkan pembayaran! + <span> + {' '} + <a href={whatsappUrl()} className='text-danger-500'> + Hubungi Kami + </a> + </span> + </div> + </div> + </div> + + <div className='col-span-3 pl-4'> + <div className='sticky top-48 w-full p-4 rounded border border-gray_r-6 bg-white'> + <h1 className='text-title-sm font-semibold mb-6'>Ringkasan Belanja</h1> + <div className='flex justify-between mb-4'> + <div className='text-gray_r-11'> + Total: + <span className='text-danger-500 font-semibold'> + + {selectedProduct().length > 0 + ? currencyFormat(totalPriceBeforeTax - totalDiscountAmount + totalTaxAmount) + : '-'} + </span> + </div> + </div> + <div className='flex gap-x-3'> + <button + type='button' + className='btn-yellow flex-1' + disabled={selectedProduct().length == 0} + onClick={() => router.push('/shop/quotation')} + > + Quotation + </button> + <button + type='button' + className='btn-solid-red flex-1' + disabled={selectedProduct().length == 0} + onClick={() => router.push('/shop/checkout')} + > + Checkout + </button> + </div> + </div> + </div> + + <div className='col-span-9 pt-2 pb-6 mt-6'> + <h1 className='text-title-sm font-semibold mb-6'>Produk Yang Mungkin Kamu Suka</h1> + <div className='grid grid-cols-5 gap-x-3 gap-y-6'> + {productRecomendation && + productRecomendation.response.products.map((product) => ( + <ProductCard product={product} key={product.id} /> + ))} + </div> + </div> + </div> + </DesktopView> + </> + ) +} + +export default Cart diff --git a/src/lib/checkout/api/checkoutApi.js b/src/lib/checkout/api/checkoutApi.js index b76c9b7f..0d0cc918 100644 --- a/src/lib/checkout/api/checkoutApi.js +++ b/src/lib/checkout/api/checkoutApi.js @@ -1,7 +1,7 @@ import odooApi from '@/core/api/odooApi' import { getAuth } from '@/core/utils/auth' -const checkoutApi = async ({ data }) => { +export const checkoutApi = async ({ data }) => { const auth = getAuth() const dataCheckout = await odooApi( 'POST', @@ -11,4 +11,14 @@ const checkoutApi = async ({ data }) => { return dataCheckout } -export default checkoutApi +export const getProductsCheckout = async (voucher) => { + const id = getAuth()?.id + let products + if(voucher){ + products = await odooApi('GET',`/api/v1/user/${id}/sale_order/checkout?voucher=${voucher}`) + }else{ + products = await odooApi('GET',`/api/v1/user/${id}/sale_order/checkout`) + } + + return products +} diff --git a/src/lib/checkout/api/getVoucher.js b/src/lib/checkout/api/getVoucher.js new file mode 100644 index 00000000..57d8acf5 --- /dev/null +++ b/src/lib/checkout/api/getVoucher.js @@ -0,0 +1,11 @@ +import odooApi from '@/core/api/odooApi' + +export const getVoucher = async (id) => { + const dataVoucher = await odooApi('GET', `/api/v1/user/${id}/voucher`) + return dataVoucher +} + +export const findVoucher = async (code, id) => { + const dataVoucher = await odooApi('GET', `/api/v1/user/${id}/voucher?code=${code}`) + return dataVoucher +} diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 07d9acb6..5a9dcf7f 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -4,32 +4,44 @@ import Link from '@/core/components/elements/Link/Link' import useAuth from '@/core/hooks/useAuth' import { getItemAddress } from '@/core/utils/address' import addressesApi from '@/lib/address/api/addressesApi' -import CartApi from '@/lib/cart/api/CartApi' import { ExclamationCircleIcon } from '@heroicons/react/24/outline' import React, { useEffect, useRef, useState } from 'react' import _ from 'lodash' -import { deleteItemCart, getCart, getItemCart } from '@/core/utils/cart' +import { deleteItemCart, getCartApi } from '@/core/utils/cart' import currencyFormat from '@/core/utils/currencyFormat' import { toast } from 'react-hot-toast' import getFileBase64 from '@/core/utils/getFileBase64' -import checkoutApi from '../api/checkoutApi' +// import checkoutApi from '../api/checkoutApi' import { useRouter } from 'next/router' import VariantGroupCard from '@/lib/variant/components/VariantGroupCard' import axios from 'axios' import Image from '@/core/components/elements/Image/Image' +import imageNext from 'next/image' import MobileView from '@/core/components/views/MobileView' import DesktopView from '@/core/components/views/DesktopView' import ExpedisiList from '../api/ExpedisiList' import whatsappUrl from '@/core/utils/whatsappUrl' import { createSlug } from '@/core/utils/slug' import BottomPopup from '@/core/components/elements/Popup/BottomPopup' +import { useQuery } from 'react-query' import { gtagPurchase } from '@/core/utils/googleTag' +import { findVoucher, getVoucher } from '../api/getVoucher' const SELF_PICKUP_ID = 32 +const { checkoutApi } = require('../api/checkoutApi') +const { getProductsCheckout } = require('../api/checkoutApi') + const Checkout = () => { const router = useRouter() const auth = useAuth() + + const [activeVoucher, SetActiveVoucher] = useState(null) + + const { data: cartCheckout } = useQuery('cartCheckout-' + activeVoucher, () => + getProductsCheckout(activeVoucher) + ) + const [selectedAddress, setSelectedAddress] = useState({ shipping: null, invoicing: null @@ -66,8 +78,6 @@ const Checkout = () => { }, [addresses]) const [products, setProducts] = useState(null) - const [totalAmount, setTotalAmount] = useState(0) - const [totalDiscountAmount, setTotalDiscountAmount] = useState(0) const [totalWeight, setTotalWeight] = useState(0) const [priceCheck, setPriceCheck] = useState(false) const [listExpedisi, setExpedisi] = useState([]) @@ -81,6 +91,60 @@ const Checkout = () => { const [selectedExpedisiService, setselectedExpedisiService] = useState(null) const [etd, setEtd] = useState(null) const [etdFix, setEtdFix] = useState(null) + const [bottomPopup, SetBottomPopup] = useState(null) + const [listVouchers, SetListVoucher] = useState(null) + const [discountVoucher, SetDiscountVoucher] = useState(0) + const [codeVoucher, SetCodeVoucher] = useState(null) + const [findCodeVoucher, SetFindVoucher] = useState(null) + const [selisihHargaCode, SetSelisihHargaCode] = useState(null) + const [buttonTerapkan, SetButtonTerapkan] = useState(false) + const [checkoutValidation, setCheckoutValidation] = useState(false) + const [loadingVoucher, setLoadingVoucher] = useState(true) + + const expedisiValidation = useRef(null) + + const voucher = async () => { + try { + let dataVoucher = await getVoucher(auth?.id) + SetListVoucher(dataVoucher) + } finally { + setLoadingVoucher(false) + } + } + const VoucherCode = async (code) => { + let dataVoucher = await findVoucher(code, auth.id) + if (dataVoucher.length <= 0) { + SetFindVoucher(1) + return + } + + let addNewLine = dataVoucher[0] + let checkList = listVouchers.findIndex((voucher) => voucher.code == addNewLine.code) + if (checkList >= 0) { + if (listVouchers[checkList].canApply) { + ToggleSwitch(code) + SetCodeVoucher(null) + } else { + SetSelisihHargaCode(listVouchers[checkList].differenceToApply) + SetFindVoucher(2) + } + return + } + if (cartCheckout?.subtotal < addNewLine.minPurchaseAmount) { + SetSelisihHargaCode(currencyFormat(addNewLine.minPurchaseAmount - cartCheckout?.subtotal)) + SetFindVoucher(2) + return + } else { + SetFindVoucher(3) + SetButtonTerapkan(true) + } + SetListVoucher((prevList) => [addNewLine, ...prevList]) + SetActiveVoucher(addNewLine.code) + } + + useEffect(() => { + SetFindVoucher(null) + }, [bottomPopup]) useEffect(() => { const loadExpedisi = async () => { @@ -93,57 +157,47 @@ const Checkout = () => { setExpedisi(dataExpedisi) } loadExpedisi() + // voucher() }, []) - useEffect(() => { - const loadProducts = async () => { - let variantIds = '' - let { query } = router - if (query?.productId) { - variantIds = query.productId - } else { - const cart = getCart() - variantIds = _.filter(cart, (o) => o.selected == true) - .map((o) => o.productId) - .join(',') + const hitungDiscountVoucher = (code) => { + let dataVoucherIndex = listVouchers.findIndex((voucher) => voucher.code == code) + let dataActiveVoucher = listVouchers[dataVoucherIndex] + + let countDiscount = dataActiveVoucher.discountVoucher + + /*if (dataActiveVoucher.discountType === 'percentage') { + countDiscount = cartCheckout?.subtotal * (dataActiveVoucher.discountAmount / 100) + if ( + dataActiveVoucher.maxDiscountAmount > 0 && + countDiscount > dataActiveVoucher.maxDiscountAmount + ) { + countDiscount = dataActiveVoucher.maxDiscountAmount } + } else { + countDiscount = dataActiveVoucher.discountAmount + }*/ - const dataProducts = await CartApi({ variantIds }) - const productsWithQuantity = dataProducts?.map((product) => { - if (product.price.priceDiscount == 0) setPriceCheck(true) - return { - ...product, - quantity: query.quantity - ? query.quantity - : getItemCart({ productId: product.id }).quantity - } - }) - setProducts(productsWithQuantity) - } - loadProducts() - }, [router]) + return countDiscount + } useEffect(() => { - if (products) { - let calculateTotalAmount = 0 - let calculateTotalDiscountAmount = 0 - let calcuateTotalWeight = 0 - products.forEach((product) => { - calculateTotalAmount += product.price.price * product.quantity - calculateTotalDiscountAmount += - (product.price.price - product.price.priceDiscount) * product.quantity - calcuateTotalWeight += product.weight * product.quantity - if (product.weight == 0) { - setCheckWeight(true) - } - }) - setTotalAmount(calculateTotalAmount) - setTotalDiscountAmount(calculateTotalDiscountAmount) - setTotalWeight(calcuateTotalWeight * 1000) - } - }, [products]) + if (!listVouchers) return + if (!activeVoucher) return + + const countDiscount = hitungDiscountVoucher(activeVoucher) + + SetDiscountVoucher(countDiscount) + }, [activeVoucher, listVouchers]) useEffect(() => { + setProducts(cartCheckout?.products) + setCheckWeight(cartCheckout?.hasProductWithoutWeight) + setTotalWeight(cartCheckout?.totalWeight.g) + }, [cartCheckout]) + + useEffect(() => { + setCheckoutValidation(false) const loadServiceRajaOngkir = async () => { const body = { origin: 2127, @@ -193,6 +247,9 @@ const Checkout = () => { useEffect(() => { if (selectedExpedisi) { let serviceType = selectedExpedisi.split(',') + if (serviceType[0] === 0) { + setSelectedExpedisi(0) + } setselectedCarrier(serviceType[0]) setselectedCarrierId(serviceType[1]) setListServiceExpedisi([]) @@ -210,6 +267,21 @@ const Checkout = () => { toast.error('Maksimal ukuran file adalah 5MB', { position: 'bottom-center' }) return } + if (selectedExpedisi === 0) { + setCheckoutValidation(true) + if (expedisiValidation.current) { + const position = expedisiValidation.current.getBoundingClientRect() + window.scrollTo({ + top: position.top - 300 + window.pageYOffset, + behavior: 'smooth' + }) + } + return + } + if (selectedCarrier != 1 && biayaKirim == 0) { + toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.') + return + } setIsLoading(true) const productOrder = products.map((product) => ({ product_id: product.id, @@ -218,10 +290,12 @@ const Checkout = () => { let data = { partner_shipping_id: auth.partnerId, partner_invoice_id: auth.partnerId, + user_id: auth.id, order_line: JSON.stringify(productOrder), delivery_amount: biayaKirim, carrier_id: selectedCarrierId, delivery_service_type: selectedExpedisiService, + voucher: activeVoucher, type: 'sale_order' } if (poNumber.current.value) data.po_number = poNumber.current.value @@ -246,20 +320,288 @@ const Checkout = () => { gtag('event', 'conversion', { send_to: 'AW-954540379/nDymCL3BhaQYENvClMcD', - value: - totalAmount - - totalDiscountAmount + - taxTotal + - Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000, + value: cartCheckout?.grandTotal + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000, currency: 'IDR', transaction_id: isCheckouted.id, event_callback: midtrans }) } - const taxTotal = (totalAmount - totalDiscountAmount) * 0.11 + + const handlingActivateCode = async () => { + VoucherCode(codeVoucher) + } + + const handleUseVoucher = async (code, isCheck) => { + if (isCheck) { + if (code === activeVoucher) { + SetActiveVoucher(null) + SetDiscountVoucher(0) + } else { + SetActiveVoucher(code) + SetFindVoucher(null) + document.getElementById('uniqCode').value = '' + SetButtonTerapkan(false) + } + } else { + SetActiveVoucher(code) + SetFindVoucher(null) + document.getElementById('uniqCode').value = '' + SetButtonTerapkan(false) + } + } + + const onChangeCodeVoucher = async (e) => { + SetCodeVoucher(e.target.value) + SetButtonTerapkan(false) + } + + const [isChecked, setIsChecked] = useState(false) + + const ToggleSwitch = (code) => { + setIsChecked(!isChecked) + handleUseVoucher(code, !isChecked) + } + + // const taxTotal = (totalAmount - totalDiscountAmount - discountVoucher) * 0.11 return ( <> + <BottomPopup + className='w-full md:!w-[40%] !min-h-[350px]' + active={bottomPopup} + close={() => SetBottomPopup(false)} + title='Gunakan Promo' + > + <div className='row'> + <div className='flex justify-between items-center'> + <div className='flex md:w-[70%]'> + <input + type='text' + id='uniqCode' + name='uniqCode' + className='form-input w-full rounded-md' + placeholder='Kode Voucher' + autoCapitalize='true' + onChange={(e) => onChangeCodeVoucher(e)} + /> + </div> + <div className='flex'> + <button + className='btn-solid-red flex-1 md:flex-none rounded-md' + type='button' + onClick={() => handlingActivateCode()} + disabled={buttonTerapkan} + > + Terapkan + </button> + </div> + </div> + {findCodeVoucher === 3 && activeVoucher === codeVoucher && ( + <div className='mt-2'> + <span className='text-caption-1 mt-2 text-green-600'> + Voucher berhasil ditambahkan{' '} + </span> + </div> + )} + {findCodeVoucher === 1 && ( + <div className='mt-2'> + <span className='text-caption-1 mt-2 text-red-600'> + Kode voucher salah / sudah tidak berlaku lagi. Coba voucher lainnya, ya. + </span> + </div> + )} + {findCodeVoucher === 2 && ( + <div className='mt-2'> + <span className='text-caption-1 mt-2 text-red-600'> + Tambah <span className='text-red-600'>{selisihHargaCode}</span> untuk pakai promo + ini + </span> + </div> + )} + + <hr className='mt-10 my-4 border-gray_r-10' /> + <div className=''> + {!loadingVoucher && !listVouchers ? ( + <div className='flex items-center justify-center mt-4 mb-4'> + <div className='text-center'> + <h1 className='font-bold mb-4'>Tidak ada voucher tersedia</h1> + <p className='text-gray-500'>Maaf, saat ini tidak ada voucher yang tersedia.</p> + </div> + </div> + ) : ( + <h3 className='font-semibold mb-4'>Promo Khusus Untuk {auth?.name}</h3> + )} + {loadingVoucher && ( + <> + <div className={`border border-solid w-full hover:cursor-pointer p-2`}> + <div class='flex items-center space-x-3'> + <div class='flex items-center justify-center h-28 w-48 mb-4 bg-gray-300 rounded dark:bg-gray-700'> + <svg + class='w-10 h-10 text-gray-200 dark:text-gray-600' + aria-hidden='true' + xmlns='http://www.w3.org/2000/svg' + fill='currentColor' + viewBox='0 0 16 20' + > + <path d='M14.066 0H7v5a2 2 0 0 1-2 2H0v11a1.97 1.97 0 0 0 1.934 2h12.132A1.97 1.97 0 0 0 16 18V2a1.97 1.97 0 0 0-1.934-2ZM10.5 6a1.5 1.5 0 1 1 0 2.999A1.5 1.5 0 0 1 10.5 6Zm2.221 10.515a1 1 0 0 1-.858.485h-8a1 1 0 0 1-.9-1.43L5.6 10.039a.978.978 0 0 1 .936-.57 1 1 0 0 1 .9.632l1.181 2.981.541-1a.945.945 0 0 1 .883-.522 1 1 0 0 1 .879.529l1.832 3.438a1 1 0 0 1-.031.988Z' /> + <path d='M5 5V.13a2.96 2.96 0 0 0-1.293.749L.879 3.707A2.98 2.98 0 0 0 .13 5H5Z' /> + </svg> + </div> + <div> + <div class='h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-72 mb-4'></div> + <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div> + <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div> + </div> + </div> + </div> + <div className={`border border-solid w-full hover:cursor-pointer p-2`}> + <div class='flex items-center space-x-3'> + <div class='flex items-center justify-center h-28 w-48 mb-4 bg-gray-300 rounded dark:bg-gray-700'> + <svg + class='w-10 h-10 text-gray-200 dark:text-gray-600' + aria-hidden='true' + xmlns='http://www.w3.org/2000/svg' + fill='currentColor' + viewBox='0 0 16 20' + > + <path d='M14.066 0H7v5a2 2 0 0 1-2 2H0v11a1.97 1.97 0 0 0 1.934 2h12.132A1.97 1.97 0 0 0 16 18V2a1.97 1.97 0 0 0-1.934-2ZM10.5 6a1.5 1.5 0 1 1 0 2.999A1.5 1.5 0 0 1 10.5 6Zm2.221 10.515a1 1 0 0 1-.858.485h-8a1 1 0 0 1-.9-1.43L5.6 10.039a.978.978 0 0 1 .936-.57 1 1 0 0 1 .9.632l1.181 2.981.541-1a.945.945 0 0 1 .883-.522 1 1 0 0 1 .879.529l1.832 3.438a1 1 0 0 1-.031.988Z' /> + <path d='M5 5V.13a2.96 2.96 0 0 0-1.293.749L.879 3.707A2.98 2.98 0 0 0 .13 5H5Z' /> + </svg> + </div> + <div> + <div class='h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-72 mb-4'></div> + <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div> + <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div> + </div> + </div> + </div> + </> + )} + {!loadingVoucher && + listVouchers?.map((item) => ( + <div key={item.id} className='relative'> + {item.canApply === false && ( + <div className='absolute w-full h-full bg-gray_r-3/40 top-0 left-0 z-50' /> + )} + + <div className={`border border-solid mb-5 w-full hover:cursor-pointer p-4 `}> + <div className={`flex gap-x-3`}> + <div className='hidden md:w-[250px] md:block'> + <Image src={item.image} alt={item.name} className={`object-cover`} /> + </div> + <div className='w-full'> + <div className='flex justify-between gap-x-2 mb-1 items-center'> + <div className=''> + <h3 className='font-semibold'>{item.name}</h3> + <div className='mt-1'> + <span className='text-sm line-clamp-3'>{item.description} </span> + </div> + </div> + <div className='flex justify-end'> + <label class='relative inline-flex items-center cursor-pointer'> + <input + type='checkbox' + value='' + class='sr-only peer' + checked={activeVoucher === item.code ? true : false} + onChange={() => ToggleSwitch(item.code)} + /> + <div class="w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-focus:ring-4 peer-focus:ring-green-300 dark:peer-focus:ring-green-800 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-green-600"></div> + </label> + </div> + </div> + <hr className='mt-3 my-4 border-gray_r-8' /> + <div className='flex justify-between items-center'> + <p className='text-justify text-sm md:text-xs'> + Kode Voucher :{' '} + <span className='text-red-500 font-semibold'>{item.code}</span> + </p> + <p className='text-sm md:text-xs'> + {activeVoucher === item.code && ( + <span className=' text-green-600'>Voucher digunakan </span> + )} + </p> + </div> + </div> + </div> + <div className='mt-3'> + <p className='text-justify text-sm '> + {!item.canApply && + item.applyStatus === 'MPA' && + item.manufactureNames != '' && ( + <p> + Tambah produk{' '} + <span className='text-red-500'>{item.manufactureNames}</span> senilai{' '} + <span className='text-red-500'> + {currencyFormat(item.differenceToApply)} + </span>{' '} + untuk pakai promo ini + </p> + )} + {!item.canApply && + item.applyStatus === 'MPA' && + item.manufactureNames === '' && ( + <p> + Tambah{' '} + <span className='text-red-500'> + {currencyFormat(item.differenceToApply)} + </span>{' '} + untuk pakai promo ini{' '} + </p> + )} + {!item.canApply && item.applyStatus === 'UM' && ( + <p> + Tambah produk{' '} + <span className='text-red-500'>{item.manufactureNames}</span> senilai{' '} + <span className='text-red-500'> + {currencyFormat(item.minPurchaseAmount)} + </span>{' '} + untuk pakai promo ini + </p> + )} + {item.canApply && ( + <p> + Potensi potongan sebesar{' '} + <span className='text-red-500'> + {currencyFormat(item.discountVoucher)} + </span> + </p> + )} + {/* {item.canApply === false + ? 'Tambah ' + + currencyFormat(item.differenceToApply) + + ' untuk pakai promo ini' + : 'Potensi potongan sebesar ' + + currencyFormat(hitungDiscountVoucher(item.code))} */} + </p> + <hr className='mt-2 my-4 border-gray_r-8' /> + <div className='flex items-center'> + <svg + aria-hidden='true' + fill='none' + stroke='currentColor' + stroke-width='1.5' + viewBox='0 0 24 24' + className='w-5 text-black' + > + <path + d='M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z' + stroke-linecap='round' + stroke-linejoin='round' + ></path> + </svg> + <span className='text-left ml-3 text-sm '> + Berakhir dalam <span className='text-red-600'>{item.remainingTime}</span>{' '} + lagi{' '} + </span> + </div> + </div> + </div> + </div> + ))} + </div> + </div> + </BottomPopup> <MobileView> <div className='p-4'> <Alert type='info' className='text-caption-2 flex gap-x-3'> @@ -298,6 +640,8 @@ const Checkout = () => { listExpedisi={listExpedisi} setSelectedExpedisi={setSelectedExpedisi} checkWeigth={checkWeigth} + checkoutValidation={checkoutValidation} + expedisiValidation={expedisiValidation} /> <Divider /> <SectionListService @@ -317,41 +661,136 @@ const Checkout = () => { <div className='text-gray_r-11 text-caption-1'>{products?.length} Barang</div> </div> <hr className='my-4 border-gray_r-6' /> - <div className='flex flex-col gap-y-4'> - <div className='flex gap-x-2 justify-between'> - <div className='text-gray_r-11'>Total Belanja</div> - <div>{currencyFormat(totalAmount)}</div> - </div> - <div className='flex gap-x-2 justify-between'> - <div className='text-gray_r-11'>Total Diskon</div> - <div className='text-danger-500'>- {currencyFormat(totalDiscountAmount)}</div> + {!cartCheckout ? ( + <div + role='status' + class='max-w-md space-y-3 divide-y divide-gray-200 animate-pulse dark:divide-gray-700 dark:border-gray-700' + > + <div class='flex items-center justify-between'> + <div> + <div class='w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700'></div> + </div> + <div class='h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12'></div> + </div> + <div class='flex items-center justify-between pt-4'> + <div> + <div class='w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700'></div> + </div> + <div class='h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12'></div> + </div> + <div class='flex items-center justify-between pt-4'> + <div> + <div class='w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700'></div> + </div> + <div class='h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12'></div> + </div> + <div class='flex items-center justify-between pt-4'> + <div> + <div class='w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700'></div> + </div> + <div class='h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12'></div> + </div> + <div class='flex items-center justify-between pt-4'> + <div> + <div class='w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700'></div> + </div> + <div class='h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12'></div> + </div> </div> - <div className='flex gap-x-2 justify-between'> - <div className='text-gray_r-11'>Subtotal</div> - <div>{currencyFormat(totalAmount - totalDiscountAmount)}</div> + ) : ( + <div className='flex flex-col gap-y-4'> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Total Belanja</div> + <div>{currencyFormat(cartCheckout?.totalPurchase)}</div> + </div> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Diskon Produk</div> + <div className='text-danger-500'> + - {currencyFormat(cartCheckout?.totalDiscount)} + </div> + </div> + {activeVoucher && ( + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Diskon Voucher</div> + <div className='text-danger-500'>- {currencyFormat(discountVoucher)}</div> + </div> + )} + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Subtotal</div> + <div>{currencyFormat(cartCheckout?.subtotal)}</div> + </div> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>PPN 11%</div> + <div>{currencyFormat(cartCheckout?.tax)}</div> + </div> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'> + Biaya Kirim <p className='text-xs mt-3'>{etdFix}</p> + </div> + <div>{currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000)}</div> + </div> </div> - <div className='flex gap-x-2 justify-between'> - <div className='text-gray_r-11'>PPN 11%</div> - <div>{currencyFormat(taxTotal)}</div> + )} + + <hr className='my-4 border-gray_r-6' /> + {!cartCheckout ? ( + <div className='flex gap-x-2 justify-between mb-4'> + <div> + {' '} + <div class='w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700'></div> + </div> + <div className='font-semibold text-gray_r-12'> + <div class='h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12'></div> + </div> </div> - <div className='flex gap-x-2 justify-between'> - <div className='text-gray_r-11'> - Biaya Kirim <p className='text-xs mt-3'>{etdFix}</p> + ) : ( + <div className='flex gap-x-2 justify-between mb-4'> + <div>Grand Total</div> + <div className='font-semibold text-gray_r-12'> + {currencyFormat( + cartCheckout?.grandTotal + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + )} </div> - <div>{currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000)}</div> </div> - </div> + )} + <hr className='my-4 border-gray_r-6' /> - <div className='flex gap-x-2 justify-between mb-4'> - <div>Grand Total</div> - <div className='font-semibold text-gray_r-12'> - {currencyFormat( - totalAmount - - totalDiscountAmount + - taxTotal + - Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 - )} - </div> + + <div className='mt-4 mb-4'> + <button + type='button' + onClick={() => { + SetBottomPopup(true) + voucher() + }} + className='text-gray-900 p-4 flex items-center justify-between rounded-lg bg-white border font-medium border-gray-300 hover:bg-gray-100 py-2.5 h-[50px] w-[100%]' + > + <div className='flex items-center justify-between gap-x-3'> + <span className='text-left text-gray_r-9'> + <Image + src={'/images/DISKONICON.svg'} + alt={''} + className='object-contain object-center h-6 rounded-md' + /> + </span> + {activeVoucher ? ( + <div className=''> + <div className='text-left text-sm text-black font-semibold'> + Potongan Senilai {currencyFormat(discountVoucher)} + </div> + <div className='text-left mt-1 text-green-600 text-xs'> + Voucher berhasil digunakan + </div> + </div> + ) : ( + <span className='text-left text-sm text-gray_r-9'> + Hemat belanja dengan promo + </span> + )} + </div> + + <span className='text-left ml-1 text-gray_r-9'>{'>'}</span> + </button> </div> {/* <p className='text-caption-2 text-gray_r-10 mb-2'>*) Belum termasuk biaya pengiriman</p> */} <p className='text-caption-2 text-gray_r-10 leading-5'> @@ -392,9 +831,7 @@ const Checkout = () => { <button className='flex-1 btn-yellow' onClick={checkout} - disabled={ - isLoading || !products || products?.length == 0 || priceCheck || selectedExpedisi == 0 - } + disabled={isLoading || !products || products?.length == 0 || priceCheck} > {isLoading ? 'Loading...' : 'Lanjut Pembayaran'} </button> @@ -437,6 +874,8 @@ const Checkout = () => { listExpedisi={listExpedisi} setSelectedExpedisi={setSelectedExpedisi} checkWeigth={checkWeigth} + checkoutValidation={checkoutValidation} + expedisiValidation={expedisiValidation} /> <Divider /> <SectionListService @@ -456,75 +895,149 @@ const Checkout = () => { </tr> </thead> <tbody> - {products?.map((product) => ( - <tr key={product.id}> - <td className='flex'> - <div className='w-[20%] flex-shrink-0'> - <Image - src={product?.parent?.image} - alt={product?.name} - className='object-contain object-center border border-gray_r-6 h-40 w-full rounded-md' - /> + {!products ? ( + <tr> + <td colSpan={4}> + <div className='container my-4'> + <div class='h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4'></div> + <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div> + <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div> + <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700'></div> </div> - <div className='px-2 text-left'> - <div className='line-clamp-2 leading-6 !text-gray_r-12 font-normal'> - {product?.parent?.name} - </div> - <div className='text-gray_r-11 mt-2'> - {product?.code}{' '} - {product?.attributes.length > 0 - ? `| ${product?.attributes.join(', ')}` - : ''} - </div> - <div className='text-gray_r-11 mt-2'> - Berat item : {product?.weight} Kg - </div> - </div> - </td> - <td> - <input - className='form-input w-16 py-2 text-center bg-gray_r-1' - type='number' - value={product?.quantity} - disabled - /> </td> - <td> - {product?.price?.discountPercentage > 0 && ( - <div className='flex gap-x-1 items-center justify-center mt-3'> - <div className='text-gray_r-11 line-through text-caption-1'> - {currencyFormat(product?.price?.price)} + </tr> + ) : ( + products?.map((product) => ( + <> + <tr + key={product.id} + className={`${product.program ? '!border-t-0 !border-b-0' : ''}`} + > + <td className='flex'> + <div className='w-[20%] flex-shrink-0'> + <Image + src={product?.parent?.image} + alt={product?.name} + className='object-contain object-center border border-gray_r-6 h-40 w-full rounded-md' + /> </div> - <div className='badge-solid-red'> - {product?.price?.discountPercentage}% + <div className='px-2 text-left'> + <div className='line-clamp-2 leading-6 !text-gray_r-12 font-normal'> + {product?.parent?.name} + </div> + <div className='text-gray_r-11 mt-2'> + {product?.code}{' '} + {product?.attributes.length > 0 + ? `| ${product?.attributes.join(', ')}` + : ''} + </div> + <div className='text-gray_r-11 mt-2'> + Berat item : {product?.weight} Kg + </div> </div> - </div> - )} - <div className='font-normal mt-1'> - {product.price.priceDiscount > 0 - ? currencyFormat(product?.price?.priceDiscount) - : 'Call For Price'} - </div> - </td> - <td> - <div className='text-danger-500 font-medium'> - {product.price.priceDiscount > 0 ? ( - currencyFormat(product?.price?.priceDiscount * product?.quantity) - ) : ( - <a - href={whatsappUrl('product', { - name: product.name, - url: createSlug('/shop/product/', product.name, product.id, true) - })} - className='underline' - > - Call For Price{' '} - </a> - )} - </div> - </td> - </tr> - ))} + </td> + <td> + <input + className='form-input w-16 py-2 text-center bg-gray_r-1' + type='number' + value={product?.quantity} + disabled + /> + </td> + <td> + {product?.price?.discountPercentage > 0 && ( + <div className='flex gap-x-1 items-center justify-center mt-3'> + <div className='text-gray_r-11 line-through text-caption-1'> + {currencyFormat(product?.price?.price)} + </div> + <div className='badge-solid-red'> + {product?.price?.discountPercentage}% + </div> + </div> + )} + <div className='font-normal mt-1'> + {product.price.priceDiscount > 0 + ? currencyFormat(product?.price?.priceDiscount) + : 'Call For Price'} + </div> + </td> + <td> + <div className='text-danger-500 font-medium'> + {product.price.priceDiscount > 0 ? ( + currencyFormat(product?.price?.priceDiscount * product?.quantity) + ) : ( + <a + href={whatsappUrl('product', { + name: product.name, + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ) + })} + className='underline' + > + Call For Price{' '} + </a> + )} + </div> + </td> + </tr> + {product.program && + product.program.items && + product.program.items.map((item) => ( + <> + <tr key={product?.program?.id} className='!border-t-0'> + <td className='flex'> + <div className='w-[20%] flex-shrink-0'> + <Image + src={item.parent.image} + alt={item.name} + className='object-contain object-center border border-gray_r-6 h-40 w-full rounded-md' + /> + </div> + <div className='px-2 text-left'> + <div className=''> + <span className='border border-solid border-red-600 rounded-md p-1 text-red-600'> + {product.program.type.label} + </span> + </div> + <div className='mt-2 line-clamp-2 leading-6'>{item.name}</div> + </div> + </td> + <td> + <input + className='form-input w-16 py-2 text-center bg-gray_r-1' + type='number' + value={1} + disabled + /> + </td> + <td> + {item?.price?.discountPercentage > 0 && ( + <div className='flex gap-x-1 items-center justify-center mt-3'> + <div className='text-gray_r-11 line-through text-caption-1'> + {currencyFormat(product?.price?.price)} + </div> + </div> + )} + <div className='font-normal mt-1'> + {item?.price.priceDiscount > 0 ? 'Gratis' : ''} + </div> + </td> + <td> + <div className='text-danger-500 font-medium'> + {item.price.priceDiscount > 0 ? 'Gratis' : ''} + </div> + </td> + <td></td> + </tr> + </> + ))} + </> + )) + )} </tbody> </table> </div> @@ -538,46 +1051,141 @@ const Checkout = () => { </div> <hr className='my-4 border-gray_r-6' /> - - <div className='flex flex-col gap-y-4'> - <div className='flex gap-x-2 justify-between'> - <div className='text-gray_r-11'>Total Belanja</div> - <div>{currencyFormat(totalAmount)}</div> - </div> - <div className='flex gap-x-2 justify-between'> - <div className='text-gray_r-11'>Total Diskon</div> - <div className='text-danger-500'>- {currencyFormat(totalDiscountAmount)}</div> + {!cartCheckout ? ( + <div + role='status' + class='max-w-md space-y-3 divide-y divide-gray-200 animate-pulse dark:divide-gray-700 dark:border-gray-700' + > + <div class='flex items-center justify-between'> + <div> + <div class='w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700'></div> + </div> + <div class='h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12'></div> + </div> + <div class='flex items-center justify-between pt-4'> + <div> + <div class='w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700'></div> + </div> + <div class='h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12'></div> + </div> + <div class='flex items-center justify-between pt-4'> + <div> + <div class='w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700'></div> + </div> + <div class='h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12'></div> + </div> + <div class='flex items-center justify-between pt-4'> + <div> + <div class='w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700'></div> + </div> + <div class='h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12'></div> + </div> + <div class='flex items-center justify-between pt-4'> + <div> + <div class='w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700'></div> + </div> + <div class='h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12'></div> + </div> </div> - <div className='flex gap-x-2 justify-between'> - <div className='text-gray_r-11'>Subtotal</div> - <div>{currencyFormat(totalAmount - totalDiscountAmount)}</div> + ) : ( + <div className='flex flex-col gap-y-4'> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Total Belanja</div> + <div>{currencyFormat(cartCheckout?.totalPurchase)}</div> + </div> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Diskon Produk</div> + <div className='text-danger-500'> + - {currencyFormat(cartCheckout?.totalDiscount)} + </div> + </div> + {activeVoucher && ( + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Diskon Voucher</div> + <div className='text-danger-500'>- {currencyFormat(discountVoucher)}</div> + </div> + )} + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Subtotal</div> + <div>{currencyFormat(cartCheckout?.subtotal)}</div> + </div> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>PPN 11%</div> + <div>{currencyFormat(cartCheckout?.tax)}</div> + </div> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'> + Biaya Kirim + <p className='text-xs mt-3'>{etdFix}</p> + </div> + <div> + {currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000)} + </div> + </div> </div> - <div className='flex gap-x-2 justify-between'> - <div className='text-gray_r-11'>PPN 11%</div> - <div>{currencyFormat(taxTotal)}</div> + )} + + <hr className='my-4 border-gray_r-6' /> + {!cartCheckout ? ( + <div className='flex gap-x-2 justify-between mb-4'> + <div> + {' '} + <div class='w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700'></div> + </div> + <div className='font-semibold text-gray_r-12'> + <div class='h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12'></div> + </div> </div> - <div className='flex gap-x-2 justify-between'> - <div className='text-gray_r-11'> - Biaya Kirim - <p className='text-xs mt-3'>{etdFix}</p> + ) : ( + <div className='flex gap-x-2 justify-between mb-4'> + <div>Grand Total</div> + <div className='font-semibold text-gray_r-12'> + {currencyFormat( + cartCheckout?.grandTotal + + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + )} </div> - <div>{currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000)}</div> </div> - </div> + )} <hr className='my-4 border-gray_r-6' /> - <div className='flex gap-x-2 justify-between mb-4'> - <div>Grand Total</div> - <div className='font-semibold text-gray_r-12'> - {currencyFormat( - totalAmount - - totalDiscountAmount + - taxTotal + - Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 - )} - </div> + <div className='mt-4 mb-4'> + <button + type='button' + onClick={() => { + SetBottomPopup(true) + voucher() + }} + className='text-gray-900 p-3 flex items-center justify-between rounded-lg bg-white border font-medium border-gray-300 hover:bg-gray-100 py-2.5 h-[50px] w-[100%]' + > + <div className='flex items-center justify-between gap-x-3'> + <span className='text-left text-gray_r-9'> + <Image + src={'/images/DISKONICON.svg'} + alt={''} + className='object-contain object-center h-6 w-full rounded-md' + /> + </span> + {activeVoucher ? ( + <div className=''> + <div className='text-left text-sm text-black font-semibold'> + Hemat {currencyFormat(discountVoucher)} + </div> + <div className='text-left mt-1 text-green-600 text-xs'> + Voucher berhasil digunakan + </div> + </div> + ) : ( + <span className='text-left text-sm text-gray_r-9'> + Hemat belanja dengan promo + </span> + )} + </div> + <span className='text-left ml-1 text-gray_r-9'>{'>'}</span> + </button> </div> + <p className='text-caption-2 text-gray_r-11 leading-5'> Dengan melakukan pembelian melalui website Indoteknik, saya menyetujui{' '} <Link href='/syarat-ketentuan' className='inline font-normal'> @@ -614,14 +1222,7 @@ const Checkout = () => { <button className='w-full btn-yellow mt-4' onClick={checkout} - disabled={ - isLoading || - !products || - products?.length == 0 || - priceCheck || - selectedCarrier == 0 || - (selectedCarrier != 1 && biayaKirim == 0) - } + disabled={isLoading || !products || products?.length == 0 || priceCheck} > {isLoading ? 'Loading...' : 'Lanjut Pembayaran'} </button> @@ -684,14 +1285,25 @@ const SectionValidation = ({ address }) => </BottomPopup> ) -const SectionExpedisi = ({ address, listExpedisi, setSelectedExpedisi, checkWeigth }) => +const SectionExpedisi = ({ + address, + listExpedisi, + setSelectedExpedisi, + checkWeigth, + checkoutValidation, + expedisiValidation +}) => address?.rajaongkirCityId > 0 && ( - <div className='p-4'> + <div className='p-4' ref={expedisiValidation}> <div className='flex justify-between items-center'> <div className='font-medium'>Pilih Expedisi : </div> - <div> - <select className='form-input' onChange={(e) => setSelectedExpedisi(e.target.value)}> - <option value='0,0'>Pengiriman</option> + <div className='w-[250px]'> + <select + className={`form-input ${checkoutValidation ? 'border-red-500 shake' : ''}`} + onChange={(e) => setSelectedExpedisi(e.target.value)} + required + > + <option value='0,0'>Pilih Pengiriman</option> <option value='1,32'>SELF PICKUP</option> {checkWeigth != true && listExpedisi.map((expedisi) => ( @@ -705,7 +1317,15 @@ const SectionExpedisi = ({ address, listExpedisi, setSelectedExpedisi, checkWeig </option> ))} </select> + {checkoutValidation && ( + <span className='text-sm text-red-500'>*silahkan pilih expedisi</span> + )} </div> + <style jsx>{` + .shake { + animation: shake 0.4s ease-in-out; + } + `}</style> </div> {checkWeigth == true && ( <p className='mt-4 text-gray_r-11 leading-6'> diff --git a/src/lib/checkout/components/CheckoutOld.jsx b/src/lib/checkout/components/CheckoutOld.jsx new file mode 100644 index 00000000..088b641b --- /dev/null +++ b/src/lib/checkout/components/CheckoutOld.jsx @@ -0,0 +1,811 @@ +import Alert from '@/core/components/elements/Alert/Alert' +import Divider from '@/core/components/elements/Divider/Divider' +import Link from '@/core/components/elements/Link/Link' +import useAuth from '@/core/hooks/useAuth' +import { getItemAddress } from '@/core/utils/address' +import addressesApi from '@/lib/address/api/addressesApi' +import CartApi from '@/lib/cart/api/CartApi' +import { ExclamationCircleIcon } from '@heroicons/react/24/outline' +import React, { useEffect, useRef, useState } from 'react' +import _ from 'lodash' +import { deleteItemCart, getCart, getItemCart } from '@/core/utils/cart' +import currencyFormat from '@/core/utils/currencyFormat' +import { toast } from 'react-hot-toast' +import getFileBase64 from '@/core/utils/getFileBase64' +import checkoutApi from '../api/checkoutApi' +import { useRouter } from 'next/router' +import VariantGroupCard from '@/lib/variant/components/VariantGroupCard' +import axios from 'axios' +import Image from '@/core/components/elements/Image/Image' +import MobileView from '@/core/components/views/MobileView' +import DesktopView from '@/core/components/views/DesktopView' +import ExpedisiList from '../api/ExpedisiList' +import whatsappUrl from '@/core/utils/whatsappUrl' +import { createSlug } from '@/core/utils/slug' +import { Button, Modal } from 'flowbite-react' +import BottomPopup from '@/core/components/elements/Popup/BottomPopup' + +const SELF_PICKUP_ID = 32 + +const Checkout = () => { + const router = useRouter() + const auth = useAuth() + const [selectedAddress, setSelectedAddress] = useState({ + shipping: null, + invoicing: null + }) + const [addresses, setAddresses] = useState(null) + + useEffect(() => { + if (!auth) return + + const getAddresses = async () => { + const dataAddresses = await addressesApi() + setAddresses(dataAddresses) + } + + getAddresses() + }, [auth]) + + useEffect(() => { + if (!addresses) return + + const matchAddress = (key) => { + const addressToMatch = getItemAddress(key) + const foundAddress = addresses.filter((address) => address.id == addressToMatch) + if (foundAddress.length > 0) { + return foundAddress[0] + } + return addresses[0] + } + + setSelectedAddress({ + shipping: matchAddress('shipping'), + invoicing: matchAddress('invoicing') + }) + }, [addresses]) + + const [products, setProducts] = useState(null) + const [totalAmount, setTotalAmount] = useState(0) + const [totalDiscountAmount, setTotalDiscountAmount] = useState(0) + const [totalWeight, setTotalWeight] = useState(0) + const [priceCheck, setPriceCheck] = useState(false) + const [listExpedisi, setExpedisi] = useState([]) + const [listserviceExpedisi, setListServiceExpedisi] = useState([]) + const [selectedExpedisi, setSelectedExpedisi] = useState(0) + const [selectedCarrierId, setselectedCarrierId] = useState(0) + const [selectedCarrier, setselectedCarrier] = useState(0) + const [biayaKirim, setBiayaKirim] = useState(0) + const [checkWeigth, setCheckWeight] = useState(false) + const [selectedServiceType, setSelectedServiceType] = useState(null) + const [selectedExpedisiService, setselectedExpedisiService] = useState(null) + const [etd, setEtd] = useState(null) + const [etdFix, setEtdFix] = useState(null) + + useEffect(() => { + const loadExpedisi = async () => { + let dataExpedisi = await ExpedisiList() + dataExpedisi = dataExpedisi.map((expedisi) => ({ + value: expedisi.id, + label: expedisi.name, + carrierId: expedisi.deliveryCarrierId + })) + setExpedisi(dataExpedisi) + } + loadExpedisi() + }, []) + + useEffect(() => { + const loadProducts = async () => { + let variantIds = '' + let { query } = router + if (query?.productId) { + variantIds = query.productId + } else { + const cart = getCart() + variantIds = _.filter(cart, (o) => o.selected == true) + .map((o) => o.productId) + .join(',') + } + + const dataProducts = await CartApi({ variantIds }) + const productsWithQuantity = dataProducts?.map((product) => { + if (product.price.priceDiscount == 0) setPriceCheck(true) + return { + ...product, + quantity: query.quantity + ? query.quantity + : getItemCart({ productId: product.id }).quantity + } + }) + setProducts(productsWithQuantity) + } + loadProducts() + }, [router]) + + useEffect(() => { + if (products) { + let calculateTotalAmount = 0 + let calculateTotalDiscountAmount = 0 + let calcuateTotalWeight = 0 + products.forEach((product) => { + calculateTotalAmount += product.price.price * product.quantity + calculateTotalDiscountAmount += + (product.price.price - product.price.priceDiscount) * product.quantity + calcuateTotalWeight += product.weight * product.quantity + if (product.weight == 0) { + setCheckWeight(true) + } + }) + setTotalAmount(calculateTotalAmount) + setTotalDiscountAmount(calculateTotalDiscountAmount) + setTotalWeight(calcuateTotalWeight * 1000) + } + }, [products]) + + useEffect(() => { + const loadServiceRajaOngkir = async () => { + const body = { + origin: 2127, + destination: selectedAddress.shipping.rajaongkirCityId, + weight: totalWeight, + courier: selectedCarrier, + originType: 'subdistrict', + destinationType: 'subdistrict' + } + setBiayaKirim(0) + const dataService = await axios('/api/rajaongkir-service?body=' + JSON.stringify(body)) + setListServiceExpedisi(dataService.data[0].costs) + if (dataService.data[0].costs[0]) { + setBiayaKirim(dataService.data[0].costs[0]?.cost[0].value) + setselectedExpedisiService( + dataService.data[0].costs[0]?.description + '-' + dataService.data[0].costs[0]?.service + ) + setEtd(dataService.data[0].costs[0]?.cost[0].etd) + toast.success('Harap pilih tipe layanan pengiriman') + } else { + toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.') + } + } + if (selectedCarrier != 0 && selectedCarrier != 1 && totalWeight > 0) { + loadServiceRajaOngkir() + } else { + setListServiceExpedisi() + setBiayaKirim(0) + setselectedExpedisiService() + setEtd() + } + }, [selectedCarrier, selectedAddress, totalWeight]) + + useEffect(() => { + if (selectedServiceType) { + let serviceType = selectedServiceType.split(',') + setBiayaKirim(serviceType[0]) + setselectedExpedisiService(serviceType[1]) + setEtd(serviceType[2]) + } + }, [selectedServiceType]) + + useEffect(() => { + if (etd) setEtdFix(calculateEstimatedArrival(etd)) + }, [etd]) + + useEffect(() => { + if (selectedExpedisi) { + let serviceType = selectedExpedisi.split(',') + setselectedCarrier(serviceType[0]) + setselectedCarrierId(serviceType[1]) + setListServiceExpedisi([]) + } + }, [selectedExpedisi]) + + const poNumber = useRef(null) + const poFile = useRef(null) + + const [isLoading, setIsLoading] = useState(false) + + const checkout = async () => { + const file = poFile.current.files[0] + if (typeof file !== 'undefined' && file.size > 5000000) { + toast.error('Maksimal ukuran file adalah 5MB', { position: 'bottom-center' }) + return + } + setIsLoading(true) + const productOrder = products.map((product) => ({ + product_id: product.id, + quantity: product.quantity + })) + let data = { + partner_shipping_id: auth.partnerId, + partner_invoice_id: auth.partnerId, + order_line: JSON.stringify(productOrder), + delivery_amount: biayaKirim, + carrier_id: selectedCarrierId, + delivery_service_type: selectedExpedisiService, + type: 'sale_order' + } + if (poNumber.current.value) data.po_number = poNumber.current.value + if (typeof file !== 'undefined') data.po_file = await getFileBase64(file) + + const isCheckouted = await checkoutApi({ data }) + if (!isCheckouted?.id) { + toast.error('Gagal melakukan transaksi, terjadi kesalahan internal') + return + } + + for (const product of products) deleteItemCart({ productId: product.id }) + const payment = await axios.post( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/midtrans-payment?transactionId=${isCheckouted.id}` + ) + setIsLoading(false) + window.location.href = payment.data.redirectUrl + } + const taxTotal = (totalAmount - totalDiscountAmount) * 0.11 + + return ( + <> + <MobileView> + <div className='p-4'> + <Alert type='info' className='text-caption-2 flex gap-x-3'> + <div> + <ExclamationCircleIcon className='w-7 text-blue-700' /> + </div> + <span className='leading-5'> + Jika mengalami kesulitan dalam melakukan pembelian di website Indoteknik. Hubungi kami + disini + </span> + </Alert> + </div> + + <Divider /> + + {selectedCarrierId == SELF_PICKUP_ID && <PickupAddress label='Alamat Pickup' />} + {selectedCarrierId != SELF_PICKUP_ID && ( + <> + <SectionAddress + address={selectedAddress.shipping} + label='Alamat Pengiriman' + url='/my/address?select=shipping' + /> + <Divider /> + <SectionAddress + address={selectedAddress.invoicing} + label='Alamat Penagihan' + url='/my/address?select=invoice' + /> + </> + )} + <Divider /> + <SectionValidation address={selectedAddress.invoicing} /> + <SectionExpedisi + address={selectedAddress.shipping} + listExpedisi={listExpedisi} + setSelectedExpedisi={setSelectedExpedisi} + checkWeigth={checkWeigth} + /> + <Divider /> + <SectionListService + listserviceExpedisi={listserviceExpedisi} + setSelectedServiceType={setSelectedServiceType} + /> + + <div className='p-4 flex flex-col gap-y-4'> + {products && <VariantGroupCard openOnClick={false} variants={products} />} + </div> + + <Divider /> + + <div className='p-4'> + <div className='flex justify-between items-center'> + <div className='font-medium'>Ringkasan Pesanan</div> + <div className='text-gray_r-11 text-caption-1'>{products?.length} Barang</div> + </div> + <hr className='my-4 border-gray_r-6' /> + <div className='flex flex-col gap-y-4'> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Total Belanja</div> + <div>{currencyFormat(totalAmount)}</div> + </div> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Total Diskon</div> + <div className='text-danger-500'>- {currencyFormat(totalDiscountAmount)}</div> + </div> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Subtotal</div> + <div>{currencyFormat(totalAmount - totalDiscountAmount)}</div> + </div> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>PPN 11%</div> + <div>{currencyFormat(taxTotal)}</div> + </div> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'> + Biaya Kirim <p className='text-xs mt-3'>{etdFix}</p> + </div> + <div>{currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000)}</div> + </div> + </div> + <hr className='my-4 border-gray_r-6' /> + <div className='flex gap-x-2 justify-between mb-4'> + <div>Grand Total</div> + <div className='font-semibold text-gray_r-12'> + {currencyFormat( + totalAmount - + totalDiscountAmount + + taxTotal + + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + )} + </div> + </div> + {/* <p className='text-caption-2 text-gray_r-10 mb-2'>*) Belum termasuk biaya pengiriman</p> */} + <p className='text-caption-2 text-gray_r-10 leading-5'> + Dengan melakukan pembelian melalui website Indoteknik, saya menyetujui{' '} + <Link href='/syarat-ketentuan' className='inline font-normal'> + Syarat & Ketentuan + </Link>{' '} + yang berlaku + </p> + </div> + + <Divider /> + + <div className='p-4'> + <div className='font-medium'>Purchase Order</div> + + <div className='mt-4 flex gap-x-3'> + <div className='w-6/12'> + <label className='form-label font-normal'>Dokumen PO</label> + <input + type='file' + className='form-input mt-2 h-12' + accept='image/*,application/pdf' + ref={poFile} + /> + </div> + <div className='w-6/12'> + <label className='form-label font-normal'>Nomor PO</label> + <input type='text' className='form-input mt-2 h-12' ref={poNumber} /> + </div> + </div> + <p className='text-caption-2 text-gray_r-11 mt-2'>Ukuran dokumen PO Maksimal 5MB</p> + </div> + + <Divider /> + + <div className='flex gap-x-3 p-4'> + <button + className='flex-1 btn-yellow' + onClick={checkout} + disabled={ + isLoading || !products || products?.length == 0 || priceCheck || selectedExpedisi == 0 + } + > + {isLoading ? 'Loading...' : 'Lanjut Pembayaran'} + </button> + </div> + {priceCheck && ( + <div className='px-4 mb-4'> + <span className='text-danger-500'> + *) Terdapat produk yang belum memiliki harga,{' '} + <a href={whatsappUrl()} className='underline'> + Hubungi Kami untuk meminta harga. + </a> + </span> + </div> + )} + </MobileView> + + <DesktopView> + <div className='container mx-auto py-10 flex'> + <div className='w-3/4 border border-gray_r-6 rounded bg-white'> + {selectedCarrierId == SELF_PICKUP_ID && <PickupAddress label='Alamat Pickup' />} + {selectedCarrierId != SELF_PICKUP_ID && ( + <> + <SectionAddress + address={selectedAddress.shipping} + label='Alamat Pengiriman' + url='/my/address?select=shipping' + /> + <Divider /> + <SectionAddress + address={selectedAddress.invoicing} + label='Alamat Penagihan' + url='/my/address?select=invoice' + /> + </> + )} + <Divider /> + <SectionValidation address={selectedAddress.invoicing} /> + <SectionExpedisi + address={selectedAddress.shipping} + listExpedisi={listExpedisi} + setSelectedExpedisi={setSelectedExpedisi} + checkWeigth={checkWeigth} + /> + <Divider /> + <SectionListService + listserviceExpedisi={listserviceExpedisi} + setSelectedServiceType={setSelectedServiceType} + /> + + <div className='p-4'> + <div className='font-medium'>Detail Pesanan</div> + <table className='table-checkout'> + <thead> + <tr> + <th>Nama Produk</th> + <th>Jumlah</th> + <th>Harga</th> + <th>Subtotal</th> + </tr> + </thead> + <tbody> + {products?.map((product) => ( + <tr key={product.id}> + <td className='flex'> + <div className='w-[20%] flex-shrink-0'> + <Image + src={product?.parent?.image} + alt={product?.name} + className='object-contain object-center border border-gray_r-6 h-40 w-full rounded-md' + /> + </div> + <div className='px-2 text-left'> + <div className='line-clamp-2 leading-6 !text-gray_r-12 font-normal'> + {product?.parent?.name} + </div> + <div className='text-gray_r-11 mt-2'> + {product?.code}{' '} + {product?.attributes.length > 0 + ? `| ${product?.attributes.join(', ')}` + : ''} + </div> + <div className='text-gray_r-11 mt-2'> + Berat item : {product?.weight} Kg + </div> + </div> + </td> + <td> + <input + className='form-input w-16 py-2 text-center bg-gray_r-1' + type='number' + value={product?.quantity} + disabled + /> + </td> + <td> + {product?.price?.discountPercentage > 0 && ( + <div className='flex gap-x-1 items-center justify-center mt-3'> + <div className='text-gray_r-11 line-through text-caption-1'> + {currencyFormat(product?.price?.price)} + </div> + <div className='badge-solid-red'> + {product?.price?.discountPercentage}% + </div> + </div> + )} + <div className='font-normal mt-1'> + {product.price.priceDiscount > 0 + ? currencyFormat(product?.price?.priceDiscount) + : 'Call For Price'} + </div> + </td> + <td> + <div className='text-danger-500 font-medium'> + {product.price.priceDiscount > 0 ? ( + currencyFormat(product?.price?.priceDiscount * product?.quantity) + ) : ( + <a + href={whatsappUrl('product', { + name: product.name, + url: createSlug('/shop/product/', product.name, product.id, true) + })} + className='underline' + > + Call For Price{' '} + </a> + )} + </div> + </td> + </tr> + ))} + </tbody> + </table> + </div> + </div> + + <div className='w-1/4 pl-4'> + <div className='sticky top-48 border border-gray_r-6 bg-white rounded p-4'> + <div className='flex justify-between items-center'> + <div className='font-medium'>Ringkasan Pesanan</div> + <div className='text-gray_r-11 text-caption-1'>{products?.length} Barang</div> + </div> + + <hr className='my-4 border-gray_r-6' /> + + <div className='flex flex-col gap-y-4'> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Total Belanja</div> + <div>{currencyFormat(totalAmount)}</div> + </div> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Total Diskon</div> + <div className='text-danger-500'>- {currencyFormat(totalDiscountAmount)}</div> + </div> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Subtotal</div> + <div>{currencyFormat(totalAmount - totalDiscountAmount)}</div> + </div> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>PPN 11%</div> + <div>{currencyFormat(taxTotal)}</div> + </div> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'> + Biaya Kirim + <p className='text-xs mt-3'>{etdFix}</p> + </div> + <div>{currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000)}</div> + </div> + </div> + + <hr className='my-4 border-gray_r-6' /> + + <div className='flex gap-x-2 justify-between mb-4'> + <div>Grand Total</div> + <div className='font-semibold text-gray_r-12'> + {currencyFormat( + totalAmount - + totalDiscountAmount + + taxTotal + + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + )} + </div> + </div> + <p className='text-caption-2 text-gray_r-11 leading-5'> + Dengan melakukan pembelian melalui website Indoteknik, saya menyetujui{' '} + <Link href='/syarat-ketentuan' className='inline font-normal'> + Syarat & Ketentuan + </Link>{' '} + yang berlaku + </p> + + <hr className='my-4 border-gray_r-6' /> + + <div className='font-medium mt-4'> + Purchase Order <span className='font-normal text-gray_r-11'>(Opsional)</span> + </div> + + <div className='mt-4 flex gap-x-3'> + <div className='w-6/12'> + <label className='form-label font-normal'>Dokumen PO</label> + <input + type='file' + className='form-input mt-2 h-12' + accept='image/*,application/pdf' + ref={poFile} + /> + </div> + <div className='w-6/12'> + <label className='form-label font-normal'>Nomor PO</label> + <input type='text' className='form-input mt-2 h-12' ref={poNumber} /> + </div> + </div> + <p className='text-caption-2 text-gray_r-11 mt-2'>Ukuran dokumen PO Maksimal 5MB</p> + + <hr className='my-4 border-gray_r-6' /> + + <button + className='w-full btn-yellow mt-4' + onClick={checkout} + disabled={ + isLoading || + !products || + products?.length == 0 || + priceCheck || + selectedCarrier == 0 || + (selectedCarrier != 1 && biayaKirim == 0) + } + > + {isLoading ? 'Loading...' : 'Lanjut Pembayaran'} + </button> + {priceCheck && ( + <div className='mt-4'> + <span className='text-danger-500'> + *) Terdapat produk yang belum memiliki harga,{' '} + <a href={whatsappUrl()} className='underline'> + Hubungi Kami untuk meminta harga. + </a> + </span> + </div> + )} + </div> + </div> + </div> + </DesktopView> + </> + ) +} + +const SectionAddress = ({ address, label, url }) => ( + <div className='p-4'> + <div className='flex justify-between items-center'> + <div className='font-medium'>{label}</div> + <Link className='text-caption-1' href={url}> + Pilih Alamat Lain + </Link> + </div> + + {address && ( + <div className='mt-4 text-caption-1'> + <div className='badge-red mb-2'> + {address.type.charAt(0).toUpperCase() + address.type.slice(1) + ' Address'} + </div> + <p className='font-medium'>{address.name}</p> + <p className='mt-2 text-gray_r-11'>{address.mobile}</p> + <p className='mt-1 text-gray_r-11'> + {address.street}, {address?.city?.name} + </p> + </div> + )} + </div> +) + +const SectionValidation = ({ address }) => + address?.rajaongkirCityId == 0 && ( + <BottomPopup active={true} title='Update Alamat'> + <div className='leading-7 text-gray_r-12/80'> + Mohon untuk memperbarui alamat Anda dengan mengklik tombol di bawah ini.{' '} + </div> + <div className='flex justify-center mt-6 gap-x-4'> + <Link + className='btn-solid-red w-full md:w-fit text-white' + href={`/my/address/${address?.id}/edit`} + > + Update Alamat + </Link> + </div> + </BottomPopup> + ) + +const SectionExpedisi = ({ address, listExpedisi, setSelectedExpedisi, checkWeigth }) => + address?.rajaongkirCityId > 0 && ( + <div className='p-4'> + <div className='flex justify-between items-center'> + <div className='font-medium'>Pilih Expedisi : </div> + <div> + <select className='form-input' onChange={(e) => setSelectedExpedisi(e.target.value)}> + <option value='0,0'>Pengiriman</option> + <option value='1,32'>SELF PICKUP</option> + {checkWeigth != true && + listExpedisi.map((expedisi) => ( + <option + disabled={checkWeigth} + value={expedisi.label + ',' + expedisi.carrierId} + key={expedisi.value} + > + {' '} + {expedisi.label.toUpperCase()}{' '} + </option> + ))} + </select> + </div> + </div> + {checkWeigth == true && ( + <p className='mt-4 text-gray_r-11 leading-6'> + Mohon maaf, pengiriman hanya tersedia untuk self pickup karena terdapat barang yang belum + diatur beratnya. Mohon atur berat barang dengan menghubungi admin melalui{' '} + <a + className='text-danger-500 inline' + href='https://api.whatsapp.com/send?phone=628128080622' + > + tautan ini + </a> + </p> + )} + </div> + ) + +const SectionListService = ({ listserviceExpedisi, setSelectedServiceType }) => + listserviceExpedisi?.length > 0 && ( + <> + <div className='p-4'> + <div className='flex justify-between items-center'> + <div className='font-medium'>Service Type Expedisi : </div> + <div> + <select className='form-input' onChange={(e) => setSelectedServiceType(e.target.value)}> + {listserviceExpedisi.map((service) => ( + <option + value={ + service.cost[0].value + + ',' + + service.description + + '-' + + service.service + + ',' + + extractDuration(service.cost[0].etd) + } + key={service.service} + > + {' '} + {service.description} - {service.service.toUpperCase()} + {extractDuration(service.cost[0].etd) && + ` (Estimasi Tiba ${extractDuration(service.cost[0].etd)} Hari)`} + </option> + ))} + </select> + </div> + </div> + </div> + <Divider /> + </> + ) + +function addDays(date, days) { + const result = new Date(date) + result.setDate(result.getDate() + days) + return result +} + +function formatDate(date) { + const day = date.getDate() + const month = date.toLocaleString('default', { month: 'short' }) + return `${day} ${month}` +} + +function calculateEstimatedArrival(duration) { + if (duration) { + let estimationDate = duration.split('-') + estimationDate[0] = parseInt(estimationDate[0]) + estimationDate[1] = parseInt(estimationDate[1]) + const from = addDays(new Date(), estimationDate[0] + 3) + const to = addDays(new Date(), estimationDate[1] + 3) + + let etdText = `*Estimasi tiba ${formatDate(from)}` + + if (estimationDate[1] > estimationDate[0]) { + etdText += ` - ${formatDate(to)}` + } + + return etdText + } + + return '' +} + +const extractDuration = (text) => { + const matches = text.match(/\d+(?:-\d+)?/g) + + if (matches && matches.length === 1) { + const parts = matches[0].split('-') + const min = parseInt(parts[0]) + const max = parseInt(parts[1]) + + if (min === max) { + return min.toString() + } + + return matches[0] + } + + return '' +} + +const PickupAddress = ({ label }) => ( + <div className='p-4'> + <div className='flex justify-between items-center'> + <div className='font-medium'>{label}</div> + </div> + <div className='mt-4 text-caption-1'> + <p className='font-medium'>Indoteknik</p> + <p className='mt-2 mb-2 text-gray_r-11 leading-6'> + Jl. Bandengan Utara Raya No.85, RT.3/RW.16, Penjaringan, Kec. Penjaringan, Kota Jkt Utara, + Daerah Khusus Ibukota Jakarta, Indonesia Kodepos : 14440 + </p> + <p className='mt-1 text-gray_r-11'>Telp : 021-2933 8828/29</p> + <p className='mt-1 text-gray_r-11'>Mobile : 0813 9000 7430</p> + </div> + </div> +) + +export default Checkout diff --git a/src/lib/product/components/Product/Product.jsx b/src/lib/product/components/Product/Product.jsx index 0547c36e..54490c26 100644 --- a/src/lib/product/components/Product/Product.jsx +++ b/src/lib/product/components/Product/Product.jsx @@ -55,7 +55,7 @@ const Product = ({ product, isVariant = false }) => { return ( <> <ProductMobile product={product} wishlist={wishlist} toggleWishlist={toggleWishlist} /> - <ProductDesktop product={product} wishlist={wishlist} toggleWishlist={toggleWishlist} /> + <ProductDesktop products={product} wishlist={wishlist} toggleWishlist={toggleWishlist} /> </> ) } diff --git a/src/lib/product/components/Product/ProductDesktop.jsx b/src/lib/product/components/Product/ProductDesktop.jsx index 6a87d022..4a1590ba 100644 --- a/src/lib/product/components/Product/ProductDesktop.jsx +++ b/src/lib/product/components/Product/ProductDesktop.jsx @@ -1,4 +1,5 @@ import Image from '@/core/components/elements/Image/Image' +import ImageNext from 'next/image' import Link from '@/core/components/elements/Link/Link' import DesktopView from '@/core/components/views/DesktopView' import currencyFormat from '@/core/utils/currencyFormat' @@ -14,14 +15,22 @@ import BottomPopup from '@/core/components/elements/Popup/BottomPopup' import ProductCard from '../ProductCard' import productSimilarApi from '../../api/productSimilarApi' import whatsappUrl from '@/core/utils/whatsappUrl' -import { gtagAddToCart } from '@/core/utils/googleTag' +import PromotionType from '@/lib/promotinProgram/components/PromotionType' +import useAuth from '@/core/hooks/useAuth' -const ProductDesktop = ({ product, wishlist, toggleWishlist }) => { +const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { const router = useRouter() + const auth = useAuth() + const { slug } = router.query + const [quantityActive, setQuantity] = useState(null) const [lowestPrice, setLowestPrice] = useState(null) + const [product, setProducts] = useState(products) const [addCartAlert, setAddCartAlert] = useState(false) + const [promotionType, setPromotionType] = useState(false) + const [promotionActiveId, setPromotionActiveId] = useState(null) + const [selectVariantPromoActive, setSelectVariantPromoActive] = useState(null) const getLowestPrice = useCallback(() => { const prices = product.variants.map((variant) => variant.price) @@ -41,9 +50,14 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => { const variantQuantityRefs = useRef([]) const setVariantQuantityRef = (variantId) => (element) => { + if (element) { + let variantIndex = product.variants.findIndex((varian) => varian.id == variantId) + product.variants[variantIndex].quantity = element.value + } variantQuantityRefs.current[variantId] = element } + const validQuantity = (quantity) => { let isValid = true if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) { @@ -53,22 +67,38 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => { return isValid } - const handleAddToCart = (variant) => { - const quantity = variantQuantityRefs.current[variant.id].value + const handleAddToCart = (variantId) => { + if (!auth) { + router.push(`/login?next=/shop/product/${slug}`) + return + } + const quantity = variantQuantityRefs.current[variantId].value + if (!validQuantity(quantity)) return - gtagAddToCart(variant, quantity) - updateItemCart({ - productId: variant.id, - quantity, - selected: true - }) + if(product.variants.length > 1){ + let variantIndex = product.variants.findIndex((varian) => varian.id == variantId) + updateItemCart({ + productId: variantId, + quantity, + programLineId: product.variants[variantIndex].programActive, + selected: true + }) + }else{ + updateItemCart({ + productId: variantId, + quantity, + programLineId: promotionActiveId, + selected: true + }) + } + setAddCartAlert(true) } const handleBuy = (variant) => { - const quantity = variantQuantityRefs.current[variant.id].value + const quantity = variantQuantityRefs.current[variant].value if (!validQuantity(quantity)) return - router.push(`/shop/checkout?productId=${variant.id}&quantity=${quantity}`) + router.push(`/shop/checkout?productId=${variant}&quantity=${quantity}`) } const variantSectionRef = useRef(null) @@ -82,6 +112,11 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => { } } + const handlePromoClick = (variantId) => { + setSelectVariantPromoActive(variantId) + setPromotionType(true) + } + const productSimilarQuery = [ product?.name, `fq=-product_id_i:${product.id}`, @@ -158,6 +193,19 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => { )} </div> </div> + {product.variants.length <= 1 && ( + <div className='pt-3'> + <div className='flex mt-1'> + <PromotionType + variantId={product.variants[0].id} + setPromotionActiveId={setPromotionActiveId} + promotionActiveId={promotionActiveId} + quantity={quantityActive} + product={product} + ></PromotionType> + </div> + </div> + )} </div> </div> @@ -245,18 +293,19 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => { type='number' className='form-input w-16 py-2 text-center bg-gray_r-1' ref={setVariantQuantityRef(product.variants[0].id)} + onChange={setVariantQuantityRef(product.variants[0].id)} defaultValue={1} /> <button type='button' - onClick={() => handleAddToCart(product.variants[0])} + onClick={() => handleAddToCart(product.variants[0].id)} className='flex-1 py-2 btn-yellow' > Keranjang </button> <button type='button' - onClick={() => handleBuy(product.variants[0])} + onClick={() => handleBuy(product.variants[0].id)} className='flex-1 py-2 btn-solid-red' > Beli @@ -306,7 +355,7 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => { </tr> </thead> <tbody> - {product.variants.map((variant) => ( + {product.variants.map((variant, index) => ( <tr key={variant.id}> <td>{variant.code}</td> <td>{variant.attributes.join(', ') || '-'}</td> @@ -341,10 +390,41 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => { type='number' className='form-input w-16 py-2 text-center bg-gray_r-1' ref={setVariantQuantityRef(variant.id)} + onChange={setVariantQuantityRef(variant.id)} defaultValue={1} /> </td> <td className='flex gap-x-3'> + {/* {variant.programActive ? ( + <ImageNext + src='/images/noun-applied-check2.svg' + alt='' + height={60} + width={60} + onClick={() => handlePromoClick(variant.id)} + className='cursor-pointer' + ></ImageNext> + ) : ( + variant.hasProgram ? ( <div className='w-[60px] flex justify-center'> + <ImageNext + src='/images/noun-discount-5796402.svg' + alt='' + height={30} + width={30} + onClick={() => handlePromoClick(variant.id)} + className='cursor-pointer' + ></ImageNext> + </div>):( <div className='w-[60px] flex justify-center'> + <ImageNext + src='/images/noun-discount-57964023.svg' + alt='' + height={30} + width={30} + className='cursor-pointer' + ></ImageNext> + </div>) + + )} */} <button type='button' onClick={() => handleAddToCart(variant)} @@ -374,7 +454,24 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => { <ProductSimilar query={productSimilarQuery} /> </LazyLoad> </div> - + <BottomPopup + className=' !h-[75%]' + title='Pakai Promo' + active={promotionType} + close={() => setPromotionType(false)} + > + <div className='flex mt-4'> + <PromotionType + isModal={true} + variantId={selectVariantPromoActive} + setPromotionActiveId={setPromotionActiveId} + promotionActiveId={promotionActiveId} + quantity={quantityActive} + product={product} + setProducts={setProducts} + ></PromotionType> + </div> + </BottomPopup> <BottomPopup className='!container' title='Berhasil Ditambahkan' diff --git a/src/lib/product/components/Product/ProductMobile.jsx b/src/lib/product/components/Product/ProductMobile.jsx index 667a4ba1..fa1e2521 100644 --- a/src/lib/product/components/Product/ProductMobile.jsx +++ b/src/lib/product/components/Product/ProductMobile.jsx @@ -14,6 +14,7 @@ import { toast } from 'react-hot-toast' import { createSlug } from '@/core/utils/slug' import BottomPopup from '@/core/components/elements/Popup/BottomPopup' import whatsappUrl from '@/core/utils/whatsappUrl' +import PromotionType from '@/lib/promotinProgram/components/PromotionType' import { gtagAddToCart } from '@/core/utils/googleTag' const ProductMobile = ({ product, wishlist, toggleWishlist }) => { @@ -24,6 +25,9 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { const [informationTab, setInformationTab] = useState(informationTabOptions[0].value) const [addCartAlert, setAddCartAlert] = useState(false) + const [promotionType, setPromotionType] = useState(false) + const [promotionActiveId, setPromotionActiveId] = useState(null) + const getLowestPrice = () => { const prices = product.variants.map((variant) => variant.price) const lowest = prices.reduce((lowest, price) => { @@ -38,7 +42,8 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { name: product.name, price: getLowestPrice(), stock: product.stockTotal, - weight: product.weight + weight: product.weight, + hasProgram: false }) const variantOptions = product.variants?.map((variant) => ({ @@ -66,7 +71,8 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { name: variant.parent.name + variantAttributes, price: variant.price, stock: variant.stock, - weight: variant.weight + weight: variant.weight, + hasProgram: variant.hasProgram }) } }, [selectedVariant, product]) @@ -90,6 +96,7 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { updateItemCart({ productId: activeVariant.id, quantity, + programLineId: promotionActiveId, selected: true }) setAddCartAlert(true) @@ -187,6 +194,17 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { isSearchable={product.variantTotal > 10} /> </div> + {activeVariant.hasProgram && ( + <div className='mt-5'> + <PromotionType + variantId={product.variants[0].id} + setPromotionActiveId={setPromotionActiveId} + promotionActiveId={promotionActiveId} + quantity={quantity} + ></PromotionType> + </div> + )} + <div className='mt-4 mb-2'>Jumlah</div> <div className='flex gap-x-3'> <div className='w-2/12'> diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx index 8b48cf06..a8964310 100644 --- a/src/lib/product/components/ProductCard.jsx +++ b/src/lib/product/components/ProductCard.jsx @@ -12,7 +12,7 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { if (variant == 'vertical') { return ( - <div className='rounded shadow-sm border border-gray_r-4 h-full bg-white'> + <div className='rounded shadow-sm border border-gray_r-4 bg-white h-[350px]'> <Link href={createSlug('/shop/product/', product?.name, product?.id)} className='border-b border-gray_r-4 relative' diff --git a/src/lib/promotinProgram/api/homepageApi.js b/src/lib/promotinProgram/api/homepageApi.js new file mode 100644 index 00000000..496af9d6 --- /dev/null +++ b/src/lib/promotinProgram/api/homepageApi.js @@ -0,0 +1,17 @@ +import odooApi from "@/core/api/odooApi" + +export const getPromotionHome = async () => { + const response = await odooApi('GET', '/api/v1/promotion/home') + return response +} + +export const getProductPromotionHome = async ({id}) => { + const response = await odooApi('GET', `/api/v1/promotion/home/${id}`) + return response +} + +export const getPromotionProgram = async ({ id }) => { + const listProgram = await odooApi('GET', `/api/v1/product_variant/${id}/promotions`) + + return listProgram +}
\ No newline at end of file diff --git a/src/lib/promotinProgram/components/HomePage.jsx b/src/lib/promotinProgram/components/HomePage.jsx new file mode 100644 index 00000000..c0968161 --- /dev/null +++ b/src/lib/promotinProgram/components/HomePage.jsx @@ -0,0 +1,116 @@ +import React, { use, useEffect, useState } from 'react' +import { Swiper, SwiperSlide } from 'swiper/react' +import 'swiper/swiper.min.css' +import Image from '@/core/components/elements/Image/Image' +import useDevice from '@/core/hooks/useDevice' +import odooApi from '@/core/api/odooApi' +import { LazyLoadComponent } from 'react-lazy-load-image-component' +import ProductSlider from '@/lib/product/components/ProductSlider' +import { PopularProductSkeleton } from '@/components/skeleton/PopularProductSkeleton' + +const { useQuery } = require('react-query') +const { getPromotionHome } = require('../api/homepageApi') +const { getProductPromotionHome } = require('../api/homepageApi') + +const HomePage = () => { + const [activeTab, setActiveTab] = useState(null) + const [activeId, setActiveId] = useState(null) + const [activeBanner, setActiveBanner] = useState(null) + const [parentPromotions, setparentPromotions] = useState(null) + + const { data: titlePromotion } = useQuery('titlePromotion', getPromotionHome) + const { data: productPromotion, refetch: productPromotionRefetch } = useQuery( + ['productPromotion', activeId], + async () => { + if (!activeId) return null + return await getProductPromotionHome({ id: activeId }) + } + ) + + useEffect(() => { + if (titlePromotion && titlePromotion.length > 0) { + setActiveTab(titlePromotion[0].name) + setActiveId(titlePromotion[0].id) + setparentPromotions(titlePromotion) + setActiveBanner(titlePromotion[0].banner) + productPromotionRefetch() + } + }, [titlePromotion, productPromotionRefetch]) + + useEffect(() => { + if (productPromotion) { + setparentPromotions((parentPromotions) => { + return parentPromotions.map((title) => { + if (title.id === activeId && title.products === undefined) { + return { + ...title, + products: productPromotion + } + } + return title + }) + }) + } + }, [productPromotion, activeId]) + + const { isMobile, isDesktop } = useDevice() + const handleTabClick = (id, label, banner) => { + setActiveTab(label) + setActiveId(id) + setActiveBanner(banner) + } + + return ( + activeBanner && ( + <div className='px-4 sm:px-0'> + <div className='flex justify-between items-center mb-4'> + <div className='font-medium sm:text-h-lg'>{activeTab}</div> + </div> + <div className='mb-4'> + <Image src={activeBanner} alt='' className='h-full w-full object-contain object-center' /> + </div> + <Swiper slidesPerView={isMobile ? 3.5 : 7.5} spaceBetween={isMobile ? 12 : 20}> + {titlePromotion?.map((item, index) => ( + <SwiperSlide key={index}> + <button + className={`py-1 px-2 rounded border flex justify-center items-center h-30 ${ + activeTab === item.name ? 'border-red-500' : 'border-gray_r-6' + }`} + onClick={() => handleTabClick(item.id, item.name, item.banner)} + > + {' '} + <Image + src={item.icon} + alt='' + className='h-full w-full object-contain object-center' + /> + </button> + </SwiperSlide> + ))} + </Swiper> + <div className='mt-4 relative min-h-[150px]'> + {parentPromotions && + parentPromotions?.map((item, index) => ( + <div + key={index} + className={`${activeId === item.id ? 'block' : 'hidden'} rounded-md`} + > + {item.products ? ( + <ProductSlider + key={index} + products={{ + products: item.products + }} + /> + ) : ( + <PopularProductSkeleton /> + )} + </div> + ))} + </div> + </div> + ) + ) +} + +export default HomePage diff --git a/src/lib/promotinProgram/components/PromotionType.jsx b/src/lib/promotinProgram/components/PromotionType.jsx new file mode 100644 index 00000000..ad7185e3 --- /dev/null +++ b/src/lib/promotinProgram/components/PromotionType.jsx @@ -0,0 +1,304 @@ +import React, { useEffect, useState } from 'react' +import Image from '@/core/components/elements/Image/Image' +import BottomPopup from '@/core/components/elements/Popup/BottomPopup' +import CountDown2 from '@/core/components/elements/CountDown/CountDown2' +import currencyFormat from '@/core/utils/currencyFormat' +import { getPromotionProgram } from '../api/homepageApi' + +const PromotionType = ({ + isModal = false, + variantId, + setPromotionActiveId, + promotionActiveId, + quantity, + product = null, + setProducts = null +}) => { + const [selectedPromo, setSelectedPromo] = useState(null) + const [promotionType, setPromotionType] = useState(false) + const [promos, setPromotionList] = useState(null) + const [activeTitle, setActiveTitle] = useState(null) + const [quantitySet, setQuantity] = useState(null) + + useEffect(() => { + const id = variantId + const listProgram = async () => { + const programs = await getPromotionProgram({ id }) + if (programs.length > 0) { + setPromotionList(programs) + setActiveTitle(programs?.[0].type.value) + } + } + listProgram() + setSelectedPromo(promotionActiveId) + if (product) { + const variant = product.variants.find((variant) => variant.id === variantId) + setQuantity(variant.quantity) + }else{ + setQuantity(quantity) + } + }, []) + + const groupingData = promos?.reduce((groups, item) => { + const promoType = item.type.value + if (!groups[promoType]) { + groups[promoType] = [] + } + groups[promoType].push(item) + + return groups + }, {}) + + const handlePromoClick = (promoId, minQty) => { + if (quantitySet >= minQty) { + if (promoId == selectedPromo) { + setSelectedPromo(null) + setPromotionActiveId(null) + if (product) { + const updateProdcuts = () => { + let variantIndex = product.variants.findIndex((varian) => varian.id == variantId) + product.variants[variantIndex].programActive = null + + setProducts(product) + } + updateProdcuts() + } + } else { + setSelectedPromo(promoId) + setPromotionActiveId(promoId) + if (product) { + const updateProdcuts = () => { + let variantIndex = product.variants.findIndex((varian) => varian.id == variantId) + product.variants[variantIndex].programActive = promoId + + setProducts(product) + } + updateProdcuts() + } + } + } + } + + const handlePopUp = () => { + if (isModal == false) { + setPromotionType(true) + } + } + + return ( + promos && ( + <> + <div className='h-[50%] relative'> + <div className='relative rounded-lg border border-solid border-gray-300 mb-2 w-full'> + <Image src='https://placehold.co/537x50.png' alt='' layout='fill' objectFit='cover' /> + <div className='h-full absolute top-0 left-0 w-full flex items-center justify-between p-2'> + <span className='font-semibold text-lg text-white'>Promo Tersedia</span> + <button type='button' onClick={() => handlePopUp()} className='py-2 btn-yellow'> + Lihat Semua + </button> + </div> + </div> + <div + className={`w-full ${ + isModal == true ? '' : 'grid grid-cols-3 gap-1 bg-gray-200 ' + } p-2 rounded-lg`} + > + {isModal === true ? ( + <div> + <div className='flex gap-2 mb-3'> + {Object.keys(groupingData).map((index) => { + return ( + <> + <button + onClick={() => setActiveTitle(index)} + className={`py-1 px-2 rounded-lg flex justify-center items-center text-sm ${ + activeTitle === index ? 'badge-yellow text-black' : '' + } `} + > + {groupingData[index][0].type.label} + </button> + </> + ) + })} + </div> + {activeTitle && + groupingData[activeTitle].map((item, i) => ( + <div + key={i} + onClick={() => handlePromoClick(item.id, item.minimumPurchaseQty)} + className={`border border-solid mb-5 w-full hover:cursor-pointer ${ + selectedPromo + ? selectedPromo === item.id + ? 'opacity-100 border-red-500 bg-red-100' + : 'opacity-50 pointer-events-none' + : 'opacity-100' + } ${ + quantitySet >= item.minimumPurchaseQty + ? '' + : 'opacity-50 pointer-events-none' + } `} + > + <div className={`flex`}> + <div className=''> + <Image + src={item.Image} + alt={item.name} + className={`flex-1 w-[170px] object-cover`} + /> + </div> + <div className='p-2 w-full'> + <div className='flex justify-between mb-1'> + <div className='text-danger-500 font-semibold mb-1 mt-1'> + Waktu Tersisa + </div> + <div> + <CountDown2 initialTime={item.remainingTime}></CountDown2> + </div> + </div> + <p className='text-justify text-gray-500 line-clamp-3'>{item.name}</p> + <div className='mt-4'> + {/* {item.type.value === 'bundling' && ( + <> */} + <div className='flex gap-x-2 mt-3 justify-between items-center'> + <div className='flex gap-x-2 items-center '> + <div className='text-gray_r-11 line-through text-caption-1 mt-1'> + {currencyFormat(item.totalSavings)} + </div> + <div className='text-danger-500 font-semibold '>Gratis</div> + </div> + <div className='text-danger-500 font-semibold '> + {quantitySet < item.minimumPurchaseQty + ? 'Tambah ' + + (parseInt(item.minimumPurchaseQty) - + parseInt(quantitySet)) + + ' lagi' + : ''} + </div> + </div> + {/* </> + )} */} + {/* {item.type.value === 'special_price' && ( + <> + <div className='flex gap-x-2 mt-3 items-center'> + <div className='text-danger-500 font-semibold '> {currencyFormat(item.totalSavings)}</div> + </div> + </> + )} + {item.type.value === 'discount_loading' && ( + <> + <div className='flex justify-between'> + <div className='text-danger-500 font-semibold '> + {currencyFormat(item.totalSavings)} + </div> + <div className='text-danger-500 font-semibold '> + {quantitySet < item.minimumPurchaseQty + ? 'Tambah ' + + (parseInt(item.minimumPurchaseQty) - + parseInt(quantitySet)) + + ' lagi' + : ''} + </div> + </div> + </> + )} */} + </div> + </div> + </div> + </div> + ))} + </div> + ) : ( + promos.map((promo, index) => { + if (index > 2) { + return null + } else { + if (index === 2 && promos.length > 2) { + return ( + <> + <div + onClick={() => setPromotionType(true)} + className={` border border-solid bg-white mb-5 w-full hover:cursor-pointer opacity-100 flex flex-col justify-center items-center`} + > + <div className='flex justify-center items-center'> + <div className='rounded-full shadow-lg w-10 h-10 flex justify-center items-center'> + <svg + aria-hidden='true' + fill='none' + stroke='currentColor' + stroke-width='1.5' + viewBox='0 0 24 24' + className='text-red-500 w-20 h-20' + > + <path + d='M12 6v12m6-6H6' + stroke-linecap='round' + stroke-linejoin='round' + ></path> + </svg> + </div> + </div> + + <span className='mt-2 text-sm'>Lihat Promo Lainya</span> + </div> + </> + ) + } + return ( + <> + <div + key={promo.id} + onClick={() => setPromotionType(true)} + className={`border border-solid bg-white mb-5 w-full hover:cursor-pointer`} + > + <div className={`items-center`}> + <div className=''> + <Image + src={promo.Image} + alt={promo.name} + className={`flex-1 w-full object-cover`} + /> + </div> + <div className='p-2'> + <div className='badge-yellow text-black mb-1'>{promo.type.label}</div> + <p className='text-justify line-clamp-2'>{promo.name}</p> + <div className='text-danger-500 font-semibold mb-1 mt-1'> + {/* {currencyFormat(promo.totalSavings)} */} + </div> + {/* <div className='w-full bg-yellow-200 rounded-full h-1.5 mb-2'> + <div className='bg-yellow-500 h-1.5 rounded-full w-[45%]'></div> + </div> */} + <div> + <CountDown2 initialTime={promo.remainingTime}></CountDown2> + </div> + </div> + </div> + </div> + </> + ) + } + }) + )} + <BottomPopup + className=' !h-[75%]' + title='Pakai Promo' + active={promotionType} + close={() => setPromotionType(false)} + > + <div className='flex mt-4'> + <PromotionType + isModal={true} + variantId={variantId} + setPromotionActiveId={setPromotionActiveId} + promotionActiveId={promotionActiveId} + quantity={quantitySet} + ></PromotionType> + </div> + </BottomPopup> + </div> + </div> + </> + ) + ) +} + +export default PromotionType diff --git a/src/lib/variant/components/VariantGroupCard.jsx b/src/lib/variant/components/VariantGroupCard.jsx index 8cb1eec4..dd5983a9 100644 --- a/src/lib/variant/components/VariantGroupCard.jsx +++ b/src/lib/variant/components/VariantGroupCard.jsx @@ -1,14 +1,55 @@ import { useState } from 'react' import VariantCard from './VariantCard' +import Image from '@/core/components/elements/Image/Image' +import currencyFormat from '@/core/utils/currencyFormat' const VariantGroupCard = ({ variants, ...props }) => { + console.log('variant', variants) const [showAll, setShowAll] = useState(false) const variantsToShow = showAll ? variants : variants.slice(0, 2) return ( <> {variantsToShow?.map((variant, index) => ( - <VariantCard key={index} product={variant} {...props} /> + <> + <VariantCard key={index} product={variant} {...props} /> + {variant.program && + variant.program.items && + variant.program.items.map((item) => ( + <div key={item.id}> + <div className='flex gap-x-3'> + <div className='w-4/12 flex items-center gap-x-2'> + <Image + src={item.parent.image} + alt={item.name} + className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md' + /> + </div> + <div className='w-8/12 flex flex-col'> + <div className='mb-2'> + <span className='border border-solid border-red-600 rounded-md p-1 text-red-600'> + {variant.program.type.label} + </span> + </div> + <p className='product-card__title wrap-line-ellipsis-2'>{item.name}</p> + <p className='text-caption-2 text-gray_r-11 mt-1'> + Berat Item : {item.weight} Kg + </p> + <div className='flex flex-wrap gap-x-1 items-center mt-1'> + {item.price.discountPercentage > 0 && ( + <> + <p className='text-caption-2 text-gray_r-11 line-through'> + {currencyFormat(item.price.price)} + </p> + </> + )} + </div> + <p className='text-caption-2 text-gray_r-12 font-bold mt-2'>Gratis</p> + </div> + </div> + </div> + ))} + </> ))} {variants.length > 2 && ( <button |
