diff options
| author | it-fixcomart <it@fixcomart.co.id> | 2024-09-23 09:22:29 +0700 |
|---|---|---|
| committer | it-fixcomart <it@fixcomart.co.id> | 2024-09-23 09:22:29 +0700 |
| commit | c13bb5c6f78032695b24cf8a6c23942eb465c6d5 (patch) | |
| tree | e48c1ab39a61fd5702f578fbfdb7a7375cc1ce43 /src/lib | |
| parent | 870bede9df9920b23f2e5cb771ff5ae6448e3ac7 (diff) | |
| parent | 4bd29979c34c1ec3b31dd384829b008eb726769c (diff) | |
Merge branch 'Feature/new-register' into Feature/switch-account
Diffstat (limited to 'src/lib')
32 files changed, 2160 insertions, 239 deletions
diff --git a/src/lib/address/components/CreateAddress.jsx b/src/lib/address/components/CreateAddress.jsx index 86519147..e315affe 100644 --- a/src/lib/address/components/CreateAddress.jsx +++ b/src/lib/address/components/CreateAddress.jsx @@ -1,76 +1,101 @@ -import HookFormSelect from '@/core/components/elements/Select/HookFormSelect' -import useAuth from '@/core/hooks/useAuth' -import { useRouter } from 'next/router' -import { Controller, useForm } from 'react-hook-form' -import * as Yup from 'yup' -import cityApi from '../api/cityApi' -import districtApi from '../api/districtApi' -import subDistrictApi from '../api/subDistrictApi' -import { useEffect, useState } from 'react' -import createAddressApi from '../api/createAddressApi' -import { toast } from 'react-hot-toast' -import { yupResolver } from '@hookform/resolvers/yup' -import Menu from '@/lib/auth/components/Menu' +import HookFormSelect from '@/core/components/elements/Select/HookFormSelect'; +import useAuth from '@/core/hooks/useAuth'; +import { useRouter } from 'next/router'; +import { Controller, useForm } from 'react-hook-form'; +import * as Yup from 'yup'; +import cityApi from '../api/cityApi'; +import districtApi from '../api/districtApi'; +import subDistrictApi from '../api/subDistrictApi'; +import { useEffect, useState } from 'react'; +import createAddressApi from '../api/createAddressApi'; +import { toast } from 'react-hot-toast'; +import { yupResolver } from '@hookform/resolvers/yup'; +import Menu from '@/lib/auth/components/Menu'; +import useAddresses from '../hooks/useAddresses'; const CreateAddress = () => { - const auth = useAuth() - const router = useRouter() + const auth = useAuth(); + const router = useRouter(); const { register, formState: { errors }, handleSubmit, watch, setValue, - control + control, } = useForm({ resolver: yupResolver(validationSchema), - defaultValues - }) - - const [cities, setCities] = useState([]) - const [districts, setDistricts] = useState([]) - const [subDistricts, setSubDistricts] = useState([]) + defaultValues, + }); + const { addresses = [] } = useAddresses(); // Ensure addresses is an array + const [cities, setCities] = useState([]); + const [districts, setDistricts] = useState([]); + const [subDistricts, setSubDistricts] = useState([]); + const [filteredTypes, setFilteredTypes] = useState(types); // State to manage filtered types useEffect(() => { const loadCities = async () => { - let dataCities = await cityApi() - dataCities = dataCities.map((city) => ({ value: city.id, label: city.name })) - setCities(dataCities) + let dataCities = await cityApi(); + dataCities = dataCities.map((city) => ({ + value: city.id, + label: city.name, + })); + setCities(dataCities); + }; + loadCities(); + }, []); + + useEffect(() => { + if (addresses) { + let hasContactAddress = false; + + for (let i = 0; i < addresses?.data?.length; i++) { + if (addresses.data[i].type === 'contact') { + hasContactAddress = true; + break; + } + } + if (hasContactAddress) { + setFilteredTypes(types.filter((type) => type.value !== 'contact')); + } else { + setFilteredTypes(types); + } } - loadCities() - }, []) + }, [auth]); - const watchCity = watch('city') + const watchCity = watch('city'); useEffect(() => { - setValue('district', '') + setValue('district', ''); if (watchCity) { const loadDistricts = async () => { - let dataDistricts = await districtApi({ cityId: watchCity }) + let dataDistricts = await districtApi({ cityId: watchCity }); dataDistricts = dataDistricts.map((district) => ({ value: district.id, - label: district.name - })) - setDistricts(dataDistricts) - } - loadDistricts() + label: district.name, + })); + setDistricts(dataDistricts); + }; + loadDistricts(); } - }, [watchCity, setValue]) + }, [watchCity, setValue]); - const watchDistrict = watch('district') + const watchDistrict = watch('district'); useEffect(() => { - setValue('subDistrict', '') + setValue('subDistrict', ''); if (watchDistrict) { const loadSubDistricts = async () => { - let dataSubDistricts = await subDistrictApi({ districtId: watchDistrict }) + let dataSubDistricts = await subDistrictApi({ + districtId: watchDistrict, + }); dataSubDistricts = dataSubDistricts.map((district) => ({ value: district.id, - label: district.name - })) - setSubDistricts(dataSubDistricts) - } - loadSubDistricts() + label: district.name, + })); + setSubDistricts(dataSubDistricts); + }; + loadSubDistricts(); } - }, [watchDistrict, setValue]) + }, [watchDistrict, setValue]); const onSubmitHandler = async (values) => { const data = { @@ -78,15 +103,15 @@ const CreateAddress = () => { city_id: values.city, district_id: values.district, sub_district_id: values.subDistrict, - parent_id: auth.partnerId - } + parent_id: auth.partnerId, + }; - const address = await createAddressApi({ data }) + const address = await createAddressApi({ data }); if (address?.id) { - toast.success('Berhasil menambahkan alamat') - router.back() + toast.success('Berhasil menambahkan alamat'); + router.back(); } - } + }; return ( <div className='max-w-none md:container mx-auto flex p-0 md:py-10'> @@ -102,10 +127,16 @@ const CreateAddress = () => { name='type' control={control} render={(props) => ( - <HookFormSelect {...props} isSearchable={false} options={types} /> + <HookFormSelect + {...props} + isSearchable={false} + options={filteredTypes} + /> )} /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.type?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.type?.message} + </div> </div> <div> @@ -116,7 +147,9 @@ const CreateAddress = () => { type='text' className='form-input' /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.name?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.name?.message} + </div> </div> <div> @@ -127,7 +160,9 @@ const CreateAddress = () => { type='email' className='form-input' /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.email?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.email?.message} + </div> </div> <div> @@ -138,7 +173,9 @@ const CreateAddress = () => { type='tel' className='form-input' /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.mobile?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.mobile?.message} + </div> </div> <div> @@ -149,7 +186,9 @@ const CreateAddress = () => { type='text' className='form-input' /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.street?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.street?.message} + </div> </div> <div> @@ -160,7 +199,9 @@ const CreateAddress = () => { type='number' className='form-input' /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.zip?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.zip?.message} + </div> </div> <div> @@ -168,9 +209,13 @@ const CreateAddress = () => { <Controller name='city' control={control} - render={(props) => <HookFormSelect {...props} options={cities} />} + render={(props) => ( + <HookFormSelect {...props} options={cities} /> + )} /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.city?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.city?.message} + </div> </div> <div> @@ -179,10 +224,16 @@ const CreateAddress = () => { name='district' control={control} render={(props) => ( - <HookFormSelect {...props} options={districts} disabled={!watchCity} /> + <HookFormSelect + {...props} + options={districts} + disabled={!watchCity} + /> )} /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.district?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.district?.message} + </div> </div> <div> @@ -191,31 +242,37 @@ const CreateAddress = () => { name='subDistrict' control={control} render={(props) => ( - <HookFormSelect {...props} options={subDistricts} disabled={!watchDistrict} /> + <HookFormSelect + {...props} + options={subDistricts} + disabled={!watchDistrict} + /> )} /> </div> </div> - <button type='submit' className='btn-yellow w-full md:w-fit mt-6 ml-0 md:ml-auto'> + <button + type='submit' + className='btn-yellow w-full md:w-fit mt-6 ml-0 md:ml-auto' + > Simpan </button> </form> </div> </div> - ) -} + ); +}; const validationSchema = Yup.object().shape({ type: Yup.string().required('Harus di-pilih'), name: Yup.string().min(3, 'Minimal 3 karakter').required('Harus di-isi'), - // email: Yup.string().email('Format harus seperti contoh@email.com').required('Harus di-isi'), mobile: Yup.string().required('Harus di-isi'), street: Yup.string().required('Harus di-isi'), zip: Yup.string().required('Harus di-isi'), city: Yup.string().required('Harus di-pilih'), - district: Yup.string().required('Harus di-pilih') -}) + district: Yup.string().required('Harus di-pilih'), +}); const defaultValues = { type: '', @@ -226,14 +283,14 @@ const defaultValues = { city: '', district: '', subDistrict: '', - zip: '' -} + zip: '', +}; const types = [ { value: 'contact', label: 'Contact Address' }, { value: 'invoice', label: 'Invoice Address' }, { value: 'delivery', label: 'Delivery Address' }, - { value: 'other', label: 'Other Address' } -] + { value: 'other', label: 'Other Address' }, +]; -export default CreateAddress +export default CreateAddress; diff --git a/src/lib/address/components/EditAddress.jsx b/src/lib/address/components/EditAddress.jsx index 580aaeb8..ff6b1f12 100644 --- a/src/lib/address/components/EditAddress.jsx +++ b/src/lib/address/components/EditAddress.jsx @@ -29,7 +29,6 @@ const EditAddress = ({ id, defaultValues }) => { resolver: yupResolver(validationSchema), defaultValues, }); - const [cities, setCities] = useState([]); const [districts, setDistricts] = useState([]); const [subDistricts, setSubDistricts] = useState([]); @@ -43,6 +42,7 @@ const EditAddress = ({ id, defaultValues }) => { setValue('npwp', dataProfile.npwp); setValue('alamat_wajib_pajak', dataProfile.alamatWajibPajak); setValue('alamat_bisnis', dataProfile.alamatBisnis); + setValue('business_name', dataProfile.name); }; if (auth) loadProfile(); }, [auth, setValue]); @@ -103,7 +103,6 @@ const EditAddress = ({ id, defaultValues }) => { loadSubDistricts(); } }, [watchDistrict, setValue, getValues]); - const onSubmitHandler = async (values) => { const data = { ...values, @@ -117,13 +116,15 @@ const EditAddress = ({ id, defaultValues }) => { let isUpdated = true; if (auth?.partnerId == id) { dataAlamat = { - ...values, id_user: auth.partnerId, company_type_id: values.companyType, industry_id: values.industry, tax_name: values.taxName, alamat_lengkap_text: values.alamat_wajib_pajak, street: values.street, + business_name: values.business_name, + name: values.business_name, + npwp: values.npwp, }; isUpdated = await odooApi( 'PUT', @@ -133,7 +134,7 @@ const EditAddress = ({ id, defaultValues }) => { } // if (isUpdated?.id) { - if (address?.id) { + if (address?.id && isUpdated?.id) { toast.success('Berhasil mengubah alamat'); router.back(); } else { @@ -195,6 +196,7 @@ const EditAddress = ({ id, defaultValues }) => { placeholder='johndoe@example.com' type='email' className='form-input' + disabled={auth?.partnerId == id && true} /> <div className='text-caption-2 text-danger-500 mt-1'> {errors.email?.message} diff --git a/src/lib/auth/components/CompanyProfile.jsx b/src/lib/auth/components/CompanyProfile.jsx index cebb15b0..6100e626 100644 --- a/src/lib/auth/components/CompanyProfile.jsx +++ b/src/lib/auth/components/CompanyProfile.jsx @@ -7,23 +7,25 @@ import { useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { toast } from 'react-hot-toast'; import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as Yup from 'yup'; const CompanyProfile = () => { const [changeConfirmation, setChangeConfirmation] = useState(false); const auth = useAuth(); const [isOpen, setIsOpen] = useState(false); const toggle = () => setIsOpen(!isOpen); - const { register, setValue, control, handleSubmit } = useForm({ - defaultValues: { - industry: '', - companyType: '', - name: '', - taxName: '', - npwp: '', - alamat_wajib_pajak: '', - alamat_bisnis: '', - }, + const { + register, + formState: { errors }, + setValue, + control, + handleSubmit, + } = useForm({ + resolver: yupResolver(validationSchema), + defaultValues, }); + const [industries, setIndustries] = useState([]); useEffect(() => { const loadIndustries = async () => { @@ -156,6 +158,9 @@ const CompanyProfile = () => { <HookFormSelect {...props} options={industries} /> )} /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.industry?.message} + </div> </div> <div className='flex flex-wrap'> <div className='w-full mb-3'>Badan Usaha</div> @@ -167,6 +172,9 @@ const CompanyProfile = () => { <HookFormSelect {...props} options={companyTypes} /> )} /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.companyType?.message} + </div> </div> <div className='w-9/12 pl-1'> <input @@ -175,6 +183,9 @@ const CompanyProfile = () => { className='form-input' placeholder='Cth: Indoteknik Dotcom Gemilang' /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.name?.message} + </div> </div> </div> <div> @@ -184,6 +195,9 @@ const CompanyProfile = () => { type='text' className='form-input mt-3' /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.taxName?.message} + </div> </div> <div> <label>Alamat Wajib Pajak</label> @@ -192,6 +206,9 @@ const CompanyProfile = () => { type='text' className='form-input mt-3' /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.alamat_wajib_pajak?.message} + </div> </div> <div> <label>Alamat Bisnis</label> @@ -200,6 +217,9 @@ const CompanyProfile = () => { type='text' className='form-input mt-3' /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.alamat_bisnis?.message} + </div> </div> <div> <label>Nomor NPWP</label> @@ -207,7 +227,11 @@ const CompanyProfile = () => { {...register('npwp')} type='text' className='form-input mt-3' + maxLength={16} /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.npwp?.message} + </div> </div> </div> <button type='submit' className='btn-yellow w-full mt-6'> @@ -220,3 +244,23 @@ const CompanyProfile = () => { }; export default CompanyProfile; + +const validationSchema = Yup.object().shape({ + alamat_bisnis: Yup.string().required('Harus di-isi'), + alamat_wajib_pajak: Yup.string().required('Harus di-isi'), + taxName: Yup.string().required('Harus di-isi'), + npwp: Yup.string().required('Harus di-isi'), + name: Yup.string().required('Harus di-isi'), + industry: Yup.string().required('Harus di-pilih'), + companyType: Yup.string().required('Harus di-pilih'), +}); + +const defaultValues = { + industry: '', + companyType: '', + name: '', + taxName: '', + npwp: '', + alamat_wajib_pajak: '', + alamat_bisnis: '', +}; diff --git a/src/lib/brand/components/BrandCard.jsx b/src/lib/brand/components/BrandCard.jsx index 731214ff..2d78d956 100644 --- a/src/lib/brand/components/BrandCard.jsx +++ b/src/lib/brand/components/BrandCard.jsx @@ -8,7 +8,7 @@ const BrandCard = ({ brand }) => { return ( <Link href={createSlug('/shop/brands/', brand.name, brand.id)} - className={`py-1 px-2 rounded border border-gray_r-6 flex justify-center items-center ${ + className={`py-1 px-2 border-gray_r-6 flex justify-center items-center hover:scale-110 transition duration-500 ease-in-out ${ isMobile ? 'h-16' : 'h-24' }`} > @@ -16,9 +16,9 @@ const BrandCard = ({ brand }) => { <Image src={brand.logo} alt={brand.name} - width={128} - height={128} - className='h-full w-full object-contain object-center' + width={50} + height={50} + className='h-full w-[122px] object-contain object-center' /> )} {!brand.logo && ( 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/api/popularProduct.js b/src/lib/category/api/popularProduct.js new file mode 100644 index 00000000..3fdfc41c --- /dev/null +++ b/src/lib/category/api/popularProduct.js @@ -0,0 +1,32 @@ + +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/product/select?${queryParams.toString()}&rows=2000&fl=manufacture_name_s,manufacture_id_i,id,display_name_s,qty_sold_f,qty_sold_f&${sort}`); + 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.display_name_s, + manufacture_name: promotion.manufacture_name_s, + manufacture_id: promotion.manufacture_id_i, + qty_sold: promotion.qty_sold_f, + }; + result.push(data); + } + return result; + };
\ No newline at end of file diff --git a/src/lib/category/components/Category.jsx b/src/lib/category/components/Category.jsx index e6ea5acf..91553295 100644 --- a/src/lib/category/components/Category.jsx +++ b/src/lib/category/components/Category.jsx @@ -2,11 +2,20 @@ import odooApi from '@/core/api/odooApi' import Link from '@/core/components/elements/Link/Link' import DesktopView from '@/core/components/views/DesktopView' import { createSlug } from '@/core/utils/slug' +import { ChevronRightIcon } from '@heroicons/react/24/outline' +import Image from 'next/image' import { useEffect, useState } from 'react' +import PopularBrand from './PopularBrand' +import { bannerApi } from '@/api/bannerApi'; +const { useQuery } = require('react-query') const Category = () => { const [categories, setCategories] = useState([]) + const [openCategories, setOpenCategory] = useState([]); + const [banner, setBanner] = useState([]); + const promotionProgram = useQuery('banner-promo-category-card', bannerApi({ type: 'banner-promo-category-card' })); + useEffect(() => { const loadCategories = async () => { let dataCategories = await odooApi('GET', '/api/v1/category/tree') @@ -26,46 +35,65 @@ const Category = () => { } loadCategories() }, []) - return ( <DesktopView> <div className='category-mega-box'> {categories?.map((category) => ( - <div key={category.id}> + <div key={category.id} className='flex'> <Link href={createSlug('/shop/category/', category.name, category.id)} - className='category-mega-box__parent' + className='category-mega-box__parent flex items-center' > + <div className='mr-2 flex justify-center items-center'> + <Image src={category.image} alt='' width={25} height={25} /> + </div> {category.name} </Link> <div className='category-mega-box__child-wrapper'> - <div className='grid grid-cols-3 gap-x-4 gap-y-6 max-h-full overflow-auto'> + <div className='grid grid-cols-3 gap-x-4 gap-y-6 max-h-full !w-[590px] overflow-auto'> {category.childs.map((child1Category) => ( - <div key={child1Category.id}> + <div key={child1Category.id} className='w-full'> <Link href={createSlug('/shop/category/', child1Category.name, child1Category.id)} - className='category-mega-box__child-one mb-4' + className='category-mega-box__child-one mb-4 w-full h-8 flex justify-center line-clamp-2' > {child1Category.name} </Link> - <div className='flex flex-col gap-y-3'> - {child1Category.childs.map((child2Category) => ( - <Link - href={createSlug( - '/shop/category/', - child2Category.name, - child2Category.id - )} - className='category-mega-box__child-two' - key={child2Category.id} - > - {child2Category.name} - </Link> + <div className='flex flex-col gap-y-3 w-full'> + {child1Category.childs.map((child2Category, index) => ( + (index < 4) && ( + <Link + href={createSlug('/shop/category/', child2Category.name, child2Category.id)} + className='category-mega-box__child-two truncate' + key={child2Category.id} + > + {child2Category.name} + </Link> + ) ))} + {child1Category.childs.length > 5 && ( + <div className='flex hover:bg-gray_r-8/35 rounded-10'> + <Link + href={createSlug('/shop/category/', child1Category.name, child1Category.id)} + className='category-mega-box__child-one flex items-center gap-4 font-bold hover:ml-4' + > + <p className='mt-2 mb-0 text-danger-500 font-semibold'>Lihat Semua</p> + <ChevronRightIcon className='w-4 text-danger-500 font-bold' /> + </Link> + </div> + )} </div> </div> ))} </div> + {/* <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> ))} diff --git a/src/lib/category/components/PopularBrand.jsx b/src/lib/category/components/PopularBrand.jsx new file mode 100644 index 00000000..8124b5b4 --- /dev/null +++ b/src/lib/category/components/PopularBrand.jsx @@ -0,0 +1,96 @@ +import odooApi from '@/core/api/odooApi' +import React, { useEffect, useState } from 'react' +import axios from 'axios'; +import { useQuery } from 'react-query' +import Link from '@/core/components/elements/Link/Link' +import { createSlug } from '@/core/utils/slug' +import Image from 'next/image' +import { ChevronRightIcon } from '@heroicons/react/24/outline' +import useProductSearch from '../../../lib/product/hooks/useProductSearch'; +import { SolrResponse } from "~/types/solr"; +import { fetchPopulerProductSolr } from '../api/popularProduct' + +const SOLR_HOST = process.env.SOLR_HOST + +const PopularBrand = ({ category }) => { + // 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(); + + // 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 + // }); + // } + // } + + // // 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); + // }; + + // // 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; + // } + // } + + // useEffect(() => { + // fetchTopBrands(); + // }, [category]); + + return ( + <div className='flex flex-col'> + {/* <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 + href={createSlug('/shop/brands/', brand.name, brand.id)} + className='category-mega-box__child-one w-8 h-full flex items-center justify-center ' + > + <Image src={`https://erp.indoteknik.com/api/image/x_manufactures/x_logo_manufacture/${brand.id}` } alt={`${brand.name}`} width={104} height={44} objectFit='cover' /> + </Link> + </div> + ))} + </div> */} + {/* {topBrands.length > 8 && ( + <div className='flex hover:bg-gray_r-8/35 rounded-10'> + <Link + href={createSlug('/shop/category/', category.name, category.id)} + className='category-mega-box__child-one flex items-center gap-4 font-bold hover:ml-4' + > + <p className='mt-2 mb-0 text-danger-500 font-semibold'>Lihat Semua Brand</p> + <ChevronRightIcon className='w-4 text-danger-500 font-bold' /> + </Link> + </div> + )} */} + </div> + ) +} + +export default PopularBrand; diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 52f1b3ed..4c7e852f 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -134,6 +134,7 @@ const Checkout = () => { setLoadingVoucher(true); let dataVoucher = await getVoucher(auth?.id, { source: query, + type: 'all,brand', }); SetListVoucher(dataVoucher); @@ -149,40 +150,94 @@ const Checkout = () => { }; const VoucherCode = async (code) => { - const source = 'code=' + code + '&source=' + query; // let dataVoucher = await findVoucher(code, auth.id, query); - let dataVoucher = await getVoucherNew(source); + let dataVoucher = await getVoucher(auth?.id, { + source: query, + code: code, + }); 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); + dataVoucher.forEach((addNewLine) => { + if (addNewLine.applyType !== 'shipping') { + // Mencari voucher dalam listVouchers + let checkList = listVouchers?.findIndex( + (voucher) => voucher.code === addNewLine.code + ); + + if (checkList >= 0) { + if (listVouchers[checkList].canApply) { + ToggleSwitch(addNewLine.code); // Perbaikan: Gunakan code voucher yang benar + SetCodeVoucher(null); + } else { + SetSelisihHargaCode(listVouchers[checkList].differenceToApply); + SetFindVoucher(2); + } + return; // Hentikan eksekusi lebih lanjut pada iterasi ini + } + // Memeriksa apakah subtotal memenuhi syarat minimal pembelian + if (cartCheckout?.subtotal < addNewLine.minPurchaseAmount) { + SetSelisihHargaCode( + currencyFormat( + addNewLine.minPurchaseAmount - cartCheckout?.subtotal + ) + ); + SetFindVoucher(2); + return; + } else { + SetFindVoucher(3); + SetButtonTerapkan(true); + } + + // Tambahkan voucher ke list dan set voucher aktif + SetListVoucher((prevList) => [addNewLine, ...prevList]); + if (addNewLine.canApply) { + SetActiveVoucher(addNewLine.code); + } } else { - SetSelisihHargaCode(listVouchers[checkList].differenceToApply); - SetFindVoucher(2); + // Mencari voucher dalam listVoucherShippings + let checkList = listVoucherShippings?.findIndex( + (voucher) => voucher.code === addNewLine.code + ); + + if (checkList >= 0) { + if (listVoucherShippings[checkList].canApply) { + ToggleSwitch(addNewLine.code); // Perbaikan: Gunakan code voucher yang benar + SetCodeVoucher(null); + } else { + SetSelisihHargaCode( + listVoucherShippings[checkList].differenceToApply + ); + SetFindVoucher(2); + } + return; // Hentikan eksekusi lebih lanjut pada iterasi ini + } + + // Memeriksa apakah subtotal memenuhi syarat minimal pembelian + if (cartCheckout?.subtotal < addNewLine.minPurchaseAmount) { + SetSelisihHargaCode( + currencyFormat( + addNewLine.minPurchaseAmount - cartCheckout?.subtotal + ) + ); + SetFindVoucher(2); + return; + } else { + SetFindVoucher(3); + SetButtonTerapkan(true); + } + + // Tambahkan voucher ke list pengiriman dan set voucher aktif pengiriman + SetListVoucherShipping((prevList) => [addNewLine, ...prevList]); + if (addNewLine.canApply) { + setActiveVoucherShipping(addNewLine.code); + } } - 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); + }); + + // let addNewLine = dataVoucher[0]; }; useEffect(() => { @@ -190,7 +245,7 @@ const Checkout = () => { }, [bottomPopup]); useEffect(() => { - voucher(); + // voucher(); const loadExpedisi = async () => { let dataExpedisi = await ExpedisiList(); dataExpedisi = dataExpedisi.map((expedisi) => ({ @@ -213,13 +268,23 @@ const Checkout = () => { }; }, []); - const hitungDiscountVoucher = (code) => { - let dataVoucherIndex = listVouchers.findIndex( - (voucher) => voucher.code == code - ); - let dataActiveVoucher = listVouchers[dataVoucherIndex]; + const hitungDiscountVoucher = (code, source) => { + let countDiscount = 0; + if (source === 'voucher') { + let dataVoucherIndex = listVouchers.findIndex( + (voucher) => voucher.code == code + ); + let dataActiveVoucher = listVouchers[dataVoucherIndex]; + + countDiscount = dataActiveVoucher.discountVoucher; + } else { + let dataVoucherIndex = listVoucherShippings.findIndex( + (voucher) => voucher.code == code + ); + let dataActiveVoucher = listVoucherShippings[dataVoucherIndex]; - let countDiscount = dataActiveVoucher.discountVoucher; + countDiscount = dataActiveVoucher.discountVoucher; + } /*if (dataActiveVoucher.discountType === 'percentage') { countDiscount = cartCheckout?.subtotal * (dataActiveVoucher.discountAmount / 100) @@ -236,14 +301,24 @@ const Checkout = () => { return countDiscount; }; - useEffect(() => { - if (!listVouchers) return; - if (!activeVoucher) return; + // useEffect(() => { + // if (!listVouchers) return; + // if (!activeVoucher) return; + + // console.log('voucher') + // const countDiscount = hitungDiscountVoucher(activeVoucher, 'voucher'); + + // SetDiscountVoucher(countDiscount); + // }, [activeVoucher, listVouchers]); - const countDiscount = hitungDiscountVoucher(activeVoucher); + // useEffect(() => { + // if (!listVoucherShippings) return; + // if (!activeVoucherShipping) return; - SetDiscountVoucher(countDiscount); - }, [activeVoucher, listVouchers]); + // const countDiscount = hitungDiscountVoucher(activeVoucherShipping, 'voucher_shipping'); + + // SetDiscountVoucherOngkir(countDiscount); + // }, [activeVoucherShipping, listVoucherShippings]); useEffect(() => { if (qVoucher === 'PASTIHEMAT' && listVouchers) { @@ -338,7 +413,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]; @@ -392,14 +467,22 @@ const Checkout = () => { 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; - } - - gtagPurchase(products, biayaKirim, isCheckouted.name); + } else { + gtagPurchase(products, biayaKirim, isCheckouted.name); + + gtag('event', 'conversion', { + send_to: 'AW-954540379/nDymCL3BhaQYENvClMcD', + value: + cartCheckout?.grandTotal + + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000, + currency: 'IDR', + transaction_id: isCheckouted.id, + }); - const midtrans = async () => { for (const product of products) deleteItemCart({ productId: product.id }); if (grandTotal > 0) { const payment = await axios.post( @@ -415,17 +498,25 @@ const Checkout = () => { '-' )}`; } - }; + } - gtag('event', 'conversion', { - send_to: 'AW-954540379/nDymCL3BhaQYENvClMcD', - value: - cartCheckout?.grandTotal + - Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000, - currency: 'IDR', - transaction_id: isCheckouted.id, - event_callback: midtrans, - }); + /* const midtrans = async () => { + for (const product of products) deleteItemCart({ productId: product.id }); + if (grandTotal > 0) { + 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; + } else { + window.location.href = `${ + process.env.NEXT_PUBLIC_SELF_HOST + }/shop/checkout/success?order_id=${isCheckouted.name.replace( + /\//g, + '-' + )}`; + } + };*/ }; const handlingActivateCode = async () => { @@ -486,6 +577,10 @@ const Checkout = () => { const finalShippingAmt = biayaKirim - discShippingAmt; + const totalDiscountVoucher = + cartCheckout?.discountVoucher + + (cartCheckout?.discountVoucherShipping || 0); + return ( <> <BottomPopup @@ -596,10 +691,25 @@ const Checkout = () => { )} <hr className='mt-8 mb-4 border-gray_r-8' /> + {/* {!loadingVoucher && + listVouchers?.length === 1 && + listVoucherShippings?.length === 1} + { + <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> + } */} {listVoucherShippings && listVoucherShippings?.length > 0 && ( <div> - <h3 className='font-semibold mb-4'>Promo Gratis Ongkir</h3> + <h3 className='font-semibold mb-4'> + Promo Extra Potongan Ongkir + </h3> {listVoucherShippings?.map((item) => ( <div key={item.id} className='relative'> <div @@ -734,16 +844,7 @@ const Checkout = () => { <hr className='mt-8 mb-4 border-gray_r-8' /> <div> - {!loadingVoucher && listVouchers?.length === 0 ? ( - <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> - ) : ( + {!loadingVoucher && listVouchers?.length > 0 && ( <h3 className='font-semibold mb-4'> Promo Khusus Untuk {auth?.name} </h3> @@ -935,7 +1036,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> @@ -1007,7 +1108,12 @@ const Checkout = () => { <div className='p-4 flex flex-col gap-y-4'> {!!products && snakecaseKeys(products).map((item, index) => ( - <CartItem key={index} item={item} editable={false} /> + <CartItem + key={index} + item={item} + editable={false} + selfPicking={selectedExpedisi === '1,32' ? true : false} + /> ))} </div> @@ -1070,7 +1176,7 @@ const Checkout = () => { <div className='flex gap-x-2 justify-between'> <div className='text-gray_r-11'>Diskon Voucher</div> <div className='text-danger-500'> - - {currencyFormat(discountVoucher)} + - {currencyFormat(cartCheckout?.discountVoucher)} </div> </div> )} @@ -1086,7 +1192,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'> @@ -1138,10 +1244,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 @@ -1298,7 +1404,12 @@ const Checkout = () => { <div className='flex flex-col gap-y-8 border-t border-gray-300 pt-8'> {!!products && snakecaseKeys(products).map((item, index) => ( - <CartItem key={index} item={item} editable={false} /> + <CartItem + key={index} + item={item} + editable={false} + selfPicking={selectedExpedisi === '1,32' ? true : false} + /> ))} </div> </div> @@ -1365,7 +1476,7 @@ const Checkout = () => { <div className='flex gap-x-2 justify-between'> <div className='text-gray_r-11'>Diskon Voucher</div> <div className='text-danger-500'> - - {currencyFormat(discountVoucher)} + - {currencyFormat(cartCheckout?.discountVoucher)} </div> </div> )} @@ -1382,7 +1493,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'> @@ -1434,10 +1545,10 @@ const Checkout = () => { className='object-contain object-center h-6 w-full rounded-md' /> </span> - {activeVoucher ? ( + {activeVoucher || activeVoucherShipping ? ( <div className=''> <div className='text-left text-sm text-black font-semibold'> - Hemat {currencyFormat(discountVoucher)} + Hemat {currencyFormat(totalDiscountVoucher)} </div> <div className='text-left mt-1 text-green-600 text-xs'> Voucher berhasil digunakan diff --git a/src/lib/home/api/CategoryPilihanApi.js b/src/lib/home/api/CategoryPilihanApi.js new file mode 100644 index 00000000..8a0b38d3 --- /dev/null +++ b/src/lib/home/api/CategoryPilihanApi.js @@ -0,0 +1,8 @@ +import odooApi from '@/core/api/odooApi' + +const categoryPilihanApi = async () => { + const dataCategoryPilihan = await odooApi('GET', '/api/v1/lob_homepage') + return dataCategoryPilihan +} + +export default categoryPilihanApi diff --git a/src/lib/home/api/categoryManagementApi.js b/src/lib/home/api/categoryManagementApi.js new file mode 100644 index 00000000..0aeb2aac --- /dev/null +++ b/src/lib/home/api/categoryManagementApi.js @@ -0,0 +1,40 @@ +export const fetchCategoryManagementSolr = async () => { + let sort ='sort=sequence_i asc'; + try { + const response = await fetch(`/solr/category_management/query?q=*:*&q.op=OR&indent=true&${sort}&&rows=20`); + 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) => { + return promotions.map((promotion) =>{ + let parsedCategories = promotion.categories.map(category => { + // Parse string JSON utama + let parsedCategory = JSON.parse(category); + + // Parse setiap elemen di child_frontend_id_i jika ada + if (parsedCategory.child_frontend_id_i) { + parsedCategory.child_frontend_id_i = parsedCategory.child_frontend_id_i.map(child => JSON.parse(child)); + } + + return parsedCategory; + }); + let productMapped = { + id: promotion.id, + name: promotion.name_s, + image: promotion.image_s, + sequence: promotion.sequence_i, + numFound: promotion.numFound_i, + categories: parsedCategories + }; + return productMapped; + }) + };
\ No newline at end of file diff --git a/src/lib/home/components/CategoryDynamic.jsx b/src/lib/home/components/CategoryDynamic.jsx new file mode 100644 index 00000000..ca104ada --- /dev/null +++ b/src/lib/home/components/CategoryDynamic.jsx @@ -0,0 +1,143 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import {fetchCategoryManagementSolr} 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 { 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 [categoryManagement, setCategoryManagement] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const loadBrand = useCallback(async () => { + setIsLoading(true) + const items = await fetchCategoryManagementSolr(); + + setIsLoading(false) + setCategoryManagement(items) + }, []) + + useEffect(() => { + loadBrand() + }, [loadBrand]) + + // const [categoryData, setCategoryData] = useState({}); + // const [subCategoryData, setSubCategoryData] = useState({}); + + // 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}`); + + // updatedCategoryData[category.categoryIdI] = countLevel1?.numFound; + + // for (const subCategory of countLevel1?.children) { + // updatedSubCategoryData[subCategory.id] = subCategory?.numFound; + // } + // } + + // setCategoryData(updatedCategoryData); + // setSubCategoryData(updatedSubCategoryData); + // } + // }; + + // fetchCategoryData(); + // }, [categoryManagement.isLoading]); + + const swiperBanner = { + modules: [Pagination, ], + classNames:'mySwiper', + slidesPerView: 3, + spaceBetween:10, + pagination: { + dynamicBullets: true, + clickable: true, + } + }; + + return ( + <div> + {categoryManagement && categoryManagement?.map((category) => { + // const countLevel1 = categoryData[category.categoryIdI] || 0; + return ( + <Skeleton key={category.id} isLoaded={!isLoading}> + <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?.id)} 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 ( + <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?.id_level_2)} 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.child_frontend_id_i.map((childCategory) => ( + <div key={childCategory.id} className=''> + <Link href={createSlug('/shop/category/', childCategory?.name, childCategory?.id_level_3)} 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> + ))} + </div> + </div> + </div> + </SwiperSlide> + ); + })} + </Swiper> + </div> + </Skeleton> + ); + })} + </div> + ); +}; + +export default CategoryDynamic; diff --git a/src/lib/home/components/CategoryDynamicMobile.jsx b/src/lib/home/components/CategoryDynamicMobile.jsx new file mode 100644 index 00000000..1061f3e4 --- /dev/null +++ b/src/lib/home/components/CategoryDynamicMobile.jsx @@ -0,0 +1,114 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import NextImage from 'next/image'; +import Link from "next/link"; +import { createSlug } from '@/core/utils/slug'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; +import {fetchCategoryManagementSolr} from '../api/categoryManagementApi' + +const CategoryDynamicMobile = () => { + const [selectedCategory, setSelectedCategory] = useState({}); + const [categoryManagement, setCategoryManagement] = useState([]) + const [isLoading, setIsLoading] = useState(false) + + const loadBrand = useCallback(async () => { + setIsLoading(true) + const items = await fetchCategoryManagementSolr(); + + setIsLoading(false) + setCategoryManagement(items) + }, []) + + useEffect(() => { + loadBrand() + }, [loadBrand]) + + useEffect(() => { + const loadPromo = async () => { + try { + if (categoryManagement?.length > 0) { + const initialSelections = categoryManagement.reduce((acc, category) => { + if (category.categories.length > 0) { + acc[category.id] = category.categories[0].id_level_2; + } + return acc; + }, {}); + setSelectedCategory(initialSelections); + } + } catch (loadError) { + // console.error("Error loading promo items:", loadError); + } + }; + + loadPromo(); + }, [categoryManagement]); + + const handleCategoryLevel2Click = (categoryIdI, idLevel2) => { + setSelectedCategory(prev => ({ + ...prev, + [categoryIdI]: idLevel2 + })); + }; + + return ( + <div className='p-4'> + {categoryManagement && categoryManagement?.map((category) => ( + <div key={category.id}> + <div className='bagian-judul flex flex-row justify-between items-center gap-3 mb-4 mt-4'> + <div className='font-semibold sm:text-h-sm mr-2'>{category?.name}</div> + <Link href={createSlug('/shop/category/', category?.name, category?.id)} className="!text-red-500 font-semibold text-sm">Lihat Semua</Link> + </div> + <Swiper slidesPerView={2.3} spaceBetween={10}> + {category.categories.map((index) => ( + <SwiperSlide key={index.id}> + <div + onClick={() => handleCategoryLevel2Click(category.id, index?.id_level_2)} + className={`border flex justify-start items-center max-w-48 max-h-16 rounded ${selectedCategory[category.id] === index?.id_level_2 ? 'bg-red-50 border-red-500 text-red-500' : 'border-gray-200 text-gray-900'}`} + > + <div className='p-1 flex justify-start items-center'> + <div className='flex flex-row justify-center items-center'> + <NextImage + src={index.image ? index.image : "/images/noimage.jpeg"} + alt={index.name} + width={30} + height={30} + 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> + </div> + </div> + </div> + </div> + </SwiperSlide> + ))} + </Swiper> + <div className='p-3 mt-2 border'> + <div className='grid grid-cols-2 gap-2 overflow-y-auto max-h-[240px]'> + {category.categories.map((index) => ( + selectedCategory[category.id] === index?.id_level_2 && index?.child_frontend_id_i.map((x) => ( + <div key={x.id}> + <Link href={createSlug('/shop/category/', x?.name, x?.id_level_3)} className="flex flex-row gap-1 border rounded group hover:border-red-500"> + <NextImage + src={x.image ? x.image : "/images/noimage.jpeg"} + 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> + </div> + </Link> + </div> + )) + ))} + </div> + </div> + </div> + ))} + </div> + ); +}; + +export default CategoryDynamicMobile; diff --git a/src/lib/home/components/CategoryPilihan.jsx b/src/lib/home/components/CategoryPilihan.jsx new file mode 100644 index 00000000..6dbf771e --- /dev/null +++ b/src/lib/home/components/CategoryPilihan.jsx @@ -0,0 +1,123 @@ +import Image from 'next/image' +import useCategoryHome from '../hooks/useCategoryHome' +import Link from '@/core/components/elements/Link/Link' +import { createSlug } from '@/core/utils/slug' +import { useEffect, useState } from 'react'; +import { bannerApi } from '../../../api/bannerApi'; +const { useQuery } = require('react-query') +import { HeroBannerSkeleton } from '../../../components/skeleton/BannerSkeleton'; +import useCategoryPilihan from '../hooks/useCategoryPilihan'; +import useDevice from '@/core/hooks/useDevice' +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; + +const CategoryPilihan = ({ id, categories }) => { + const { isDesktop, isMobile } = useDevice() + const { categoryPilihan } = useCategoryPilihan(); + const heroBanner = useQuery('categoryPilihan', bannerApi({ type: 'banner-category-list' })); + return ( + categoryPilihan.length > 0 && ( + <section> + {isDesktop && ( + <div> + <div className='flex flex-row items-center mb-4'> + <div className='font-semibold sm:text-h-lg mr-2'>LOB Kategori Pilihan</div> + <p className='text-gray_r-10 text-sm'>200 Rb+ Produk Unggulan & 800+ Brand Rekomendasi tersedia!</p> + </div> + {heroBanner.data && + heroBanner.data?.length > 0 && ( + <div className='flex w-full h-full justify-center mb-4 bg-cover bg-center'> + <Link key={heroBanner.data[0].id} href={heroBanner.data[0].url}> + <Image + width={1260} + height={170} + quality={100} + src={heroBanner.data[0].image} + alt={heroBanner.data[0].name} + className='h-full object-cover w-full' + /> + </Link> + </div> + )} + <div className="group/item grid grid-cols-6 gap-y-2 w-full h-full col-span-2 "> + {categoryPilihan?.data?.map((category) => ( + <div key={category.id} className="KartuInti h-48 w-60 max-w-sm lg:max-w-full flex flex-col border-[1px] border-gray-200 relative group"> + <div className='KartuB absolute h-48 w-60 inset-0 flex items-center justify-center '> + <div className="group/edit flex items-center justify-end h-48 w-60 flex-col group-hover/item:visible"> + <div className=' h-36 flex justify-end items-end'> + <Image className='group-hover:scale-105 transition-transform duration-300 ' src={category?.image? category?.image : '/images/noimage.jpeg'} width={120} height={120} alt={category?.name} /> + </div> + <h2 className="text-gray-700 content-center h-12 border-t-[1px] px-1 w-60 border-gray-200 font-normal text-sm text-center">{category?.industries}</h2> + </div> + </div> + <div className='KartuA relative inset-0 flex h-36 w-60 items-center justify-center opacity-0 group-hover:opacity-75 group-hover:bg-[#E20613] transition-opacity '> + <Link + href={createSlug('/shop/lob/', category?.industries, category?.id)} + className='category-mega-box__parent text-white rounded-lg' + > + Lihat semua + </Link> + </div> + </div> + ))} + </div> + </div> + )} + {isMobile && ( + <div className='p-4'> + <div className='flex flex-row items-center mb-4'> + <div className='font-semibold sm:text-h-md mr-2'>LOB Kategori Pilihan</div> + {/* <p className='text-gray_r-10 text-sm'>200 Rb+ Produk Unggulan & 800+ Brand Rekomendasi tersedia!</p> */} + </div> + <div className='flex'> + {heroBanner.data && + heroBanner.data?.length > 0 && ( + <div className=' object-fill '> + <Link key={heroBanner.data[0].id} href={heroBanner.data[0].url}> + <Image + width={439} + height={150} + quality={100} + src={heroBanner.data[0].image} + alt={heroBanner.data[0].name} + className='object-cover' + /> + </Link> + </div> + )} + </div> + <Swiper slidesPerView={2.1} spaceBetween={10}> + {categoryPilihan?.data?.map((category) => ( + <SwiperSlide key={category.id}> + <div key={category.id} className="KartuInti mt-2 h-48 w-48 max-w-sm lg:max-w-full flex flex-col border-[1px] border-gray-200 relative group"> + <div className='KartuB absolute h-48 w-48 inset-0 flex items-center justify-center '> + <div className="group/edit flex items-center justify-end h-48 w-48 flex-col group-hover/item:visible"> + <div className=' h-36 flex justify-end items-end'> + <Image className='group-hover:scale-105 transition-transform duration-300 ' src={category?.image? category?.image : '/images/noimage.jpeg'} width={120} height={120} alt={category?.name} /> + </div> + <h2 className="text-gray-700 content-center h-12 border-t-[1px] px-1 w-48 border-gray-200 font-normal text-sm text-center">{category?.industries}</h2> + </div> + </div> + <div className='KartuA relative inset-0 flex h-36 w-48 items-center justify-center opacity-0 group-hover:opacity-75 group-hover:bg-[#E20613] transition-opacity '> + <Link + href={createSlug('/shop/lob/', category?.industries, category?.id)} + className='category-mega-box__parent text-white rounded-lg' + > + Lihat semua + </Link> + </div> + </div> + </SwiperSlide> + ))} + + </Swiper> + + </div> + )} + </section> + + ) + ) +} + +export default CategoryPilihan diff --git a/src/lib/home/components/PreferredBrand.jsx b/src/lib/home/components/PreferredBrand.jsx index 6b64a444..eefced60 100644 --- a/src/lib/home/components/PreferredBrand.jsx +++ b/src/lib/home/components/PreferredBrand.jsx @@ -1,4 +1,5 @@ import { Swiper, SwiperSlide } from 'swiper/react' +import { Navigation, Pagination, Autoplay } from 'swiper'; import { useCallback, useEffect, useState } from 'react' import usePreferredBrand from '../hooks/usePreferredBrand' import PreferredBrandSkeleton from './Skeleton/PreferredBrandSkeleton' @@ -8,7 +9,7 @@ import Link from '@/core/components/elements/Link/Link' import axios from 'axios' const PreferredBrand = () => { - let query = 'level_s' + let query = '' let params = 'prioritas' const [isLoading, setIsLoading] = useState(true) const [startWith, setStartWith] = useState(null) @@ -17,7 +18,7 @@ const PreferredBrand = () => { const loadBrand = useCallback(async () => { setIsLoading(true) const name = startWith ? `${startWith}*` : '' - const result = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/brands?params=${name}`) + const result = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/brands?rows=20`) setIsLoading(false) setManufactures((manufactures) => [...result.data]) @@ -34,38 +35,51 @@ const PreferredBrand = () => { useEffect(() => { loadBrand() - }, [loadBrand]) + }, []) - const { preferredBrands } = usePreferredBrand(query) + // const { preferredBrands } = usePreferredBrand(query) const { isMobile, isDesktop } = useDevice() - + const swiperBanner = { + modules:[Navigation, Pagination, Autoplay], + autoplay: { + delay: 4000, + disableOnInteraction: false + }, + loop: true, + className: 'h-[70px] md:h-[100px] w-full', + slidesPerView: isMobile ? 4 : 8, + spaceBetween: isMobile ? 12 : 0, + pagination: { + dynamicBullets: true, + dynamicMainBullets: isMobile ? 6 : 8, + clickable: true, + } + } + const preferredBrandsData = manufactures ? manufactures.slice(0, 20) : [] return ( <div className='px-4 sm:px-0'> <div className='flex justify-between items-center mb-4'> - <div className='font-semibold sm:text-h-lg'>Brand Pilihan</div> + <h1 className='font-semibold text-[14px] sm:text-h-lg'><Link href='/shop/brands' className='!text-black font-semibold'>Brand Pilihan</Link></h1> {isDesktop && ( <Link href='/shop/brands' className='!text-red-500 font-semibold'> 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 />} + {!manufactures.isLoading && ( + <Swiper {...swiperBanner}> + {preferredBrandsData.map((manufacture) => ( + <SwiperSlide key={manufacture.id}> + <BrandCard brand={manufacture} /> + </SwiperSlide> + ))} + </Swiper> )} </div> - {manufactures.isLoading && <PreferredBrandSkeleton />} - {!manufactures.isLoading && ( - <Swiper slidesPerView={isMobile ? 3.5 : 7.5} spaceBetween={isMobile ? 12 : 24} freeMode> - {manufactures.map((manufacture) => ( - <SwiperSlide key={manufacture.id}> - <BrandCard brand={manufacture} /> - </SwiperSlide> - ))} - </Swiper> - )} </div> ) } -export default PreferredBrand +export default PreferredBrand
\ No newline at end of file diff --git a/src/lib/home/components/PromotionProgram.jsx b/src/lib/home/components/PromotionProgram.jsx index c2f76069..ae8d5d6f 100644 --- a/src/lib/home/components/PromotionProgram.jsx +++ b/src/lib/home/components/PromotionProgram.jsx @@ -16,7 +16,7 @@ const BannerSection = () => { return ( <div className='px-4 sm:px-0'> <div className='flex justify-between items-center mb-4 '> - <div className='font-semibold sm:text-h-lg'>Promo Tersedia</div> + <h1 className='font-semibold text-[14px] sm:text-h-lg'> <Link href='/shop/promo' className='!text-black font-semibold' >Promo Tersedia</Link></h1> {isDesktop && ( <Link href='/shop/promo' className='!text-red-500 font-semibold'> Lihat Semua diff --git a/src/lib/home/hooks/useCategoryPilihan.js b/src/lib/home/hooks/useCategoryPilihan.js new file mode 100644 index 00000000..12a86f7e --- /dev/null +++ b/src/lib/home/hooks/useCategoryPilihan.js @@ -0,0 +1,13 @@ +import categoryPilihanApi from '../api/CategoryPilihanApi' +import { useQuery } from 'react-query' + +const useCategoryPilihan = () => { + const fetchCategoryPilihan = async () => await categoryPilihanApi() + const { isLoading, data } = useQuery('categoryPilihanApi', fetchCategoryPilihan) + + return { + categoryPilihan: { data, isLoading } + } +} + +export default useCategoryPilihan
\ No newline at end of file diff --git a/src/lib/lob/components/Breadcrumb.jsx b/src/lib/lob/components/Breadcrumb.jsx new file mode 100644 index 00000000..5722fd39 --- /dev/null +++ b/src/lib/lob/components/Breadcrumb.jsx @@ -0,0 +1,55 @@ +import odooApi from '@/core/api/odooApi' +import { createSlug } from '@/core/utils/slug' +import { + Breadcrumb as ChakraBreadcrumb, + BreadcrumbItem, + BreadcrumbLink, + Skeleton +} from '@chakra-ui/react' +import Link from 'next/link' +import React from 'react' +import { useQuery } from 'react-query' + +/** + * Render a breadcrumb component. + * + * @param {object} categoryId - The ID of the category. + * @return {JSX.Element} The breadcrumb component. + */ +const Breadcrumb = ({ categoryId }) => { + const breadcrumbs = useQuery( + `lob-breadcrumbs/${categoryId}`, + async () => await odooApi('GET', `/api/v1/lob_homepage/${categoryId}/category_id`) + ) + return ( + <div className='container mx-auto py-4 md:py-6'> + <Skeleton isLoaded={!breadcrumbs.isLoading} className='w-2/3'> + <ChakraBreadcrumb> + <BreadcrumbItem> + <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'> + Home + </BreadcrumbLink> + </BreadcrumbItem> + + {breadcrumbs?.data?.map((category, index) => ( + <BreadcrumbItem key={index} isCurrentPage={index === breadcrumbs.data.length - 1}> + {index === breadcrumbs.data.length - 1 ? ( + <BreadcrumbLink className='whitespace-nowrap'>{category.industries}</BreadcrumbLink> + ) : ( + <BreadcrumbLink + as={Link} + href={createSlug('/shop/lob/', category.industries, category.id)} + className='!text-danger-500 whitespace-nowrap' + > + {category.industries} + </BreadcrumbLink> + )} + </BreadcrumbItem> + ))} + </ChakraBreadcrumb> + </Skeleton> + </div> + ) +} + +export default Breadcrumb 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/api/productSimilarApi.js b/src/lib/product/api/productSimilarApi.js index ecd6724a..51cc17fa 100644 --- a/src/lib/product/api/productSimilarApi.js +++ b/src/lib/product/api/productSimilarApi.js @@ -19,7 +19,7 @@ const productSimilarApi = async ({ query, source }) => { } } const dataProductSimilar = await axios( - `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?q=${query}&page=1&orderBy=popular-weekly&operation=OR&priceFrom=1` + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?q=${query}&page=1&orderBy=popular-weekly&operation=OR&priceFrom=1&source=similar` ) if (dataflashSale) { dataProductSimilar.data.response.products = [ diff --git a/src/lib/product/components/CategorySection.jsx b/src/lib/product/components/CategorySection.jsx new file mode 100644 index 00000000..a287fa78 --- /dev/null +++ b/src/lib/product/components/CategorySection.jsx @@ -0,0 +1,105 @@ +import Image from "next/image"; +import Link from 'next/link'; +import { createSlug } from '@/core/utils/slug'; +import useDevice from '@/core/hooks/useDevice'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; +import { useQuery } from 'react-query'; +import { useRouter } from 'next/router'; +import { + ChevronDownIcon, + ChevronUpIcon, // Import ChevronUpIcon for toggling + DocumentCheckIcon, + HeartIcon, +} from '@heroicons/react/24/outline'; +import { useState } from 'react'; // Import useState +import { getIdFromSlug } from '@/core/utils/slug' + +const CategorySection = ({ categories }) => { + const { isDesktop, isMobile } = useDevice(); + const [isOpenCategory, setIsOpenCategory] = useState(false); // State to manage category visibility + + const handleToggleCategories = () => { + setIsOpenCategory(!isOpenCategory); + }; + + + const displayedCategories = isOpenCategory ? categories : categories.slice(0, 10); + + return ( + <section> + {isDesktop && ( + <div className="group/item grid grid-cols-5 gap-y-2 gap-x-2 w-full h-full col-span-2 "> + {displayedCategories.map((category) => ( + <Link href={createSlug('/shop/category/', category?.name, category?.id)} key={category?.id} passHref> + <div className="group transition-colors duration-300 "> + <div className="KartuInti h-12 w-26 max-w-sm lg:max-w-full flex flex-col border-[2px] border-gray-200 group-hover:border-red-400 rounded relative "> + <div className="flex items-center justify-start h-full px-1 flex-row "> + <Image className="h-full p-1" src={category?.image1920 ? category?.image1920 : '/images/noimage.jpeg'} width={56} height={48} alt={category?.name} /> + <h2 className="text-gray-700 group-hover:text-[#E20613] line-clamp-2 content-center h-fit w-60 px-1 font-semibold text-sm text-start">{category?.name}</h2> + </div> + </div> + </div> + </Link> + ))} + </div> + )} + {isDesktop && categories.length > 10 && ( + <div className="w-full flex justify-center mt-4"> + <button + onClick={handleToggleCategories} + className="flex justify-end mt-4 text-red-500 font-bold px-4 py-2 rounded" + > + {isOpenCategory ? 'Sembunyikan' : 'Lihat semua'} + {isOpenCategory ? ( + <ChevronUpIcon className="ml-auto w-5 font-bold" /> + ) : ( + <ChevronDownIcon className="ml-auto w-5 font-bold" /> + )} + </button> + </div> + )} + + {isMobile && ( + <div className="py-4"> + <Swiper slidesPerView={2.3} spaceBetween={10}> + {categories.map((category) => ( + <SwiperSlide key={category?.id}> + <Link href={createSlug('/shop/category/', category?.name, category?.id)} passHref> + <div className="group transition-colors duration-300"> + <div className="KartuInti min-h-16 max-h-16 w-26 max-w-sm lg:max-w-full flex flex-col border-[2px] border-gray-200 group-hover:bg-red-200 group-hover:border-red-400 rounded relative"> + <div className="flex items-center justify-center h-full px-1 flex-row"> + <Image + src={category?.image1920 ? category?.image1920 : '/images/noimage.jpeg'} + width={56} + height={48} + alt={category?.name} + className="p-3" + /> + <h2 className="text-gray-700 group-hover:text-[#E20613] line-clamp-2 content-center h-fit w-60 px-1 font-semibold text-sm text-start"> + {category?.name} + </h2> + </div> + </div> + </div> + </Link> + </SwiperSlide> + ))} + </Swiper> + {/* {categories.length > 10 && ( + <div className="w-full flex justify-end mt-4"> + <button + onClick={handleToggleCategories} + className="flex justify-end mt-4 bg-red-500 text-white text-sm px-4 py-2 rounded" + > + {isOpenCategory ? 'Sembunyikan Semua' : 'Lihat Semua'} + </button> + </div> + )} */} + </div> + )} + </section> + ) +} + +export default CategorySection diff --git a/src/lib/product/components/LobSectionCategory.jsx b/src/lib/product/components/LobSectionCategory.jsx new file mode 100644 index 00000000..5cd467e9 --- /dev/null +++ b/src/lib/product/components/LobSectionCategory.jsx @@ -0,0 +1,80 @@ +import Image from "next/image"; +import Link from 'next/link'; +import { createSlug } from '@/core/utils/slug'; +import useDevice from '@/core/hooks/useDevice'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; +import { useQuery } from 'react-query'; +import { useRouter } from 'next/router'; +import { + ChevronDownIcon, + ChevronUpIcon, // Import ChevronUpIcon for toggling + DocumentCheckIcon, + HeartIcon, +} from '@heroicons/react/24/outline'; +import { useState } from 'react'; // Import useState +import { getIdFromSlug } from '@/core/utils/slug' + +const LobSectionCategory = ({ categories }) => { + const { isDesktop, isMobile } = useDevice(); + const [isOpenCategory, setIsOpenCategory] = useState(false); // State to manage category visibility + + const handleToggleCategories = () => { + setIsOpenCategory(!isOpenCategory); + }; + + const displayedCategories = categories[0]?.categoryIds; + return ( + <section> + {isDesktop && ( + <div className="group/item grid grid-flow-col gap-y-2 gap-x-4 w-full h-full"> + {displayedCategories?.map((category) => ( + <Link + href={createSlug('/shop/category/', category?.name, category?.id)} + key={category?.id} + passHref + className="block hover:scale-105 transition-transform duration-300 bg-cover bg-center h-[144px]" + style={{ + backgroundImage: `url('${category?.image ? category?.image : 'https://erp.indoteknik.com/web/image?model=x_banner.banner&id=5&field=x_banner_image&unique=09202023100557'}')`, + }} + > + </Link> + ))} + </div> + )} + + {isMobile && ( + <div className="py-4"> + <Swiper slidesPerView={1.2} spaceBetween={10}> + {displayedCategories?.map((category) => ( + <SwiperSlide key={category?.id}> + <Link + href={createSlug('/shop/category/', category?.name, category?.id)} + key={category?.id} + passHref + className="block bg-cover bg-center h-[144px]" + style={{ + backgroundImage: `url('${category?.image ? category?.image : 'https://erp.indoteknik.com/web/image?model=x_banner.banner&id=5&field=x_banner_image&unique=09202023100557'}')`, + }} + > + </Link> + </SwiperSlide> + ))} + </Swiper> + {categories.length > 10 && ( + <div className="w-full flex justify-end mt-4"> + <button + onClick={handleToggleCategories} + className="flex justify-end mt-4 bg-red-500 text-white text-sm px-4 py-2 rounded" + > + {isOpenCategory ? 'Sembunyikan Semua' : 'Lihat Semua'} + </button> + </div> + )} + </div> + )} + </section> + ) +} + +export default LobSectionCategory diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx index 35e2a665..22ce0533 100644 --- a/src/lib/product/components/ProductCard.jsx +++ b/src/lib/product/components/ProductCard.jsx @@ -146,13 +146,22 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { )} </Link> <div className='p-2 sm:p-3 pb-3 text-caption-2 sm:text-body-2 leading-5'> - {product?.manufacture?.name ? ( - <Link href={URL.manufacture} className='mb-1'> - {product.manufacture.name} + <div className='flex justify-between '> + {product?.manufacture?.name ? ( + <Link href={URL.manufacture} className='mb-1 mt-1'> + {product.manufacture.name} + </Link> + ) : ( + <div>-</div> + )} + {product?.isInBu && ( + <Link href='/panduan-pick-up-service' className='group'> + <Image src='/images/PICKUP-NOW.png' className='group-hover:scale-105 transition-transform duration-200' alt='pickup now' width={90} height={12} /> </Link> - ) : ( - <div>-</div> - )} + + + )} + </div> <Link href={URL.product} className={`mb-2 !text-gray_r-12 leading-6 block line-clamp-3 h-[64px]`} @@ -292,9 +301,18 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { </div> )} {product?.manufacture?.name ? ( - <Link href={URL.manufacture} className='mb-1'> + <div className='flex justify-between'> + <Link href={URL.manufacture} className='mb-1'> {product.manufacture.name} </Link> + {/* {product?.is_in_bu && ( + <div className='bg-red-500 rounded'> + <span className='p-[6px] text-xs text-white'> + Click & Pickup + </span> + </div> + )} */} + </div> ) : ( <div>-</div> )} diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx index a8073036..73fecab5 100644 --- a/src/lib/product/components/ProductFilterDesktop.jsx +++ b/src/lib/product/components/ProductFilterDesktop.jsx @@ -22,6 +22,7 @@ import { formatCurrency } from '@/core/utils/formatValue' const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = null }) => { + const router = useRouter() const { query } = router const [order, setOrder] = useState(query?.orderBy) @@ -107,7 +108,11 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu const slug = Array.isArray(router.query.slug) ? router.query.slug[0] : router.query.slug; if (slug) { - router.push(`${prefixUrl}/${slug}?${params}`) + if(prefixUrl.includes('category') || prefixUrl.includes('lob')){ + router.push(`${prefixUrl}?${params}`) + }else{ + router.push(`${prefixUrl}/${slug}?${params}`) + } } else { router.push(`${prefixUrl}?${params}`) } diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index fb9017f4..1edc31c9 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -26,6 +26,10 @@ import ProductSearchSkeleton from './Skeleton/ProductSearchSkeleton'; import SideBanner from '~/modules/side-banner'; import FooterBanner from '~/modules/footer-banner'; +import CategorySection from './CategorySection'; +import LobSectionCategory from './LobSectionCategory'; +import { getIdFromSlug } from '@/core/utils/slug' +import { data } from 'autoprefixer'; const ProductSearch = ({ query, @@ -37,11 +41,108 @@ const ProductSearch = ({ const { page = 1 } = query; const [q, setQ] = useState(query?.q || '*'); const [search, setSearch] = useState(query?.q || '*'); - const [limit, setLimit] = useState(query?.limit || 30); + const [limit, setLimit] = useState(router.query?.limit || 30); const [orderBy, setOrderBy] = useState(router.query?.orderBy); + const [finalQuery, setFinalQuery] = useState({}); + const [queryFinal, setQueryFinal] = useState({}); + const [dataCategoriesProduct, setDataCategoriesProduct] = useState([]) + const [dataCategoriesLob, setDataCategoriesLob] = useState([]) + const categoryId = getIdFromSlug(prefixUrl) + const [data, setData] = useState([]) + const [dataLob, setDataLob] = useState([]); if (defaultBrand) query.brand = defaultBrand.toLowerCase(); + const dataIdCategories = [] + useEffect(() => { + if(prefixUrl.includes('category')){ + const loadProduct = async () => { + const getCategoriesId = await odooApi('GET', `/api/v1/category/numFound?parent_id=${categoryId}`); + if (getCategoriesId) { + setDataCategoriesProduct(getCategoriesId); + } + }; + loadProduct(); + }else if(prefixUrl.includes('lob')){ + const loadProduct = async () => { + const lobData = await odooApi('GET', `/api/v1/lob_homepage/${categoryId}/category_id`); + + if (lobData) { + setDataLob(lobData); + } + }; + loadProduct(); + + } + }, [categoryId]); + + const collectIds = (category) => { + const ids = []; + function recurse(cat) { + if (cat && cat.id) { + ids.push(cat.id); + } + if (Array.isArray(cat.children)) { + cat.children.forEach(recurse); + } + } + recurse(category); + return ids; + }; + useEffect(() => { + if(prefixUrl.includes('category')){ + const ids = collectIds(dataCategoriesProduct); + const newQuery = { + fq: `category_id_ids:(${ids.join(' OR ')})`, + page : router.query.page? router.query.page : 1, + brand : router.query.brand? router.query.brand : '', + category : router.query.category? router.query.category : '', + priceFrom : router.query.priceFrom? router.query.priceFrom : '', + priceTo : router.query.priceTo? router.query.priceTo : '', + limit : router.query.limit? router.query.limit : '', + orderBy : router.query.orderBy? router.query.orderBy : '' + }; + setFinalQuery(newQuery); + } else if (prefixUrl.includes('lob')){ + + const fetchCategoryData = async () => { + if (dataLob[0]?.categoryIds) { + + for (const cate of dataLob[0].categoryIds) { + + dataIdCategories.push(cate.childId) + } + + + const mergedArray = dataIdCategories.flat(); + + const newQuery = { + fq: `category_id_ids:(${mergedArray.join(' OR ')})`, + category : router.query.category? router.query.category : '', + page : router.query.page? router.query.page : 1, + brand : router.query.brand? router.query.brand : '', + priceFrom : router.query.priceFrom? router.query.priceFrom : '', + priceTo : router.query.priceTo? router.query.priceTo : '', + limit : router.query.limit? router.query.limit : '', + orderBy : router.query.orderBy? router.query.orderBy : '' + }; + + setFinalQuery(newQuery); + + } + }; + fetchCategoryData(); + } + }, [dataCategoriesProduct, dataLob]); + + useEffect(() => { + if (prefixUrl.includes('category') || prefixUrl.includes('lob')) { + setQueryFinal({ ...finalQuery, q, limit, orderBy }); + } else { + setQueryFinal({ ...query, q, limit, orderBy }); + } + }, [prefixUrl,dataCategoriesProduct, query, finalQuery]); + const { productSearch } = useProductSearch({ - query: { ...query, q, limit, orderBy }, + query: queryFinal, operation: 'AND', }); const [products, setProducts] = useState(null); @@ -53,22 +154,24 @@ const ProductSearch = ({ const numRows = [30, 50, 80, 100]; const [brandValues, setBrand] = useState( !router.pathname.includes('brands') - ? query.brand - ? query.brand.split(',') + ? router.query.brand + ? router.query.brand.split(',') : [] : [] ); const [categoryValues, setCategory] = useState( - query?.category?.split(',') || [] + router.query?.category?.split(',') || router.query?.category?.split(',') ); - const [priceFrom, setPriceFrom] = useState(query?.priceFrom || null); - const [priceTo, setPriceTo] = useState(query?.priceTo || null); + + const [priceFrom, setPriceFrom] = useState(router.query?.priceFrom || null); + const [priceTo, setPriceTo] = useState(router.query?.priceTo || null); const pageCount = Math.ceil(productSearch.data?.response.numFound / limit); const productStart = productSearch.data?.responseHeader.params.start; const productRows = limit; const productFound = productSearch.data?.response.numFound; - + const [dataCategories, setDataCategories] = useState([]) + useEffect(() => { if (productFound == 0 && query.q && !spellings) { searchSpellApi({ query: query.q }).then((response) => { @@ -98,7 +201,7 @@ const ProductSearch = ({ }); } }, [productFound, query, spellings]); - + let id = [] useEffect(() => { const checkIfBrand = async () => { const brand = await axios( @@ -115,6 +218,21 @@ const ProductSearch = ({ checkIfBrand(); } }, [q]); + + useEffect(() => { + if(prefixUrl.includes('category')){ + const loadCategories = async () => { + const getCategories = await odooApi('GET', `/api/v1/category/child?parent_id=${categoryId}`) + if(getCategories){ + setDataCategories(getCategories) + } + } + loadCategories() + } + }, []) + + + const brands = []; for ( @@ -224,7 +342,7 @@ const ProductSearch = ({ q: router.query.q, orderBy: orderBy, brand: brandValues.join(','), - category: categoryValues.join(','), + category: categoryValues?.join(','), priceFrom, priceTo, }; @@ -264,6 +382,7 @@ const ProductSearch = ({ const isNotReadyStockPage = router.asPath !== '/shop/search?orderBy=stock'; + console.log('is spelling', spellings); return ( <> <MobileView> @@ -324,6 +443,8 @@ const ProductSearch = ({ SpellingComponent )} </div> + <LobSectionCategory categories={dataLob}/> + <CategorySection categories={dataCategories}/> {productFound > 0 && ( <div className='flex items-center gap-x-2 mb-5 justify-between'> @@ -364,6 +485,7 @@ const ProductSearch = ({ pageCount={pageCount} currentPage={parseInt(page)} url={`${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} + // url={prefixUrl.includes('category') || prefixUrl.includes('lob')? `${prefixUrl}?${toQuery(_.omit(finalQuery, ['page']))}` : `${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} className='mt-6 mb-2' /> @@ -412,7 +534,10 @@ const ProductSearch = ({ <SideBanner /> </div> + <div className='w-9/12 pl-6'> + <LobSectionCategory categories={dataLob}/> + <CategorySection categories={dataCategories}/> {bannerPromotionHeader && bannerPromotionHeader?.image && ( <div className='mb-3'> <Image @@ -451,7 +576,7 @@ const ProductSearch = ({ /> <div className='flex justify-between items-center mb-5'> <div className='leading-6 text-gray_r-11'> - {!spellings ? ( + {spellings?.length < 1 || !spellings ? ( <> Menampilkan {pageCount > 1 ? ( @@ -550,6 +675,7 @@ const ProductSearch = ({ pageCount={pageCount} currentPage={parseInt(page)} url={`${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} + // url={prefixUrl.includes('category') || prefixUrl.includes('lob')? `${prefixUrl}?${toQuery(_.omit(finalQuery, ['page']))}` : `${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} className='!justify-end' /> </div> 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; diff --git a/src/lib/tracking-order/api/trackingOrder.js b/src/lib/tracking-order/api/trackingOrder.js new file mode 100644 index 00000000..cc48c40c --- /dev/null +++ b/src/lib/tracking-order/api/trackingOrder.js @@ -0,0 +1,8 @@ +import odooApi from "@/core/api/odooApi"; + +export const trackingOrder = async ({query}) => { + const params = new URLSearchParams(query).toString(); + const list = await odooApi('GET', `/api/v1/tracking_order?${params}`) + + return list; +} diff --git a/src/lib/tracking-order/component/TrackingOrder.jsx b/src/lib/tracking-order/component/TrackingOrder.jsx new file mode 100644 index 00000000..394979c1 --- /dev/null +++ b/src/lib/tracking-order/component/TrackingOrder.jsx @@ -0,0 +1,139 @@ +import { yupResolver } from '@hookform/resolvers/yup' +import React, { useEffect, useState } from 'react' +import { useForm } from 'react-hook-form' +import * as Yup from 'yup' +import Manifest from '@/lib/treckingAwb/component/Manifest' +import { trackingOrder } from '../api/trackingOrder' +import { useQuery } from 'react-query' +import { Spinner } from '@chakra-ui/react'; +import { Search } from 'lucide-react'; +import whatsappUrl from '@/core/utils/whatsappUrl'; +import Link from 'next/link' + +const TrackingOrder = () => { + const [idAWB, setIdAWB] = useState(null) + const [inputQuery, setInputQuery] = useState(null) + const [buttonClick, setButtonClick] = useState(false) + const [apiError, setApiError] = useState(null) // State to store API error message + + const closePopup = () => { + setIdAWB(null) + setButtonClick(false) + setInputQuery(null) + setApiError(null) // Reset error message on close + } + + const { + register, + handleSubmit, + formState: { errors }, + control, + reset + } = useForm({ + resolver: yupResolver(validationSchema), + defaultValues + }) + + const query = { + email: inputQuery?.email, + so: inputQuery?.id + } + + const { data: tracking, isLoading, isError, error } = useQuery( + ['tracking', query], + () => trackingOrder({ query: query }), + { + enabled: !!query.email && !!query.so, + onSuccess: (data) => { + if (buttonClick) { + if (data?.code === 403 || data?.code === 400 || data?.code === 404) { + setApiError(data?.description); + } else if (data?.pickings?.length > 0) { + setIdAWB(data.pickings[0]?.id); + } else { + setApiError('No pickings data available'); + } + setButtonClick(false); + setInputQuery(null); + } + }, + } + ); + + const onSubmitHandler = async (values) => { + setInputQuery(values) + setButtonClick(true) + } + + return ( + <div className='container mx-auto flex py-10 flex-col'> + <h1 className='text-h-sm md:text-title-sm font-semibold mb-6'>Tracking Order</h1> + <div className='flex justify-start items-start'> + <span className='text-base w-full'> + {`Untuk melacak pesanan Anda, masukkan Nomor Transaksi di kotak bawah ini dan masukkan Email login anda lalu tekan tombol "Lacak". Nomor Transaksi ini dapat Anda lihat dalam menu `} + <Link href='/my/transactions' className='text-red-500'> + Daftar Transaksi + </Link> + {`. Jika mengalami kesulitan `} + <Link href='https://wa.me/6281717181922' target='_blank' rel='noreferrer' className='text-red-500'> + hubungi kami + </Link> + {`.`} + </span> + </div> + <div> + <form onSubmit={handleSubmit(onSubmitHandler)} className='flex mt-4 flex-row w-full '> + <div className='w-[90%] grid grid-cols-2 gap-4'> + <div className='flex flex-col '> + <label className='form-label mb-2'>ID Pesanan*</label> + <input + {...register('id')} + placeholder='dapat dilihat pada email konfirmasi anda' + type='text' + className='form-input mb-2' + aria-invalid={errors.id?.message} + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.id?.message}</div> + </div> + <div className='flex flex-col '> + <label className='form-label mb-2'>Email Penagihan*</label> + <input + {...register('email')} + placeholder='Email yang anda gunakan saat pembayaran' + type='text' + className='form-input' + aria-invalid={errors.email?.message} + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.email?.message}</div> + </div> + </div> + <div className={` ${errors.id?.message ? 'mt-2' : 'mt-5'} flex items-center ml-4`}> + <button + type='submit' + className='bg-red-600 border border-red-600 rounded-md text-sm text-white w-24 h-11 mb-1 content-center flex flex-row justify-center items-center' + > + {isLoading && <Spinner size='xs' className='mr-2'/>} + {!isLoading && <Search size={16} strokeWidth={1} className='mr-2'/>} + <p>Lacak</p> + </button> + </div> + </form> + {/* Display the API error message */} + {apiError && <div className='text-danger-500 mt-4'>{apiError}</div>} + <Manifest idAWB={idAWB} closePopup={closePopup} /> + </div> + </div> + ) +} + +const validationSchema = Yup.object().shape({ + email: Yup.string().email('Format harus seperti contoh@email.com').required('Harus di-isi'), + id: Yup.string().required('Harus di-isi'), +}) + +const defaultValues = { + email: '', + id: '' +} + +export default TrackingOrder diff --git a/src/lib/treckingAwb/component/Manifest.jsx b/src/lib/treckingAwb/component/Manifest.jsx index cf2fa4ed..fbc95702 100644 --- a/src/lib/treckingAwb/component/Manifest.jsx +++ b/src/lib/treckingAwb/component/Manifest.jsx @@ -40,10 +40,18 @@ const Manifest = ({ idAWB, closePopup }) => { const getManifest = async () => { setIsLoading(true) const auth = getAuth() - const list = await odooApi( - 'GET', - `/api/v1/partner/${auth.partnerId}/stock-picking/${idAWB}/tracking` - ) + let list + if(auth){ + list = await odooApi( + 'GET', + `/api/v1/partner/${auth.partnerId}/stock-picking/${idAWB}/tracking` + ) + }else{ + list = await odooApi( + 'GET', + `/api/v1/stock-picking/${idAWB}/tracking` + ) + } setManifests(list) setIsLoading(false) } |
