summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/core/api/biteShip.js30
-rw-r--r--src/core/api/odooApi.js36
-rw-r--r--src/core/components/elements/Navbar/NavbarMobile.jsx4
-rw-r--r--src/core/components/elements/Navbar/TopBannerMobile.jsx72
-rw-r--r--src/lib/address/api/editPartnerApi.js12
-rw-r--r--src/lib/address/components/Addresses.jsx17
-rw-r--r--src/lib/address/components/CreateAddress.jsx407
-rw-r--r--src/lib/address/components/EditAddress.jsx238
-rw-r--r--src/lib/checkout/api/ExpedisiList.js11
-rw-r--r--src/lib/checkout/api/checkoutApi.js9
-rw-r--r--src/lib/checkout/api/getRatesCourier.js22
-rw-r--r--src/lib/checkout/components/Checkout.jsx373
-rw-r--r--src/lib/checkout/components/SectionExpedition.jsx505
-rw-r--r--src/lib/checkout/stores/stateCheckout.js30
-rw-r--r--src/lib/checkout/stores/useAdress.js21
-rw-r--r--src/lib/checkout/utils/functionCheckouit.js92
-rw-r--r--src/lib/home/components/BannerSection.jsx158
-rw-r--r--src/lib/home/components/CategoryHomeId.jsx2
-rw-r--r--src/lib/home/components/PopupBannerPromotion.jsx260
-rw-r--r--src/lib/invoice/components/Invoice.jsx158
-rw-r--r--src/lib/maps/components/PinPointMap.jsx226
-rw-r--r--src/lib/maps/stores/useMaps.js32
-rw-r--r--src/lib/pengajuan-tempo/component/FinishTempo.jsx14
-rw-r--r--src/lib/pengajuan-tempo/component/PengajuanTempo.jsx2
-rw-r--r--src/lib/product/components/ProductSearch.jsx4
-rw-r--r--src/lib/shipment/components/Shipments.jsx2
-rw-r--r--src/lib/transaction/components/Transaction.jsx316
-rw-r--r--src/lib/treckingAwb/api/getManifest.js17
-rw-r--r--src/lib/treckingAwb/component/InformationSection.jsx77
-rw-r--r--src/lib/treckingAwb/component/Manifest.jsx146
-rw-r--r--src/pages/api/banner-section.js74
-rw-r--r--src/pages/api/biteship-service.js24
-rw-r--r--src/pages/index.jsx3
-rw-r--r--src/pages/my/address/[id]/edit.jsx5
-rw-r--r--src/pages/pengajuan-tempo/[status].jsx6
35 files changed, 2630 insertions, 775 deletions
diff --git a/src/core/api/biteShip.js b/src/core/api/biteShip.js
new file mode 100644
index 00000000..f18421d8
--- /dev/null
+++ b/src/core/api/biteShip.js
@@ -0,0 +1,30 @@
+import axios from 'axios';
+
+const biteShipAPI = async (method, url, body = {}) => {
+ try {
+ const key = process.env.NEXT_PUBLIC_BITSEHIP_KEY;
+ const baseUrl = process.env.NEXT_PUBLIC_BITE_SHIP_HOST;
+
+ const axiosParameter = {
+ method,
+ url: baseUrl + url,
+ headers: {
+ Authorization: `Bearer ${key}`, // Tambahkan "Bearer " di depan key
+ 'Content-Type': 'application/json',
+ },
+ data: body, // Tidak perlu JSON.stringify
+ };
+
+ const data = await axios(axiosParameter);
+
+ return { success: true, data: data };
+ } catch (error) {
+ console.log(error);
+ return {
+ success: false,
+ data: {},
+ };
+ }
+};
+
+export default biteShipAPI;
diff --git a/src/core/api/odooApi.js b/src/core/api/odooApi.js
index 504d097a..ab3dedb0 100644
--- a/src/core/api/odooApi.js
+++ b/src/core/api/odooApi.js
@@ -42,17 +42,30 @@ const odooApi = async (method, url, data = {}, headers = {}) => {
url: process.env.NEXT_PUBLIC_ODOO_API_HOST + url,
headers: { Authorization: token, ...headers },
};
- if (auth) axiosParameter.headers['Token'] = auth.token;
- if (method.toUpperCase() == 'POST')
- axiosParameter.headers['Content-Type'] =
- 'application/x-www-form-urlencoded';
- if (Object.keys(data).length > 0)
- axiosParameter.data = new URLSearchParams(
- Object.entries(data)
- ).toString();
+
+ if (auth) {
+ axiosParameter.headers['Token'] = auth.token;
+ }
+
+ // Tentukan format data berdasarkan metode dan data
+ if (Object.keys(data).length > 0) {
+ if (method.toUpperCase() === 'POST') {
+ // Gunakan URL-encoded untuk POST
+ axiosParameter.data = new URLSearchParams(
+ Object.entries(data)
+ ).toString();
+ axiosParameter.headers['Content-Type'] =
+ 'application/x-www-form-urlencoded';
+ } else {
+ // Gunakan JSON untuk GET/PUT atau metode lainnya
+ axiosParameter.data = data;
+ axiosParameter.headers['Content-Type'] = 'application/json';
+ }
+ }
let res = await axios(axiosParameter);
- if (res.data.status.code == 401) {
+
+ if (res.data?.status?.code === 401) {
if (connectionAttempt < maxConnectionAttempt) {
await renewToken();
return odooApi(method, url, data, headers);
@@ -62,10 +75,13 @@ const odooApi = async (method, url, data = {}, headers = {}) => {
return false;
}
}
+
return camelcaseObjectDeep(res.data.result) || [];
} catch (error) {
- // console.log(error);
+ console.error('API Error:', error);
+ throw error; // Opsional, lempar error agar bisa ditangkap di level atas
}
};
+
export default odooApi;
diff --git a/src/core/components/elements/Navbar/NavbarMobile.jsx b/src/core/components/elements/Navbar/NavbarMobile.jsx
index 47182a47..7c148440 100644
--- a/src/core/components/elements/Navbar/NavbarMobile.jsx
+++ b/src/core/components/elements/Navbar/NavbarMobile.jsx
@@ -11,7 +11,7 @@ import Image from 'next/image';
import { useEffect, useState } from 'react';
import MobileView from '../../views/MobileView';
import Link from '../Link/Link';
-import TopBanner from './TopBanner';
+import TopBannerMobile from './TopBannerMobile';
import { useCartStore } from '~/modules/cart/stores/useCartStore';
import useAuth from '@/core/hooks/useAuth';
@@ -53,7 +53,7 @@ const NavbarMobile = () => {
return (
<MobileView>
- <TopBanner />
+ <TopBannerMobile />
<nav className='px-4 py-2 pb-3 sticky top-0 z-50 bg-white shadow'>
<div className='flex justify-between items-center mb-2'>
<Link href='/'>
diff --git a/src/core/components/elements/Navbar/TopBannerMobile.jsx b/src/core/components/elements/Navbar/TopBannerMobile.jsx
new file mode 100644
index 00000000..c3f42f85
--- /dev/null
+++ b/src/core/components/elements/Navbar/TopBannerMobile.jsx
@@ -0,0 +1,72 @@
+import Image from 'next/image';
+import { useQuery } from 'react-query';
+import useDevice from '@/core/hooks/useDevice';
+import odooApi from '@/core/api/odooApi';
+import SmoothRender from '~/components/ui/smooth-render';
+import Link from '../Link/Link';
+import { background } from '@chakra-ui/react';
+import { useEffect, useState } from 'react';
+
+const TopBannerMobile = ({ onLoad = () => {} }) => {
+ const [topBannerMobile, setTopBannerMobile] = useState([]);
+ const { isDesktop, isMobile } = useDevice();
+
+ useEffect(() => {
+ const fetchData = async () => {
+ const res = await fetch(`/api/hero-banner?type=top-banner-mobile`);
+ const { data } = await res.json();
+ if (data) {
+ setTopBannerMobile(data);
+ }
+ };
+
+ fetchData();
+ }, []);
+
+ // const topBannerMobile = useQuery({
+ // queryKey: 'topBannerMobile',
+ // queryFn: async () => await odooApi('GET', '/api/v1/banner?type=top-banner'),
+ // refetchOnWindowFocus: false,
+ // });
+
+ // const backgroundColor = topBannerMobile.data?.[0]?.backgroundColor || 'transparent';
+ const hasData = topBannerMobile?.length > 0;
+ const data = topBannerMobile?.[0] || null;
+
+ useEffect(() => {
+ if (hasData) {
+ onLoad();
+ }
+ }, [hasData, onLoad]);
+
+
+ if (!hasData || !data?.image) {
+ return null;
+ }
+ return (
+ <SmoothRender
+ isLoaded={hasData}
+ duration='700ms'
+ delay='300ms'
+ className='h-auto'
+ >
+ <Link
+ href={data?.url}
+ aria-label='panduan pick up barang'
+ className='block w-full'
+ >
+ <Image
+ src={data?.image}
+ alt='Panduan Pick Up Barang'
+ width={0}
+ height={0}
+ sizes='100vw'
+ className='w-full h-auto'
+ priority
+ />
+ </Link>
+ </SmoothRender>
+ );
+};
+
+export default TopBannerMobile;
diff --git a/src/lib/address/api/editPartnerApi.js b/src/lib/address/api/editPartnerApi.js
new file mode 100644
index 00000000..866ee9d2
--- /dev/null
+++ b/src/lib/address/api/editPartnerApi.js
@@ -0,0 +1,12 @@
+import odooApi from '@/core/api/odooApi'
+
+const editPartnerApi = async ({ id, data }) => {
+ const dataPartner = await odooApi('POST', `/api/v1/partner/${id}`, data, {
+ headers: {
+ 'Content-Type': 'application/json',
+ }
+ });
+ return dataPartner;
+}
+
+export default editPartnerApi; \ No newline at end of file
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 97db7ed8..963a19aa 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 {
+ addressMaps,
+ selectedPosition,
+ detailAddress,
+ setAddressMaps,
+ pinedMaps,
+ setPinedMaps
+ } = 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');
@@ -64,6 +92,20 @@ const CreateAddress = () => {
}, [watchState, setValue]);
useEffect(() => {
+ if (detailAddress && Object.keys(detailAddress).length > 0) {
+ const selectedCities = cities.find(
+ (city) =>
+ city.label.toLowerCase() === detailAddress?.district.toLowerCase()
+ ) || 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 +139,21 @@ const CreateAddress = () => {
}
}, [watchCity, setValue]);
+ useEffect(() => {
+ if (detailAddress && 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', '');
@@ -115,6 +172,22 @@ const CreateAddress = () => {
}
}, [watchDistrict, setValue]);
+ useEffect(() => {
+ if (detailAddress && 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,
@@ -123,8 +196,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,171 +208,201 @@ 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 Koordinat 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'>Koordinat Alamat</label>
+ {addressMaps ? (
+ <div className='flex items-center'>
+ <button type='button' className="flex items-center justify-center me-3 p-2 bg-red-500 text-white rounded-full hover:bg-red-600 transition">
+ <MapPinIcon class='h-6 w-6' onClick={() => setPinedMaps(true)} />{' '}
+ </button>
+ <span> {addressMaps} </span>
+ </div>
+ ) : (
+ <Button variant='plain' style={{ padding: 0 }} onClick={() => setPinedMaps(true)}>
+ <button type='button' className="flex items-center justify-center me-3 p-2 bg-red-500 text-white rounded-full hover:bg-red-600 transition">
+ <MapPinIcon className="h-6 w-6" />
+ </button>
+ Pin Koordinat 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 ba6bd25b..deaa8a3e 100644
--- a/src/lib/address/components/EditAddress.jsx
+++ b/src/lib/address/components/EditAddress.jsx
@@ -8,12 +8,20 @@ import districtApi from '../api/districtApi';
import subDistrictApi from '../api/subDistrictApi';
import addressApi from '@/lib/address/api/addressApi';
import editAddressApi from '../api/editAddressApi';
+import editPartnerApi from '../api/editPartnerApi';
import HookFormSelect from '@/core/components/elements/Select/HookFormSelect';
import { toast } from 'react-hot-toast';
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';
+import { data } from 'autoprefixer';
const EditAddress = ({ id, defaultValues }) => {
const auth = useAuth();
@@ -35,7 +43,36 @@ const EditAddress = ({ id, defaultValues }) => {
const [cities, setCities] = useState([]);
const [districts, setDistricts] = useState([]);
const [subDistricts, setSubDistricts] = useState([]);
+ const [tempAddress, setTempAddress] = useState(getValues('addressMap'));
+ const { addressMaps,
+ selectedPosition,
+ detailAddress,
+ pinedMaps,
+ setPinedMaps } = useMaps();
+
+ 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?.street);
+ }
+ }, [detailAddress, setValue]);
+
useEffect(() => {
const loadProfile = async () => {
const dataProfile = await addressApi({ id: auth.partnerId });
@@ -52,7 +89,7 @@ const EditAddress = ({ id, defaultValues }) => {
};
if (auth) loadProfile();
}, [auth?.parentId]);
-
+
useEffect(() => {
const loadStates = async () => {
let dataStates = await stateApi({ tempo: false });
@@ -64,11 +101,11 @@ const EditAddress = ({ id, defaultValues }) => {
};
loadStates();
}, []);
-
+
const watchState = watch('state');
useEffect(() => {
+ setValue('city', '');
if (watchState) {
- setValue('city', '');
const loadCities = async () => {
let dataCities = await cityApi({ stateId: watchState });
dataCities = dataCities.map((city) => ({
@@ -85,16 +122,30 @@ const EditAddress = ({ id, defaultValues }) => {
loadCities();
}
}, [watchState, setValue, getValues]);
-
- const watchCity = watch('city');
+
useEffect(() => {
- if (watchCity) {
- // setValue('district', '');
- const loadDistricts = async () => {
- let dataDistricts = await districtApi({ cityId: watchCity });
- dataDistricts = dataDistricts.map((district) => ({
- value: district.id,
- label: district.name,
+ if (Object.keys(detailAddress).length > 0) {
+ const selectedCities = cities.find(
+ (city) =>
+ city.label.toLowerCase() === detailAddress?.district.toLowerCase()
+ ) || 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(() => {
+ if (watchCity) {
+ // setValue('district', '');
+ const loadDistricts = async () => {
+ let dataDistricts = await districtApi({ cityId: watchCity });
+ dataDistricts = dataDistricts.map((district) => ({
+ value: district.id,
+ label: district.name,
}));
setDistricts(dataDistricts);
let oldDistrict = getValues('oldDistrict');
@@ -106,7 +157,24 @@ const EditAddress = ({ id, defaultValues }) => {
loadDistricts();
}
}, [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(() => {
if (watchDistrict) {
@@ -130,48 +198,126 @@ 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]);
+
+ useEffect(() => {
+ if (id) {
+ setValue('id', id);
+ }
+ }, [id, setValue]);
+
const onSubmitHandler = async (values) => {
const data = {
...values,
phone: values.mobile,
- state_id: values.state,
- city_id: values.city,
- district_id: values.district,
- sub_district_id: values.subDistrict,
+ state_id: parseInt(values.state, 10),
+ city_id: parseInt(values.city, 10),
+ district_id: parseInt(values.district, 10),
+ sub_district_id: parseInt(values.subDistrict, 10),
+ 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,
+ try {
+ const address = await editAddressApi({ id, data });
+ console.log('Response address:', address);
+
+ let isUpdated = null;
+
+ // Jika company dan partnerId sama dengan id, maka update data alamat wajib pajak
+ const isCompanyEditingSelf = auth.company && auth.partnerId == id;
+
+ if (isCompanyEditingSelf) {
+ const dataProfile = await addressApi({ id: auth.partnerId });
+ const dataAlamat = {
+ id_user: auth.id,
+ company_type_id: dataProfile.companyTypeId,
+ industry_id: dataProfile.industryId,
+ tax_name: values.taxName,
+ npwp: values.npwp,
+ alamat_lengkap_text: values.alamat_wajib_pajak || values.street,
street: values.street,
+ email: values.email,
+ mobile: values.mobile,
};
- isUpdated = await odooApi(
- 'PUT',
- `/api/v1/partner/${auth.parentId}`,
- dataAlamat
- );
+
+ const isUpdated = await editPartnerApi({
+ id: auth.partnerId,
+ data: dataAlamat,
+ });
+
+ console.log('Response isUpdated:', isUpdated);
}
+
+ // Validasi kondisi sukses
+ const isSuccess = !!address?.id;
+
+ if (isSuccess) {
+ toast.success('Berhasil mengubah alamat');
+ router.back();
+ } else {
+ const errorMsg =
+ address?.message ||
+ isUpdated?.message ||
+ 'Gagal memperbarui alamat, silakan coba lagi.';
+ toast.error(errorMsg);
+ }
+ } catch (error) {
+ console.error('Catch error:', error);
+ toast.error(error?.message || 'Terjadi kesalahan tidak terduga.');
}
+
+ const dataProfile = await addressApi({ id: auth.partnerId });
+ console.log('ini adalah', dataProfile);
+
+
// if (isUpdated?.id) {
- if (address?.id && (auth.company && auth?.partnerId == id ? isUpdated?.id : true)) {
- toast.success('Berhasil mengubah alamat');
- router.back();
- } else {
- toast.error('Terjadi kesalahan internal');
- router.back();
- }
+ // 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
+ initialLatitude={selectedPosition?.lat}
+ initialLongitude={selectedPosition?.lng}
+ initialAddress={tempAddress}
+ />
+
+ </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 />
@@ -184,6 +330,24 @@ 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'>Koordinat Alamat</label>
+ {tempAddress ? (
+ <div className='flex gap-x-2 items-center'>
+ <button type='button' className="flex items-center justify-center me-3 p-2 badge-solid-red text-white rounded-full hover:bg-red-500 transition">
+ <MapPinIcon class='h-6 w-6' onClick={() => setPinedMaps(true)} />{' '}
+ </button>
+ <span> {tempAddress} </span>
+ </div>
+ ) : (
+ <Button variant='plain' style={{ padding: 0 }} onClick={() => setPinedMaps(true)}>
+ <button type='button' className="flex items-center justify-center me-3 p-2 badge-solid-red text-white rounded-full hover:bg-red-500 transition">
+ <MapPinIcon className="h-6 w-6" />
+ </button>
+ Pin Koordinat 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>
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/checkoutApi.js b/src/lib/checkout/api/checkoutApi.js
index fd982fff..c30d9631 100644
--- a/src/lib/checkout/api/checkoutApi.js
+++ b/src/lib/checkout/api/checkoutApi.js
@@ -18,3 +18,12 @@ export const getProductsCheckout = async (query) => {
const result = await odooApi('GET', url);
return result;
};
+
+export const getProductsSla = async ({data}) => {
+ const dataSLA = await odooApi(
+ 'GET',
+ `/api/v1/product/variants/sla`,
+ data
+ )
+ return dataSLA
+}
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 5256a328..d8ede118 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;
@@ -50,9 +55,7 @@ function convertToInternational(number) {
}
const Checkout = () => {
- const PPN = process.env.NEXT_PUBLIC_PPN
- ? parseFloat(process.env.NEXT_PUBLIC_PPN)
- : 0;
+ const PPN = process.env.NEXT_PUBLIC_PPN ? parseFloat(process.env.NEXT_PUBLIC_PPN) : 0;
const router = useRouter();
const query = router.query.source ?? null;
const qVoucher = router.query.voucher ?? null;
@@ -68,14 +71,21 @@ const Checkout = () => {
source: query,
voucher: activeVoucher,
voucher_shipping: activeVoucherShipping,
- })
+ }),
+ {
+ keepPreviousData: true, // Menjaga data sebelumnya sampai data baru tersedia
+ }
);
- 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;
@@ -105,26 +115,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);
@@ -135,11 +151,29 @@ 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,
+ selectedCourierId,
+ selectedService,
+ listExpedisi,
+ setExpedisi,
+ productSla
+ } = useCheckout();
const expedisiValidation = useRef(null);
@@ -147,31 +181,16 @@ const Checkout = () => {
if (!listVouchers) {
try {
setLoadingVoucher(true);
- const productCategories = products
- ?.reduce((categories, product) => {
- if (product.categories && Array.isArray(product.categories)) {
- product.categories.forEach((category) => {
- if (category.id && !categories.includes(category.id)) {
- categories.push(category.id);
- }
- });
- }
- return categories;
- }, [])
- .join(',');
-
let dataVoucher = await getVoucher(auth?.id, {
source: query,
type: 'all,brand',
- partner_id: auth?.partnerId,
- voucher_category: productCategories, // Add the product categories
+ partner_id : auth?.partnerId,
});
SetListVoucher(dataVoucher);
let dataVoucherShipping = await getVoucher(auth?.id, {
source: query,
type: 'shipping',
- voucher_category: productCategories, // Add the product categories
});
SetListVoucherShipping(dataVoucherShipping);
} finally {
@@ -181,29 +200,17 @@ const Checkout = () => {
};
const VoucherCode = async (code) => {
- const productCategories = products
- ?.reduce((categories, product) => {
- if (product.categories && Array.isArray(product.categories)) {
- product.categories.forEach((category) => {
- if (category.id && !categories.includes(category.id)) {
- categories.push(category.id);
- }
- });
- }
- return categories;
- }, [])
- .join(',');
-
+ // let dataVoucher = await findVoucher(code, auth.id, query);
let dataVoucher = await getVoucher(auth?.id, {
source: query,
code: code,
- voucher_category: productCategories, // Add the product categories
});
if (dataVoucher.length <= 0) {
SetFindVoucher(1);
return;
}
+
dataVoucher.forEach((addNewLine) => {
if (addNewLine.applyType !== 'shipping') {
// Mencari voucher dalam listVouchers
@@ -296,6 +303,7 @@ const Checkout = () => {
value: expedisi.id,
label: expedisi.name,
carrierId: expedisi.deliveryCarrierId,
+ logo: expedisi.image,
}));
setExpedisi(dataExpedisi);
};
@@ -312,58 +320,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;
@@ -381,71 +337,6 @@ 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(() => {
- 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 (selectedExpedisi) {
- let serviceType = selectedExpedisi.split(',');
- if (serviceType[0] === 0) return;
-
- setselectedCarrier(serviceType[0]);
- setselectedCarrierId(serviceType[1]);
- setListServiceExpedisi([]);
- }
- }, [selectedExpedisi]);
-
const poNumber = useRef(null);
const poFile = useRef(null);
@@ -472,7 +363,7 @@ const Checkout = () => {
});
return;
}
- if (selectedExpedisi === 0) {
+ if (selectedCourier === 0 || !selectedCourier) {
setCheckoutValidation(true);
if (expedisiValidation.current) {
const position = expedisiValidation.current.getBoundingClientRect();
@@ -483,9 +374,17 @@ const Checkout = () => {
}
return;
}
- if (selectedCarrier != 1 && biayaKirim == 0) {
- toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.');
- return;
+ if (selectedCourierId !== SELF_PICKUP_ID) { // Menggunakan selectedCourierId karena lebih spesifik dan numerik
+ if (!selectedService) { // Jika kurir bukan Self Pickup, maka harus ada layanan yang dipilih
+ toast.error('Harap pilih tipe layanan pengiriman');
+ return;
+ }
+ // Validasi biaya kirim hanya untuk kurir selain Self Pickup (dan ID kurir 1 jika itu kasus khusus)
+ // Jika selectedCourierId adalah 1 (misalnya kurir internal yang bisa gratis), lewati validasi biayaKirim 0
+ if (selectedCourierId !== 1 && biayaKirim === 0) {
+ toast.error('Maaf, layanan tidak tersedia untuk ekspedisi ini. Mohon pilih ekspedisi lain atau layanan lain.');
+ return;
+ }
}
setIsLoading(true);
const productOrder = products.map((product) => ({
@@ -493,23 +392,37 @@ const Checkout = () => {
quantity: product.quantity,
available_quantity: product?.availableQuantity,
}));
+
+ let eta_courier = 0;
+ let eta_courier_start = 0;
+
+ if (selectedCourierId !== SELF_PICKUP_ID && etd) {
+ const estimated_courier = etd.split('-').map(Number);
+ eta_courier = Math.max(...estimated_courier);
+ eta_courier_start = Math.min(...estimated_courier);
+ }
+
+ // let estimated_courier = etd.split('-').map(Number);
+ // let eta_courier = Math.max(...estimated_courier);
+ // let eta_courier_start = Math.min(...estimated_courier);
+
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: selectedCourierId,
+ estimated_arrival_days_start : parseInt(eta_courier_start) + parseInt(productSla),
+ estimated_arrival_days: parseInt(eta_courier) + parseInt(productSla),
+ 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';
}
@@ -517,8 +430,8 @@ const Checkout = () => {
if (typeof file == 'undefined') {
toast.error(
'Nomor PO ' +
- poNumber.current.value +
- ' telah dimasukkan, Harap upload file PO yang dimaksud'
+ poNumber.current.value +
+ ' telah dimasukkan, Harap upload file PO yang dimaksud'
);
setIsLoading(false);
return;
@@ -570,24 +483,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 () => {
@@ -761,19 +656,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>
@@ -1159,8 +1041,8 @@ const Checkout = () => {
</Skeleton>
)}
<Divider />
- <SectionValidation address={selectedAddress.shipping} />
- <SectionExpedisi
+ <SectionValidation address={selectedAddress.invoicing} />
+ {/* <SectionExpedisi
address={selectedAddress.shipping}
listExpedisi={listExpedisi}
setSelectedExpedisi={setSelectedExpedisi}
@@ -1173,7 +1055,7 @@ const Checkout = () => {
<SectionListService
listserviceExpedisi={listserviceExpedisi}
setSelectedServiceType={setSelectedServiceType}
- />
+ /> */}
<div className='p-4 flex flex-col gap-y-4'>
{!!products &&
@@ -1188,7 +1070,7 @@ const Checkout = () => {
</div>
<Divider />
-
+ {products && <SectionExpedition products={products} />}
<div className='p-4'>
<div className='flex justify-between items-center'>
<div className='font-medium'>Ringkasan Pesanan</div>
@@ -1255,14 +1137,15 @@ const Checkout = () => {
<div>{currencyFormat(cartCheckout?.subtotal)}</div>
</div>
<div className='flex gap-x-2 justify-between'>
- <div className='text-gray_r-11'>
- PPN {((PPN - 1) * 100).toFixed(0)}%
- </div>
+ <div className='text-gray_r-11'>PPN {((PPN - 1) * 100).toFixed(0)}%</div>
<div>{currencyFormat(cartCheckout?.tax)}</div>
</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, productSla)}
+ </p>
</div>
<div>
{currencyFormat(
@@ -1386,7 +1269,6 @@ const Checkout = () => {
className='flex-1 btn-yellow'
onClick={checkout}
disabled={
- isLoading ||
!products ||
products?.length == 0 ||
priceCheck ||
@@ -1458,9 +1340,10 @@ const Checkout = () => {
/>
</Skeleton>
)}
+ {products && <SectionExpedition products={products} />}
<Divider />
- <SectionValidation address={selectedAddress.shipping} />
- <SectionExpedisi
+ <SectionValidation address={selectedAddress.invoicing} />
+ {/* <SectionExpedisi
address={selectedAddress.shipping}
listExpedisi={listExpedisi}
setSelectedExpedisi={setSelectedExpedisi}
@@ -1473,7 +1356,7 @@ const Checkout = () => {
<SectionListService
listserviceExpedisi={listserviceExpedisi}
setSelectedServiceType={setSelectedServiceType}
- />
+ /> */}
<div className='p-4'>
<div className='font-medium mb-6'>Detail Pesanan</div>
@@ -1561,15 +1444,15 @@ const Checkout = () => {
<div>{currencyFormat(cartCheckout?.subtotal)}</div>
</div>
<div className='flex gap-x-2 justify-between'>
- <div className='text-gray_r-11'>
- PPN {((PPN - 1) * 100).toFixed(0)}%
- </div>
+ <div className='text-gray_r-11'>PPN {((PPN - 1) * 100).toFixed(0)}%</div>
<div>{currencyFormat(cartCheckout?.tax)}</div>
</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>
+ <p className='text-xs mt-1'>
+ {formatShipmentRange(etd, unit, productSla)}
+ </p>
</div>
<div>
{currencyFormat(
@@ -1691,7 +1574,6 @@ const Checkout = () => {
className='w-full btn-yellow mt-4'
onClick={checkout}
disabled={
- isLoading ||
!products ||
products?.length == 0 ||
priceCheck ||
@@ -1740,14 +1622,29 @@ 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 cursor-pointer'>
+ <MapPinIcon
+ className={
+ address.addressMap
+ ? `h-6 w-6 text-gray-500`
+ : `h-6 w-6 text-red-500`
+ }
+ />
+ {address.addressMap ? (
+ <label>Sudah Pinpoint</label>
+ ) : (
+ <Link href={'/my/address/' + address.id + '/edit'} target='_blank' className='cursor-pointer'>
+ <label className='text-red-500 cursor-pointer '>Belum Pinpoint</label>
+ </Link>
+ )}
+ </div>
</div>
)}
</div>
);
const SectionValidation = ({ address }) =>
- address?.stateId == 0 ||
- (address?.rajaongkirCityId == 0 && (
+ address?.stateId == 0 && (
<BottomPopup active={true} title='Update Alamat'>
<div className='leading-7 text-gray_r-12/80'>
Mohon untuk memperbarui alamat Anda dengan mengklik tombol di bawah ini.{' '}
@@ -1761,17 +1658,17 @@ const SectionValidation = ({ address }) =>
</Link>
</div>
</BottomPopup>
- ));
+ );
const SectionExpedisi = ({
- address,
- listExpedisi,
- setSelectedExpedisi,
- checkWeigth,
- checkoutValidation,
- expedisiValidation,
- loadingRajaOngkir,
-}) =>
+ address,
+ listExpedisi,
+ setSelectedExpedisi,
+ checkWeigth,
+ checkoutValidation,
+ expedisiValidation,
+ loadingRajaOngkir,
+ }) =>
address?.rajaongkirCityId > 0 && (
<div className='p-4' ref={expedisiValidation}>
<div className='flex justify-between items-center'>
@@ -1823,9 +1720,9 @@ const SectionExpedisi = ({
)}
</div>
<style jsx>{`
- .shake {
- animation: shake 0.4s ease-in-out;
- }
+ .shake {
+ animation: shake 0.4s ease-in-out;
+ }
`}</style>
</div>
{checkWeigth == true && (
diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx
new file mode 100644
index 00000000..7a02c6e9
--- /dev/null
+++ b/src/lib/checkout/components/SectionExpedition.jsx
@@ -0,0 +1,505 @@
+import { Skeleton, Spinner } from '@chakra-ui/react';
+import axios from 'axios';
+import { AnimatePresence, motion } from 'framer-motion';
+import React, { useEffect, useRef, 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';
+import Image from 'next/image';
+import toast from 'react-hot-toast';
+
+import odooApi from '@/core/api/odooApi';
+import { getProductsSla } from '../api/checkoutApi';
+
+function mappingItems(products) {
+ return products?.map((item) => ({
+ // name: item.parent.name || item?.name || 'Unknown Product',
+ name: item?.name,
+ description: `${item.code} - ${item.name}`,
+ value: item.price.priceDiscount,
+ weight: item.weight * 1000,
+ quantity: item.quantity,
+ }));
+}
+
+function reverseMappingCourier(couriersOdoo, couriers, includeInstant = false) {
+ // Buat peta courier berdasarkan nama courier dari couriers
+ const courierMap = couriers.reduce((acc, item) => {
+ const { courier_name, courier_code, courier_service_code } = item;
+ const key = courier_code.toLowerCase();
+
+ if (
+ !includeInstant && (['hours'].includes(item.shipment_duration_unit.toLowerCase()) || item.service_type == 'same_day')
+
+ ) {
+ return acc;
+ }
+
+ if (!acc[key]) {
+ acc[key] = {
+ courier_name: item.courier_name,
+ courier_code: courier_code,
+ service_type: {},
+ };
+ }
+
+ acc[key].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: courier_service_code,
+ description: item.description,
+ };
+
+ return acc;
+ }, {});
+
+ // Iterasi berdasarkan couriersOdoo
+ return couriersOdoo.map((courierOdoo) => {
+ const courierNameKey = courierOdoo.label.toLowerCase();
+ const carrierId = courierOdoo.carrierId;
+
+ const mappedCourier = courierMap[courierNameKey] || false;
+
+ if (!mappedCourier) {
+ return {
+ ...courierOdoo,
+ courier: false,
+ };
+ }
+
+ return {
+ ...courierOdoo,
+ courier: {
+ ...mappedCourier,
+ courier_id_odoo: carrierId,
+ },
+ };
+ });
+}
+
+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;
+ }, {});
+}
+
+// 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 [serviceOptions, setServiceOptions] = useState([]);
+ const [isOpen, setIsOpen] = useState(false);
+ const [onFocusSelectedCourier, setOnFocuseSelectedCourier] = useState(false);
+ const [couriers, setCouriers] = useState(null);
+ const [slaProducts, setSlaProducts] = useState(null);
+ const [savedServiceOptions, setSavedServiceOptions] = useState([]);
+
+ const {
+ checkWeigth,
+ checkoutValidation,
+ setBiayaKirim,
+ setUnit,
+ setEtd,
+ selectedCourier,
+ setSelectedCourier,
+ selectedService,
+ setSelectedService,
+ listExpedisi,
+ productSla,
+ setProductSla,
+ setSelectedCourierId,
+ } = useCheckout();
+
+ let destination = {};
+ let items = mappingItems(products);
+
+ 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 fetchSlaProducts = async () => {
+ try {
+ let productsMapped = products.map((item) => ({
+ id: item.id,
+ quantity: item.quantity,
+ }));
+
+ let data = {
+ products: JSON.stringify(productsMapped),
+ }
+ const res = await odooApi('POST', `/api/v1/product/variants/sla`, data);
+ setSlaProducts(res);
+ } catch (error) {
+ console.error('Failed to fetch expedition rates:', error);
+ }
+ };
+
+ useEffect(() => {
+ fetchSlaProducts();
+ }, []);
+
+ useEffect(() => {
+ if (slaProducts) {
+ let productSla = slaProducts?.slaTotal;
+ if (slaProducts.slaUnit === 'jam') {
+ productSla = 1;
+ }
+ setProductSla(productSla);
+ }
+ }, [slaProducts]);
+
+ 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,
+ }
+ );
+
+ useEffect(() => {
+ const instant = slaProducts?.includeInstant || false;
+ if (data) {
+ const couriers = reverseMappingCourier(
+ listExpedisi,
+ data?.data?.pricing,
+ instant
+ );
+ setCouriers(couriers);
+ }
+ }, [data, slaProducts]);
+
+ const onCourierChange = (code) => {
+ setIsOpen(false);
+ setOnFocuseSelectedCourier(false);
+ const courier = code;
+ setSelectedService(null);
+ setBiayaKirim(0);
+ if (courier !== 0 && courier !== 32) {
+ if (courier.courier) {
+ setSelectedCourier(courier.courier.courier_code);
+ setSelectedCourierId(courier.carrierId);
+ setServiceOptions(Object.values(courier.courier.service_type));
+ } else {
+ if (
+ (courier.label === 'GRAB' || courier.label === 'GOJEK') &&
+ !addressMaps
+ ) {
+ toast.error(
+ 'Maaf, layanan kurir ' +
+ courier.label +
+ ' tidak tersedia. Karena Anda Belum Melakukan Pengaturan PinPoint Alamat Pegiriman.'
+ );
+ } else {
+ toast.error(
+ 'Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'
+ );
+ }
+ setServiceOptions([]);
+ }
+ } else {
+ setSelectedCourier(courier === 32 ? 'SELF PICKUP' : null);
+ setSelectedCourierId(courier);
+ setServiceOptions([]);
+ }
+ };
+
+ const handleOnFocuse = (value) => {
+ setOnFocuseSelectedCourier(!value);
+ setIsOpen(false);
+ };
+
+ const handleSelect = (service) => {
+ setSelectedService(service);
+ setBiayaKirim(service?.price);
+ setEtd(service?.shipment_range);
+ setUnit(service?.shipment_unit);
+ setIsOpen(false);
+ };
+
+ useEffect(() => {
+ if (serviceOptions.length > 0) {
+ setSavedServiceOptions(serviceOptions);
+ }
+}, [serviceOptions]);
+
+ return (
+ <form >
+ <div className='px-4 py-2'>
+ <div className='flex justify-between items-center'>
+ <div className='font-medium'>Pilih Ekspedisi: </div>
+ <div className='w-[350px] max'>
+ <div className='px-4 py-2'>
+ <div className='flex justify-between items-center'>
+ <div className='w-[450px]'>
+ <div className='relative'>
+ {/* Custom Select Input Field */}
+ <div
+ className='w-full p-2 border rounded-lg bg-white cursor-pointer'
+ onClick={() => handleOnFocuse(onFocusSelectedCourier)}
+ >
+ {selectedCourier ? (
+ <div className='flex justify-between'>
+ <span>{selectedCourier}</span>
+ </div>
+ ) : (
+ <span className='text-gray-500'>Pilih Expedisi</span>
+ )}
+ </div>
+
+ {/* Dropdown Options */}
+ {onFocusSelectedCourier && (
+ <div className='absolute w-full bg-white border rounded-lg mt-1 shadow-lg z-10 max-h-[200px] overflow-y-auto'>
+ {!isLoading ? (
+ <>
+ <div
+ key={32}
+ onClick={() => onCourierChange(32)}
+ className='flex justify-between p-2 items-center hover:bg-gray-100 cursor-pointer'
+ >
+ <div>
+ <p className='font-semibold'>SELF PICKUP</p>
+ </div>
+ </div>
+ {couriers?.map((courier) => (
+ <div
+ key={courier?.courier?.courier_code}
+ onClick={() => onCourierChange(courier)}
+ className='flex justify-between p-2 items-center hover:bg-gray-100 cursor-pointer'
+ >
+ <div>
+ <p className='font-semibold'>
+ {courier?.label}
+ </p>
+ </div>
+ <span className='font-semibold'>
+ <Image
+ src={courier?.logo}
+ alt={courier?.courier?.courier_name}
+ width={50}
+ height={50}
+ />
+ </span>
+ </div>
+ ))}
+ </>
+ ) : (
+ <>
+ <Skeleton height={40} containerClassName='w-full' />
+ <Skeleton height={40} containerClassName='w-full' />
+ </>
+ )}
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ </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>
+
+ {(serviceOptions.length > 0 ||
+ selectedService )&&
+ 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>
+ {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,
+ productSla
+ )}
+ </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..52210d7f
--- /dev/null
+++ b/src/lib/checkout/stores/stateCheckout.js
@@ -0,0 +1,30 @@
+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 : [],
+ productSla : null,
+ 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 }),
+ setProductSla : (productSla) => set({ productSla })
+
+
+})) \ 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..a7fa8c5a
--- /dev/null
+++ b/src/lib/checkout/utils/functionCheckouit.js
@@ -0,0 +1,92 @@
+import { m } from 'framer-motion';
+import { min } from 'moment/moment';
+
+export function formatShipmentRange(
+ shipmentDurationRange,
+ shipmentDurationUnit,
+ productSLA
+) {
+ if (!shipmentDurationRange || !shipmentDurationUnit) {
+ return '';
+ }
+ let minRange, maxRange;
+
+ console.log('ini masuk format shipment range', shipmentDurationRange, shipmentDurationUnit, productSLA);
+
+ // Cek apakah durasi berupa range atau angka tunggal
+ if (shipmentDurationRange.includes('-')) {
+ [minRange, maxRange] = shipmentDurationRange.split(' - ').map(Number);
+ // if (minRange === maxRange) {
+ // maxRange = minRange + 3;
+ // }
+ } else {
+ minRange = Number(shipmentDurationRange); // Jika angka tunggal
+ maxRange = Number(shipmentDurationRange);
+ }
+
+ 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 + productSLA));
+
+ maxDate = new Date(start);
+ maxDate.setDate(start.getDate() + (maxRange + productSLA));
+ } else if (shipmentDurationUnit === 'hours') {
+ minDate = new Date(start);
+ minDate.setDate(start.getDate() + (1 + productSLA));
+
+ maxDate = new Date(start);
+ maxDate.setDate(start.getDate() + (1 + productSLA + 1));
+ // 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'.");
+ }
+
+ 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 maxDate.getDate();
+}
+
+function formatDate(date) {
+ const day = date.getDate();
+ const month = date.toLocaleString('default', { month: 'short' });
+ return `${day} ${month}`;
+}
diff --git a/src/lib/home/components/BannerSection.jsx b/src/lib/home/components/BannerSection.jsx
index 898f1bf5..1eac9592 100644
--- a/src/lib/home/components/BannerSection.jsx
+++ b/src/lib/home/components/BannerSection.jsx
@@ -1,62 +1,126 @@
import Link from '@/core/components/elements/Link/Link';
-import Image from 'next/image';
import { useEffect, useState } from 'react';
-import { bannerApi } from '../../../api/bannerApi';
-
-const { useQuery } = require('react-query');
-const { default: bannerSectionApi } = require('../api/bannerSectionApi');
+import useDevice from '@/core/hooks/useDevice';
+import { Swiper, SwiperSlide } from 'swiper/react';
const BannerSection = () => {
- const [data, setData] = useState(null);
- const [shouldFetch, setShouldFetch] = useState(false);
-
+ const [privateBrandData, setPrivateBrandData] = useState([]);
+ const [homeBannerData, setHomeBannerData] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const { isMobile, isDesktop } = useDevice();
useEffect(() => {
- const fetchCategoryData = async () => {
- const res = await fetch('/api/banner-section');
- const { data } = await res.json();
- if (data) {
- setData(data);
+ const fetchAllBanners = async () => {
+ try {
+ // Fetch private brand banners
+ const privateBrandRes = await fetch(
+ '/api/banner-section?type=private-brand'
+ );
+ if (privateBrandRes.ok) {
+ const privateBrandResult = await privateBrandRes.json();
+ setPrivateBrandData(privateBrandResult.data || []);
+ }
+
+ // Fetch home banners
+ const homeBannerRes = await fetch(
+ '/api/banner-section?type=home-banner'
+ );
+ if (homeBannerRes.ok) {
+ const homeBannerResult = await homeBannerRes.json();
+ setHomeBannerData(homeBannerResult.data || []);
+ }
+ } catch (err) {
+ setError('Network error');
+ } finally {
+ setLoading(false);
}
};
- fetchCategoryData();
+ fetchAllBanners();
}, []);
- // const fetchBannerSection = async () => await bannerSectionApi();
- const getBannerSection = useQuery(
- 'bannerSection',
- bannerApi({ type: 'home-banner' }),
- {
- enabled: shouldFetch,
- onSuccess: (data) => {
- if (data) {
- localStorage.setItem('Homepage_bannerSection', JSON.stringify(data));
- setData(data);
- }
- },
- }
- );
+ // if (loading) return <div>Loading...</div>;
+ // if (error) return <div>Error: {error}</div>;
- const bannerSection = data;
return (
- bannerSection &&
- bannerSection?.length > 0 && (
- <div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
- {bannerSection?.map((banner) => (
- <Link key={banner.id} href={banner.url}>
- <Image
- width={1024}
- height={512}
- quality={85}
- src={banner.image}
- alt={banner.name}
- className='h-auto w-full rounded'
- loading='eager'
- />
- </Link>
- ))}
- </div>
- )
+ <div className='space-y-12'>
+ {/* Private Brand Section */}
+ {privateBrandData && privateBrandData.length > 0 && (
+ <div className='px-4 sm:px-0'>
+ <div
+ className='text-black font-semibold sm:text-h-lg mb-6'
+ id='private-brand'
+ >
+ Private Brand
+ </div>
+
+ {/* Desktop Grid View */}
+ {isDesktop && (
+ <div className='grid grid-cols-3 sm:grid-cols-3 gap-4 rounded-md'>
+ {privateBrandData.map((banner, index) => (
+ <Link key={banner.id || index} href={banner.url || '#'}>
+ <img
+ width={439}
+ height={150}
+ quality={85}
+ src={banner.image}
+ alt={banner.name || `Private Brand Banner ${index + 1}`}
+ className='rounded hover:scale-105 transition duration-500 ease-in-out'
+ loading='eager'
+ />
+ </Link>
+ ))}
+ </div>
+ )}
+
+ {/* Mobile Swiper View */}
+ {isMobile && (
+ <Swiper slidesPerView={1.1} spaceBetween={8} freeMode>
+ {privateBrandData.map((banner, index) => (
+ <SwiperSlide key={banner.id || index}>
+ <Link href={banner.url || '#'}>
+ <img
+ width={350}
+ height={100}
+ quality={70}
+ src={banner.image}
+ alt={banner.name || `Private Brand Banner ${index + 1}`}
+ className='rounded'
+ loading='eager'
+ />
+ </Link>
+ </SwiperSlide>
+ ))}
+ </Swiper>
+ )}
+ </div>
+ )}
+
+ {/* Home Banner Section */}
+ {homeBannerData && homeBannerData.length > 0 && (
+ <div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
+ {homeBannerData.map((banner, index) => (
+ <div key={banner.id || index} className='relative'>
+ <Link href={banner.url || '#'}>
+ <img
+ width={1024}
+ height={512}
+ quality={85}
+ src={banner.image}
+ alt={banner.name || `Home Banner ${index + 1}`}
+ // className='h-40 w-full rounded object-cover transition-transform hover:scale-105'
+ className='h-auto w-full rounded'
+ loading='eager'
+ onError={(e) => {
+ e.target.style.display = 'none';
+ }}
+ />
+ </Link>
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
);
};
diff --git a/src/lib/home/components/CategoryHomeId.jsx b/src/lib/home/components/CategoryHomeId.jsx
index 9f436dac..db473a1c 100644
--- a/src/lib/home/components/CategoryHomeId.jsx
+++ b/src/lib/home/components/CategoryHomeId.jsx
@@ -8,7 +8,7 @@ const CategoryHomeId = () => {
return (
<div>
<h1 className='font-semibold text-[14px] sm:text-h-lg mb-6 px-4 sm:px-0'>
- Kategori Pilihan
+ Paket Bundling / Paket UMKM
</h1>
<div className='flex flex-col gap-y-10'>
{categoryHomeIds.data?.map((id) => (
diff --git a/src/lib/home/components/PopupBannerPromotion.jsx b/src/lib/home/components/PopupBannerPromotion.jsx
new file mode 100644
index 00000000..538f35e6
--- /dev/null
+++ b/src/lib/home/components/PopupBannerPromotion.jsx
@@ -0,0 +1,260 @@
+import { useRouter } from 'next/router';
+import { useEffect, useState, useRef } from 'react';
+import Image from 'next/image';
+import Link from 'next/link';
+import { X } from 'lucide-react';
+import { getAuth } from '~/libs/auth';
+import useDevice from '@/core/hooks/useDevice';
+import { createPortal } from 'react-dom';
+
+const PagePopupInformation = () => {
+ const router = useRouter();
+ const isHomePage = router.pathname === '/';
+ const auth = getAuth();
+ const { isDesktop } = useDevice();
+
+ const [active, setActive] = useState(false);
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ const popupRef = useRef(null);
+ const [position, setPosition] = useState({ x: 20, y: window.innerHeight - 170 });
+ const dragStartPos = useRef({ x: 0, y: 0 });
+ const isDragging = useRef(false);
+ const isTouching = useRef(false);
+ const [isSnapping, setIsSnapping] = useState(false);
+ const [containerLeft, setContainerLeft] = useState(0);
+
+ const [isClient, setIsClient] = useState(false);
+
+ useEffect(() => {
+ setIsClient(true);
+ }, []);
+
+ useEffect(() => {
+ if (isHomePage && !auth) {
+ setActive(true);
+ fetchData();
+ }
+ }, [isHomePage, auth]);
+
+ const fetchData = async () => {
+ try {
+ const res = await fetch(`/api/hero-banner?type=dragable-banner`);
+ const { data } = await res.json();
+ if (Array.isArray(data) && data[0]?.image) {
+ setData(data[0]);
+ } else {
+ setActive(false);
+ }
+ } catch (error) {
+ console.error('Failed to fetch popup banner:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ const handleResizeOrZoom = () => {
+ if (!popupRef.current) return;
+
+ const popupWidth = popupRef.current.offsetWidth || 85;
+ const popupHeight = popupRef.current.offsetHeight || 85;
+
+ setPosition({
+ x: 20,
+ y: window.innerHeight - popupHeight - 20,
+ });
+ };
+
+ window.addEventListener('resize', handleResizeOrZoom);
+
+ return () => {
+ window.removeEventListener('resize', handleResizeOrZoom);
+ };
+ }, []);
+
+
+ const updateContainerLeft = () => {
+ const container = document.querySelector('.container');
+ if (container) {
+ setContainerLeft(container.getBoundingClientRect().left);
+ }
+ };
+
+ useEffect(() => {
+ updateContainerLeft();
+ window.addEventListener('resize', updateContainerLeft);
+ window.addEventListener('scroll', updateContainerLeft);
+ return () => {
+ window.removeEventListener('resize', updateContainerLeft);
+ window.removeEventListener('scroll', updateContainerLeft);
+ };
+ }, []);
+
+ useEffect(() => {
+ if (isDesktop && containerLeft) {
+ const popupWidth = popupRef.current?.offsetWidth || 85;
+ setPosition({
+ x: containerLeft - popupWidth - 20,
+ y: window.innerHeight - 130,
+ });
+ }
+ }, [isDesktop, containerLeft]);
+
+ const startDrag = (clientX, clientY) => {
+ dragStartPos.current = { x: clientX - position.x, y: clientY - position.y };
+ isDragging.current = false;
+ setIsSnapping(false);
+ };
+
+ const moveDrag = (clientX, clientY) => {
+ isDragging.current = true;
+ const popupWidth = popupRef.current?.offsetWidth || 85;
+ const popupHeight = popupRef.current?.offsetHeight || 85;
+ const maxX = window.innerWidth - popupWidth - 20;
+
+ const topPadding = isDesktop ? 0 : 130;
+ const bottomPadding = isDesktop ? 0 : 80;
+ const maxY = window.innerHeight - popupHeight - bottomPadding;
+
+ const minX = 0;
+
+ let newX = clientX - dragStartPos.current.x;
+ let newY = clientY - dragStartPos.current.y;
+
+ newX = Math.max(minX, Math.min(newX, maxX));
+ newY = Math.max(topPadding, Math.min(newY, maxY));
+
+ setPosition({ x: newX, y: newY });
+ };
+
+ const endDrag = () => {
+ if (isDragging.current) {
+ setIsSnapping(true);
+
+ if (isDesktop) {
+ const popupWidth = popupRef.current?.offsetWidth || 85;
+ setPosition({
+ x: containerLeft - popupWidth - 20,
+ y: window.innerHeight - 130,
+ });
+ } else {
+ const popupWidth = popupRef.current?.offsetWidth || 85;
+ const screenMiddle = window.innerWidth / 2;
+
+ setPosition(pos => ({
+ x: pos.x + popupWidth / 2 < screenMiddle ? 20 : window.innerWidth - popupWidth - 20,
+ y: pos.y,
+ }));
+ }
+ }
+ isDragging.current = false;
+ isTouching.current = false;
+ };
+
+
+
+ const handleMouseDown = e => {
+ e.preventDefault();
+ startDrag(e.clientX, e.clientY);
+ window.addEventListener('mousemove', handleMouseMove);
+ window.addEventListener('mouseup', handleMouseUp);
+ };
+
+ const handleMouseMove = e => moveDrag(e.clientX, e.clientY);
+
+ const handleMouseUp = () => {
+ endDrag();
+ window.removeEventListener('mousemove', handleMouseMove);
+ window.removeEventListener('mouseup', handleMouseUp);
+ };
+
+ const handleTouchStart = e => {
+ if (e.touches.length === 1) {
+ isTouching.current = true;
+ startDrag(e.touches[0].clientX, e.touches[0].clientY);
+ }
+ };
+
+ useEffect(() => {
+ const handleTouchMove = e => {
+ if (isTouching.current) {
+ e.preventDefault();
+ moveDrag(e.touches[0].clientX, e.touches[0].clientY);
+ }
+ };
+
+ const handleTouchEnd = () => endDrag();
+
+ window.addEventListener('touchmove', handleTouchMove, { passive: false });
+ window.addEventListener('touchend', handleTouchEnd);
+ return () => {
+ window.removeEventListener('touchmove', handleTouchMove);
+ window.removeEventListener('touchend', handleTouchEnd);
+ };
+ }, []);
+
+ if (!active || !data || loading) return null;
+
+ const popupContent = (
+ <div
+ ref={popupRef}
+ className={`relative pointer-events-auto ${isSnapping ? 'transition-transform duration-300 ease-out' : ''}`}
+ style={{
+ transform: `translate(${position.x}px, ${position.y}px)`,
+ cursor: 'grab',
+ width: '85px',
+ }}
+ onMouseDown={handleMouseDown}
+ onTouchStart={handleTouchStart}
+ >
+ <Link
+ href={typeof data.url === 'boolean' && data.url === false ? '/' : data.url}
+ onClick={e => {
+ if (isDragging.current) {
+ e.preventDefault();
+ isDragging.current = false;
+ } else {
+ setActive(false);
+ }
+ }}
+ draggable="false"
+ >
+ <Image
+ src={data.image}
+ alt={data.name || 'popup'}
+ width={85}
+ height={85}
+ className="w-full h-auto select-none"
+ draggable="false"
+ />
+ </Link>
+
+ <button
+ onClick={() => setActive(false)}
+ className="absolute -top-2 -right-2 z-10 p-1 bg-red-500 rounded-full hover:bg-red-600 transition-colors"
+ aria-label="Close popup"
+ >
+ <X className="w-3 h-3 text-white" />
+ </button>
+ </div>
+ );
+
+ if (isDesktop && isClient) {
+ return createPortal(
+ <div className="fixed z-[9999] pointer-events-none top-0 left-0 w-screen h-screen">
+ {popupContent}
+ </div>,
+ document.body
+ );
+ }
+
+ return (
+ <div className="fixed z-[9999] pointer-events-none top-[40px] left-0">
+ {popupContent}
+ </div>
+ );
+};
+
+export default PagePopupInformation;
diff --git a/src/lib/invoice/components/Invoice.jsx b/src/lib/invoice/components/Invoice.jsx
index 15bfa746..a26b231f 100644
--- a/src/lib/invoice/components/Invoice.jsx
+++ b/src/lib/invoice/components/Invoice.jsx
@@ -1,60 +1,66 @@
-import Spinner from '@/core/components/elements/Spinner/Spinner'
-import useInvoice from '../hooks/useInvoice'
-import { downloadInvoice, downloadTaxInvoice } from '../utils/invoices'
-import Divider from '@/core/components/elements/Divider/Divider'
-import VariantGroupCard from '@/lib/variant/components/VariantGroupCard'
-import currencyFormat from '@/core/utils/currencyFormat'
-import MobileView from '@/core/components/views/MobileView'
-import DesktopView from '@/core/components/views/DesktopView'
-import Menu from '@/lib/auth/components/Menu'
-import Link from '@/core/components/elements/Link/Link'
-import Image from '@/core/components/elements/Image/Image'
-import { createSlug } from '@/core/utils/slug'
-import { useEffect, useState } from 'react'
+import Spinner from '@/core/components/elements/Spinner/Spinner';
+import useInvoice from '../hooks/useInvoice';
+import { downloadInvoice, downloadTaxInvoice } from '../utils/invoices';
+import Divider from '@/core/components/elements/Divider/Divider';
+import VariantGroupCard from '@/lib/variant/components/VariantGroupCard';
+import currencyFormat from '@/core/utils/currencyFormat';
+import MobileView from '@/core/components/views/MobileView';
+import DesktopView from '@/core/components/views/DesktopView';
+import Menu from '@/lib/auth/components/Menu';
+import Link from '@/core/components/elements/Link/Link';
+import Image from '@/core/components/elements/Image/Image';
+import { createSlug } from '@/core/utils/slug';
+import { useEffect, useState } from 'react';
const Invoice = ({ id }) => {
- const PPN = process.env.NEXT_PUBLIC_PPN
- const { invoice } = useInvoice({ id })
+ const PPN = process.env.NEXT_PUBLIC_PPN;
+ const { invoice } = useInvoice({ id });
- const [totalAmount, setTotalAmount] = useState(0)
- const [totalDiscountAmount, setTotalDiscountAmount] = useState(0)
+ const [totalAmount, setTotalAmount] = useState(0);
+ const [totalDiscountAmount, setTotalDiscountAmount] = useState(0);
+
+ const amountBeforePPN = invoice.data?.amountTotal / PPN;
+ const taxAmount = invoice.data?.amountTotal - amountBeforePPN;
useEffect(() => {
if (invoice?.data?.products) {
- let calculateTotalAmount = 0
- let calculateTotalDiscountAmount = 0
+ let calculateTotalAmount = 0;
+ let calculateTotalDiscountAmount = 0;
invoice.data.products.forEach((product) => {
- calculateTotalAmount += product.price.price * product.quantity
+ calculateTotalAmount += product.price.price * product.quantity;
calculateTotalDiscountAmount +=
- (product.price.price - product.price.priceDiscount) * product.quantity
- })
- setTotalAmount(calculateTotalAmount)
- setTotalDiscountAmount(calculateTotalDiscountAmount)
+ (product.price.price - product.price.priceDiscount) *
+ product.quantity;
+ });
+ setTotalAmount(calculateTotalAmount);
+ setTotalDiscountAmount(calculateTotalDiscountAmount);
}
- }, [invoice])
+ }, [invoice]);
if (invoice.isLoading) {
return (
<div className='flex justify-center my-6'>
<Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' />
</div>
- )
+ );
}
- const address = invoice.data?.customer
- let fullAddress = []
- if (address?.street) fullAddress.push(address.street)
- if (address?.subDistrict?.name) fullAddress.push(address.subDistrict.name)
- if (address?.district?.name) fullAddress.push(address.district.name)
- if (address?.city?.name) fullAddress.push(address.city.name)
- fullAddress = fullAddress.join(', ')
+ const address = invoice.data?.customer;
+ let fullAddress = [];
+ if (address?.street) fullAddress.push(address.street);
+ if (address?.subDistrict?.name) fullAddress.push(address.subDistrict.name);
+ if (address?.district?.name) fullAddress.push(address.district.name);
+ if (address?.city?.name) fullAddress.push(address.city.name);
+ fullAddress = fullAddress.join(', ');
return (
invoice.data?.name && (
<>
<MobileView>
<div className='flex flex-col gap-y-4 p-4'>
- <DescriptionRow label='No Invoice'>{invoice.data?.name}</DescriptionRow>
+ <DescriptionRow label='No Invoice'>
+ {invoice.data?.name}
+ </DescriptionRow>
<DescriptionRow label='Status Transaksi'>
{invoice.data?.amountResidual > 0 ? (
<span className='badge-solid-red'>Belum Lunas</span>
@@ -68,13 +74,18 @@ const Invoice = ({ id }) => {
<DescriptionRow label='Ketentuan Pembayaran'>
{invoice.data?.paymentTerm}
</DescriptionRow>
- {invoice.data?.amountResidual > 0 && invoice.invoiceDate != invoice.invoiceDateDue && (
- <DescriptionRow label='Tanggal Jatuh Tempo'>
- {invoice.data?.invoiceDateDue}
- </DescriptionRow>
- )}
- <DescriptionRow label='Nama Sales'>{invoice.data?.sales}</DescriptionRow>
- <DescriptionRow label='Tanggal Invoice'>{invoice.data?.invoiceDate}</DescriptionRow>
+ {invoice.data?.amountResidual > 0 &&
+ invoice.invoiceDate != invoice.invoiceDateDue && (
+ <DescriptionRow label='Tanggal Jatuh Tempo'>
+ {invoice.data?.invoiceDateDue}
+ </DescriptionRow>
+ )}
+ <DescriptionRow label='Nama Sales'>
+ {invoice.data?.sales}
+ </DescriptionRow>
+ <DescriptionRow label='Tanggal Invoice'>
+ {invoice.data?.invoiceDate}
+ </DescriptionRow>
<div className='flex items-center'>
<p className='text-gray_r-11 leading-none'>Invoice</p>
<button
@@ -104,8 +115,12 @@ const Invoice = ({ id }) => {
<div className='flex flex-col gap-y-4 p-4 border-t border-gray_r-6'>
<DescriptionRow label='Nama'>{address?.name}</DescriptionRow>
- <DescriptionRow label='Email'>{address?.email || '-'}</DescriptionRow>
- <DescriptionRow label='No Telepon'>{address?.mobile || '-'}</DescriptionRow>
+ <DescriptionRow label='Email'>
+ {address?.email || '-'}
+ </DescriptionRow>
+ <DescriptionRow label='No Telepon'>
+ {address?.mobile || '-'}
+ </DescriptionRow>
<DescriptionRow label='Alamat'>{fullAddress}</DescriptionRow>
</div>
@@ -128,10 +143,14 @@ const Invoice = ({ id }) => {
<Menu />
</div>
<div className='w-9/12 p-4 bg-white border border-gray_r-6 rounded'>
- <h1 className='text-title-sm font-semibold mb-6'>Invoice & Faktur Pajak</h1>
+ <h1 className='text-title-sm font-semibold mb-6'>
+ Invoice & Faktur Pajak
+ </h1>
<div className='flex items-center gap-x-2 mb-3'>
- <span className='text-h-sm font-medium'>{invoice?.data?.name}</span>
+ <span className='text-h-sm font-medium'>
+ {invoice?.data?.name}
+ </span>
{invoice?.data?.amountResidual > 0 ? (
<div className='badge-solid-red h-fit'>Belum Lunas</div>
) : (
@@ -180,14 +199,16 @@ const Invoice = ({ id }) => {
</div>
</div>
- <div className='text-h-sm font-semibold mt-6 mb-4'>Rincian Pembelian</div>
+ <div className='text-h-sm font-semibold mt-6 mb-4'>
+ Rincian Pembelian
+ </div>
<table className='table-data'>
<thead>
<tr>
<th>Nama Produk</th>
<th>Jumlah</th>
<th>Harga</th>
- <th>Diskon</th>
+ {/* <th>Diskon</th> */}
<th>Subtotal</th>
</tr>
</thead>
@@ -229,13 +250,22 @@ const Invoice = ({ id }) => {
</div>
</td>
<td>{product.quantity}</td>
- <td>{currencyFormat(product.price.price)}</td>
<td>
+ {currencyFormat(
+ product.price.priceDiscount - taxAmount
+ )}
+ </td>
+ {/* <td>
{product.price.discountPercentage > 0
? `${product.price.discountPercentage}%`
: ''}
+ </td> */}
+ <td>
+ {currencyFormat(
+ product.price.priceDiscount * product.quantity -
+ taxAmount
+ )}
</td>
- <td>{currencyFormat(product.price.priceDiscount * product.quantity)}</td>
</tr>
))}
</tbody>
@@ -244,20 +274,30 @@ const Invoice = ({ id }) => {
<div className='flex justify-end mt-4'>
<div className='w-1/4 grid grid-cols-2 gap-y-2 text-gray_r-12/80'>
<div className='text-right'>Subtotal</div>
- <div className='text-right font-medium'>{currencyFormat(totalAmount)}</div>
+ <div className='text-right font-medium'>
+ {currencyFormat(
+ totalAmount - totalDiscountAmount - taxAmount
+ )}
+ </div>
- <div className='text-right'>Total Diskon</div>
+ {/* <div className='text-right'>Total Diskon</div>
<div className='text-right font-medium'>
- {currencyFormat(-totalDiscountAmount)}
+ {currencyFormat(totalDiscountAmount)}
+ </div> */}
+ <div className='text-right'>
+ PPN {((PPN - 1) * 100).toFixed(0)}%
+ </div>
+ <div className='text-right font-medium'>
+ {currencyFormat(
+ invoice.data?.amountTotal -
+ invoice.data?.amountTotal / PPN
+ )}
</div>
<div className='text-right'>Grand Total</div>
<div className='text-right font-medium text-gray_r-12'>
{currencyFormat(invoice.data?.amountTotal)}
</div>
-
- <div className='text-right'>PPN {((PPN - 1) * 100).toFixed(0)}% (Incl.)</div>
- <div className='text-right font-medium'>{currencyFormat(invoice.data?.amountTotal - totalAmount)}</div>
</div>
</div>
</div>
@@ -265,14 +305,14 @@ const Invoice = ({ id }) => {
</DesktopView>
</>
)
- )
-}
+ );
+};
const DescriptionRow = ({ children, label }) => (
<div className='grid grid-cols-2'>
<span className='text-gray_r-11'>{label}</span>
<span className='text-right'>{children}</span>
</div>
-)
+);
-export default Invoice
+export default Invoice;
diff --git a/src/lib/maps/components/PinPointMap.jsx b/src/lib/maps/components/PinPointMap.jsx
new file mode 100644
index 00000000..c46d838a
--- /dev/null
+++ b/src/lib/maps/components/PinPointMap.jsx
@@ -0,0 +1,226 @@
+import React, { useState, useCallback, useRef, useEffect } 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';
+
+const containerStyle = {
+ width: '100%',
+ height: '400px',
+};
+
+const defaultCenter = {
+ lat: -6.2,
+ lng: 106.816666,
+};
+
+const PinpointLocation = ({ initialLatitude, initialLongitude, initialAddress }) => {
+ const { isLoaded } = useJsApiLoader({
+ googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_API_KEY,
+ libraries: ['places'],
+ });
+
+ const {
+ addressMaps,
+ setAddressMaps,
+ selectedPosition,
+ setSelectedPosition,
+ setDetailAddress,
+ setPinedMaps,
+ } = useMaps();
+
+ const [tempAddress, setTempAddress] = useState(initialAddress || '');
+ const [tempPosition, setTempPosition] = useState(
+ initialLatitude && initialLongitude
+ ? { lat: parseFloat(initialLatitude), lng: parseFloat(initialLongitude) }
+ : selectedPosition.lat && selectedPosition.lng
+ ? selectedPosition
+ : defaultCenter
+ );
+
+ const [markerIcon, setMarkerIcon] = useState(null);
+
+ const autocompleteRef = useRef(null);
+
+ useEffect(() => {
+ if (isLoaded && window.google) {
+ setMarkerIcon({
+ url: 'https://cdn.pixabay.com/photo/2014/04/03/10/03/google-309740_1280.png',
+ scaledSize: new window.google.maps.Size(25, 40),
+ });
+ }
+
+ // If we have initial coordinates but no address, fetch the address
+ if (initialLatitude && initialLongitude && !initialAddress) {
+ getAddress(parseFloat(initialLatitude), parseFloat(initialLongitude));
+ }
+ }, [isLoaded, initialLatitude, initialLongitude, initialAddress]);
+
+ 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 formattedAddress = data.results[0].formatted_address;
+
+ const details = {
+ street:
+ getAddressComponent(addressComponents, 'route') +
+ ' ' +
+ getAddressComponent(addressComponents, 'street_number'),
+ 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(formattedAddress);
+ }
+ } catch (error) {
+ console.error('Error fetching address:', error);
+ }
+ };
+
+ const onMapClick = useCallback((event) => {
+ const lat = event.latLng.lat();
+ const lng = event.latLng.lng();
+ const newPosition = { lat, lng };
+ setTempPosition(newPosition);
+ 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();
+ const newPosition = { lat, lng };
+ setTempPosition(newPosition);
+ setTempAddress(place.formatted_address);
+ getAddress(lat, lng);
+ }
+ };
+
+ const handleUseCurrentLocation = () => {
+ if (navigator.geolocation) {
+ navigator.geolocation.getCurrentPosition(
+ (position) => {
+ const lat = position.coords.latitude;
+ const lng = position.coords.longitude;
+ const newPosition = { lat, lng };
+ setTempPosition(newPosition);
+ 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);
+ setPinedMaps(false);
+ };
+
+ 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...'
+ value={tempAddress}
+ onChange={(e) => setTempAddress(e.target.value)}
+ style={{ width: '100%', padding: '8px' }}
+ />
+ </Autocomplete>
+ ) : (
+ <p>Loading autocomplete...</p>
+ )}
+ </div>
+
+ <div>
+ {isLoaded ? (
+ <GoogleMap
+ mapContainerStyle={containerStyle}
+ center={tempPosition}
+ zoom={15}
+ onClick={onMapClick}
+ >
+ {markerIcon && (
+ <Marker
+ position={tempPosition}
+ draggable={true}
+ onDragEnd={(e) => {
+ const lat = e.latLng.lat();
+ const lng = e.latLng.lng();
+ const newPosition = { lat, lng };
+ setTempPosition(newPosition);
+ getAddress(lat, lng);
+ }}
+ icon={markerIcon}
+ />
+ )}
+ </GoogleMap>
+ ) : (
+ <p>Loading map...</p>
+ )}
+ </div>
+
+ <div style={{ marginTop: '20px' }}>
+ <Button variant='solid' onClick={handleUseCurrentLocation}>
+ <LocateFixed className='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 className='h-8 w-8 text-gray-500 mr-3' />
+ <label>{tempAddress || 'Pilih lokasi di peta'}</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; \ No newline at end of file
diff --git a/src/lib/maps/stores/useMaps.js b/src/lib/maps/stores/useMaps.js
new file mode 100644
index 00000000..c57a05ad
--- /dev/null
+++ b/src/lib/maps/stores/useMaps.js
@@ -0,0 +1,32 @@
+import { create } from "zustand";
+
+const center = {
+ lat: -6.200000, // Default latitude (Jakarta)
+ lng: 106.816666, // Default longitude (Jakarta)
+};
+
+export const useMaps = create((set) => ({
+ // State existing
+ selectedPosition: center,
+ addressMaps: '',
+ detailAddress: {},
+ pinedMaps: false,
+
+ // State tambahan untuk penyimpanan posisi sementara
+ tempPositionCreate: null,
+ tempPositionEdit: null,
+
+ // Setter existing
+ setSelectedPosition: (position) => set({ selectedPosition: position }),
+ setAddressMaps: (addressMaps) => set({ addressMaps }),
+ setDetailAddress: (detailAddress) => set({ detailAddress }),
+ setPinedMaps: (pinedMaps) => set({ pinedMaps }),
+
+ // Setter tambahan untuk posisi sementara
+ setTempPositionCreate: (position) => set({ tempPositionCreate: position }),
+ setTempPositionEdit: (position) => set({ tempPositionEdit: position }),
+
+ // Opsional: Reset jika ingin clear saat keluar halaman
+ resetTempPositionCreate: () => set({ tempPositionCreate: null }),
+ resetTempPositionEdit: () => set({ tempPositionEdit: null }),
+}));
diff --git a/src/lib/pengajuan-tempo/component/FinishTempo.jsx b/src/lib/pengajuan-tempo/component/FinishTempo.jsx
index bfcd0909..aacb9ef3 100644
--- a/src/lib/pengajuan-tempo/component/FinishTempo.jsx
+++ b/src/lib/pengajuan-tempo/component/FinishTempo.jsx
@@ -95,14 +95,20 @@ const FinishTempo = ({ query }) => {
'Proses pengajuan tempo anda sudah berhasil terdaftar di indoteknik.com. Nikmati pembelian anda di website indoteknik dengan menggunakan pembayaran tempo'}
</div>
<Link
- href={query?.status == 'switch-account' ? `/my/profile` : `/my/tempo/`}
+ href={
+ query?.status === 'switch-account'
+ ? '/my/profile'
+ : query?.status === 'approve'
+ ? '/my/tempo/'
+ : '/'
+ }
className='btn-solid-red rounded-md text-base flex flex-row items-center justify-center'
>
- {query?.status == 'switch-account'
+ {query?.status === 'switch-account'
? 'Ubah Akun'
- : query?.status == 'approve'
+ : query?.status === 'approve'
? 'Lihat Detail Tempo'
- : 'Lihat Status Pendaftaran'}
+ : 'Kembali Ke Beranda'}
<ChevronRightIcon className='w-5' />
</Link>
</div>
diff --git a/src/lib/pengajuan-tempo/component/PengajuanTempo.jsx b/src/lib/pengajuan-tempo/component/PengajuanTempo.jsx
index 7cf201b7..d59bfd75 100644
--- a/src/lib/pengajuan-tempo/component/PengajuanTempo.jsx
+++ b/src/lib/pengajuan-tempo/component/PengajuanTempo.jsx
@@ -527,7 +527,7 @@ const PengajuanTempo = () => {
toast.dismiss(toastId);
setIsLoading(false);
- toast.error('Terjadi kesalahan dalam pengiriman formulir hehehehe');
+ toast.error('Terjadi kesalahan dalam pengiriman formulir');
console.error(error);
}
};
diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx
index eb86485d..2fb3138a 100644
--- a/src/lib/product/components/ProductSearch.jsx
+++ b/src/lib/product/components/ProductSearch.jsx
@@ -498,7 +498,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'
/>
@@ -688,7 +688,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'
/>
diff --git a/src/lib/shipment/components/Shipments.jsx b/src/lib/shipment/components/Shipments.jsx
index 20dbb013..aaf778c3 100644
--- a/src/lib/shipment/components/Shipments.jsx
+++ b/src/lib/shipment/components/Shipments.jsx
@@ -251,7 +251,7 @@ const Shipments = () => {
<tr>
<th>Tanggal</th>
<th>No. Resi</th>
- <th>No. Pengiriman</th>
+ <th>No. Dokumen</th>
<th>Sales Order</th>
<th>Purchase Order</th>
<th>Expedisi</th>
diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx
index 9216f7f7..2c1a6208 100644
--- a/src/lib/transaction/components/Transaction.jsx
+++ b/src/lib/transaction/components/Transaction.jsx
@@ -46,6 +46,8 @@ import {
downloadTaxInvoice,
} from '@/lib/invoice/utils/invoices';
import axios from 'axios';
+import InformationSection from '../../treckingAwb/component/InformationSection';
+import { Button } from '@chakra-ui/react';
const Transaction = ({ id }) => {
const PPN = process.env.NEXT_PUBLIC_PPN;
const router = useRouter();
@@ -225,70 +227,32 @@ const Transaction = ({ id }) => {
transaction.refetch();
};
- const countWeight = (products) => {
- if (!products || !Array.isArray(products)) return 0.0;
-
- const weight = products.reduce(
- (total, product) => total + (product.weight * product.quantity || 0),
- 0
- );
- return weight + ' Kg';
- };
-
- // const memoizeVariantGroupCard = useMemo(
- // () => (
- // <div className='p-4 pt-0 flex flex-col gap-y-3'>
- // <VariantGroupCard variants={transaction.data?.products} buyMore />
- // <div className='font-medium'>Info Pengiriman</div>
- // <div className='flex justify-between mt-1'>
- // <p className='text-gray_r-12/70'>Metode Pembayaran</p>
- // <p>
- // {transaction.data?.paymentType
- // ? transaction.data?.paymentType
- // ?.replace(/_/g, ' ')
- // .replace(/\b\w/g, (char) => char.toUpperCase())
- // : '-'}
- // </p>
- // </div>
- // <div className='flex justify-between mt-1'>
- // <p className='text-gray_r-12/70'>Berat Barang</p>
- // <p>{transaction.data?.pickings[0]?.weightTotal}</p>
- // </div>
- // <hr className='mt-1 border border-gray-100' />
- // <div className='flex justify-between mt-1'>
- // <p className='text-gray_r-12/70'>Total Belanja</p>
- // <p>{currencyFormat(totalAmount)}</p>
- // </div>
- // <div className='flex justify-between mt-1'>
- // <p className='text-gray_r-12/70'>Diskon Belanja</p>
- // <p>{'- ' + currencyFormat(totalDiscountAmount)}</p>
- // </div>
- // <div className='flex justify-between mt-1'>
- // <p className='text-gray_r-12/70'>Subtotal</p>
- // <p>{currencyFormat(transaction.data?.amountUntaxed)}</p>
- // </div>
- // <div className='flex justify-between mt-1'>
- // <p className='text-gray_r-12/70'>
- // PPN {((PPN - 1) * 100).toFixed(0)}%
- // </p>
- // <p>{currencyFormat(transaction.data?.amountTax)}</p>
- // </div>
- // <div className='flex justify-between mt-1'>
- // <p className='text-gray_r-12/70'>Biaya Pengiriman</p>
- // <p>{currencyFormat(transaction.data?.deliveryAmount)}</p>
- // </div>
- // <div className='flex justify-between mt-1 font-medium'>
- // <p className='text-gray_r-12/70'>Asuransi Pengiriman</p>
- // <p>{currencyFormat(transaction.data?.amountTotal)}</p>
- // </div>
- // <div className='flex justify-between mt-1 font-medium'>
- // <p>Grand Total</p>
- // <p>{currencyFormat(transaction.data?.amountTotal)}</p>
- // </div>
- // </div>
- // ),
- // [transaction.data]
- // );
+ const memoizeVariantGroupCard = useMemo(
+ () => (
+ <div className='p-4 pt-0 flex flex-col gap-y-3'>
+ <VariantGroupCard variants={transaction.data?.products} buyMore />
+ <div className='flex justify-between mt-1'>
+ <p className='text-gray_r-12/70'>Subtotal</p>
+ <p>{currencyFormat(transaction.data?.amountUntaxed)}</p>
+ </div>
+ <div className='flex justify-between mt-1'>
+ <p className='text-gray_r-12/70'>
+ PPN {((PPN - 1) * 100).toFixed(0)}%
+ </p>
+ <p>{currencyFormat(transaction.data?.amountTax)}</p>
+ </div>
+ <div className='flex justify-between mt-1'>
+ <p className='text-gray_r-12/70'>Biaya Pengiriman</p>
+ <p>{currencyFormat(transaction.data?.deliveryAmount)}</p>
+ </div>
+ <div className='flex justify-between mt-1 font-medium'>
+ <p>Grand Total</p>
+ <p>{currencyFormat(transaction.data?.amountTotal)}</p>
+ </div>
+ </div>
+ ),
+ [transaction.data]
+ );
const memoizeVariantGroupCardReject = useMemo(
() => (
@@ -595,6 +559,16 @@ const Transaction = ({ id }) => {
<Divider />
<div className='flex flex-col gap-y-4 p-4'>
+ <DescriptionRow label='Status Transaksi'>
+ <div className='flex justify-end'>
+ <TransactionStatusBadge status={transaction.data?.status} />
+ </div>
+ </DescriptionRow>
+ <DescriptionRow label='Status Transaksi'>
+ <div className='flex justify-end font-semibold text-red-500'>
+ {transaction.data?.expectedReadyToShip}
+ </div>
+ </DescriptionRow>
<DescriptionRow label='No Transaksi'>
<p className='font-semibold'>{transaction.data?.name}</p>
</DescriptionRow>
@@ -709,31 +683,7 @@ const Transaction = ({ id }) => {
</div>
</DescriptionRow>
</div>
- {/* <div className='flex flex-col gap-y-3 mt-4'>
- {transaction?.data?.pickings?.map((airway) => (
- <button
- className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left'
- key={airway?.id}
- onClick={() => setIdAWB(airway?.id)}
- >
- <div>
- <span className='text-sm text-gray_r-11'>
- No Resi : {airway?.trackingNumber || '-'}{' '}
- </span>
- <p className='mt-1 font-medium'>{airway?.name}</p>
- </div>
- <div className='flex gap-x-2'>
- <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1'>
- {airway?.delivered ? 'Pesanan Tiba' : 'Sedang Dikirim'}
- </div>
- <ChevronRightIcon className='w-5 stroke-2' />
- </div>
- </button>
- ))}
- </div>
- {transaction?.data?.pickings == 0 && (
- <div className='badge-red text-sm px-2'>Belum ada pengiriman</div>
- )} */}
+
</div>
{/* <Divider />
@@ -948,48 +898,25 @@ const Transaction = ({ id }) => {
)}
</div>
- <div className=''>
- <div
- class='flex items-center p-4 mb-4 text-sm border border-yellow-500 text-yellow-800 rounded-lg bg-yellow-50'
- role='alert'
- >
- <svg
- class='flex-shrink-0 inline w-5 h-5 mr-2'
- aria-hidden='true'
- fill='currentColor'
- viewBox='0 0 20 20'
- >
- <path d='M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z' />
- </svg>
- <span class='sr-only'>Info Transaksi</span>
- <div className='text-justify flex flex-col gap-1'>
- <p className='font-bold text-black'>Info Transaksi</p>
- <span className='text-black'>
- Lorem ipsum dolor sit amet, consectetur adipiscing elit,
- sed do eiusmod tempor incididunt ut labore et dolore magna
- aliqua.
- </span>
- </div>
- </div>
- </div>
- <div className='flex flex-row items-center justify-between'>
- <div className='flex items-center gap-x-2 mb-3'>
- <span className='text-h-sm font-medium'>
- {transaction?.data?.name}
- </span>
- <TransactionStatusBadge status={transaction?.data?.status} />
- </div>
- <div>
- <div className='relative inline-block text-left'>
- <button
- onClick={() => downloadQuotation(transaction.data)}
- type='button'
- className='btn-light bg-slate-50 mt-3 w-full gap-2 items-center flex flex-row !text-gray_r-11 px-4 py-3 mb-2'
- >
- <p>Export to PDF</p>
- </button>
- </div>
- </div>
+ {/*new-release*/}
+ {/*<div className='flex items-center justify-between mb-3'>*/}
+ {/* <div className='flex items-center gap-x-2'>*/}
+ {/* <span className='text-h-sm font-medium'>*/}
+ {/* {transaction?.data?.name}*/}
+ {/* </span>*/}
+ {/* <TransactionStatusBadge status={transaction?.data?.status} />*/}
+ {/* </div>*/}
+ {/* <div className='text-h-sm'>*/}
+ {/* Estimasi Barang Siap:{' '}*/}
+ {/* <span className='text-red-500 font-semibold'>*/}
+ {/* {transaction?.data?.expectedReadyToShip}*/}
+ {/* </span>*/}
+ {/* </div>*/}
+ <div className='flex items-center gap-x-2 mb-3'>
+ <span className='text-h-sm font-medium'>
+ {transaction?.data?.name}
+ </span>
+ <TransactionStatusBadge status={transaction?.data?.status} />
</div>
{/* <div className='flex gap-x-4'>
<button
@@ -1284,54 +1211,103 @@ const Transaction = ({ id }) => {
</div>
</div>
- <hr className='mt-4 mb-4 border border-gray-100' />
-
- {/* <div className='text-h-sm font-semibold mt-10 mb-4'>
- Informasi Pelanggan
- </div>
- <div className='grid grid-cols-2 gap-x-4'>
- <div className='border border-gray_r-6 rounded p-3'>
- <div className='font-medium mb-4'>Detail Pelanggan</div>
- <SectionContent
- address={transaction?.data?.address?.customer}
- />
+ <div className='flex gap-x-3'>
+ <div className='w-1/2'>
+ <div className='text-h-sm font-semibold mt-10 mb-4'>
+ Informasi Pelanggan
+ </div>
+ <div className='border border-gray_r-6 rounded p-3'>
+ <div className='font-medium mb-4'>Detail Pelanggan</div>
+ <SectionContent
+ address={transaction?.data?.address?.customer}
+ />
+ </div>
</div>
- </div>
- <div className='flex '>
<div className='w-1/2'>
<div className='text-h-sm font-semibold mt-10 mb-4'>
- Pengiriman
+ Informasi Pengiriman
</div>
{transaction?.data?.pickings.length == 0 && (
<div className='badge-red text-sm'>
Belum ada pengiriman
</div>
)}
- <div className='grid grid-cols-1 gap-1 w-2/3'>
- {transaction?.data?.pickings?.map((airway) => (
- <button
- className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left h-20'
- key={airway?.id}
- onClick={() => setIdAWB(airway?.id)}
- >
- <div>
- <span className='text-sm text-gray_r-11'>
- No Resi : {airway?.trackingNumber || '-'}{' '}
- </span>
- <p className='mt-1 font-medium'>{airway?.name}</p>
- </div>
- <div className='flex gap-x-2'>
- <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1'>
- {airway?.delivered
- ? 'Pesanan Tiba'
- : 'Sedang Dikirim'}
- </div>
- <ChevronRightIcon className='w-5 stroke-2' />
- </div>
- </button>
- ))}
- </div>
+ {/* <div className='grid grid-cols-1 gap-1 w-1/2'> */}
+ {transaction?.data?.pickings?.map((airway) => (
+ <div
+ key={airway?.id}
+ className='border border-gray_r-6 rounded p-3'
+ >
+ <InformationSection manifests={airway} />
+ <div className='p-4'>
+ <button
+ className='bg-transparent text-red-600 hover:underline p-0 font-semibold'
+ onClick={() => {
+ if (airway?.waybillNumber == '-') {
+ toast.error('Nomor Resi belum tersedia');
+ return;
+ }
+ setIdAWB(airway.id);
+ }}
+ >
+ Lacak Pengiriman
+ </button>
+ </div>
+ </div>
+ // <button
+ // className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left h-20'
+ // key={airway?.id}
+ // onClick={() => setIdAWB(airway?.id)}
+ // >
+ // <div>
+ // <span className='text-sm text-gray_r-11'>
+ // No Resi : {airway?.trackingNumber || '-'}{' '}
+ // </span>
+ // <p className='mt-1 font-medium'>{airway?.name}</p>
+ // </div>
+ // <div className='flex gap-x-2'>
+ // <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1'>
+ // {airway?.delivered
+ // ? 'Pesanan Tiba'
+ // : 'Sedang Dikirim'}
+ // </div>
+ // <ChevronRightIcon className='w-5 stroke-2' />
+ // </div>
+ // </button>
+ ))}
+ {/* </div> */}
</div>
+ </div>
+
+ <div className='flex '>
+ {/*New release*/}
+ {/* <div className='grid grid-cols-1 gap-1 w-2/3'>*/}
+ {/* {transaction?.data?.pickings?.map((airway) => (*/}
+ {/* <button*/}
+ {/* className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left h-20'*/}
+ {/* key={airway?.id}*/}
+ {/* onClick={() => setIdAWB(airway?.id)}*/}
+ {/* >*/}
+ {/* <div>*/}
+ {/* <p className='text-sm text-gray_r-11'>*/}
+ {/* {airway?.name}*/}
+ {/* </p>*/}
+ {/* <span className='text-md text-bold mt-1'>*/}
+ {/* No Resi : {airway?.trackingNumber || '-'}{' '}*/}
+ {/* </span>*/}
+ {/* </div>*/}
+ {/* <div className='flex gap-x-2'>*/}
+ {/* <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1 text-center'>*/}
+ {/* {airway?.delivered*/}
+ {/* ? 'Pesanan Tiba'*/}
+ {/* : 'Sedang Dikirim'}*/}
+ {/* </div>*/}
+ {/* <ChevronRightIcon className='w-5 stroke-2' />*/}
+ {/* </div>*/}
+ {/* </button>*/}
+ {/* ))}*/}
+ {/* </div>*/}
+ {/*</div>*/}
<div className='invoice w-1/2 '>
<div className='text-h-sm font-semibold mt-10 mb-4 '>
Invoice
@@ -1362,7 +1338,7 @@ const Transaction = ({ id }) => {
))}
</div>
</div>
- </div> */}
+ </div>
<div className='text-h-sm font-semibold mt-4 mb-4'>
Rincian Pembelian
diff --git a/src/lib/treckingAwb/api/getManifest.js b/src/lib/treckingAwb/api/getManifest.js
index 7d78a5f2..5523a164 100644
--- a/src/lib/treckingAwb/api/getManifest.js
+++ b/src/lib/treckingAwb/api/getManifest.js
@@ -1,9 +1,12 @@
-const { default: odooApi } = require("@/core/api/odooApi")
-const { getAuth } = require("@/core/utils/auth")
+const { default: odooApi } = require('@/core/api/odooApi');
+const { getAuth } = require('@/core/utils/auth');
-export const getManifest = async ({id}) => {
- const auth = getAuth()
- const manifest = await odooApi('GET', `/api/v1/partner/${auth.partnerId}/stock-picking/${id}/tracking`)
+export const getManifest = async ({ id }) => {
+ const auth = getAuth();
+ const manifest = await odooApi(
+ 'GET',
+ `/api/v1/partner/${auth.partnerId}/stock-picking/${id}/tracking`
+ );
- return manifest
-} \ No newline at end of file
+ return manifest;
+};
diff --git a/src/lib/treckingAwb/component/InformationSection.jsx b/src/lib/treckingAwb/component/InformationSection.jsx
new file mode 100644
index 00000000..a2297af3
--- /dev/null
+++ b/src/lib/treckingAwb/component/InformationSection.jsx
@@ -0,0 +1,77 @@
+import { useState } from 'react';
+import toast from 'react-hot-toast';
+
+const InformationSection = ({ manifests }) => {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopyClick = () => {
+ const textToCopy = manifests?.waybillNumber;
+ navigator.clipboard.writeText(textToCopy);
+ setCopied(true);
+ toast.success('No Resi Berhasil di Copy');
+ setTimeout(() => setCopied(false), 2000); // Reset copied state after 2 seconds
+ };
+
+ return (
+ <div className=''>
+ <div className='grid gap-y-4 p-4'>
+ <div className='grid grid-cols-[150px_auto] mb-4'>
+ <span className='font-semibold '>Nomor Resi</span>
+ <div className='flex gap-x-3'>
+ <span className='font-semibold'>: {manifests?.waybillNumber} </span>
+ <button
+ className={`${copied ? 'text-gray-400' : 'text-red-600 '}`}
+ onClick={() => handleCopyClick()}
+ >
+ <svg
+ aria-hidden='true'
+ fill='none'
+ stroke='currentColor'
+ stroke-width='1.5'
+ viewBox='0 0 24 24'
+ className='w-5 h-6'
+ >
+ <path
+ d='M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75'
+ stroke-linecap='round'
+ stroke-linejoin='round'
+ ></path>
+ </svg>
+ </button>
+ </div>
+ </div>
+ {/*<div className='grid grid-cols-[150px_auto]'>*/}
+ {/* <span>Barang</span>*/}
+ {/* <span className='font-semibold'>: {manifests?.products}</span>*/}
+ {/*</div>*/}
+ <div className='grid grid-cols-[150px_auto]'>
+ <span>Kurir</span>
+ <span className='font-semibold'>
+ {' '}
+ : {manifests?.deliveryOrder?.carrier}
+ </span>
+ </div>
+ <div className='grid grid-cols-[150px_auto]'>
+ <span>Jenis Service</span>
+ <span className='font-semibold'>
+ {' '}
+ : {manifests?.deliveryOrder?.service}
+ </span>
+ </div>
+ <div className='grid grid-cols-[150px_auto]'>
+ <span>Tanggal Dikirim</span>
+ <span className='font-semibold'> : {manifests?.deliveredDate}</span>
+ </div>
+ <div className='grid grid-cols-[150px_auto]'>
+ <span>Estimasi Tiba</span>
+ <span className='font-semibold'>
+ :{' '}
+ <span className='text-red-600 font-semibold'>{manifests?.eta}</span>
+ </span>
+ </div>
+ </div>
+ </div>
+ );
+};
+
+export default InformationSection;
diff --git a/src/lib/treckingAwb/component/Manifest.jsx b/src/lib/treckingAwb/component/Manifest.jsx
index 109bc832..5b456ccf 100644
--- a/src/lib/treckingAwb/component/Manifest.jsx
+++ b/src/lib/treckingAwb/component/Manifest.jsx
@@ -6,6 +6,32 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-hot-toast';
import ImageNext from 'next/image';
import { list } from 'postcss';
+import InformationSection from './InformationSection';
+import Link from 'next/link';
+
+function capitalizeFirstLetter(str) {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+}
+
+function capitalizeWords(str) {
+ return str
+ .split(' ')
+ .map((word) => capitalizeFirstLetter(word))
+ .join(' ');
+}
+
+function mappingLiveTracking(kurir, resi){
+ let url = null
+ switch (kurir){
+ case('grab'):
+ url = 'https://express.grab.com/track/orders?ids='+resi
+ break;
+ default:
+ url = false
+ }
+
+ return url
+}
const Manifest = ({ idAWB, closePopup }) => {
const [manifests, setManifests] = useState(null);
@@ -60,15 +86,7 @@ const Manifest = ({ idAWB, closePopup }) => {
}
}, [idAWB]);
- const [copied, setCopied] = useState(false);
-
- const handleCopyClick = () => {
- const textToCopy = manifests?.waybillNumber;
- navigator.clipboard.writeText(textToCopy);
- setCopied(true);
- toast.success('No Resi Berhasil di Copy');
- setTimeout(() => setCopied(false), 2000); // Reset copied state after 2 seconds
- };
+ const isLiveTracking = mappingLiveTracking(manifests?.deliveryOrder?.carrier.toLowerCase(), manifests?.waybillNumber)
return (
<>
@@ -85,7 +103,7 @@ const Manifest = ({ idAWB, closePopup }) => {
{!isLoading && (
<BottomPopup
key={manifests?.waybillNumber}
- title='Detail Pengiriman'
+ title='Lacak Pengiriman'
active={idAWB}
close={closePopup}
>
@@ -101,12 +119,23 @@ const Manifest = ({ idAWB, closePopup }) => {
<p className='text-yellow-600 text-sm'>Sedang Dikirim</p>
</div>
)}
+ {manifests?.status === 'cancelled' && (
+ <div className='bg-red-800 p-2 rounded '>
+ <p className='text-white text-sm'>Di Batalkan</p>
+ </div>
+ )}
+ {manifests?.status === 'on_hold' && (
+ <div className='bg-red-800 p-2 rounded '>
+ <p className='text-white text-sm'>Ditunda Sementara </p>
+ </div>
+ )}
{manifests?.status === 'pending' && (
<div className='bg-red-100 p-2 rounded '>
<p className='text-red-600 text-sm'>Pending</p>
</div>
)}
</div>
+<<<<<<< HEAD
<div className=''>
<h1 className='text-body-1'>
Estimasi tiba pada{' '}
@@ -180,25 +209,96 @@ const Manifest = ({ idAWB, closePopup }) => {
} `}
/>
)}
+=======
+>>>>>>> new-release
+
+ <InformationSection manifests={manifests} />
+
+ <hr className='mt-4' />
+ {manifests?.isDelay && (
+ <div
+ class='flex items-center p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50'
+ role='alert'
+ >
+ <svg
+ class='flex-shrink-0 inline w-4 h-4 mr-3'
+ aria-hidden='true'
+ fill='currentColor'
+ viewBox='0 0 20 20'
+ >
+ <path d='M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z' />
+ </svg>
+ <div>Pesanan anda mungkin mengalami keterlambatan tiba</div>
+ </div>
+ )}
+ <div className='pt-4'>
+ <ol className='relative'>
+ {manifests?.manifests?.map((manifest, index) => {
+ const isFirst = index === 0;
+ const isDelivered = manifests.delivered === true;
+ const isBiteship = manifests.isBiteship === true;
+ const statusTitle =
+ isDelivered && isFirst && !isBiteship
+ ? 'Pesanan sampai'
+ : isBiteship
+ ? capitalizeWords(manifest.status)
+ : '';
- <time class='text-sm leading-none text-gray-400'>
+ return (
+ <li
+ className='grid grid-cols-[80px_20px_1fr] gap-2 mb-6 items-start'
+ key={index}
+ >
+ {/* Kolom 1: Tanggal + Jam */}
+ <div className='text-sm text-gray-400 whitespace-nowrap'>
{formatCustomDate(manifest.datetime)}
- </time>
- {manifests.delivered == true && index == 0 && (
- <p
- class={`leading-6 font-semibold text-sm text-green-600 `}
- >
- Sudah Sampai
+ </div>
+
+ {/* Kolom 2: Bullet/Poin */}
+ <div className='relative flex items-start justify-center pt-1'>
+ <div
+ className={`w-3 h-3 rounded-full border ${
+ index === 0 && manifests.delivered
+ ? 'bg-green-600 border-green-600'
+ : 'bg-gray-400 border-white'
+ }`}
+ />
+ </div>
+
+ {/* Kolom 3: Status dan Deskripsi */}
+ <div>
+ <p className='text-sm font-semibold text-green-600'>
+ {manifests?.deliveryOrder.carrier != 'Self Pick Up'
+ ? capitalizeWords(manifest.status)
+ : ''}
</p>
- )}
- <p class={`leading-6 text-[12px] text-gray_r-11`}>
- {manifest.description}
- </p>
+ <p
+ className='text-xs text-gray-600'
+ dangerouslySetInnerHTML={{
+ __html: manifest.description,
+ }}
+ />
+ </div>
</li>
- </>
- ))}
+ );
+ })}
</ol>
</div>
+ <div className='pt-2'>
+ {
+ isLiveTracking &&
+ manifests?.status === 'shipment' && (
+ <Link
+ href={isLiveTracking}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="inline-block px-4 py-2 bg-red-600 text-white font-semibold rounded-lg shadow-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-opacity-75"
+ >
+ Live Tracking
+ </Link>
+ )
+ }
+ </div>
</BottomPopup>
)}
</>
diff --git a/src/pages/api/banner-section.js b/src/pages/api/banner-section.js
index 7d7040c0..ab48f3f4 100644
--- a/src/pages/api/banner-section.js
+++ b/src/pages/api/banner-section.js
@@ -12,22 +12,81 @@ const connectRedis = async () => {
};
export default async function handler(req, res) {
+ if (req.method !== 'GET') {
+ return res.status(405).json({ error: 'Method not allowed' });
+ }
+
try {
await connectRedis();
- const cacheKey = 'hero-banner';
- // await client.del(cacheKey);
+
+ const type = req.query.type || 'home-banner';
+
+ if (type === 'private-brand') {
+ // Handle multiple private brand banner types
+ const bannerTypes = [
+ 'banner-brand-footer',
+ 'banner-brand-tengah-footer',
+ 'banner-brand-kanan-footer',
+ ];
+
+ const allBanners = [];
+
+ for (const brandType of bannerTypes) {
+ const brandCacheKey = `homepage_bannerSection_${brandType}`;
+
+ let cachedData = await client.get(brandCacheKey);
+
+ if (cachedData) {
+ const data = JSON.parse(cachedData);
+ allBanners.push(...(data || []));
+ } else {
+ try {
+ const dataBannerSections = await odooApi(
+ 'GET',
+ `/api/v1/banner?type=${brandType}`
+ );
+
+ if (dataBannerSections && dataBannerSections.length > 0) {
+ await client.set(
+ brandCacheKey,
+ JSON.stringify(dataBannerSections),
+ 'EX',
+ 259200
+ );
+ allBanners.push(...dataBannerSections);
+ }
+ } catch (error) {
+ continue;
+ }
+ }
+ }
+
+ return res.status(200).json({
+ data: allBanners,
+ total: allBanners.length,
+ });
+ }
+
+ // Handle home-banner and other single types
+ let cacheKey;
+ let apiEndpoint;
+
+ if (type === 'home-banner') {
+ cacheKey = 'hero-banner';
+ apiEndpoint = '/api/v1/banner?type=home-banner';
+ } else {
+ cacheKey = `homepage_bannerSection_${type}`;
+ apiEndpoint = `/api/v1/banner?type=${type}`;
+ }
+
let cachedData = await client.get(cacheKey);
if (cachedData) {
const data = JSON.parse(cachedData);
return res.status(200).json({ data });
} else {
- const dataBannerSections = await odooApi(
- 'GET',
- '/api/v1/banner?type=home-banner'
- );
+ const dataBannerSections = await odooApi('GET', apiEndpoint);
- // Simpan hasil fetch ke Redis dengan masa kadaluarsa 3 hari (259200 detik)
await client.set(
cacheKey,
JSON.stringify(dataBannerSections),
@@ -38,7 +97,6 @@ export default async function handler(req, res) {
return res.status(200).json({ data: dataBannerSections });
}
} catch (error) {
- console.error('Error interacting with Redis or fetching data:', error);
return res.status(500).json({ error: 'Internal Server Error' });
}
}
diff --git a/src/pages/api/biteship-service.js b/src/pages/api/biteship-service.js
new file mode 100644
index 00000000..ed9e2a9f
--- /dev/null
+++ b/src/pages/api/biteship-service.js
@@ -0,0 +1,24 @@
+import biteShipAPI from '../../core/api/biteShip';
+
+export default async function handler(req, res) {
+ const { body } = req.query;
+
+ const parsedBody = JSON.parse(body);
+ console.log(parsedBody);
+
+ try {
+ let result = await biteShipAPI('POST', '/v1/rates/couriers', parsedBody);
+ console.log('ini result', result);
+
+ if (result && result.data && result.data.data) {
+ res.status(200).json(result.data.data);
+ } else {
+ res
+ .status(500)
+ .json({ error: 'Unexpected response structure from Biteship API' });
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ res.status(400).json({ error: error.message });
+ }
+}
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
index df5047a3..809f00ae 100644
--- a/src/pages/index.jsx
+++ b/src/pages/index.jsx
@@ -5,6 +5,7 @@ import DelayRender from '@/core/components/elements/DelayRender/DelayRender';
import DesktopView from '@/core/components/views/DesktopView';
import MobileView from '@/core/components/views/MobileView';
import PreferredBrandSkeleton from '@/lib/home/components/Skeleton/PreferredBrandSkeleton';
+import PagePopupInformation from '@/lib/home/components/PopupBannerPromotion';
import dynamic from 'next/dynamic';
import { useRef } from 'react';
import { getAuth } from '~/libs/auth';
@@ -116,6 +117,7 @@ export default function Home({ categoryId }) {
<PagePopupIformation />
<DesktopView>
+ <PagePopupInformation />
<div className='container mx-auto'>
<div
className='flex min-h-[400px] h-[460px]'
@@ -160,6 +162,7 @@ export default function Home({ categoryId }) {
</div>
</DesktopView>
<MobileView>
+ <PagePopupInformation />
<DelayRender renderAfter={200}>
<HeroBanner />
</DelayRender>
diff --git a/src/pages/my/address/[id]/edit.jsx b/src/pages/my/address/[id]/edit.jsx
index 19d7af41..006785d9 100644
--- a/src/pages/my/address/[id]/edit.jsx
+++ b/src/pages/my/address/[id]/edit.jsx
@@ -45,7 +45,10 @@ export async function getServerSideProps(context) {
oldSubDistrict: address.subDistrict?.id || '',
subDistrict: '',
business_name: '',
+ longtitude: address?.longtitude || 0,
+ latitude: address?.latitude || 0,
+ addressMap: address?.addressMap || '',
+
};
- // console.log('ini default',defaultValues);
return { props: { id, defaultValues } };
}
diff --git a/src/pages/pengajuan-tempo/[status].jsx b/src/pages/pengajuan-tempo/[status].jsx
index 29886892..eff30e46 100644
--- a/src/pages/pengajuan-tempo/[status].jsx
+++ b/src/pages/pengajuan-tempo/[status].jsx
@@ -8,12 +8,6 @@ import Seo from '@/core/components/Seo';
import { getAuth } from '~/libs/auth';
export async function getServerSideProps(context) {
- const { status } = context.query;
- await axios.post(
- `${process.env.NEXT_PUBLIC_SELF_HOST}/api/pengajuan-tempo/${status}`,
- {},
- { headers: context.req.headers }
- );
return { props: {} };
}