diff options
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/address/components/Addresses.jsx | 17 | ||||
| -rw-r--r-- | src/lib/address/components/CreateAddress.jsx | 400 | ||||
| -rw-r--r-- | src/lib/address/components/EditAddress.jsx | 116 | ||||
| -rw-r--r-- | src/lib/checkout/api/ExpedisiList.js | 11 | ||||
| -rw-r--r-- | src/lib/checkout/api/getRatesCourier.js | 22 | ||||
| -rw-r--r-- | src/lib/checkout/components/Checkout.jsx | 120 | ||||
| -rw-r--r-- | src/lib/checkout/components/SectionExpedition.jsx | 304 | ||||
| -rw-r--r-- | src/lib/checkout/stores/stateCheckout.js | 16 | ||||
| -rw-r--r-- | src/lib/checkout/stores/useAdress.js | 21 | ||||
| -rw-r--r-- | src/lib/maps/components/PinPointMap.jsx | 192 | ||||
| -rw-r--r-- | src/lib/maps/stores/useMaps.js | 15 | ||||
| -rw-r--r-- | src/lib/product/components/ProductSearch.jsx | 4 |
12 files changed, 977 insertions, 261 deletions
diff --git a/src/lib/address/components/Addresses.jsx b/src/lib/address/components/Addresses.jsx index 9ca617ae..1007b9f8 100644 --- a/src/lib/address/components/Addresses.jsx +++ b/src/lib/address/components/Addresses.jsx @@ -9,6 +9,7 @@ import MobileView from '@/core/components/views/MobileView'; import DesktopView from '@/core/components/views/DesktopView'; import Menu from '@/lib/auth/components/Menu'; import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import { MapPinIcon } from 'lucide-react'; const Addresses = () => { const router = useRouter(); @@ -17,7 +18,7 @@ const Addresses = () => { const selectedAddress = getItemAddress(select || ''); const [changeConfirmation, setChangeConfirmation] = useState(false); const [selectedForChange, setSelectedForChange] = useState(null); // State baru untuk simpan alamat yang akan diubah - + const changeSelectedAddress = (id) => { if (!select) return; updateItemAddress(select, id); @@ -177,6 +178,20 @@ const AddressCard = ({ <p className='mt-2 text-gray_r-11'>{address.mobile}</p> )} <p className='mt-1 leading-6 text-gray_r-11'>{address.street}</p> + + <div className='flex items-center mt-4'> + {address.addressMap ? ( + <> + <MapPinIcon class='h-7 w-8 text-yellow-600 mr-3 font-semibold' /> + <p className='text-yellow-600 font-semibold'>Sudah PinPoint</p> + </> + ) : ( + <> + <MapPinIcon class='h-7 w-8 text-red-600 mr-2 font-semibold' /> + <p className='text-red-600 font-semibold text-sm'>Belum PinPoint</p> + </> + )} + </div> </div> <button onClick={() => { diff --git a/src/lib/address/components/CreateAddress.jsx b/src/lib/address/components/CreateAddress.jsx index 9d70e8fc..404143f9 100644 --- a/src/lib/address/components/CreateAddress.jsx +++ b/src/lib/address/components/CreateAddress.jsx @@ -1,18 +1,24 @@ import HookFormSelect from '@/core/components/elements/Select/HookFormSelect'; import useAuth from '@/core/hooks/useAuth'; +import Menu from '@/lib/auth/components/Menu'; +import { yupResolver } from '@hookform/resolvers/yup'; import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; +import { toast } from 'react-hot-toast'; import * as Yup from 'yup'; import cityApi from '../api/cityApi'; +import createAddressApi from '../api/createAddressApi'; import districtApi from '../api/districtApi'; +import stateApi from '../api/stateApi'; 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'; -import stateApi from '../api/stateApi'; + +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import { Button } from '@chakra-ui/react'; +import { MapPinIcon } from 'lucide-react'; +import PinPointMap from '../../maps/components/PinPointMap'; +import { useMaps } from '../../maps/stores/useMaps'; const CreateAddress = () => { const auth = useAuth(); @@ -34,6 +40,25 @@ const CreateAddress = () => { const [districts, setDistricts] = useState([]); const [subDistricts, setSubDistricts] = useState([]); const [filteredTypes, setFilteredTypes] = useState(types); // State to manage filtered types + const [pinedMaps, setPinedMaps] = useState(false); + const { + addressMaps, + selectedPosition, + detailAddress, + } = useMaps(); + + useEffect(() => { + if (detailAddress) { + setValue('zip', detailAddress.postalCode); + const selectedState = states.find( + (state) => + detailAddress?.province.includes(state.label) || + state.label.includes(detailAddress?.province) + ); + setValue('state', selectedState?.value); + + } + }, [detailAddress, setValue]); useEffect(() => { const loadState = async () => { @@ -48,11 +73,12 @@ const CreateAddress = () => { }, []); const watchState = watch('state'); + console.log(watchState); useEffect(() => { setValue('city', ''); if (watchState) { const loadCities = async () => { - let dataCities = await cityApi({stateId: watchState}); + let dataCities = await cityApi({ stateId: watchState }); dataCities = dataCities.map((city) => ({ value: city.id, label: city.name, @@ -64,6 +90,21 @@ const CreateAddress = () => { }, [watchState, setValue]); useEffect(() => { + if (detailAddress) { + const selectedCities = cities.find( + (city) => + detailAddress.district + .toLowerCase() + .includes(city.label.toLowerCase()) || + city.label + .toLowerCase() + .includes(detailAddress.district.toLowerCase()) + ); + setValue('city', selectedCities?.value); + } + }, [cities, detailAddress, setValue]); + + useEffect(() => { if (addresses) { let hasContactAddress = false; @@ -97,6 +138,21 @@ const CreateAddress = () => { } }, [watchCity, setValue]); + useEffect(() => { + if (detailAddress) { + const selectedDistrict = districts.find( + (district) => + detailAddress.subDistrict + .toLowerCase() + .includes(district.label.toLowerCase()) || + district.label + .toLowerCase() + .includes(detailAddress.subDistrict.toLowerCase()) + ); + setValue('district', selectedDistrict?.value); + } + }, [districts, detailAddress, setValue]); + const watchDistrict = watch('district'); useEffect(() => { setValue('subDistrict', ''); @@ -115,6 +171,22 @@ const CreateAddress = () => { } }, [watchDistrict, setValue]); + useEffect(() => { + if (detailAddress) { + const selectedSubDistrict = subDistricts.find( + (district) => + detailAddress.village + .toLowerCase() + .includes(district.label.toLowerCase()) || + district.label + .toLowerCase() + .includes(detailAddress.village.toLowerCase()) + ); + + setValue('subDistrict', selectedSubDistrict?.value); + } + }, [subDistricts, detailAddress, setValue]); + const onSubmitHandler = async (values) => { const data = { ...values, @@ -123,8 +195,10 @@ const CreateAddress = () => { district_id: values.district, sub_district_id: values.subDistrict, parent_id: auth.partnerId, + latitude: selectedPosition?.lat, + longtitude: selectedPosition?.lng, + address_map: addressMaps, }; - const address = await createAddressApi({ data }); if (address?.id) { toast.success('Berhasil menambahkan alamat'); @@ -133,167 +207,197 @@ const CreateAddress = () => { }; return ( - <div className='max-w-none md:container mx-auto flex p-0 md:py-10'> - <div className='hidden md:block w-3/12 pr-4'> - <Menu /> - </div> - <div className='w-full md:w-9/12 p-4 bg-white border border-gray_r-6 rounded'> - <form onSubmit={handleSubmit(onSubmitHandler)}> - <div className='grid grid-cols-1 md:grid-cols-2 gap-4'> - <div> - <label className='form-label mb-2'>Label Alamat</label> - <Controller - name='type' - control={control} - render={(props) => ( - <HookFormSelect - {...props} - isSearchable={false} - options={filteredTypes} - /> - )} - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.type?.message} - </div> + <> + <BottomPopup + className=' !h-[75%]' + title='Pin Maps Address' + active={pinedMaps} + close={() => setPinedMaps(false)} + > + <div className='flex mt-4'> + <PinPointMap /> + </div> + </BottomPopup> + <div className='max-w-none md:container mx-auto flex p-0 md:py-10'> + <div className='hidden md:block w-3/12 pr-4'> + <Menu /> + </div> + <div className='w-full md:w-9/12 p-4 bg-white border border-gray_r-6 rounded'> + <form onSubmit={handleSubmit(onSubmitHandler)}> + <div className='mb-4 items-start'> + <label className='form-label mb-2'>PinPoint</label> + {addressMaps ? ( + <div className='flex gap-x-2 items-center'> + <MapPinIcon class='h-6 w-6 text-gray-500 mr-3' />{' '} + <span> {addressMaps} </span> + </div> + ) : ( + <Button variant='plain' onClick={() => setPinedMaps(true)}> + <MapPinIcon class='h-6 w-6 text-gray-500 mr-3' /> + Pin Alamat + </Button> + )} </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4'> + <div> + <label className='form-label mb-2'>Label Alamat</label> + <Controller + name='type' + control={control} + render={(props) => ( + <HookFormSelect + {...props} + isSearchable={false} + options={filteredTypes} + /> + )} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.type?.message} + </div> + </div> - <div> - <label className='form-label mb-2'>Nama</label> - <input - {...register('name')} - placeholder='John Doe' - type='text' - className='form-input' - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.name?.message} + <div> + <label className='form-label mb-2'>Nama</label> + <input + {...register('name')} + placeholder='John Doe' + type='text' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.name?.message} + </div> </div> - </div> - <div> - <label className='form-label mb-2'>Email</label> - <input - {...register('email')} - placeholder='contoh@email.com' - type='email' - className='form-input' - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.email?.message} + <div> + <label className='form-label mb-2'>Email</label> + <input + {...register('email')} + placeholder='contoh@email.com' + type='email' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.email?.message} + </div> </div> - </div> - <div> - <label className='form-label mb-2'>Mobile</label> - <input - {...register('mobile')} - placeholder='08xxxxxxxx' - type='tel' - className='form-input' - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.mobile?.message} + <div> + <label className='form-label mb-2'>Mobile</label> + <input + {...register('mobile')} + placeholder='08xxxxxxxx' + type='tel' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.mobile?.message} + </div> </div> - </div> - <div> - <label className='form-label mb-2'>Alamat</label> - <input - {...register('street')} - placeholder='Jl. Bandengan Utara 85A' - type='text' - className='form-input' - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.street?.message} + <div> + <label className='form-label mb-2'>Alamat</label> + <input + {...register('street')} + placeholder='Jl. Bandengan Utara 85A' + type='text' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.street?.message} + </div> </div> - </div> - <div> - <label className='form-label mb-2'>Kode Pos</label> - <input - {...register('zip')} - placeholder='10100' - type='number' - className='form-input' - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.zip?.message} + <div> + <label className='form-label mb-2'>Kode Pos</label> + <input + {...register('zip')} + placeholder='10100' + type='number' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.zip?.message} + </div> </div> - </div> - <div> - <label className='form-label mb-2'>Provinsi</label> - <Controller - name='state' - control={control} - render={(props) => ( - <HookFormSelect {...props} options={states} /> - )} - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.state?.message} + <div> + <label className='form-label mb-2'>Provinsi</label> + <Controller + name='state' + control={control} + render={(props) => ( + <HookFormSelect {...props} options={states} /> + )} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.state?.message} + </div> </div> - </div> - <div> - <label className='form-label mb-2'>Kota</label> - <Controller - name='city' - control={control} - render={(props) => ( - <HookFormSelect {...props} options={cities} disabled={!watchState}/> - )} - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.city?.message} + <div> + <label className='form-label mb-2'>Kota</label> + <Controller + name='city' + control={control} + render={(props) => ( + <HookFormSelect + {...props} + options={cities} + disabled={!watchState} + /> + )} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.city?.message} + </div> </div> - </div> - <div> - <label className='form-label mb-2'>Kecamatan</label> - <Controller - name='district' - control={control} - render={(props) => ( - <HookFormSelect - {...props} - options={districts} - disabled={!watchCity} - /> - )} - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.district?.message} + <div> + <label className='form-label mb-2'>Kecamatan</label> + <Controller + name='district' + control={control} + render={(props) => ( + <HookFormSelect + {...props} + options={districts} + disabled={!watchCity} + /> + )} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.district?.message} + </div> </div> - </div> - <div> - <label className='form-label mb-2'>Kelurahan</label> - <Controller - name='subDistrict' - control={control} - render={(props) => ( - <HookFormSelect - {...props} - options={subDistricts} - disabled={!watchDistrict} - /> - )} - /> + <div> + <label className='form-label mb-2'>Kelurahan</label> + <Controller + name='subDistrict' + control={control} + render={(props) => ( + <HookFormSelect + {...props} + options={subDistricts} + disabled={!watchDistrict} + /> + )} + /> + </div> </div> - </div> - <button - type='submit' - className='btn-yellow w-full md:w-fit mt-6 ml-0 md:ml-auto' - > - Simpan - </button> - </form> + <button + type='submit' + className='btn-yellow w-full md:w-fit mt-6 ml-0 md:ml-auto' + > + Simpan + </button> + </form> + </div> </div> - </div> + </> ); }; diff --git a/src/lib/address/components/EditAddress.jsx b/src/lib/address/components/EditAddress.jsx index 23cf72a9..0b3b0aa3 100644 --- a/src/lib/address/components/EditAddress.jsx +++ b/src/lib/address/components/EditAddress.jsx @@ -14,6 +14,12 @@ import Menu from '@/lib/auth/components/Menu'; import useAuth from '@/core/hooks/useAuth'; import odooApi from '@/core/api/odooApi'; import stateApi from '../api/stateApi'; +import { MapPinIcon } from 'lucide-react'; +import { Button } from '@chakra-ui/react'; +import { useMaps } from '../../maps/stores/useMaps'; + +import PinPointMap from '../../maps/components/PinPointMap'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; const EditAddress = ({ id, defaultValues }) => { const auth = useAuth(); @@ -34,7 +40,19 @@ const EditAddress = ({ id, defaultValues }) => { const [states, setStates] = useState([]); const [cities, setCities] = useState([]); const [districts, setDistricts] = useState([]); - const [subDistricts, setSubDistricts] = useState([]); + const [subDistricts, setSubDistricts] = useState([]); + const [pinedMaps, setPinedMaps] = useState(false); + const [tempAddress, setTempAddress] = useState(getValues('address_maps')); + const { addressMaps, selectedPosition, detailAddress } = useMaps(); + + useEffect(() => { + if (addressMaps) { + setTempAddress(addressMaps); + setValue('address_map', addressMaps); + setValue('longtitude', selectedPosition.lng); + setValue('latitude', selectedPosition.lat); + } + }, [addressMaps, selectedPosition, setValue]); useEffect(() => { const loadProfile = async () => { @@ -60,12 +78,12 @@ const EditAddress = ({ id, defaultValues }) => { setStates(dataStates); }; loadStates(); - },[]) + }, []); const watchState = watch('state'); useEffect(() => { setValue('city', ''); - if(watchState) { + if (watchState) { const loadCities = async () => { let dataCities = await cityApi({ stateId: watchState }); dataCities = dataCities.map((city) => ({ @@ -81,7 +99,6 @@ const EditAddress = ({ id, defaultValues }) => { }; loadCities(); } - }, [watchState, setValue, getValues]); const watchCity = watch('city'); @@ -136,40 +153,65 @@ const EditAddress = ({ id, defaultValues }) => { city_id: values.city, district_id: values.district, sub_district_id: values.subDistrict, + longtitude: selectedPosition?.lng, + latitude: selectedPosition?.lat, + address_map: addressMaps, }; if (!auth.company) { data.alamat_lengkap_text = values.street; } - const address = await editAddressApi({ id, data }); - let dataAlamat; - let isUpdated = true; - if (auth.company) { - if (auth?.partnerId == id) { - dataAlamat = { - id_user: auth.partnerId, - alamat_lengkap_text: values.alamat_wajib_pajak, - street: values.street, - }; - isUpdated = await odooApi( - 'PUT', - `/api/v1/partner/${auth.parentId}`, - dataAlamat - ); + try { + const address = await editAddressApi({ id, data }); + let dataAlamat; + let isUpdated = true; + if (auth.company) { + if (auth?.partnerId == id) { + dataAlamat = { + id_user: auth.partnerId, + alamat_lengkap_text: values.alamat_wajib_pajak, + street: values.street, + }; + isUpdated = await odooApi( + 'PUT', + `/api/v1/partner/${auth.parentId}`, + dataAlamat + ); + } } - } - - // if (isUpdated?.id) { - if (address?.id && auth.company ? isUpdated?.id : true) { - toast.success('Berhasil mengubah alamat'); - router.back(); - } else { + if (address?.id) { + toast.success('Berhasil mengubah alamat'); + router.back(); + } else { + toast.error('Terjadi kesalahan internal'); + router.back(); + } + } catch (error) { toast.error('Terjadi kesalahan internal'); router.back(); } + + // if (isUpdated?.id) { + // if (address?.id && auth.company ? isUpdated?.id : true) { + // toast.success('Berhasil mengubah alamat'); + // router.back(); + // } else { + // toast.error('Terjadi kesalahan internal'); + // router.back(); + // } }; return ( <> + <BottomPopup + className=' !h-[75%]' + title='Pin Maps Address' + active={pinedMaps} + close={() => setPinedMaps(false)} + > + <div className='flex mt-4'> + <PinPointMap /> + </div> + </BottomPopup> <div className='max-w-none md:container mx-auto flex p-0 md:py-10'> <div className='hidden md:block w-3/12 pr-4'> <Menu /> @@ -182,6 +224,20 @@ const EditAddress = ({ id, defaultValues }) => { {auth?.partnerId == id && <div className='badge-green'>Utama</div>} </div> <form onSubmit={handleSubmit(onSubmitHandler)}> + <div className='mb-4 items-start'> + <label className='form-label mb-2'>PinPoint</label> + {tempAddress ? ( + <div className='flex gap-x-2 items-center'> + <MapPinIcon class='h-6 w-6 text-gray-500 mr-3' />{' '} + <span> {tempAddress} </span> + </div> + ) : ( + <Button variant='plain' onClick={() => setPinedMaps(true)}> + <MapPinIcon class='h-6 w-6 text-gray-500 mr-3' /> + Pin Alamat + </Button> + )} + </div> <div className='grid grid-cols-1 md:grid-cols-2 gap-4'> <div> <label className='form-label mb-2'>Label Alamat</label> @@ -287,7 +343,11 @@ const EditAddress = ({ id, defaultValues }) => { name='city' control={control} render={(props) => ( - <HookFormSelect {...props} options={cities} disabled={!watchState} /> + <HookFormSelect + {...props} + options={cities} + disabled={!watchState} + /> )} /> <div className='text-caption-2 text-danger-500 mt-1'> @@ -348,7 +408,7 @@ const validationSchema = Yup.object().shape({ mobile: Yup.string().required('Harus di-isi'), street: Yup.string().required('Harus di-isi'), zip: Yup.string().required('Harus di-isi'), - state : Yup.string().required('Harus di-pilih'), + state: Yup.string().required('Harus di-pilih'), city: Yup.string().required('Harus di-pilih'), district: Yup.string().required('Harus di-pilih'), }); diff --git a/src/lib/checkout/api/ExpedisiList.js b/src/lib/checkout/api/ExpedisiList.js index ca22bec1..67ef93e2 100644 --- a/src/lib/checkout/api/ExpedisiList.js +++ b/src/lib/checkout/api/ExpedisiList.js @@ -1,8 +1,7 @@ -import odooApi from '@/core/api/odooApi' - +import odooApi from '@/core/api/odooApi'; const ExpedisiList = async () => { - const dataExpedisi = await odooApi('GET', '/api/v1/courier') - return dataExpedisi -} + const dataExpedisi = await odooApi('GET', '/api/v1/courier'); + return dataExpedisi; +}; -export default ExpedisiList +export default ExpedisiList; diff --git a/src/lib/checkout/api/getRatesCourier.js b/src/lib/checkout/api/getRatesCourier.js new file mode 100644 index 00000000..8db02d50 --- /dev/null +++ b/src/lib/checkout/api/getRatesCourier.js @@ -0,0 +1,22 @@ +import axios from "axios"; +import biteShipAPI from "../../../core/api/biteShip"; + +const GetRatesCourierBiteship = async ({ destination, items }) => { + const couriers = process.env.NEXT_PUBLIC_BITESHIP_CODE_COURIERS; + let body = { + ...destination, + couriers: 'gojek, grab, deliveree, lalamove, jne, tiki, ninja, lion, rara, sicepat, jnt, pos, idexpress, rpx, wahana, jdl, pos, anteraja, sap, paxel, borzo', + items: items, + }; + + const response = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/biteship-service?method=POST&url=/v1/rates/couriers&body=` + JSON.stringify(body)); + + // const featch = await biteShipAPI('POST', '/v1/rates/couriers', body); + console.log('ini featch', response); + + + return response; +}; + + +export default GetRatesCourierBiteship
\ No newline at end of file diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 6fb5cdb4..92a94834 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -28,9 +28,13 @@ import getFileBase64 from '@/core/utils/getFileBase64'; import { gtagPurchase } from '@/core/utils/googleTag'; import whatsappUrl from '@/core/utils/whatsappUrl'; import addressesApi from '@/lib/address/api/addressesApi'; +import { MapPinIcon } from 'lucide-react'; import CartItem from '~/modules/cart/components/Item.tsx'; import ExpedisiList from '../api/ExpedisiList'; -import { findVoucher, getVoucher, getVoucherNew } from '../api/getVoucher'; +import { getVoucher } from '../api/getVoucher'; +import { useAddress } from '../stores/useAdress'; +import SectionExpedition from './SectionExpedition'; +import { useCheckout } from '../stores/stateCheckout'; const SELF_PICKUP_ID = 32; @@ -68,11 +72,15 @@ const Checkout = () => { }) ); - const [selectedAddress, setSelectedAddress] = useState({ - shipping: null, - invoicing: null, - }); - const [addresses, setAddresses] = useState(null); + const { + selectedAddress, + setSelectedAddress, + addresses, + setAddresses, + setAddressMaps, + setCoordinate, + setPostalCode, + } = useAddress(); useEffect(() => { if (!auth) return; @@ -102,13 +110,22 @@ const Checkout = () => { return addresses[0]; }; + let ship = matchAddress('shipping'); + setSelectedAddress({ shipping: matchAddress('shipping'), invoicing: matchAddress('invoicing'), }); + setPostalCode(ship?.zip); + if (ship?.addressMap) { + setAddressMaps(ship?.addressMap); + setCoordinate({ + destination_latitude: ship?.latitude, + destination_longitude: ship?.longtitude, + }); + } }, [addresses]); - const [products, setProducts] = useState(null); const [totalWeight, setTotalWeight] = useState(0); const [priceCheck, setPriceCheck] = useState(false); const [listExpedisi, setExpedisi] = useState([]); @@ -116,8 +133,6 @@ const Checkout = () => { const [selectedExpedisi, setSelectedExpedisi] = useState(0); const [selectedCarrierId, setselectedCarrierId] = useState(0); const [selectedCarrier, setselectedCarrier] = useState(0); - const [biayaKirim, setBiayaKirim] = useState(0); - const [checkWeigth, setCheckWeight] = useState(false); const [selectedServiceType, setSelectedServiceType] = useState(null); const [selectedExpedisiService, setselectedExpedisiService] = useState(null); const [etd, setEtd] = useState(null); @@ -132,11 +147,11 @@ const Checkout = () => { const [findCodeVoucher, SetFindVoucher] = useState(null); const [selisihHargaCode, SetSelisihHargaCode] = useState(null); const [buttonTerapkan, SetButtonTerapkan] = useState(false); - const [checkoutValidation, setCheckoutValidation] = useState(false); const [loadingVoucher, setLoadingVoucher] = useState(true); const [loadingRajaOngkir, setLoadingRajaOngkir] = useState(false); const [grandTotal, setGrandTotal] = useState(0); - const [hasFlashSale, setHasFlashSale] = useState(false); + + const {checkWeigth, setCheckWeight, hasFlashSale, setHasFlashSale, checkoutValidation, setCheckoutValidation, biayaKirim, setBiayaKirim, products, setProducts} = useCheckout(); const expedisiValidation = useRef(null); @@ -280,58 +295,6 @@ const Checkout = () => { }; }, []); - 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]; - - countDiscount = dataActiveVoucher.discountVoucher; - } - - /*if (dataActiveVoucher.discountType === 'percentage') { - countDiscount = cartCheckout?.subtotal * (dataActiveVoucher.discountAmount / 100) - if ( - dataActiveVoucher.maxDiscountAmount > 0 && - countDiscount > dataActiveVoucher.maxDiscountAmount - ) { - countDiscount = dataActiveVoucher.maxDiscountAmount - } - } else { - countDiscount = dataActiveVoucher.discountAmount - }*/ - - return countDiscount; - }; - - // useEffect(() => { - // if (!listVouchers) return; - // if (!activeVoucher) return; - - // console.log('voucher') - // const countDiscount = hitungDiscountVoucher(activeVoucher, 'voucher'); - - // SetDiscountVoucher(countDiscount); - // }, [activeVoucher, listVouchers]); - - // useEffect(() => { - // if (!listVoucherShippings) return; - // if (!activeVoucherShipping) return; - - // const countDiscount = hitungDiscountVoucher(activeVoucherShipping, 'voucher_shipping'); - - // SetDiscountVoucherOngkir(countDiscount); - // }, [activeVoucherShipping, listVoucherShippings]); - useEffect(() => { if (qVoucher === 'PASTIHEMAT' && listVouchers) { let code = qVoucher; @@ -606,6 +569,9 @@ const Checkout = () => { cartCheckout?.discountVoucher + (cartCheckout?.discountVoucherShipping || 0); + + console.log('etd', etd, calculateEstimatedArrival(etd), splitDuration(etd)); + return ( <> <BottomPopup @@ -716,19 +682,6 @@ 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> @@ -1411,6 +1364,7 @@ const Checkout = () => { /> </Skeleton> )} + {products && <SectionExpedition products={products} />} <Divider /> <SectionValidation address={selectedAddress.invoicing} /> <SectionExpedisi @@ -1691,6 +1645,20 @@ const SectionAddress = ({ address, label, url }) => ( <p className='mt-1 text-gray_r-11'> {address.street}, {address?.city?.name} </p> + <div className='flex gap-x-2 items-center mt-4'> + <MapPinIcon + className={ + address.addressMap + ? `h-6 w-6 text-gray-500` + : `h-6 w-6 text-red-500` + } + /> + {address.addressMap ? ( + <label>Sudah Pinpoint</label> + ) : ( + <label className='text-red-500 '>Belum Pinpoint</label> + )} + </div> </div> )} </div> diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx new file mode 100644 index 00000000..27224e5b --- /dev/null +++ b/src/lib/checkout/components/SectionExpedition.jsx @@ -0,0 +1,304 @@ +import { Spinner } from '@chakra-ui/react'; +import axios from 'axios'; +import { AnimatePresence, motion } from 'framer-motion'; +import React, { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { useQuery } from 'react-query'; +import { useAddress } from '../stores/useAdress'; + +import currencyFormat from '@/core/utils/currencyFormat'; +import { useCheckout } from '../stores/stateCheckout'; + +function mappingItems(products) { + return products?.map((item) => ({ + name: item.parent.name, + description: `${item.code} - ${item.name}`, + value: item.price.priceDiscount, + weight: item.weight * 1000 * item.quantity, + quantity: item.quantity, + canInstant: item.availableQuantity > item.quantity ? true : false, + })); +} + +function mappingCourier(couriers, notIncludeInstant = false) { + return couriers?.reduce((result, item) => { + const { courier_code, courier_service_code } = item; + if ( + notIncludeInstant && + ['hours'].includes(item.shipment_duration_unit.toLowerCase()) + ) { + return result; + } + + // Jika courier_code belum ada di result, buat objek baru untuknya + if (!result[courier_code]) { + result[courier_code] = { + courier_name: item.courier_name, + courier_code: courier_code, + service_type: { + [courier_service_code]: { + service_name: item.courier_service_name, + duration: item.duration, + shipment_duration: item.duration, + price: item.price, + service_type: item.service_type, + description: item.description, + }, + }, + }; + } else { + result[courier_code].service_type[courier_service_code] = { + service_name: item.courier_service_name, + duration: item.duration, + shipment_duration: item.duration, + price: item.price, + service_type: item.service_type, + description: item.description, + }; + } + + return result; + }, {}); +} + +function hasCanInstantFalse(items) { + return items.some((item) => item.canInstant === false); +} + +// interface CourierService { +// courier_name: string; +// courier_code: string; +// service_type: { +// [key: string]: { +// service_name: string; +// duration: number; +// shipment_duration: number; +// price: number; +// service_type: string; +// description: string; +// }; +// }; +// } + +// interface ServiceOption { +// service_name: string; +// duration: number; +// shipment_duration: number; +// price: number; +// service_type: string; +// description: string; +// } + +export default function SectionExpedition({ products }) { + const { addressMaps, coordinate, postalCode } = useAddress(); + const { control, handleSubmit } = useForm(); + const [selectedCourier, setSelectedCourier] = useState(''); + const [serviceOptions, setServiceOptions] = useState([]); + const [selectedService, setSelectedService] = useState(null); + const [isOpen, setIsOpen] = useState(false); + + const { checkWeigth, checkoutValidation } = useCheckout(); + + let destination = {}; + let items = mappingItems(products); + + let notIncludeInstant = hasCanInstantFalse(items); + console.log('instanCourier', items); + if (addressMaps) { + destination = { + origin_latitude: -6.3031123, + origin_longitude: 106.7794934999, + ...coordinate, + }; + } else if (postalCode) { + destination = { + origin_postal_code: 12440, + destination_postal_code: postalCode, + }; + } + + const fetchExpedition = async () => { + let body = { + ...destination, + couriers: + 'gojek,grab,deliveree,lalamove,jne,tiki,ninja,lion,rara,sicepat,jnt,pos,idexpress,rpx,wahana,jdl,pos,anteraja,sap,paxel,borzo', + items: items, + }; + try { + const response = await axios.get(`/api/biteship-service`, { + params: { body: JSON.stringify(body) }, + }); + return response; + } catch (error) { + console.error('Failed to fetch expedition rates:', error); + } + }; + + const { data, isLoading } = useQuery( + ['expedition', JSON.stringify(destination), JSON.stringify(items)], + fetchExpedition, + { + enabled: + Boolean(Object.keys(destination).length) && + items?.length > 0 && + !checkWeigth, + staleTime: Infinity, + cacheTime: Infinity, + } + ); + + console.log('data', data); + + const couriers = mappingCourier(data?.data?.pricing, true) || null; + + const onCourierChange = (e) => { + setIsOpen(false); + const courier = e.target.value; + setSelectedService(null); + setSelectedCourier(courier); + console.log('courier', courier); + // Menentukan layanan berdasarkan pengiriman yang dipilih + if (courier && courier !== '0' && courier !== '32' && couriers[courier]) { + setServiceOptions(Object.values(couriers[courier]?.service_type)); + } else { + setServiceOptions([]); + } + }; + + const onSubmit = (data) => { + console.log(data); + }; + + const handleSelect = (service) => { + setSelectedService(service); + setIsOpen(false); + }; + + return ( + <form onSubmit={handleSubmit(onSubmit)}> + <div className='px-4 py-2'> + <div className='flex justify-between items-center'> + <div className='font-medium'>Pilih Ekspedisi: </div> + <div className='w-[250px]'> + <div className='flex items-center gap-x-4'> + <select + className={`form-input `} + onChange={onCourierChange} + required + > + <option value='0'>Pilih Pengiriman</option> + <option value='32'>SELF PICKUP</option> + {couriers && + Object.values(couriers)?.map((expedisi) => ( + <option + value={expedisi.courier_code} + key={expedisi.courier_name} + > + {' '} + {expedisi.courier_name} + </option> + ))} + </select> + + <AnimatePresence> + {isLoading && ( + <motion.div + initial={{ opacity: 0, width: 0 }} + animate={{ opacity: 1, width: '28px' }} + exit={{ opacity: 0, width: 0 }} + transition={{ + duration: 0.25, + }} + className='overflow-hidden' + > + <Spinner thickness='3px' speed='0.5s' color='red.500' /> + </motion.div> + )} + </AnimatePresence> + </div> + {checkoutValidation && ( + <span className='text-sm text-red-500'> + *silahkan pilih expedisi + </span> + )} + </div> + <style jsx>{` + .shake { + animation: shake 0.4s ease-in-out; + } + `}</style> + </div> + {checkWeigth == true && ( + <p className='mt-4 text-gray_r-11 leading-6'> + Mohon maaf, pengiriman hanya tersedia untuk self pickup karena + terdapat barang yang belum diatur beratnya. Mohon atur berat barang + dengan menghubungi admin melalui{' '} + <a + className='text-danger-500 inline' + href='https://api.whatsapp.com/send?phone=6281717181922' + > + tautan ini + </a> + </p> + )} + </div> + + {selectedCourier && + selectedCourier !== '32' && + selectedCourier !== '0' && ( + <div className='px-4 py-2'> + <div className='flex justify-between items-center'> + <div className='font-medium'>Tipe Layanan Ekspedisi: </div> + <div className='w-[350px]'> + <div className='relative'> + {/* Custom Select Input Field */} + <div + className='w-full p-2 border rounded-lg bg-white cursor-pointer' + onClick={() => setIsOpen(!isOpen)} + > + {selectedService ? ( + <div className='flex justify-between'> + <span>{selectedService.service_name}</span> + <span className='font-semibold'> + {currencyFormat(selectedService.price)} + </span> + </div> + ) : ( + <span className='text-gray-500'> + Pilih layanan pengiriman + </span> + )} + </div> + + {/* Dropdown Options */} + {isOpen && ( + <div className='absolute w-full bg-white border rounded-lg mt-1 shadow-lg z-10'> + {serviceOptions.map((service) => ( + <div + key={service.service_type} + onClick={() => handleSelect(service)} + className='flex justify-between p-2 items-center hover:bg-gray-100 cursor-pointer' + > + <div> + <p className='font-semibold'> + {service.service_name} + </p> + <p className='text-gray-600 text-sm'> + Estimasi Tiba {service.duration} + </p> + </div> + <span className='font-semibold'> + {currencyFormat(service.price)} + </span> + </div> + ))} + </div> + )} + </div> + </div> + </div> + </div> + )} + </form> + ); +} diff --git a/src/lib/checkout/stores/stateCheckout.js b/src/lib/checkout/stores/stateCheckout.js new file mode 100644 index 00000000..97b0c64e --- /dev/null +++ b/src/lib/checkout/stores/stateCheckout.js @@ -0,0 +1,16 @@ +import { create } from "zustand"; + +export const useCheckout = create((set) => ({ + products : null, + checkWeigth : false, + hasFlashSale : false, + checkoutValidation : false, + biayaKirim : 0, + etd : null, + setCheckWeight : (checkWeigth) => set({ checkWeigth }), + setHasFlashSale : (hasFlashSale) => set({ hasFlashSale }), + setCheckoutValidation : (checkoutValidation) => set({ checkoutValidation }), + setBiayaKirim : (biayaKirim) => set({ biayaKirim }), + setProducts : (products) => set({ products }), + +}))
\ No newline at end of file diff --git a/src/lib/checkout/stores/useAdress.js b/src/lib/checkout/stores/useAdress.js new file mode 100644 index 00000000..5274ecfe --- /dev/null +++ b/src/lib/checkout/stores/useAdress.js @@ -0,0 +1,21 @@ +import { create } from 'zustand'; + +export const useAddress = create((set) => ({ + selectedAddress: { + shipping: null, + invoicing: null, + }, + addresses: null, + addressMaps : null, + coordinate : { + destination_latitude : null, + destination_longitude : null + }, + postalCode : null, + setAddresses: (addresses) => set({ addresses }), + setSelectedAddress: (selectedAddress) => set({ selectedAddress }), + setCoordinate: (coordinate) => set({ coordinate }), + setPostalCode: (postalCode) => set({ postalCode }), + setAddressMaps: (addressMaps) => set({ addressMaps }), + +})); diff --git a/src/lib/maps/components/PinPointMap.jsx b/src/lib/maps/components/PinPointMap.jsx new file mode 100644 index 00000000..201fdeb4 --- /dev/null +++ b/src/lib/maps/components/PinPointMap.jsx @@ -0,0 +1,192 @@ +import React, { useState, useCallback, useRef } from 'react'; +import { + GoogleMap, + useJsApiLoader, + Marker, + Autocomplete, +} from '@react-google-maps/api'; +import { useMaps } from '../stores/useMaps'; +import { LocateFixed, MapPinIcon } from 'lucide-react'; +import { Button } from '@chakra-ui/react'; +import { useForm } from 'react-hook-form'; + +const containerStyle = { + width: '100%', + height: '400px', +}; + +const center = { + lat: -6.2, // Default latitude (Jakarta) + lng: 106.816666, // Default longitude (Jakarta) +}; + +const PinpointLocation = () => { + const { isLoaded } = useJsApiLoader({ + googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_API_KEY, // Pastikan API key ada di .env.local + libraries: ['places'], + }); + + const { addressMaps, setAddressMaps, selectedPosition, setSelectedPosition, setDetailAddress } = + useMaps(); + + const [tempAddress, setTempAddress] = useState(''); + const [tempPosition, setTempPosition] = useState(center); + const { setValue } = useForm(); + + const autocompleteRef = useRef(null); + + const onMapClick = useCallback((event) => { + const lat = event.latLng.lat(); + const lng = event.latLng.lng(); + setTempPosition({ lat, lng }); + getAddress(lat, lng); + }, []); + + const handlePlaceSelect = () => { + const place = autocompleteRef.current.getPlace(); + if (place && place.geometry) { + const lat = place.geometry.location.lat(); + const lng = place.geometry.location.lng(); + setTempPosition({ lat, lng }); + setTempAddress(place.formatted_address); + } + }; + + const getAddressComponent = (components, type) => { + const component = components.find((comp) => comp.types.includes(type)); + return component ? component.long_name : ''; + }; + + const getAddress = async (lat, lng) => { + try { + const response = await fetch( + `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${process.env.NEXT_PUBLIC_GOOGLE_API_KEY}` + ); + const data = await response.json(); + if (data.results[0]) { + const addressComponents = data.results[0].address_components; + const details = { + province: getAddressComponent( + addressComponents, + 'administrative_area_level_1' + ), + district: getAddressComponent( + addressComponents, + 'administrative_area_level_2' + ), + subDistrict: getAddressComponent( + addressComponents, + 'administrative_area_level_3' + ), + village: getAddressComponent( + addressComponents, + 'administrative_area_level_4' + ), + postalCode: getAddressComponent(addressComponents, 'postal_code'), + }; + setDetailAddress(details); + console.log(details); + setTempAddress(data.results[0].formatted_address); + } + } catch (error) { + console.error('Error fetching address:', error); + } + }; + + const handleUseCurrentLocation = () => { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position) => { + const lat = position.coords.latitude; + const lng = position.coords.longitude; + setTempPosition({ lat, lng }); + getAddress(lat, lng); + }, + (error) => { + console.error('Error getting current location:', error); + } + ); + } + }; + + const handleSavePinpoint = (event) => { + event.preventDefault(); + if (tempAddress === '') { + alert('Silahkan pilih lokasi terlebih dahulu'); + return; + } + setSelectedPosition(tempPosition); + setAddressMaps(tempAddress); + }; + + return ( + <div className='w-full'> + <h3>Tentukan Pinpoint Lokasi</h3> + <div style={{ marginBottom: '10px' }}> + {isLoaded ? ( + <Autocomplete + onLoad={(ref) => (autocompleteRef.current = ref)} + onPlaceChanged={handlePlaceSelect} + > + <input + type='text' + placeholder='Cari Alamat...' + style={{ width: '100%', padding: '8px' }} + /> + </Autocomplete> + ) : ( + <p>Loading autocomplete...</p> + )} + </div> + + <div> + {isLoaded ? ( + <GoogleMap + mapContainerStyle={containerStyle} + center={tempPosition} + zoom={15} + onClick={onMapClick} + > + <Marker + position={tempPosition} + draggable={true} + onDragEnd={(e) => onMapClick(e)} + icon={{ + url: 'https://maps.google.com/mapfiles/ms/icons/red-pushpin.png', + scaledSize: new window.google.maps.Size(40, 40), + }} + /> + </GoogleMap> + ) : ( + <p>Loading map...</p> + )} + </div> + + <div style={{ marginTop: '20px' }}> + <Button variant='solid' onClick={handleUseCurrentLocation}> + <LocateFixed class='h-6 w-6 text-gray-500 mr-2' /> Gunakan Lokasi Saat + ini + </Button> + </div> + + <div style={{ marginTop: '10px' }}> + <p>PinPoint :</p> + <div className='flex gap-x-2 shadow-md rounded-sm text-gray-500 p-3 items-center'> + <MapPinIcon class='h-8 w-8 text-gray-500 mr-3' /> + <label> {tempAddress}</label> + </div> + </div> + + <div className='mt-6 flex justify-end'> + <button + className='p-3 border border-red-500 bg-red-600 text-white font-semibold rounded-lg' + onClick={handleSavePinpoint} + > + Simpan Lokasi Ini + </button> + </div> + </div> + ); +}; + +export default PinpointLocation; diff --git a/src/lib/maps/stores/useMaps.js b/src/lib/maps/stores/useMaps.js new file mode 100644 index 00000000..83f476bc --- /dev/null +++ b/src/lib/maps/stores/useMaps.js @@ -0,0 +1,15 @@ +import { create } from "zustand"; + +const center = { + lat: -6.200000, // Default latitude (Jakarta) + lng: 106.816666, // Default longitude (Jakarta) +}; + +export const useMaps = create((set) => ({ + selectedPosition: center, + addressMaps: '', + detailAddress: {}, + setSelectedPosition: (position) => set({ selectedPosition: position }), + setAddressMaps: (addressMaps) => set({ addressMaps }), + setDetailAddress: (detailAddress) => set({ detailAddress }), + }));
\ No newline at end of file diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index f7b044aa..3e342bf0 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -501,7 +501,7 @@ const ProductSearch = ({ <Pagination pageCount={pageCount} currentPage={parseInt(page)} - url={`${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} + url={`${prefixUrl}?${toQuery(_.omit(query, ['page', 'fq']))}`} // url={prefixUrl.includes('category') || prefixUrl.includes('lob')? `${prefixUrl}?${toQuery(_.omit(finalQuery, ['page']))}` : `${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} className='mt-6 mb-2' /> @@ -691,7 +691,7 @@ const ProductSearch = ({ <Pagination pageCount={pageCount} currentPage={parseInt(page)} - url={`${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} + url={`${prefixUrl}?${toQuery(_.omit(query, ['page', 'fq']))}`} // url={prefixUrl.includes('category') || prefixUrl.includes('lob')? `${prefixUrl}?${toQuery(_.omit(finalQuery, ['page']))}` : `${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} className='!justify-end' /> |
