diff options
| author | IT Fixcomart <it@fixcomart.co.id> | 2024-08-08 04:44:00 +0000 |
|---|---|---|
| committer | IT Fixcomart <it@fixcomart.co.id> | 2024-08-08 04:44:00 +0000 |
| commit | 3a63f2e3958738747d0be07cd5d37620aed83b37 (patch) | |
| tree | 9448518b2eebeec2c859028dcfe104a044e3f314 | |
| parent | cf3bcefe77132c2ac40612a502eefaa88b56e7a8 (diff) | |
| parent | bad8abd263bd43c59dad8e61e2f5f35bfe5b7746 (diff) | |
Merged in Feature/new-cart-popup (pull request #220)
Feature/new cart popup
| -rw-r--r-- | src-migrate/modules/product-detail/components/AddToCart.tsx | 70 | ||||
| -rw-r--r-- | src-migrate/modules/product-promo/components/AddToCart.tsx | 56 | ||||
| -rw-r--r-- | src/contexts/ProductCartContext.js | 4 | ||||
| -rw-r--r-- | src/core/components/elements/Navbar/NavbarDesktop.jsx | 63 | ||||
| -rw-r--r-- | src/lib/quotation/components/Quotation.jsx | 7 | ||||
| -rw-r--r-- | src/lib/quotation/components/Quotationheader.jsx | 266 |
6 files changed, 436 insertions, 30 deletions
diff --git a/src-migrate/modules/product-detail/components/AddToCart.tsx b/src-migrate/modules/product-detail/components/AddToCart.tsx index 6c9aedf8..2f87311f 100644 --- a/src-migrate/modules/product-detail/components/AddToCart.tsx +++ b/src-migrate/modules/product-detail/components/AddToCart.tsx @@ -1,4 +1,5 @@ import BottomPopup from '@/core/components/elements/Popup/BottomPopup' +import style from '../styles/price-action.module.css'; import { Button, Link, useToast } from '@chakra-ui/react' import product from 'next-seo/lib/jsonld/product' import { useRouter } from 'next/router' @@ -11,6 +12,9 @@ import ProductSimilar from '../../../../src/lib/product/components/ProductSimila import { IProductDetail } from '~/types/product'; import ImageNext from 'next/image'; import { useProductCartContext } from '@/contexts/ProductCartContext' +import { createSlug } from '~/libs/slug' +import formatCurrency from '~/libs/formatCurrency' +import { useProductDetail } from '../stores/useProductDetail'; type Props = { variantId: number | null, @@ -32,10 +36,13 @@ const AddToCart = ({ isClosable: true }) + const { + askAdminUrl, + } = useProductDetail(); + const [product, setProducts] = useState(products); - const { productCart, setRefreshCart, setProductCart, refreshCart, isLoading, setIsloading } = - useProductCartContext() + const {setRefreshCart} = useProductCartContext() const productSimilarQuery = [ product?.name, @@ -50,16 +57,16 @@ const AddToCart = ({ router.push(`/login?next=${currentUrl}`) return; } - + if ( !variantId || isNaN(quantity) || typeof auth !== 'object' ) return; - + setRefreshCart(true); setAddCartAlert(true); - + toast.promise( upsertUserCart({ userId: auth.id, @@ -78,7 +85,7 @@ const AddToCart = ({ ) - + if (source === 'buy') { router.push('/shop/checkout?source=buy') } @@ -118,8 +125,55 @@ const AddToCart = ({ height={80} /> </div> - <div className='ml-3 flex flex-1 items-center font-normal'> - {product.name} + <div className='ml-3 flex flex-1 items-start font-medium justify-center flex-col gap-y-1'> + {!!product.manufacture.name ? ( + <Link + href={createSlug('/shop/brands/', product.manufacture.name, product.manufacture.id.toString())} + className=' hover:underline' + color={"red"} + > + {product.manufacture.name} + </Link> + ) : '-'} + <p className='text-ellipsis overflow-hidden'> + {product.name} + </p> + <p> + {product.code} + </p> + {!!product.lowest_price && product.lowest_price.price > 0 && ( + <> + <div className='flex items-end gap-x-2'> + {product.lowest_price.discount_percentage > 0 && ( + <> + <div className='badge-solid-red'> + {Math.floor(product.lowest_price.discount_percentage)}% + </div> + <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'> + Rp {formatCurrency(product.lowest_price.price || 0)} + </div> + </> + )} + <div className='text-danger-500 font-semibold'> + Rp {formatCurrency(product.lowest_price.price_discount || 0)} + </div> + </div> + </> + )} + + {!!product.lowest_price && product.lowest_price.price === 0 && ( + <span> + Hubungi kami untuk dapatkan harga terbaik,{' '} + <Link + href={askAdminUrl} + target='_blank' + className='font-medium underline' + color={'red'} + > + klik disini + </Link> + </span> + )} </div> <div className='ml-3 flex items-center font-normal'> <Link diff --git a/src-migrate/modules/product-promo/components/AddToCart.tsx b/src-migrate/modules/product-promo/components/AddToCart.tsx index 10904f90..8567dad8 100644 --- a/src-migrate/modules/product-promo/components/AddToCart.tsx +++ b/src-migrate/modules/product-promo/components/AddToCart.tsx @@ -16,6 +16,9 @@ import LazyLoad from 'react-lazy-load' import ProductSimilar from '../../../../src/lib/product/components/ProductSimilar'; import { IProductDetail } from '~/types/product'; import { useProductCartContext } from '@/contexts/ProductCartContext' +import { createSlug } from '~/libs/slug' +import formatCurrency from '~/libs/formatCurrency' +import { useProductDetail } from '../../product-detail/stores/useProductDetail'; type Props = { promotion: IPromotion product: IProductDetail @@ -24,10 +27,12 @@ type Props = { type Status = 'idle' | 'loading' | 'success' const ProductPromoAddToCart = ({product, promotion }: Props) => { + const auth = getAuth() const toast = useToast() const router = useRouter() + const {askAdminUrl} = useProductDetail(); const [status, setStatus] = useState<Status>('idle') const { productCart, setRefreshCart, setProductCart, refreshCart, isLoading, setIsloading } = useProductCartContext() @@ -126,8 +131,55 @@ const ProductPromoAddToCart = ({product, promotion }: Props) => { height={80} /> </div> - <div className='ml-3 flex flex-1 items-center font-normal'> - {product.name} + <div className='ml-3 flex flex-1 items-start font-medium justify-center flex-col gap-y-1'> + {!!product.manufacture.name ? ( + <Link + href={createSlug('/shop/brands/', product.manufacture.name, product.manufacture.id.toString())} + className=' hover:underline' + color={"red"} + > + {product.manufacture.name} + </Link> + ) : '-'} + <p className='text-ellipsis overflow-hidden'> + {product.name} + </p> + <p> + {product.code} + </p> + {!!product.lowest_price && product.lowest_price.price > 0 && ( + <> + <div className='flex items-end gap-x-2'> + {product.lowest_price.discount_percentage > 0 && ( + <> + <div className='badge-solid-red'> + {Math.floor(product.lowest_price.discount_percentage)}% + </div> + <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'> + Rp {formatCurrency(product.lowest_price.price || 0)} + </div> + </> + )} + <div className='text-danger-500 font-semibold'> + Rp {formatCurrency(product.lowest_price.price_discount || 0)} + </div> + </div> + </> + )} + + {!!product.lowest_price && product.lowest_price.price === 0 && ( + <span> + Hubungi kami untuk dapatkan harga terbaik,{' '} + <Link + href={askAdminUrl} + target='_blank' + className='font-medium underline' + color={'red'} + > + klik disini + </Link> + </span> + )} </div> <div className='ml-3 flex items-center font-normal'> <Link diff --git a/src/contexts/ProductCartContext.js b/src/contexts/ProductCartContext.js index 06e97563..19889e07 100644 --- a/src/contexts/ProductCartContext.js +++ b/src/contexts/ProductCartContext.js @@ -4,12 +4,14 @@ const ProductCartContext = createContext() export const ProductCartProvider = ({ children }) => { const [productCart, setProductCart] = useState(null) + const [productQuotation, setProductQuotation] = useState(null) + const [refreshQuotation, setRefreshQuotation] = useState(false) const [refreshCart, setRefreshCart] = useState(false) const [isLoading, setIsloading] = useState(false) return ( <ProductCartContext.Provider - value={{ productCart, setProductCart, refreshCart, setRefreshCart, isLoading, setIsloading }} + value={{ productCart, setProductCart, refreshCart, setRefreshCart, isLoading, setIsloading, productQuotation, setProductQuotation, refreshQuotation, setRefreshQuotation }} > {children} </ProductCartContext.Provider> diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx index 2ddf5efe..b6cdb174 100644 --- a/src/core/components/elements/Navbar/NavbarDesktop.jsx +++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx @@ -5,7 +5,9 @@ import { createSlug } from '@/core/utils/slug'; import whatsappUrl from '@/core/utils/whatsappUrl'; import IndoteknikLogo from '@/images/logo.png'; import Cardheader from '@/lib/cart/components/Cartheader'; +import Quotationheader from '@/lib/quotation/components/QuotationHeader' import Category from '@/lib/category/components/Category'; +import { useProductCartContext } from '@/contexts/ProductCartContext'; import { ChevronDownIcon, DocumentCheckIcon, @@ -29,6 +31,7 @@ import { useDisclosure, } from '@chakra-ui/react'; import style from "./style/NavbarDesktop.module.css"; +import useTransactions from '@/lib/transaction/hooks/useTransactions'; const Search = dynamic(() => import('./Search'), { ssr: false }); const TopBanner = dynamic(() => import('./TopBanner'), { ssr: false }); @@ -38,7 +41,10 @@ const NavbarDesktop = () => { const auth = useAuth(); const [cartCount, setCartCount] = useState(0); - + const [quotationCount, setQuotationCount] = useState(0); + const { productCart, setProductCart, refreshCart, setRefreshCart, isLoading, setIsloading, productQuotation, setProductQuotation, refreshQuotation, setRefreshQuotation } = + useProductCartContext(); + const [pendingTransactions, setPendingTransactions] = useState([]) const [templateWA, setTemplateWA] = useState(null); const [payloadWA, setPayloadWa] = useState(null); const [urlPath, setUrlPath] = useState(null); @@ -47,6 +53,17 @@ const NavbarDesktop = () => { const { product } = useProductContext(); const { isOpen, onOpen, onClose } = useDisclosure(); + + const query = { + context: 'quotation', + site: + (auth?.webRole === null && auth?.site ? auth.site : null), + }; + + const { transactions } = useTransactions({ query }); + const data = transactions?.data?.saleOrders.filter( + (transaction) => transaction.status === 'draft' + ); const [showPopup, setShowPopup] = useState(false); const [isTop, setIsTop] = useState(true); @@ -89,6 +106,12 @@ const NavbarDesktop = () => { }, []); useEffect(() => { + setProductQuotation(data); + setPendingTransactions(data); + }, [transactions.data]); + + + useEffect(() => { if (router.pathname === '/shop/product/[slug]') { setPayloadWa({ name: product?.name, @@ -96,11 +119,11 @@ const NavbarDesktop = () => { url: createSlug('/shop/product/', product?.name, product?.id, true), }); setTemplateWA('product'); - + setUrlPath(router.asPath); } }, [product, router]); - + useEffect(() => { const handleCartChange = () => { const cart = async () => { @@ -109,15 +132,31 @@ const NavbarDesktop = () => { }; cart(); }; - handleCartChange(); - + handleCartChange(); + window.addEventListener('localStorageChange', handleCartChange); - + return () => { window.removeEventListener('localStorageChange', handleCartChange); }; }, []); + useEffect(() => { + const handleQuotationChange = () => { + const quotation = async () => { + setQuotationCount(pendingTransactions?.length); + }; + quotation(); + }; + handleQuotationChange(); + + window.addEventListener('localStorageChange', handleQuotationChange); + + return () => { + window.removeEventListener('localStorageChange', handleQuotationChange); + }; + }, [pendingTransactions]); + return ( <DesktopView> <TopBanner onLoad={handleTopBannerLoad} /> @@ -180,17 +219,7 @@ const NavbarDesktop = () => { <Search /> </div> <div className='flex gap-x-4 items-center'> - <Link - href='/my/transactions' - target='_blank' - rel='noreferrer' - className='flex items-center gap-x-2 !text-gray_r-12/80' - > - <DocumentCheckIcon className='w-7' /> - Daftar - <br /> - Quotation - </Link> + <Quotationheader quotationCount={quotationCount} /> <Cardheader cartCount={cartCount} /> diff --git a/src/lib/quotation/components/Quotation.jsx b/src/lib/quotation/components/Quotation.jsx index df234dc2..3fac6f29 100644 --- a/src/lib/quotation/components/Quotation.jsx +++ b/src/lib/quotation/components/Quotation.jsx @@ -9,6 +9,7 @@ import _ from 'lodash'; import { deleteItemCart, getCart, getItemCart } from '@/core/utils/cart'; import currencyFormat from '@/core/utils/currencyFormat'; import { toast } from 'react-hot-toast'; +import { useProductCartContext } from '@/contexts/ProductCartContext'; // import checkoutApi from '@/lib/checkout/api/checkoutApi' import { useRouter } from 'next/router'; import VariantGroupCard from '@/lib/variant/components/VariantGroupCard'; @@ -38,11 +39,12 @@ const { getProductsCheckout } = require('@/lib/checkout/api/checkoutApi'); const Quotation = () => { const router = useRouter(); const auth = useAuth(); - + const { data: cartCheckout } = useQuery('cartCheckout', () => getProductsCheckout() - ); +); +const { setRefreshQuotation } = useProductCartContext(); const SELF_PICKUP_ID = 32; const [products, setProducts] = useState(null); @@ -293,6 +295,7 @@ const Quotation = () => { if (isSuccess?.id) { for (const product of products) deleteItemCart({ productId: product.id }); router.push(`/shop/quotation/finish?id=${isSuccess.id}`); + setRefreshQuotation(true); return; } diff --git a/src/lib/quotation/components/Quotationheader.jsx b/src/lib/quotation/components/Quotationheader.jsx new file mode 100644 index 00000000..14743fd6 --- /dev/null +++ b/src/lib/quotation/components/Quotationheader.jsx @@ -0,0 +1,266 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { createSlug } from '@/core/utils/slug'; +import useAuth from '@/core/hooks/useAuth'; +import { useRouter } from 'next/router'; +import odooApi from '@/core/api/odooApi'; +import { useProductCartContext } from '@/contexts/ProductCartContext'; +import Image from '@/core/components/elements/Image/Image'; +import whatsappUrl from '@/core/utils/whatsappUrl'; +import { AnimatePresence, motion } from 'framer-motion'; +import style from '../../../../src-migrate/modules/cart/styles/item-promo.module.css'; +import useTransactions from '../../transaction/hooks/useTransactions'; +import currencyFormat from '@/core/utils/currencyFormat'; +const { DocumentCheckIcon, PhotoIcon } = require('@heroicons/react/24/outline'); +const { default: Link } = require('next/link'); + +const Quotationheader = (quotationCount) => { + const auth = useAuth(); + const query = { + context: 'quotation', + site: auth?.webRole === null && auth?.site ? auth.site : null, + }; + + const router = useRouter(); + const [subTotal, setSubTotal] = useState(null); + const [buttonLoading, SetButtonTerapkan] = useState(false); + const itemLoading = [1, 2, 3]; + const [countQuotation, setCountQuotation] = useState(null); + const { productCart, setProductCart, refreshCart, setRefreshCart, isLoading, setIsloading, productQuotation, setProductQuotation, refreshQuotation, setRefreshQuotation } = + useProductCartContext(); + + const [isHovered, setIsHovered] = useState(false); + const [isTop, setIsTop] = useState(true); + + const qotation = useMemo(() => { + return productQuotation || []; + }, [productQuotation]); + + const handleMouseEnter = () => { + setIsHovered(true); + getCart(); + }; + + const handleMouseLeave = () => { + setIsHovered(false); + }; + + const getCart = () => { + if (!productQuotation && auth) { + refreshCartf(); + } + }; + let { transactions } = useTransactions({ query }); + + const refreshCartf = useCallback(async () => { + setIsloading(true); + let pendingTransactions = transactions?.data?.saleOrders.filter(transaction => transaction.status === 'draft'); + setProductQuotation(pendingTransactions); + setCountQuotation(pendingTransactions?.length ? pendingTransactions?.length : pendingTransactions?.length); + setIsloading(false); + }, [setProductQuotation, setIsloading]); + + useEffect(() => { + if (!qotation) return + + let calculateTotalDiscountAmount = 0 + for (const product of qotation) { + // if (qotation.quantity == '') continue + calculateTotalDiscountAmount += product.amountUntaxed + } + let subTotal = calculateTotalDiscountAmount + setSubTotal(subTotal) + }, [qotation]) + + useEffect(() => { + if (refreshQuotation) { + refreshCartf(); + } + setRefreshQuotation(false); + }, [refreshQuotation, refreshCartf, setRefreshQuotation]); + + useEffect(() => { + setCountQuotation(quotationCount.quotationCount); + }, [quotationCount]); + + useEffect(() => { + const handleScroll = () => { + setIsTop(window.scrollY === 0); + }; + window.addEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + + const handleCheckout = async () => { + SetButtonTerapkan(true); + let checkoutAll = await odooApi('POST', `/api/v1/user/${auth.id}/cart/select-all`); + router.push('/shop/quotation'); + }; + + return ( + <div className='relative group'> + <div> + <Link + href='/my/quotations' + target='_blank' + rel='noreferrer' + className='flex items-center gap-x-2 !text-gray_r-12/80' + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + > + <div className={`relative ${countQuotation > 0 && 'mr-2'}`}> + <DocumentCheckIcon className='w-7' /> + {countQuotation > 0 && ( + <span className='absolute -top-2 -right-2 badge-solid-red rounded-full w-5 h-5 flex items-center justify-center'> + {countQuotation} + </span> + )} + </div> + <span> + List + <br /> + Quotation + </span> + </Link> + </div> + <AnimatePresence> + {isHovered && ( + <> + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1, top: isTop ? 230 : 155 }} + exit={{ opacity: 0 }} + transition={{ duration: 0.15, top: { duration: 0.3 } }} + className={`fixed left-0 w-full h-full bg-black/50 z-10`} + /> + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1, transition: { duration: 0.2 } }} + exit={{ opacity: 0, transition: { duration: 0.3 } }} + className='absolute z-10 left-0 w-96' + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + > + <motion.div + initial={{ height: 0 }} + animate={{ height: 'auto' }} + exit={{ height: 0 }} + className='w-full max-w-md p-2 bg-white border border-gray-200 rounded-lg shadow overflow-hidden' + > + <div className='p-2 flex justify-between items-center'> + <h5 className='text-base font-semibold leading-none'>Daftar Quotation</h5> + <Link href='/my/quotations' class='text-sm font-medium text-red-600 underline'> + Lihat Semua + </Link> + </div> + <hr className='mt-3 mb-3 border border-gray-100' /> + <div className='flow-root max-h-[250px] overflow-y-auto'> + {!auth && ( + <div className='justify-center p-4'> + <p className='text-gray-500 text-center '> + Silahkan{' '} + <Link href='/login' className='text-red-600 underline leading-6'> + Login + </Link>{' '} + Untuk Melihat Daftar Quotation Anda + </p> + </div> + )} + {isLoading && + itemLoading.map((item) => ( + <div key={item} role='status' className='max-w-sm animate-pulse'> + <div className='flex items-center space-x-4 mb- 2'> + <div className='flex-shrink-0'> + <PhotoIcon className='h-16 w-16 text-gray-500' /> + </div> + <div className='flex-1 min-w-0'> + <div className='h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4'></div> + <div className='h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px] mb-2.5'></div> + <div className='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div> + </div> + </div> + </div> + ))} + {auth && qotation.length === 0 && !isLoading && ( + <div className='justify-center p-4'> + <p className='text-gray-500 text-center '> + Tidak Ada Quotation + </p> + </div> + )} + {auth && qotation.length > 0 && !isLoading && ( + <> + <ul role='list' className='divide-y divide-gray-200 dark:divide-gray-700'> + {qotation && + qotation?.map((product, index) => ( + <> + <li className='py-1 sm:py-2'> + <div className='flex justify-between border p-2 flex-col gap-y-2 hover:border-red-500'> + <Link + href={`/my/quotations/${product?.id}`} + className='hover:border-red-500' + > + <div className='flex justify-between mb-2'> + <div className='flex flex-row items-center'> + <p className='tanggal text-xs opacity-80 mr-[2px]'>Sales : </p> + <p className='tanggal text-xs text-red-500 font-semibold'>{product.sales}</p> + </div> + <div className='flex flex-row items-center'> + <p className='text-xs opacity-80 mr-[2px]'>Status :</p> + <p className='badge-red h-fit text-xs whitespace-nowrap'>Pending Quotation</p> + </div> + </div> + <div className='flex justify-between mb-2'> + <div className='flex flex-col items-start'> + <p className=' text-xs opacity-80 mr-[2px]'>No. Transaksi</p> + <p className=' text-sm text-red-500 font-semibold'> {product.name}</p> + </div> + <div className='flex flex-col items-end'> + <p className='text-xs opacity-80 mr-[2px]'>No. Purchase Order</p> + <p className='font-semibold text-sm text-red-500'> {product.purchaseOrderName ? product.purchaseOrderName : '-'}</p> + </div> + </div> + <div className='my-0.5 h-0.5 bg-gray-200'></div> + <div className='bagian bawah flex justify-between mt-2'> + <p className='font-semibold text-sm'>Total</p> + <p className='font-semibold text-sm'>{currencyFormat(product.amountUntaxed)}</p> + </div> + </Link> + </div> + </li> + </> + ))} + </ul> + <hr /> + </> + )} + </div> + {auth && qotation.length > 0 && !isLoading && ( + <> + <div className='mt-3'> + <span className='text-gray-400 text-caption-2'>Subtotal Sebelum PPN : </span> + <span className='font-semibold text-red-600'>{currencyFormat(subTotal)}</span> + </div> + <div className='mt-5 mb-2'> + <button + type='button' + className='btn-yellow rounded-lg w-full' + onClick={handleCheckout} + disabled={buttonLoading} + > + {buttonLoading ? 'Loading...' : 'Buat Quotation'} + </button> + </div> + </> + )} + </motion.div> + </motion.div> + </> + )} + </AnimatePresence> + </div> + ); +}; + +export default Quotationheader; |
