diff options
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/cart/components/Cartheader.jsx | 264 | ||||
| -rw-r--r-- | src/lib/category/components/Category.jsx | 4 | ||||
| -rw-r--r-- | src/lib/category/components/PopularBrand.jsx | 88 | ||||
| -rw-r--r-- | src/lib/checkout/components/Checkout.jsx | 29 | ||||
| -rw-r--r-- | src/lib/home/api/categoryManagementApi.js | 47 | ||||
| -rw-r--r-- | src/lib/home/components/CategoryDynamic.jsx | 196 | ||||
| -rw-r--r-- | src/lib/home/components/CategoryDynamicMobile.jsx | 4 | ||||
| -rw-r--r-- | src/lib/home/components/PreferredBrand.jsx | 5 | ||||
| -rw-r--r-- | src/lib/product/api/productSearchApi.js | 2 | ||||
| -rw-r--r-- | src/lib/product/components/ProductSearch.jsx | 1 | ||||
| -rw-r--r-- | src/lib/promo/api/productSearchApi.js | 11 | ||||
| -rw-r--r-- | src/lib/promo/hooks/usePromotionSearch.js | 15 | ||||
| -rw-r--r-- | src/lib/quotation/components/Quotation.jsx | 7 | ||||
| -rw-r--r-- | src/lib/quotation/components/Quotationheader.jsx | 265 |
14 files changed, 784 insertions, 154 deletions
diff --git a/src/lib/cart/components/Cartheader.jsx b/src/lib/cart/components/Cartheader.jsx index 19f79bc9..ddb77c1f 100644 --- a/src/lib/cart/components/Cartheader.jsx +++ b/src/lib/cart/components/Cartheader.jsx @@ -1,14 +1,20 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { getCartApi } from '../api/CartApi' +import currencyFormat from '@/core/utils/currencyFormat' +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' const { ShoppingCartIcon, PhotoIcon } = require('@heroicons/react/24/outline') const { default: Link } = require('next/link') const Cardheader = (cartCount) => { + const router = useRouter() const [subTotal, setSubTotal] = useState(null) const [buttonLoading, SetButtonTerapkan] = useState(false) @@ -19,7 +25,7 @@ const Cardheader = (cartCount) => { useProductCartContext() const [isHovered, setIsHovered] = useState(false) - + const [isTop, setIsTop] = useState(true) const products = useMemo(() => { return productCart?.products || [] }, [productCart]) @@ -42,7 +48,7 @@ const Cardheader = (cartCount) => { setIsloading(true) let cart = await getCartApi() setProductCart(cart) - setCountCart(cart.productTotal) + setCountCart(cart?.productTotal) setIsloading(false) }, [setProductCart, setIsloading]) @@ -75,14 +81,26 @@ const Cardheader = (cartCount) => { useEffect(() => { setCountCart(cartCount.cartCount) + setRefreshCart(false) }, [cartCount]) + 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/checkout') } + return ( <div className='relative group'> <div> @@ -109,6 +127,246 @@ const Cardheader = (cartCount) => { </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'>Keranjang Belanja</h5> + <Link href='/shop/cart' 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 Keranjang Belanja 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 && products.length === 0 && !isLoading && ( + <div className='justify-center p-4'> + <p className='text-gray-500 text-center '> + Tidak Ada Produk di Keranjang Belanja Anda + </p> + </div> + )} + {auth && products.length > 0 && !isLoading && ( + <> + <ul role='list' className='divide-y divide-gray-200 dark:divide-gray-700'> + {products && + products?.map((product, index) => ( + <> + <li className='py-1 sm:py-2'> + <div className='flex items-center space-x-4'> + <div className='bagian gambar flex-shrink-0'> + {product.cartType === 'promotion' && ( + <Image + src={product.imageProgram[0]} + alt={product.name} + className='object-contain object-center border border-gray_r-6 h-16 w-16 rounded-md' + /> + )} + {product.cartType === 'product' && ( + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + <Image + src={product?.parent?.image} + alt={product?.name} + className='object-contain object-center border border-gray_r-6 h-16 w-16 rounded-md' + /> + </Link> + )} + </div> + <div className='bagian tulisan dan harga flex-1 min-w-0'> + {product.cartType === 'promotion' && ( + <p className='text-caption-2 font-medium text-gray-900 truncate dark:text-white'> + {product.name} + </p> + )} + {product.cartType === 'product' && ( + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + {' '} + <p className='text-caption-2 font-medium text-gray-900 truncate dark:text-white'> + {product.parent.name} + </p> + </Link> + )} + {product?.hasFlashsale && ( + <div className='flex gap-x-1 items-center mb-2 mt-1'> + <div className='badge-solid-red'> + {product?.price?.discountPercentage}% + </div> + <div className='text-gray_r-11 line-through text-caption-2'> + {currencyFormat(product?.price?.price)} + </div> + </div> + )} + + <div className='flex justify-between items-center'> + <div className='font-semibold text-sm text-red-600'> + {product?.price?.priceDiscount > 0 ? ( + currencyFormat(product?.price?.priceDiscount) + ) : ( + <span className='text-gray_r-12/90 font-normal text-caption-1'> + <a + href={whatsappUrl('product', { + name: product.name, + manufacture: product.manufacture?.name, + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ) + })} + className='text-danger-500 underline' + rel='noopener noreferrer' + target='_blank' + > + Call For Price + </a> + </span> + )} + </div> + </div> + </div> + </div> + <div className="flex flex-col w-3/4"> + {product.products?.map((product) => + <div key={product.id} className='md:ml-8 ml-4 mt-2 flex'> + <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className='md:h-12 md:w-12 md:min-w-[48px] h-10 w-10 min-w-[40px] border border-gray-300 rounded '> + {product?.image && <Image src={product.image} alt={product.name} width={40} height={40} className='w-full h-full object-fill' />} + </Link> + + <div className="ml-4 w-full flex flex-col gap-y-1"> + <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className="text-caption-2 font-medium text-gray-900 truncate dark:text-white"> + {product.displayName} + </Link> + + <div className='flex w-full'> + <div className="flex flex-col"> + {/* <div className="text-gray-500 text-caption-1">{product.code}</div> */} + <div> + <span className="text-gray-500 text-caption-1">Berat Barang: </span> + <span className="text-gray-500 text-caption-1">{product.packageWeight} Kg</span> + </div> + </div> + </div> + </div> + + </div> + )} + {product.freeProducts?.map((product) => + <div key={product.id} className='md:ml-8 ml-4 mt-2 flex'> + <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className='md:h-12 md:w-12 md:min-w-[48px] h-10 w-10 min-w-[40px] border border-gray-300 rounded '> + {product?.image && <Image src={product.image} alt={product.name} width={40} height={40} className='w-full h-full object-fill' />} + </Link> + + <div className="ml-4 w-full flex flex-col gap-y-1"> + <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className="text-caption-2 font-medium text-gray-900 truncate dark:text-white"> + {product.displayName} + </Link> + + <div className='flex w-full'> + <div className="flex flex-col"> + {/* <div className="text-gray-500 text-caption-1">{product.code}</div> */} + <div> + <span className="text-gray-500 text-caption-1">Berat Barang: </span> + <span className="text-gray-500 text-caption-1">{product.packageWeight} Kg</span> + </div> + </div> + </div> + </div> + + </div> + )} + </div> + </li> + </> + ))} + </ul> + <hr /> + </> + )} + </div> + {auth && products.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-solid-red rounded-lg w-full' + onClick={handleCheckout} + disabled={buttonLoading} + > + {buttonLoading ? 'Loading...' : 'Lanjutkan Ke Pembayaran'} + </button> + </div> + </> + )} + </motion.div> + </motion.div> + </> + )} + </AnimatePresence> </div> ) } diff --git a/src/lib/category/components/Category.jsx b/src/lib/category/components/Category.jsx index f76e6e42..91553295 100644 --- a/src/lib/category/components/Category.jsx +++ b/src/lib/category/components/Category.jsx @@ -86,14 +86,14 @@ const Category = () => { </div> ))} </div> - <div className='category-mega-box__child-wrapper !w-[260px] !flex !flex-col !gap-4'> + {/* <div className='category-mega-box__child-wrapper !w-[260px] !flex !flex-col !gap-4'> <PopularBrand category={category} /> {Array.isArray(promotionProgram?.data) && promotionProgram?.data.length > 0 && promotionProgram?.data[0]?.map((banner, index) => ( <div key={index} className='flex w-60 h-20 object-cover'> <Image src={`${banner.image}`} alt={`${banner.name}`} width={275} height={4} /> </div> ))} - </div> + </div> */} </div> </div> ))} diff --git a/src/lib/category/components/PopularBrand.jsx b/src/lib/category/components/PopularBrand.jsx index 4777fded..8124b5b4 100644 --- a/src/lib/category/components/PopularBrand.jsx +++ b/src/lib/category/components/PopularBrand.jsx @@ -13,60 +13,60 @@ import { fetchPopulerProductSolr } from '../api/popularProduct' const SOLR_HOST = process.env.SOLR_HOST const PopularBrand = ({ category }) => { - const [topBrands, setTopBrands] = useState([]); + // const [topBrands, setTopBrands] = useState([]); - const fetchTopBrands = async () => { - try { - const items = await fetchPopulerProductSolr(`category_id_ids:(${category?.categoryDataIds?.join(' OR ')})`); - const getTop12UniqueBrands = (prod) => { - const brandMap = new Map(); + // const fetchTopBrands = async () => { + // try { + // const items = await fetchPopulerProductSolr(`category_id_ids:(${category?.categoryDataIds?.join(' OR ')})`); + // const getTop12UniqueBrands = (prod) => { + // const brandMap = new Map(); - for (const product of prod) { - const { manufacture_name, manufacture_id, qty_sold } = product; + // for (const product of prod) { + // const { manufacture_name, manufacture_id, qty_sold } = product; - if (brandMap.has(manufacture_name)) { - // Update the existing brand's qty_sold - brandMap.set(manufacture_name, { - name: manufacture_name, - id: manufacture_id, - qty_sold: brandMap.get(manufacture_name).qty_sold + qty_sold - }); - } else { - // Add a new brand to the map - brandMap.set(manufacture_name, { - name: manufacture_name, - id: manufacture_id, - qty_sold - }); - } - } + // if (brandMap.has(manufacture_name)) { + // // Update the existing brand's qty_sold + // brandMap.set(manufacture_name, { + // name: manufacture_name, + // id: manufacture_id, + // qty_sold: brandMap.get(manufacture_name).qty_sold + qty_sold + // }); + // } else { + // // Add a new brand to the map + // brandMap.set(manufacture_name, { + // name: manufacture_name, + // id: manufacture_id, + // qty_sold + // }); + // } + // } - // Convert the map to an array and sort by qty_sold in descending order - const sortedBrands = Array.from(brandMap.values()).sort((a, b) => b.qty_sold - a.qty_sold); + // // Convert the map to an array and sort by qty_sold in descending order + // const sortedBrands = Array.from(brandMap.values()).sort((a, b) => b.qty_sold - a.qty_sold); - // Return the top 12 brands - return sortedBrands.slice(0, 18); - }; + // // Return the top 12 brands + // return sortedBrands.slice(0, 18); + // }; - // Using the fetched products - const products = items; - const top12UniqueBrands = getTop12UniqueBrands(products); + // // Using the fetched products + // const products = items; + // const top12UniqueBrands = getTop12UniqueBrands(products); - // Set the top 12 brands to the state - setTopBrands(top12UniqueBrands); - } catch (error) { - console.error("Error fetching data from Solr", error); - throw error; - } - } + // // Set the top 12 brands to the state + // setTopBrands(top12UniqueBrands); + // } catch (error) { + // console.error("Error fetching data from Solr", error); + // throw error; + // } + // } - useEffect(() => { - fetchTopBrands(); - }, [category]); + // useEffect(() => { + // fetchTopBrands(); + // }, [category]); return ( <div className='flex flex-col'> - <div className='grid grid-cols-3 max-h-full w-full gap-2'> + {/* <div className='grid grid-cols-3 max-h-full w-full gap-2'> {topBrands.map((brand, index) => ( <div key={index} className='w-full flex items-center justify-center pb-2'> <Link @@ -77,7 +77,7 @@ const PopularBrand = ({ category }) => { </Link> </div> ))} - </div> + </div> */} {/* {topBrands.length > 8 && ( <div className='flex hover:bg-gray_r-8/35 rounded-10'> <Link diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 54acdf7c..f63ef457 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -174,7 +174,6 @@ const Checkout = () => { } return; // Hentikan eksekusi lebih lanjut pada iterasi ini } - // Memeriksa apakah subtotal memenuhi syarat minimal pembelian if (cartCheckout?.subtotal < addNewLine.minPurchaseAmount) { SetSelisihHargaCode( @@ -191,7 +190,9 @@ const Checkout = () => { // Tambahkan voucher ke list dan set voucher aktif SetListVoucher((prevList) => [addNewLine, ...prevList]); - SetActiveVoucher(addNewLine.code); + if (addNewLine.canApply) { + SetActiveVoucher(addNewLine.code); + } } else { // Mencari voucher dalam listVoucherShippings let checkList = listVoucherShippings?.findIndex( @@ -227,7 +228,9 @@ const Checkout = () => { // Tambahkan voucher ke list pengiriman dan set voucher aktif pengiriman SetListVoucherShipping((prevList) => [addNewLine, ...prevList]); - setActiveVoucherShipping(addNewLine.code); + if (addNewLine.canApply) { + setActiveVoucherShipping(addNewLine.code); + } } }); @@ -407,7 +410,7 @@ const Checkout = () => { Math.round(parseInt(finalShippingAmt * 1.1) / 1000) * 1000; const finalGT = GT < 0 ? 0 : GT; setGrandTotal(finalGT); - }, [biayaKirim, cartCheckout?.grandTotal, activeVoucher]); + }, [biayaKirim, cartCheckout?.grandTotal, activeVoucher, activeVoucherShipping]); const checkout = async () => { const file = poFile.current.files[0]; @@ -494,7 +497,7 @@ const Checkout = () => { } } - const midtrans = async () => { + /* const midtrans = async () => { for (const product of products) deleteItemCart({ productId: product.id }); if (grandTotal > 0) { const payment = await axios.post( @@ -510,7 +513,7 @@ const Checkout = () => { '-' )}`; } - }; + };*/ }; const handlingActivateCode = async () => { @@ -701,7 +704,9 @@ const Checkout = () => { {listVoucherShippings && listVoucherShippings?.length > 0 && ( <div> - <h3 className='font-semibold mb-4'>Promo Extra Potongan Ongkir</h3> + <h3 className='font-semibold mb-4'> + Promo Extra Potongan Ongkir + </h3> {listVoucherShippings?.map((item) => ( <div key={item.id} className='relative'> <div @@ -1028,7 +1033,7 @@ const Checkout = () => { </div> <span className='leading-5'> Jika mengalami kesulitan dalam melakukan pembelian di website - Indoteknik. Hubungi kami disini + Indoteknik. <a href={whatsappUrl()}>Hubungi kami disini</a> </span> </Alert> </div> @@ -1184,7 +1189,7 @@ const Checkout = () => { <div className='text-gray_r-11'> Biaya Kirim <p className='text-xs mt-1'>{etdFix}</p> </div> - <div>{currencyFormat(biayaKirim)}</div> + <div>{currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000)}</div> </div> {activeVoucherShipping && voucherShippingAmt && ( <div className='flex gap-x-2 justify-between'> @@ -1236,10 +1241,10 @@ const Checkout = () => { className='object-contain object-center h-6 rounded-md' /> </span> - {activeVoucher ? ( + {activeVoucher || activeVoucherShipping ? ( <div className=''> <div className='text-left text-sm text-black font-semibold'> - Potongan Senilai {currencyFormat(discountVoucher)} + Potongan Senilai {currencyFormat(totalDiscountVoucher)} </div> <div className='text-left mt-1 text-green-600 text-xs'> Voucher berhasil digunakan @@ -1485,7 +1490,7 @@ const Checkout = () => { Biaya Kirim <p className='text-xs mt-1'>{etdFix}</p> </div> - <div>{currencyFormat(biayaKirim)}</div> + <div>{currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000) }</div> </div> {activeVoucherShipping && voucherShippingAmt && ( <div className='flex gap-x-2 justify-between'> diff --git a/src/lib/home/api/categoryManagementApi.js b/src/lib/home/api/categoryManagementApi.js index b70d60ce..63edd629 100644 --- a/src/lib/home/api/categoryManagementApi.js +++ b/src/lib/home/api/categoryManagementApi.js @@ -1,8 +1,43 @@ -import odooApi from '@/core/api/odooApi' +// import odooApi from '@/core/api/odooApi' -const categoryManagementApi = async () => { - const dataCategoryManagement = await odooApi('GET', '/api/v1/categories_management') - return dataCategoryManagement -} +// const categoryManagementApi = async () => { +// const dataCategoryManagement = await odooApi('GET', '/api/v1/categories_management') +// return dataCategoryManagement +// } -export default categoryManagementApi +// export default categoryManagementApi + + + +export const fetchPopulerProductSolr = async (category_id_ids) => { + let sort ='sort=qty_sold_f desc'; + try { + const queryParams = new URLSearchParams({ q: category_id_ids }); + const response = await fetch(`/solr/category_management/query?q=*:*&q.op=OR&indent=true`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + const promotions = await map(data.response.docs); + return promotions; + } catch (error) { + console.error("Error fetching promotion data:", error); + return []; + } + }; + + const map = async (promotions) => { + const result = []; + for (const promotion of promotions) { + const data = { + id: promotion.id, + name: promotion.name_s, + image: promotion.image_s, + sequence: promotion.sequence_i, + numFound: promotion.numFound_i, + categories_level_2:promotion.categories_level_2 + }; + result.push(data); + } + return result; + };
\ No newline at end of file diff --git a/src/lib/home/components/CategoryDynamic.jsx b/src/lib/home/components/CategoryDynamic.jsx index b7798a24..11a15d6d 100644 --- a/src/lib/home/components/CategoryDynamic.jsx +++ b/src/lib/home/components/CategoryDynamic.jsx @@ -1,105 +1,149 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import useCategoryManagement from '../hooks/useCategoryManagement'; +import {fetchPopulerProductSolr} from '../api/categoryManagementApi' import NextImage from 'next/image'; import Link from "next/link"; import { createSlug } from '@/core/utils/slug'; import odooApi from '@/core/api/odooApi'; -import { Skeleton} from '@chakra-ui/react' +import { Skeleton } from '@chakra-ui/react'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; +import 'swiper/css/navigation'; +import 'swiper/css/pagination'; +import { Navigation, Pagination, Autoplay } from 'swiper'; const CategoryDynamic = () => { + + const [manufactures, setManufactures] = useState([]) + const loadBrand = useCallback(async () => { + // setIsLoading(true) + //Get brand from odoo + /*const result = await odooApi( + 'GET', + `/api/v1/manufacture?limit=0&offset=${manufactures.length}&name=${name}` + )*/ + + // Change get brands from solr + const items = await fetchPopulerProductSolr(); + + console.log("items",items) + + // setIsLoading(false) + // setManufactures((manufactures) => [...result.data]) + }, []) + + useEffect(() => { + loadBrand() + }, [loadBrand]) const { categoryManagement } = useCategoryManagement(); - const [categoryData, setCategoryData] = useState({}); - const [subCategoryData, setSubCategoryData] = useState({}); + // const [categoryData, setCategoryData] = useState({}); + // const [subCategoryData, setSubCategoryData] = useState({}); - useEffect(() => { - const fetchCategoryData = async () => { - if (categoryManagement && categoryManagement.data) { - const updatedCategoryData = {}; - const updatedSubCategoryData = {}; + // useEffect(() => { + // const fetchCategoryData = async () => { + // if (categoryManagement && categoryManagement.data) { + // const updatedCategoryData = {}; + // const updatedSubCategoryData = {}; - for (const category of categoryManagement.data) { - const countLevel1 = await odooApi('GET', `/api/v1/category/numFound?parent_id=${category.categoryIdI}`); + // for (const category of categoryManagement.data) { + // const countLevel1 = await odooApi('GET', `/api/v1/category/numFound?parent_id=${category.categoryIdI}`); - updatedCategoryData[category.categoryIdI] = countLevel1?.numFound; + // updatedCategoryData[category.categoryIdI] = countLevel1?.numFound; - for (const subCategory of countLevel1.children) { - updatedSubCategoryData[subCategory.id] = subCategory.numFound; - } - } + // for (const subCategory of countLevel1?.children) { + // updatedSubCategoryData[subCategory.id] = subCategory?.numFound; + // } + // } - setCategoryData(updatedCategoryData); - setSubCategoryData(updatedSubCategoryData); - } - }; + // setCategoryData(updatedCategoryData); + // setSubCategoryData(updatedSubCategoryData); + // } + // }; - fetchCategoryData(); - }, [categoryManagement.isLoading]); + // fetchCategoryData(); + // }, [categoryManagement.isLoading]); + + const swiperBanner = { + modules: [Pagination, ], + classNames:'mySwiper', + slidesPerView: 3, + spaceBetween:10, + pagination: { + dynamicBullets: true, + clickable: true, + } + }; return ( <div> {categoryManagement && categoryManagement.data?.map((category) => { - const countLevel1 = categoryData[category.categoryIdI] || 0; + // const countLevel1 = categoryData[category.categoryIdI] || 0; return ( <Skeleton key={category.id} isLoaded={categoryManagement}> - <div key={category.id}> - <div className='bagian-judul flex flex-row justify-start items-center gap-3 mb-4 mt-4'> - <div className='font-semibold sm:text-h-lg mr-2'>{category.name}</div> - <Skeleton isLoaded={countLevel1 !=0}> - <p className={`text-gray_r-10 text-sm`}>{countLevel1} Produk tersedia</p> - </Skeleton> - <Link href={createSlug('/shop/category/', category?.name, category?.categoryIdI)} className="!text-red-500 font-semibold">Lihat Semua</Link> - </div> - <div className='grid grid-cols-3 gap-2'> - {category.categories.map((subCategory) => { - const countLevel2 = subCategoryData[subCategory.idLevel2] || 0; + <div key={category.id}> + <div className='bagian-judul flex flex-row justify-start items-center gap-3 mb-4 mt-4'> + <div className='font-semibold sm:text-h-lg mr-2'>{category.name}</div> + {/* <Skeleton isLoaded={countLevel1 != 0}> + <p className={`text-gray_r-10 text-sm`}>{countLevel1} Produk tersedia</p> + </Skeleton> */} + <Link href={createSlug('/shop/category/', category?.name, category?.categoryIdI)} className="!text-red-500 font-semibold">Lihat Semua</Link> + </div> + + {/* Swiper for SubCategories */} + <Swiper {...swiperBanner} + > + {category.categories.map((subCategory) => { + // const countLevel2 = subCategoryData[subCategory.idLevel2] || 0; - return ( - <div key={subCategory.id} className='border rounded justify-start items-start'> - <div className='p-3'> - <div className='flex flex-row border rounded mb-2 justify-start items-center'> - <NextImage - src={subCategory.image ? subCategory.image : "/images/noimage.jpeg"} - alt={subCategory.name} - width={90} - height={30} - className='object-fit' - /> - <div className='bagian-judul flex flex-col justify-center items-start gap-2 ml-2'> - <div className='font-semibold text-lg mr-2'>{subCategory.name}</div> - <Skeleton isLoaded={countLevel2 != 0}> - <p className={`text-gray_r-10 text-sm`}> - {countLevel2} Produk tersedia - </p> - </Skeleton> - <Link href={createSlug('/shop/category/', subCategory?.name, subCategory?.idLevel2)} className="!text-red-500 font-semibold">Lihat Semua</Link> - </div> - </div> - <div className='grid grid-cols-2 gap-2 overflow-y-auto max-h-[240px]'> - {subCategory.childFrontendIdI.map((childCategory) => ( - <div key={childCategory.id}> - <Link href={createSlug('/shop/category/', childCategory?.name, childCategory?.idLevel3)} className="flex flex-row gap-2 border rounded group hover:border-red-500"> - <NextImage - src={childCategory.image ? childCategory.image : "/images/noimage.jpeg"} - alt={childCategory.name} - className='p-2 ml-1' - width={40} - height={40} - /> - <div className='bagian-judul flex flex-col justify-center items-center gap-2 break-words line-clamp-2 group-hover:text-red-500'> - <div className='font-semibold line-clamp-2 group-hover:text-red-500 text-sm mr-2'>{childCategory.name}</div> + return ( + <SwiperSlide key={subCategory.id}> + <div className='border rounded justify-start items-start '> + <div className='p-3'> + <div className='flex flex-row border rounded mb-2 justify-start items-center'> + <NextImage + src={subCategory.image ? subCategory.image : "/images/noimage.jpeg"} + alt={subCategory.name} + width={90} + height={30} + className='object-fit p-4' + /> + <div className='bagian-judul flex flex-col justify-center items-start gap-2 ml-2'> + <div className='font-semibold text-lg mr-2'>{subCategory?.name}</div> + {/* <Skeleton isLoaded={countLevel2 != 0}> + <p className={`text-gray_r-10 text-sm`}> + {countLevel2} Produk tersedia + </p> + </Skeleton> */} + <Link href={createSlug('/shop/category/', subCategory?.name, subCategory?.idLevel2)} className="!text-red-500 font-semibold">Lihat Semua</Link> + </div> + </div> + <div className='grid grid-cols-2 gap-2 overflow-y-auto max-h-[240px] min-h-[240px] content-start'> + {subCategory.childFrontendIdI.map((childCategory) => ( + <div key={childCategory.id} className=''> + <Link href={createSlug('/shop/category/', childCategory?.name, childCategory?.idLevel3)} className="flex flex-row gap-2 border rounded group hover:border-red-500"> + <NextImage + src={childCategory.image ? childCategory.image : "/images/noimage.jpeg"} + alt={childCategory.name} + className='p-2 ml-1' + width={40} + height={40} + /> + <div className='bagian-judul flex flex-col justify-center items-center gap-2 break-words line-clamp-2 group-hover:text-red-500'> + <div className='font-semibold line-clamp-2 group-hover:text-red-500 text-sm mr-2'>{childCategory.name}</div> + </div> + </Link> </div> - </Link> + ))} </div> - ))} + </div> </div> - </div> - </div> - ); - })} + </SwiperSlide> + ); + })} + </Swiper> </div> - </div> - </Skeleton> + </Skeleton> ); })} </div> diff --git a/src/lib/home/components/CategoryDynamicMobile.jsx b/src/lib/home/components/CategoryDynamicMobile.jsx index c1433a2d..2877a5a7 100644 --- a/src/lib/home/components/CategoryDynamicMobile.jsx +++ b/src/lib/home/components/CategoryDynamicMobile.jsx @@ -59,11 +59,10 @@ const CategoryDynamicMobile = () => { alt={index.name} width={30} height={30} - className='object-' + className='' /> <div className='bagian-judul flex flex-col justify-center items-start gap-1 ml-2'> <div className='font-semibold text-[10px] line-clamp-1'>{index.name}</div> - <p className='text-gray_r-10 text-[10px]'>999 rb+ Produk</p> </div> </div> </div> @@ -82,6 +81,7 @@ const CategoryDynamicMobile = () => { alt={x.name} width={40} height={40} + className='p-2' /> <div className='bagian-judul flex flex-col justify-center items-start gap-1 break-words line-clamp-2 group-hover:text-red-500'> <div className='font-semibold line-clamp-2 group-hover:text-red-500 text-[10px]'>{x.name}</div> diff --git a/src/lib/home/components/PreferredBrand.jsx b/src/lib/home/components/PreferredBrand.jsx index 56268db7..b30fa5c9 100644 --- a/src/lib/home/components/PreferredBrand.jsx +++ b/src/lib/home/components/PreferredBrand.jsx @@ -65,11 +65,6 @@ const PreferredBrand = () => { Lihat Semua </Link> )} - {isMobile && ( - <Link href='/shop/brands' className='!text-red-500 font-semibold sm:text-h-sm'> - Lihat Semua - </Link> - )} </div> <div className=''> {manufactures.isLoading && <PreferredBrandSkeleton />} diff --git a/src/lib/product/api/productSearchApi.js b/src/lib/product/api/productSearchApi.js index 1626b7b7..8ff8e57d 100644 --- a/src/lib/product/api/productSearchApi.js +++ b/src/lib/product/api/productSearchApi.js @@ -1,7 +1,7 @@ import _ from 'lodash-contrib' import axios from 'axios' -const productSearchApi = async ({ query, operation = 'AND' }) => { +const productSearchApi = async ({ query, operation = 'OR' }) => { const dataProductSearch = await axios( `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?${query}&operation=${operation}` ) diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index d6e649a8..1edc31c9 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -87,7 +87,6 @@ const ProductSearch = ({ recurse(category); return ids; }; - useEffect(() => { if(prefixUrl.includes('category')){ const ids = collectIds(dataCategoriesProduct); diff --git a/src/lib/promo/api/productSearchApi.js b/src/lib/promo/api/productSearchApi.js new file mode 100644 index 00000000..2f792fd4 --- /dev/null +++ b/src/lib/promo/api/productSearchApi.js @@ -0,0 +1,11 @@ +import _ from 'lodash-contrib' +import axios from 'axios' + +const productSearchApi = async ({ query, operation = 'AND' }) => { + const dataProductSearch = await axios( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/promo?${query}&operation=${operation}` + ) + return dataProductSearch.data +} + +export default productSearchApi diff --git a/src/lib/promo/hooks/usePromotionSearch.js b/src/lib/promo/hooks/usePromotionSearch.js new file mode 100644 index 00000000..1a194646 --- /dev/null +++ b/src/lib/promo/hooks/usePromotionSearch.js @@ -0,0 +1,15 @@ +import { useQuery } from 'react-query' +import productSearchApi from '../api/productSearchApi' +import _ from 'lodash-contrib' + +const usePromotionSearch = ({ query, operation }) => { + const queryString = _.toQuery(query) + const fetchProductSearch = async () => await productSearchApi({ query: queryString , operation : operation}) + const productSearch = useQuery(`promoSearch-${queryString}`, fetchProductSearch) + + return { + productSearch + } +} + +export default usePromotionSearch diff --git a/src/lib/quotation/components/Quotation.jsx b/src/lib/quotation/components/Quotation.jsx index df234dc2..0ad042de 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 { setRefreshCart } = 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}`); + setRefreshCart(true); return; } diff --git a/src/lib/quotation/components/Quotationheader.jsx b/src/lib/quotation/components/Quotationheader.jsx new file mode 100644 index 00000000..4529c977 --- /dev/null +++ b/src/lib/quotation/components/Quotationheader.jsx @@ -0,0 +1,265 @@ +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 } = + 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 (refreshCart) { + refreshCartf(); + } + setRefreshCart(false); + }, [ refreshCartf, setRefreshCart]); + + useEffect(() => { + setCountQuotation(quotationCount.quotationCount); + setProductQuotation(quotationCount.data); + }, [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('/my/quotations'); + }; + + 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> + </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> */} + <hr className='mt-3 mb-3 border border-gray-100' /> + <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 ml-1'> + <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-solid-red rounded-lg w-full' + onClick={handleCheckout} + disabled={buttonLoading} + > + {buttonLoading ? 'Loading...' : 'Lihat Semua'} + </button> + </div> + </> + )} + </motion.div> + </motion.div> + </> + )} + </AnimatePresence> + </div> + ); +}; + +export default Quotationheader; |
