summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/address/components/Addresses.jsx17
-rw-r--r--src/lib/address/components/CreateAddress.jsx402
-rw-r--r--src/lib/address/components/EditAddress.jsx181
-rw-r--r--src/lib/checkout/api/ExpedisiList.js11
-rw-r--r--src/lib/checkout/api/getRatesCourier.js22
-rw-r--r--src/lib/checkout/components/Checkout.jsx291
-rw-r--r--src/lib/checkout/components/SectionExpedition.jsx346
-rw-r--r--src/lib/checkout/stores/stateCheckout.js28
-rw-r--r--src/lib/checkout/stores/useAdress.js21
-rw-r--r--src/lib/checkout/utils/functionCheckouit.js79
-rw-r--r--src/lib/maps/components/PinPointMap.jsx196
-rw-r--r--src/lib/maps/stores/useMaps.js15
-rw-r--r--src/lib/product/components/ProductSearch.jsx4
13 files changed, 1258 insertions, 355 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..5f041eb2 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,27 @@ 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,
+ setAddressMaps,
+ } = 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);
+ setValue('street', detailAddress?.street);
+
+ }
+ }, [detailAddress, setValue]);
useEffect(() => {
const loadState = async () => {
@@ -45,6 +72,7 @@ const CreateAddress = () => {
setState(dataState);
};
loadState();
+ setAddressMaps('');
}, []);
const watchState = watch('state');
@@ -52,7 +80,7 @@ const CreateAddress = () => {
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 +92,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 +140,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 +173,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 +197,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 +209,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 cursor-pointer' onClick={() => setPinedMaps(true)} />{' '}
+ <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..7675db9d 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,34 @@ 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('addressMap'));
+ const { addressMaps, selectedPosition, detailAddress } = useMaps();
+
+ console.log('ini adalah',);
+
+ useEffect(() => {
+ if (addressMaps) {
+ setTempAddress(addressMaps);
+ setValue('addressMap', addressMaps);
+ setValue('longtitude', selectedPosition.lng);
+ setValue('latitude', selectedPosition.lat);
+ }
+ }, [addressMaps, selectedPosition, setValue]);
+
+ useEffect(() => {
+ if (Object.keys(detailAddress).length > 0) {
+ setValue('zip', detailAddress.postalCode);
+ const selectedState = states.find(
+ (state) =>
+ detailAddress?.province.includes(state.label) ||
+ state.label.includes(detailAddress?.province)
+ );
+ setValue('state', selectedState?.value);
+ setValue('street', detailAddress?.route);
+ }
+ }, [detailAddress, setValue]);
useEffect(() => {
const loadProfile = async () => {
@@ -60,12 +93,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,9 +114,23 @@ const EditAddress = ({ id, defaultValues }) => {
};
loadCities();
}
-
}, [watchState, setValue, getValues]);
+ useEffect(() => {
+ if (Object.keys(detailAddress).length > 0) {
+ 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]);
+
const watchCity = watch('city');
useEffect(() => {
setValue('district', '');
@@ -105,6 +152,23 @@ const EditAddress = ({ id, defaultValues }) => {
}
}, [watchCity, setValue, getValues]);
+ useEffect(() => {
+ if (Object.keys(detailAddress).length > 0) {
+ 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', '');
@@ -128,6 +192,24 @@ const EditAddress = ({ id, defaultValues }) => {
loadSubDistricts();
}
}, [watchDistrict, setValue, getValues]);
+
+
+ useEffect(() => {
+ if (Object.keys(detailAddress).length > 0) {
+ 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,
@@ -136,40 +218,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 +289,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 cursor-pointer' onClick={() => setPinedMaps(true)} />{' '}
+ <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 +408,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 +473,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..0471dc6c 100644
--- a/src/lib/checkout/components/Checkout.jsx
+++ b/src/lib/checkout/components/Checkout.jsx
@@ -28,9 +28,14 @@ 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';
+import { formatShipmentRange, getToDate } from '../utils/functionCheckouit';
const SELF_PICKUP_ID = 32;
@@ -68,11 +73,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,26 +111,32 @@ 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([]);
const [listserviceExpedisi, setListServiceExpedisi] = useState([]);
const [selectedExpedisi, setSelectedExpedisi] = useState(0);
const [selectedCarrierId, setselectedCarrierId] = useState(0);
const [selectedCarrier, setselectedCarrier] = useState(0);
- const [biayaKirim, setBiayaKirim] = useState(0);
- const [checkWeigth, setCheckWeight] = useState(false);
const [selectedServiceType, setSelectedServiceType] = useState(null);
const [selectedExpedisiService, setselectedExpedisiService] = useState(null);
- const [etd, setEtd] = useState(null);
- const [etdFix, setEtdFix] = useState(null);
+ // const [etd, setEtd] = useState(null);
+ // const [etdFix, setEtdFix] = useState(null);
const [bottomPopup, SetBottomPopup] = useState(null);
const [bottomPopupTnC, SetBottomPopupTnC] = useState(null);
const [itemTnC, setItemTnC] = 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, products, setProducts, etd, unit, selectedCourier, selectedService, listExpedisi, setExpedisi} = useCheckout();
const expedisiValidation = useRef(null);
@@ -275,63 +290,12 @@ const Checkout = () => {
window.onpopstate = handlePopState;
+
return () => {
window.onpopstate = null;
};
}, []);
- 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;
@@ -349,70 +313,70 @@ const Checkout = () => {
setHasFlashSale(hasFlashSale);
}, [cartCheckout]);
- useEffect(() => {
- setCheckoutValidation(false);
- const loadServiceRajaOngkir = async () => {
- setLoadingRajaOngkir(true);
- const body = {
- origin: 2127,
- destination: selectedAddress.shipping.rajaongkirCityId,
- weight: totalWeight,
- courier: selectedCarrier,
- originType: 'subdistrict',
- destinationType: 'subdistrict',
- };
- setBiayaKirim(0);
- const dataService = await axios(
- '/api/rajaongkir-service?body=' + JSON.stringify(body)
- );
- setLoadingRajaOngkir(false);
- setListServiceExpedisi(dataService.data[0].costs);
- if (dataService.data[0].costs[0]) {
- setBiayaKirim(dataService.data[0].costs[0]?.cost[0].value);
- setselectedExpedisiService(
- dataService.data[0].costs[0]?.description +
- '-' +
- dataService.data[0].costs[0]?.service
- );
- setEtd(dataService.data[0].costs[0]?.cost[0].etd);
- toast.success('Harap pilih tipe layanan pengiriman');
- } else {
- toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.');
- }
- };
- if (selectedCarrier != 0 && selectedCarrier != 1 && totalWeight > 0) {
- loadServiceRajaOngkir();
- } else {
- setListServiceExpedisi();
- setBiayaKirim(0);
- setselectedExpedisiService();
- setEtd();
- }
- }, [selectedCarrier, selectedAddress, totalWeight]);
+ // useEffect(() => {
+ // setCheckoutValidation(false);
+ // const loadServiceRajaOngkir = async () => {
+ // setLoadingRajaOngkir(true);
+ // const body = {
+ // origin: 2127,
+ // destination: selectedAddress.shipping.rajaongkirCityId,
+ // weight: totalWeight,
+ // courier: selectedCarrier,
+ // originType: 'subdistrict',
+ // destinationType: 'subdistrict',
+ // };
+ // setBiayaKirim(0);
+ // const dataService = await axios(
+ // '/api/rajaongkir-service?body=' + JSON.stringify(body)
+ // );
+ // setLoadingRajaOngkir(false);
+ // setListServiceExpedisi(dataService.data[0].costs);
+ // if (dataService.data[0].costs[0]) {
+ // setBiayaKirim(dataService.data[0].costs[0]?.cost[0].value);
+ // setselectedExpedisiService(
+ // dataService.data[0].costs[0]?.description +
+ // '-' +
+ // dataService.data[0].costs[0]?.service
+ // );
+ // setEtd(dataService.data[0].costs[0]?.cost[0].etd);
+ // toast.success('Harap pilih tipe layanan pengiriman');
+ // } else {
+ // toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.');
+ // }
+ // };
+ // if (selectedCarrier != 0 && selectedCarrier != 1 && totalWeight > 0) {
+ // loadServiceRajaOngkir();
+ // } else {
+ // setListServiceExpedisi();
+ // setBiayaKirim(0);
+ // setselectedExpedisiService();
+ // setEtd();
+ // }
+ // }, [selectedCarrier, selectedAddress, totalWeight]);
- useEffect(() => {
- if (selectedServiceType) {
- let serviceType = selectedServiceType.split(',');
- setBiayaKirim(serviceType[0]);
- setselectedExpedisiService(serviceType[1]);
- setEtd(serviceType[2]);
- }
- }, [selectedServiceType]);
+ // useEffect(() => {
+ // if (selectedServiceType) {
+ // let serviceType = selectedServiceType.split(',');
+ // setBiayaKirim(serviceType[0]);
+ // setselectedExpedisiService(serviceType[1]);
+ // setEtd(serviceType[2]);
+ // }
+ // }, [selectedServiceType]);
- useEffect(() => {
- if (etd) setEtdFix(calculateEstimatedArrival(etd));
- }, [etd]);
+ // useEffect(() => {
+ // if (etd) setEtdFix(calculateEstimatedArrival(etd));
+ // }, [etd]);
- useEffect(() => {
- if (selectedExpedisi) {
- let serviceType = selectedExpedisi.split(',');
- if (serviceType[0] === 0) return;
+ // useEffect(() => {
+ // if (selectedExpedisi) {
+ // let serviceType = selectedExpedisi.split(',');
+ // if (serviceType[0] === 0) return;
- setselectedCarrier(serviceType[0]);
- setselectedCarrierId(serviceType[1]);
- setListServiceExpedisi([]);
- }
- }, [selectedExpedisi]);
+ // setselectedCarrier(serviceType[0]);
+ // setselectedCarrierId(serviceType[1]);
+ // setListServiceExpedisi([]);
+ // }
+ // }, [selectedExpedisi]);
const poNumber = useRef(null);
const poFile = useRef(null);
@@ -440,7 +404,7 @@ const Checkout = () => {
});
return;
}
- if (selectedExpedisi === 0) {
+ if (selectedCourier === 0 || !selectedCourier) {
setCheckoutValidation(true);
if (expedisiValidation.current) {
const position = expedisiValidation.current.getBoundingClientRect();
@@ -451,7 +415,11 @@ const Checkout = () => {
}
return;
}
- if (selectedCarrier != 1 && biayaKirim == 0) {
+ if(!selectedService){
+ toast.error('Harap pilih tipe layanan pengiriman');
+ return;
+ }
+ if (selectedCourier != 1 && biayaKirim == 0) {
toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.');
return;
}
@@ -462,22 +430,21 @@ const Checkout = () => {
available_quantity: product?.availableQuantity,
}));
let data = {
- // partner_shipping_id: auth.partnerId,
+ // partner_shipping_id: auth.partnerId,,
// partner_invoice_id: auth.partnerId,
partner_shipping_id: selectedAddress?.shipping?.id || auth.partnerId,
partner_invoice_id: selectedAddress?.invoicing?.id || auth.partnerId,
user_id: auth.id,
order_line: JSON.stringify(productOrder),
delivery_amount: biayaKirim,
- carrier_id: selectedCarrierId,
- estimated_arrival_days: splitDuration(etd),
- delivery_service_type: selectedExpedisiService,
+ carrier_id: selectedCourier,
+ estimated_arrival_days: getToDate(etd, unit),
+ delivery_service_type: selectedService?.service_type,
flash_sale: hasFlashSale, // dibuat negasi untuk ngetest kebalikan nilai false
voucher: activeVoucher,
voucher_shipping: activeVoucherShipping,
type: 'sale_order',
};
-
if (query) {
data.source = 'buy';
}
@@ -524,24 +491,6 @@ const Checkout = () => {
)}`;
}
}
-
- /* 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 () => {
@@ -716,19 +665,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>
@@ -1115,7 +1051,7 @@ const Checkout = () => {
)}
<Divider />
<SectionValidation address={selectedAddress.invoicing} />
- <SectionExpedisi
+ {/* <SectionExpedisi
address={selectedAddress.shipping}
listExpedisi={listExpedisi}
setSelectedExpedisi={setSelectedExpedisi}
@@ -1128,7 +1064,7 @@ const Checkout = () => {
<SectionListService
listserviceExpedisi={listserviceExpedisi}
setSelectedServiceType={setSelectedServiceType}
- />
+ /> */}
<div className='p-4 flex flex-col gap-y-4'>
{!!products &&
@@ -1215,7 +1151,7 @@ const Checkout = () => {
</div>
<div className='flex gap-x-2 justify-between'>
<div className='text-gray_r-11'>
- Biaya Kirim <p className='text-xs mt-1'>{etdFix}</p>
+ Biaya Kirim <p className='text-xs mt-1'>{formatShipmentRange(etd, unit)}</p>
</div>
<div>
{currencyFormat(
@@ -1411,9 +1347,10 @@ const Checkout = () => {
/>
</Skeleton>
)}
+ {products && <SectionExpedition products={products} />}
<Divider />
<SectionValidation address={selectedAddress.invoicing} />
- <SectionExpedisi
+ {/* <SectionExpedisi
address={selectedAddress.shipping}
listExpedisi={listExpedisi}
setSelectedExpedisi={setSelectedExpedisi}
@@ -1426,7 +1363,7 @@ const Checkout = () => {
<SectionListService
listserviceExpedisi={listserviceExpedisi}
setSelectedServiceType={setSelectedServiceType}
- />
+ /> */}
<div className='p-4'>
<div className='font-medium mb-6'>Detail Pesanan</div>
@@ -1520,7 +1457,7 @@ const Checkout = () => {
<div className='flex gap-x-2 justify-between'>
<div className='text-gray_r-11'>
Biaya Kirim
- <p className='text-xs mt-1'>{etdFix}</p>
+ <p className='text-xs mt-1'>{formatShipmentRange(etd, unit)}</p>
</div>
<div>
{currencyFormat(
@@ -1691,6 +1628,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..2098d5b5
--- /dev/null
+++ b/src/lib/checkout/components/SectionExpedition.jsx
@@ -0,0 +1,346 @@
+import { Skeleton, 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';
+import { formatShipmentRange } from '../utils/functionCheckouit';
+
+function mappingItems(products) {
+ return products?.map((item) => ({
+ name: item.parent.name,
+ description: `${item.code} - ${item.name}`,
+ value: item.price.priceDiscount,
+ weight: item.weight * 1000,
+ quantity: item.quantity,
+ canInstant: item.availableQuantity > item.quantity ? true : false,
+ }));
+}
+
+function mappingCourier(couriersOdoo, couriers, notIncludeInstant = false) {
+ const validCourierMap = couriersOdoo.reduce((acc, courier) => {
+ acc[courier.label.toLowerCase()] = courier.carrierId;
+ return acc;
+ }, {});
+
+ return couriers?.reduce((result, item) => {
+ const { courier_name, courier_code, courier_service_code } = item;
+ if (!validCourierMap[courier_name.toLowerCase()]) {
+ return result; // Jika tidak ada, lewati item ini
+ }
+
+ if (
+ notIncludeInstant &&
+ ['hours'].includes(item.shipment_duration_unit.toLowerCase())
+ ) {
+ return result;
+ }
+
+ const carrierId = validCourierMap[courier_name];
+
+ // 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,
+ courier_id_odoo: carrierId,
+ service_type: {
+ [courier_service_code]: {
+ service_name: item.courier_service_name,
+ duration: item.duration,
+ shipment_range: item.shipment_duration_range,
+ shipment_unit: item.shipment_duration_unit,
+ 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_range: item.shipment_duration_range,
+ shipment_unit: item.shipment_duration_unit,
+ 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 [serviceOptions, setServiceOptions] = useState([]);
+ const [isOpen, setIsOpen] = useState(false);
+ const [onFocusSelectedCourier, setOnFocuseSelectedCourier] = useState(false);
+
+ const {
+ checkWeigth,
+ checkoutValidation,
+ setBiayaKirim,
+ setUnit,
+ setEtd,
+ selectedCourier,
+ setSelectedCourier,
+ selectedService,
+ setSelectedService,
+ listExpedisi,
+ } = useCheckout();
+
+ let destination = {};
+ let items = mappingItems(products);
+
+ let notIncludeInstant = hasCanInstantFalse(items);
+ if (addressMaps) {
+ destination = {
+ origin_latitude: -6.3031123,
+ origin_longitude: 106.7794934999,
+ ...coordinate,
+ };
+ } else if (postalCode) {
+ destination = {
+ origin_postal_code: 14440,
+ 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 &&
+ onFocusSelectedCourier,
+ staleTime: Infinity,
+ cacheTime: Infinity,
+ }
+ );
+
+ const couriers =
+ mappingCourier(listExpedisi, data?.data?.pricing, notIncludeInstant) ||
+ null;
+
+ console.log('ini scourier', data?.data?.pricing);
+
+ const onCourierChange = (e) => {
+ setIsOpen(false);
+ const courier = e.target.value;
+ setSelectedService(null);
+ setSelectedCourier(courier);
+ // setSelectedCourierId(Object.values(couriers[courier]?.courier_id_odoo);
+ setBiayaKirim(0);
+ if (courier && courier !== '0' && courier !== '32' && couriers[courier]) {
+ setServiceOptions(Object.values(couriers[courier]?.service_type));
+ } else {
+ setServiceOptions([]);
+ }
+ };
+
+ const onSubmit = (data) => {
+ 1;
+ console.log(data);
+ };
+
+ const handleSelect = (service) => {
+ setSelectedService(service);
+ setBiayaKirim(service?.price);
+ setEtd(service?.shipment_range);
+ setUnit(service?.shipment_unit);
+ 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}
+ onMouseDown={() => setOnFocuseSelectedCourier(true)}
+ required
+ >
+ {!isLoading ? (
+ <>
+ <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>
+ ))}
+ </>
+ ) : (
+ <>
+ <option disabled className='skeleton-option'>
+ <Skeleton height={40} containerClassName='w-full' />
+ </option>
+ <option disabled className='skeleton-option'>
+ <Skeleton height={40} containerClassName='w-full' />
+ </option>
+ <option disabled className='skeleton-option'>
+ <Skeleton height={40} containerClassName='w-full' />
+ </option>
+ </>
+ )}
+ </select>
+ </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(
+ Math.round(parseInt(selectedService?.price * 1.1) / 1000) *
+ 1000
+ )}
+ </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'>
+ {formatShipmentRange(
+ service.shipment_range,
+ service.shipment_unit
+ )}
+ </p>
+ </div>
+ <span className='font-semibold'>
+ {currencyFormat(
+ Math.round(parseInt(service?.price * 1.1) / 1000) *
+ 1000
+ )}
+ </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..4fefbbed
--- /dev/null
+++ b/src/lib/checkout/stores/stateCheckout.js
@@ -0,0 +1,28 @@
+import { create } from "zustand";
+
+export const useCheckout = create((set) => ({
+ products : null,
+ checkWeigth : false,
+ hasFlashSale : false,
+ checkoutValidation : false,
+ biayaKirim : 0,
+ etd : null,
+ unit : null,
+ selectedCourier : null,
+ selectedCourierId : null,
+ selectedService : null,
+ listExpedisi : [],
+ setCheckWeight : (checkWeigth) => set({ checkWeigth }),
+ setHasFlashSale : (hasFlashSale) => set({ hasFlashSale }),
+ setCheckoutValidation : (checkoutValidation) => set({ checkoutValidation }),
+ setBiayaKirim : (biayaKirim) => set({ biayaKirim }),
+ setProducts : (products) => set({ products }),
+ setEtd : (etd) => set({ etd }),
+ setUnit : (unit) => set({ unit }),
+ setSelectedCourier : (selectedCourier) => set({ selectedCourier }),
+ setSelectedService : (selectedService) => set({ selectedService }),
+ setSelectedCourierId : (selectedCourierId) => set({ selectedCourierId }),
+ setExpedisi : (listExpedisi) => set({ listExpedisi })
+
+
+})) \ 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/checkout/utils/functionCheckouit.js b/src/lib/checkout/utils/functionCheckouit.js
new file mode 100644
index 00000000..b299f289
--- /dev/null
+++ b/src/lib/checkout/utils/functionCheckouit.js
@@ -0,0 +1,79 @@
+export function formatShipmentRange(
+ shipmentDurationRange,
+ shipmentDurationUnit
+) {
+ if (!shipmentDurationRange || !shipmentDurationUnit) {
+ return '';
+ }
+ let minRange, maxRange;
+
+ // Cek apakah durasi berupa range atau angka tunggal
+ if (shipmentDurationRange.includes('-')) {
+ [minRange, maxRange] = shipmentDurationRange.split(' - ').map(Number);
+ } else {
+ minRange = maxRange = Number(shipmentDurationRange); // Jika angka tunggal
+ }
+
+ const start = new Date(); // Tanggal saat ini
+
+ let minDate, maxDate;
+
+ // Hitung estimasi berdasarkan unit waktu
+ if (shipmentDurationUnit === 'days' || shipmentDurationUnit === 'day') {
+ minDate = new Date(start);
+ minDate.setDate(start.getDate() + (minRange + 3));
+
+ maxDate = new Date(start);
+ maxDate.setDate(start.getDate() + (maxRange + 3));
+ } else if (shipmentDurationUnit === 'hours') {
+ minDate = new Date(start.getTime() + (minRange + 3) * 60 * 60 * 1000);
+ maxDate = new Date(start.getTime() + (maxRange + 3) * 60 * 60 * 1000);
+ } else {
+ throw new Error("Unsupported unit. Please use 'days' or 'hours'.");
+ }
+
+ console.log('min max', minDate, maxDate);
+
+ const minDateStr = formatDate(minDate);
+ const maxDateStr = formatDate(maxDate);
+ if (minDateStr === maxDateStr) {
+ return `Estimasi tiba ${minDateStr}`;
+ }
+ return `Estimasi tiba ${minDateStr} - ${maxDateStr}`;
+}
+
+export function getToDate(shipmentDurationRange, shipmentDurationUnit) {
+ if (!shipmentDurationRange || !shipmentDurationUnit) {
+ return '';
+ }
+ const start = new Date(); // Tanggal saat ini
+
+ let maxRange;
+
+ // Cek apakah durasi berupa range atau angka tunggal
+ if (shipmentDurationRange.includes('-')) {
+ [, maxRange] = shipmentDurationRange.split(' - ').map(Number);
+ } else {
+ maxRange = Number(shipmentDurationRange); // Jika angka tunggal
+ }
+
+ let maxDate;
+
+ // Hitung estimasi berdasarkan unit waktu
+ if (shipmentDurationUnit === 'days' || shipmentDurationUnit === 'day') {
+ maxDate = new Date(start);
+ maxDate.setDate(start.getDate() + (maxRange + 3));
+ } else if (shipmentDurationUnit === 'hours') {
+ maxDate = new Date(start.getTime() + (maxRange + 3) * 60 * 60 * 1000);
+ } else {
+ throw new Error("Unsupported unit. Please use 'days' or 'hours'.");
+ }
+
+ return formatDate(maxDate);
+}
+
+function formatDate(date) {
+ const day = date.getDate();
+ const month = date.toLocaleString('default', { month: 'short' });
+ return `${day} ${month}`;
+}
diff --git a/src/lib/maps/components/PinPointMap.jsx b/src/lib/maps/components/PinPointMap.jsx
new file mode 100644
index 00000000..acff5d67
--- /dev/null
+++ b/src/lib/maps/components/PinPointMap.jsx
@@ -0,0 +1,196 @@
+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 = {
+ route : getAddressComponent(addressComponents, 'route')+' '+getAddressComponent(addressComponents, 'street_number')+' '+getAddressComponent(addressComponents, 'administrative_area_level_7')+' '+getAddressComponent(addressComponents, 'administrative_area_level_6'),
+ 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);
+ 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;
+ }
+ // console.log('tempPosition', tempPosition.lat);
+ getAddress(tempPosition.lat, tempPosition.lng);
+ setSelectedPosition(tempPosition);
+ setAddressMaps(tempAddress);
+ };
+
+ console.log('set selected position',selectedPosition);
+
+ 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'
/>