From c7c3e3fd6f221447a0c81459a45c090aa0714334 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Mon, 28 Oct 2024 17:03:34 +0700 Subject: maps component --- package.json | 1 + src/lib/address/components/CreateAddress.jsx | 323 +++++++++++++++------------ src/lib/maps/components/PinPointMap.jsx | 163 ++++++++++++++ src/lib/maps/stores/useMaps.js | 13 ++ src/lib/product/components/ProductSearch.jsx | 4 +- 5 files changed, 360 insertions(+), 144 deletions(-) create mode 100644 src/lib/maps/components/PinPointMap.jsx create mode 100644 src/lib/maps/stores/useMaps.js diff --git a/package.json b/package.json index a846749c..82ad1d33 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@hookform/resolvers": "^2.9.10", "@react-email/components": "^0.0.2", "@react-email/render": "^0.0.6", + "@react-google-maps/api": "^2.20.3", "@tailwindcss/line-clamp": "^0.4.2", "axios": "^1.1.3", "camelcase-object-deep": "^1.1.7", diff --git a/src/lib/address/components/CreateAddress.jsx b/src/lib/address/components/CreateAddress.jsx index 9d70e8fc..70307401 100644 --- a/src/lib/address/components/CreateAddress.jsx +++ b/src/lib/address/components/CreateAddress.jsx @@ -1,7 +1,7 @@ import HookFormSelect from '@/core/components/elements/Select/HookFormSelect'; import useAuth from '@/core/hooks/useAuth'; import { useRouter } from 'next/router'; -import { Controller, useForm } from 'react-hook-form'; +import { Controller, set, useForm } from 'react-hook-form'; import * as Yup from 'yup'; import cityApi from '../api/cityApi'; import districtApi from '../api/districtApi'; @@ -14,6 +14,12 @@ 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 PinPointMap from '../../maps/components/PinPointMap'; +import { Button } from '@chakra-ui/react'; +import { MapPinIcon } from 'lucide-react'; +import { useMaps } from '../../maps/stores/useMaps'; + const CreateAddress = () => { const auth = useAuth(); const router = useRouter(); @@ -34,6 +40,8 @@ const CreateAddress = () => { const [districts, setDistricts] = useState([]); const [subDistricts, setSubDistricts] = useState([]); const [filteredTypes, setFilteredTypes] = useState(types); // State to manage filtered types + const [pinedMaps, setPinedMaps] = useState(false); + const { addressMaps, setAddressMaps } = useMaps(); useEffect(() => { const loadState = async () => { @@ -52,7 +60,7 @@ const CreateAddress = () => { setValue('city', ''); if (watchState) { const loadCities = async () => { - let dataCities = await cityApi({stateId: watchState}); + let dataCities = await cityApi({ stateId: watchState }); dataCities = dataCities.map((city) => ({ value: city.id, label: city.name, @@ -133,167 +141,198 @@ const CreateAddress = () => { }; return ( -
-
- -
-
-
-
-
- - ( - - )} - /> -
- {errors.type?.message} -
+ <> + setPinedMaps(false)} + > +
+ +
+
+
+
+ +
+
+ +
+ + {addressMaps ? ( +
+ {' '} + {addressMaps} +
+ ) : ( + + )} +
+
+
+ + ( + + )} + /> +
+ {errors.type?.message} +
+
-
- - -
- {errors.name?.message} +
+ + +
+ {errors.name?.message} +
-
-
- - -
- {errors.email?.message} +
+ + +
+ {errors.email?.message} +
-
-
- - -
- {errors.mobile?.message} +
+ + +
+ {errors.mobile?.message} +
-
-
- - -
- {errors.street?.message} +
+ + +
+ {errors.street?.message} +
-
-
- - -
- {errors.zip?.message} +
+ + +
+ {errors.zip?.message} +
-
-
- - ( - - )} - /> -
- {errors.state?.message} +
+ + ( + + )} + /> +
+ {errors.state?.message} +
-
-
- - ( - - )} - /> -
- {errors.city?.message} +
+ + ( + + )} + /> +
+ {errors.city?.message} +
-
-
- - ( - - )} - /> -
- {errors.district?.message} +
+ + ( + + )} + /> +
+ {errors.district?.message} +
-
-
- - ( - - )} - /> +
+ + ( + + )} + /> +
-
- - + + +
-
+ ); }; diff --git a/src/lib/maps/components/PinPointMap.jsx b/src/lib/maps/components/PinPointMap.jsx new file mode 100644 index 00000000..0781d8a8 --- /dev/null +++ b/src/lib/maps/components/PinPointMap.jsx @@ -0,0 +1,163 @@ +import React, { useState, useCallback, useRef } from 'react'; +import { + GoogleMap, + useJsApiLoader, + Marker, + Autocomplete, +} from '@react-google-maps/api'; +import { useMaps } from '../stores/useMaps'; +import { LocateFixed, MapPinIcon } from 'lucide-react'; +import { Button } from '@chakra-ui/react'; + +const containerStyle = { + width: '100%', + height: '400px', +}; + +const center = { + lat: -6.2, // Default latitude (Jakarta) + lng: 106.816666, // Default longitude (Jakarta) +}; + +const PinpointLocation = () => { + const { isLoaded } = useJsApiLoader({ + googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_API_KEY, // Pastikan API key ada di .env.local + libraries: ['places'], + }); + + const { addressMaps, setAddressMaps, selectedPosition, setSelectedPosition } = + useMaps(); + + const [tempAddress, setTempAddress] = useState(''); + const [tempPosition, setTempPosition] = useState(center); + + const autocompleteRef = useRef(null); + + const onMapClick = useCallback((event) => { + const lat = event.latLng.lat(); + const lng = event.latLng.lng(); + setTempPosition({ lat, lng }); + getAddress(lat, lng); + }, []); + + const handlePlaceSelect = () => { + const place = autocompleteRef.current.getPlace(); + if (place && place.geometry) { + const lat = place.geometry.location.lat(); + const lng = place.geometry.location.lng(); + setTempPosition({ lat, lng }); + setTempAddress(place.formatted_address); + } + }; + + const 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]) { + setTempAddress(data.results[0].formatted_address); + } + } catch (error) { + console.error('Error fetching address:', error); + } + }; + + const handleUseCurrentLocation = () => { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position) => { + const lat = position.coords.latitude; + const lng = position.coords.longitude; + setTempPosition({ lat, lng }); + getAddress(lat, lng); + }, + (error) => { + console.error('Error getting current location:', error); + } + ); + } + }; + + const handleSavePinpoint = (event) => { + event.preventDefault(); + if(tempAddress === '') { + alert('Silahkan pilih lokasi terlebih dahulu'); + return; + } + setSelectedPosition(tempPosition); + setAddressMaps(tempAddress); + }; + + return ( +
+

Tentukan Pinpoint Lokasi

+
+ {isLoaded ? ( + (autocompleteRef.current = ref)} + onPlaceChanged={handlePlaceSelect} + > + + + ) : ( +

Loading autocomplete...

+ )} +
+ +
+ {isLoaded ? ( + + onMapClick(e)} + icon={{ + url: 'https://maps.google.com/mapfiles/ms/icons/red-pushpin.png', + scaledSize: new window.google.maps.Size(40, 40), + }} + /> + + ) : ( +

Loading map...

+ )} +
+ +
+ +
+ +
+

PinPoint :

+
+ + +
+
+ +
+ +
+
+ ); +}; + +export default PinpointLocation; diff --git a/src/lib/maps/stores/useMaps.js b/src/lib/maps/stores/useMaps.js new file mode 100644 index 00000000..1720e663 --- /dev/null +++ b/src/lib/maps/stores/useMaps.js @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +const center = { + lat: -6.200000, // Default latitude (Jakarta) + lng: 106.816666, // Default longitude (Jakarta) +}; + +export const useMaps = create((set) => ({ + selectedPosition: center, + addressMaps: '', + setSelectedPosition: (position) => set({ selectedPosition: position }), + setAddressMaps: (addressMaps) => set({ addressMaps }), + })); \ No newline at end of file diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index f7b044aa..3e342bf0 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -501,7 +501,7 @@ const ProductSearch = ({ @@ -691,7 +691,7 @@ const ProductSearch = ({ -- cgit v1.2.3 From bc4fe87e012cc1b06572ca12f1a3b92f6d1757e0 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Thu, 31 Oct 2024 09:09:05 +0700 Subject: detail adress --- src/lib/address/components/Addresses.jsx | 15 ++++++++++ src/lib/address/components/CreateAddress.jsx | 9 ++++-- src/lib/maps/components/PinPointMap.jsx | 43 +++++++++++++++++++++++----- src/pages/my/address/[id]/edit.jsx | 5 +++- 4 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/lib/address/components/Addresses.jsx b/src/lib/address/components/Addresses.jsx index 9ca617ae..d852f52c 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(); @@ -177,6 +178,20 @@ const AddressCard = ({

{address.mobile}

)}

{address.street}

+ +
+ {address.addressMap ? ( + <> + +

Sudah PinPoint

+ + ) : ( + <> + +

Belum PinPoint

+ + )} +
- @@ -152,8 +178,11 @@ const PinpointLocation = () => {
-
diff --git a/src/pages/my/address/[id]/edit.jsx b/src/pages/my/address/[id]/edit.jsx index 19d7af41..fdae8bb6 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, + address_maps: address?.address_maps ? JSON.stringify(address?.addressMaps) : '', + }; - // console.log('ini default',defaultValues); return { props: { id, defaultValues } }; } -- cgit v1.2.3 From 053c801a8c43688c2c4eec6800368898a81bfc39 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Thu, 31 Oct 2024 16:55:07 +0700 Subject: integrasi google maps service --- src/lib/address/components/Addresses.jsx | 6 +- src/lib/address/components/CreateAddress.jsx | 88 +++++++++++++++++--- src/lib/address/components/EditAddress.jsx | 116 ++++++++++++++++++++------- src/lib/maps/components/PinPointMap.jsx | 4 +- src/lib/maps/stores/useMaps.js | 2 + src/pages/my/address/[id]/edit.jsx | 2 +- 6 files changed, 171 insertions(+), 47 deletions(-) diff --git a/src/lib/address/components/Addresses.jsx b/src/lib/address/components/Addresses.jsx index d852f52c..1007b9f8 100644 --- a/src/lib/address/components/Addresses.jsx +++ b/src/lib/address/components/Addresses.jsx @@ -18,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); @@ -187,8 +187,8 @@ const AddressCard = ({ ) : ( <> - -

Belum PinPoint

+ +

Belum PinPoint

)}
diff --git a/src/lib/address/components/CreateAddress.jsx b/src/lib/address/components/CreateAddress.jsx index a53be0fa..404143f9 100644 --- a/src/lib/address/components/CreateAddress.jsx +++ b/src/lib/address/components/CreateAddress.jsx @@ -1,23 +1,23 @@ 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 { Controller, set, useForm } from 'react-hook-form'; +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 PinPointMap from '../../maps/components/PinPointMap'; 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 = () => { @@ -41,7 +41,24 @@ const CreateAddress = () => { const [subDistricts, setSubDistricts] = useState([]); const [filteredTypes, setFilteredTypes] = useState(types); // State to manage filtered types const [pinedMaps, setPinedMaps] = useState(false); - const { addressMaps, setAddressMaps, selectedPosition, setSelectedPosition } = useMaps(); + const { + addressMaps, + selectedPosition, + detailAddress, + } = useMaps(); + + useEffect(() => { + if (detailAddress) { + setValue('zip', detailAddress.postalCode); + const selectedState = states.find( + (state) => + detailAddress?.province.includes(state.label) || + state.label.includes(detailAddress?.province) + ); + setValue('state', selectedState?.value); + + } + }, [detailAddress, setValue]); useEffect(() => { const loadState = async () => { @@ -56,6 +73,7 @@ const CreateAddress = () => { }, []); const watchState = watch('state'); + console.log(watchState); useEffect(() => { setValue('city', ''); if (watchState) { @@ -71,6 +89,21 @@ const CreateAddress = () => { } }, [watchState, setValue]); + useEffect(() => { + if (detailAddress) { + const selectedCities = cities.find( + (city) => + detailAddress.district + .toLowerCase() + .includes(city.label.toLowerCase()) || + city.label + .toLowerCase() + .includes(detailAddress.district.toLowerCase()) + ); + setValue('city', selectedCities?.value); + } + }, [cities, detailAddress, setValue]); + useEffect(() => { if (addresses) { let hasContactAddress = false; @@ -105,6 +138,21 @@ const CreateAddress = () => { } }, [watchCity, setValue]); + useEffect(() => { + if (detailAddress) { + const selectedDistrict = districts.find( + (district) => + detailAddress.subDistrict + .toLowerCase() + .includes(district.label.toLowerCase()) || + district.label + .toLowerCase() + .includes(detailAddress.subDistrict.toLowerCase()) + ); + setValue('district', selectedDistrict?.value); + } + }, [districts, detailAddress, setValue]); + const watchDistrict = watch('district'); useEffect(() => { setValue('subDistrict', ''); @@ -123,6 +171,22 @@ const CreateAddress = () => { } }, [watchDistrict, setValue]); + useEffect(() => { + if (detailAddress) { + const selectedSubDistrict = subDistricts.find( + (district) => + detailAddress.village + .toLowerCase() + .includes(district.label.toLowerCase()) || + district.label + .toLowerCase() + .includes(detailAddress.village.toLowerCase()) + ); + + setValue('subDistrict', selectedSubDistrict?.value); + } + }, [subDistricts, detailAddress, setValue]); + const onSubmitHandler = async (values) => { const data = { ...values, @@ -133,10 +197,9 @@ const CreateAddress = () => { parent_id: auth.partnerId, latitude: selectedPosition?.lat, longtitude: selectedPosition?.lng, - address_maps: JSON.stringify(addressMaps), + address_map: addressMaps, }; -console.log('ini data',data); - // const address = await createAddressApi({ data }); + const address = await createAddressApi({ data }); if (address?.id) { toast.success('Berhasil menambahkan alamat'); router.back(); @@ -174,7 +237,6 @@ console.log('ini data',data); Pin Alamat )} -
diff --git a/src/lib/address/components/EditAddress.jsx b/src/lib/address/components/EditAddress.jsx index 23cf72a9..0b3b0aa3 100644 --- a/src/lib/address/components/EditAddress.jsx +++ b/src/lib/address/components/EditAddress.jsx @@ -14,6 +14,12 @@ import Menu from '@/lib/auth/components/Menu'; import useAuth from '@/core/hooks/useAuth'; import odooApi from '@/core/api/odooApi'; import stateApi from '../api/stateApi'; +import { MapPinIcon } from 'lucide-react'; +import { Button } from '@chakra-ui/react'; +import { useMaps } from '../../maps/stores/useMaps'; + +import PinPointMap from '../../maps/components/PinPointMap'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; const EditAddress = ({ id, defaultValues }) => { const auth = useAuth(); @@ -34,7 +40,19 @@ const EditAddress = ({ id, defaultValues }) => { const [states, setStates] = useState([]); const [cities, setCities] = useState([]); const [districts, setDistricts] = useState([]); - const [subDistricts, setSubDistricts] = useState([]); + const [subDistricts, setSubDistricts] = useState([]); + const [pinedMaps, setPinedMaps] = useState(false); + const [tempAddress, setTempAddress] = useState(getValues('address_maps')); + const { addressMaps, selectedPosition, detailAddress } = useMaps(); + + useEffect(() => { + if (addressMaps) { + setTempAddress(addressMaps); + setValue('address_map', addressMaps); + setValue('longtitude', selectedPosition.lng); + setValue('latitude', selectedPosition.lat); + } + }, [addressMaps, selectedPosition, setValue]); useEffect(() => { const loadProfile = async () => { @@ -60,12 +78,12 @@ const EditAddress = ({ id, defaultValues }) => { setStates(dataStates); }; loadStates(); - },[]) + }, []); const watchState = watch('state'); useEffect(() => { setValue('city', ''); - if(watchState) { + if (watchState) { const loadCities = async () => { let dataCities = await cityApi({ stateId: watchState }); dataCities = dataCities.map((city) => ({ @@ -81,7 +99,6 @@ const EditAddress = ({ id, defaultValues }) => { }; loadCities(); } - }, [watchState, setValue, getValues]); const watchCity = watch('city'); @@ -136,40 +153,65 @@ const EditAddress = ({ id, defaultValues }) => { city_id: values.city, district_id: values.district, sub_district_id: values.subDistrict, + longtitude: selectedPosition?.lng, + latitude: selectedPosition?.lat, + address_map: addressMaps, }; if (!auth.company) { data.alamat_lengkap_text = values.street; } - const address = await editAddressApi({ id, data }); - let dataAlamat; - let isUpdated = true; - if (auth.company) { - if (auth?.partnerId == id) { - dataAlamat = { - id_user: auth.partnerId, - alamat_lengkap_text: values.alamat_wajib_pajak, - street: values.street, - }; - isUpdated = await odooApi( - 'PUT', - `/api/v1/partner/${auth.parentId}`, - dataAlamat - ); + try { + const address = await editAddressApi({ id, data }); + let dataAlamat; + let isUpdated = true; + if (auth.company) { + if (auth?.partnerId == id) { + dataAlamat = { + id_user: auth.partnerId, + alamat_lengkap_text: values.alamat_wajib_pajak, + street: values.street, + }; + isUpdated = await odooApi( + 'PUT', + `/api/v1/partner/${auth.parentId}`, + dataAlamat + ); + } } - } - - // if (isUpdated?.id) { - if (address?.id && auth.company ? isUpdated?.id : true) { - toast.success('Berhasil mengubah alamat'); - router.back(); - } else { + if (address?.id) { + toast.success('Berhasil mengubah alamat'); + router.back(); + } else { + toast.error('Terjadi kesalahan internal'); + router.back(); + } + } catch (error) { toast.error('Terjadi kesalahan internal'); router.back(); } + + // if (isUpdated?.id) { + // if (address?.id && auth.company ? isUpdated?.id : true) { + // toast.success('Berhasil mengubah alamat'); + // router.back(); + // } else { + // toast.error('Terjadi kesalahan internal'); + // router.back(); + // } }; return ( <> + setPinedMaps(false)} + > +
+ +
+
@@ -182,6 +224,20 @@ const EditAddress = ({ id, defaultValues }) => { {auth?.partnerId == id &&
Utama
}
+
+ + {tempAddress ? ( +
+ {' '} + {tempAddress} +
+ ) : ( + + )} +
@@ -287,7 +343,11 @@ const EditAddress = ({ id, defaultValues }) => { name='city' control={control} render={(props) => ( - + )} />
@@ -348,7 +408,7 @@ const validationSchema = Yup.object().shape({ mobile: Yup.string().required('Harus di-isi'), street: Yup.string().required('Harus di-isi'), zip: Yup.string().required('Harus di-isi'), - state : Yup.string().required('Harus di-pilih'), + state: Yup.string().required('Harus di-pilih'), city: Yup.string().required('Harus di-pilih'), district: Yup.string().required('Harus di-pilih'), }); diff --git a/src/lib/maps/components/PinPointMap.jsx b/src/lib/maps/components/PinPointMap.jsx index 1fa40036..201fdeb4 100644 --- a/src/lib/maps/components/PinPointMap.jsx +++ b/src/lib/maps/components/PinPointMap.jsx @@ -26,7 +26,7 @@ const PinpointLocation = () => { libraries: ['places'], }); - const { addressMaps, setAddressMaps, selectedPosition, setSelectedPosition } = + const { addressMaps, setAddressMaps, selectedPosition, setSelectedPosition, setDetailAddress } = useMaps(); const [tempAddress, setTempAddress] = useState(''); @@ -84,7 +84,7 @@ const PinpointLocation = () => { ), postalCode: getAddressComponent(addressComponents, 'postal_code'), }; - setValue('state', details?.province); + setDetailAddress(details); console.log(details); setTempAddress(data.results[0].formatted_address); } diff --git a/src/lib/maps/stores/useMaps.js b/src/lib/maps/stores/useMaps.js index 1720e663..83f476bc 100644 --- a/src/lib/maps/stores/useMaps.js +++ b/src/lib/maps/stores/useMaps.js @@ -8,6 +8,8 @@ const center = { export const useMaps = create((set) => ({ selectedPosition: center, addressMaps: '', + detailAddress: {}, setSelectedPosition: (position) => set({ selectedPosition: position }), setAddressMaps: (addressMaps) => set({ addressMaps }), + setDetailAddress: (detailAddress) => set({ detailAddress }), })); \ No newline at end of file diff --git a/src/pages/my/address/[id]/edit.jsx b/src/pages/my/address/[id]/edit.jsx index fdae8bb6..26cde5e1 100644 --- a/src/pages/my/address/[id]/edit.jsx +++ b/src/pages/my/address/[id]/edit.jsx @@ -47,7 +47,7 @@ export async function getServerSideProps(context) { business_name: '', longtitude: address?.longtitude || 0, latitude: address?.latitude || 0, - address_maps: address?.address_maps ? JSON.stringify(address?.addressMaps) : '', + addressMap: address?.address_map || '', }; return { props: { id, defaultValues } }; -- cgit v1.2.3 From ed0adaad2125b46a6c039ce89b38b3cbb8379342 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Fri, 1 Nov 2024 13:42:10 +0700 Subject: add label pinpoint di pagecheckout --- src/lib/checkout/components/Checkout.jsx | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 0e180d9c..97254ec0 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -1,4 +1,4 @@ -import { Skeleton, Spinner } from '@chakra-ui/react'; +import { Button, Skeleton, Spinner } from '@chakra-ui/react'; import { BanknotesIcon, ChevronLeftIcon, @@ -31,6 +31,7 @@ import addressesApi from '@/lib/address/api/addressesApi'; import CartItem from '~/modules/cart/components/Item.tsx'; import ExpedisiList from '../api/ExpedisiList'; import { findVoucher, getVoucher, getVoucherNew } from '../api/getVoucher'; +import { MapPinIcon } from 'lucide-react'; const SELF_PICKUP_ID = 32; @@ -413,7 +414,12 @@ const Checkout = () => { Math.round(parseInt(finalShippingAmt * 1.1) / 1000) * 1000; const finalGT = GT < 0 ? 0 : GT; setGrandTotal(finalGT); - }, [biayaKirim, cartCheckout?.grandTotal, activeVoucher, activeVoucherShipping]); + }, [ + biayaKirim, + cartCheckout?.grandTotal, + activeVoucher, + activeVoucherShipping, + ]); const checkout = async () => { const file = poFile.current.files[0]; @@ -500,7 +506,7 @@ const Checkout = () => { } } - /* const midtrans = async () => { + /* const midtrans = async () => { for (const product of products) deleteItemCart({ productId: product.id }); if (grandTotal > 0) { const payment = await axios.post( @@ -1192,7 +1198,11 @@ const Checkout = () => {
Biaya Kirim

{etdFix}

-
{currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000)}
+
+ {currencyFormat( + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + )} +
{activeVoucherShipping && voucherShippingAmt && (
@@ -1493,7 +1503,11 @@ const Checkout = () => { Biaya Kirim

{etdFix}

-
{currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000) }
+
+ {currencyFormat( + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + )} +
{activeVoucherShipping && voucherShippingAmt && (
@@ -1658,9 +1672,13 @@ const SectionAddress = ({ address, label, url }) => (

{address.street}, {address?.city?.name}

+
+ + {address.addressMap ? : } +
)} -
+
); const SectionValidation = ({ address }) => -- cgit v1.2.3 From ebba81f144b860eaf3bd7a9ef2b1c63a2ff021e0 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Thu, 7 Nov 2024 12:59:56 +0700 Subject: get couries biteship --- src/core/api/biteShip.js | 26 ++++++++++ src/lib/checkout/api/ExpedisiList.js | 25 ++++++++-- src/lib/checkout/components/Checkout.jsx | 58 +++-------------------- src/lib/checkout/components/SectionExpedition.tsx | 12 +++++ src/lib/checkout/stores/useAdress.js | 6 +++ 5 files changed, 70 insertions(+), 57 deletions(-) create mode 100644 src/core/api/biteShip.js create mode 100644 src/lib/checkout/components/SectionExpedition.tsx create mode 100644 src/lib/checkout/stores/useAdress.js diff --git a/src/core/api/biteShip.js b/src/core/api/biteShip.js new file mode 100644 index 00000000..9e9e8567 --- /dev/null +++ b/src/core/api/biteShip.js @@ -0,0 +1,26 @@ +import axios from 'axios'; + +const biteShipAPI = async (method, url, body = {}) => { + try { + const key = process.env.NEXT_PUBLIC_BITE_SHIP_KEY; + let axiosParameter = { + method, + url: process.env.NEXT_PUBLIC_BITE_SHIP_HOST + url, + headers: { Authorization: key, 'Content-Type': 'application/json' }, + }; + if (Object.keys(body).length > 0) + axiosParameter.data = JSON.stringify(body); + + 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/lib/checkout/api/ExpedisiList.js b/src/lib/checkout/api/ExpedisiList.js index ca22bec1..110295b7 100644 --- a/src/lib/checkout/api/ExpedisiList.js +++ b/src/lib/checkout/api/ExpedisiList.js @@ -1,8 +1,23 @@ -import odooApi from '@/core/api/odooApi' +import odooApi from '@/core/api/odooApi'; +import axios from 'axios'; +import biteShipAPI from '../../../core/api/biteShip'; 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 +const GetRatesCourierBiteship = async ({ destination, items }) => { + const couriers = process.env.NEXT_PUBLIC_BITESHIP_CODE_COURIERS; + let body = { + destination, + couriers: couriers, + items + }; + + const featch = await biteShipAPI('POST', '/v1/rates/couriers', body); + + return featch; +}; + +export { GetRatesCourierBiteship, ExpedisiList} ; diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 97254ec0..6da27745 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -269,58 +269,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; @@ -1698,6 +1646,12 @@ const SectionValidation = ({ address }) => ); +const SectionExpedisiBiteship = ({ listExpedisi, setSelectedExpedisi }) => ( + <> + +); + + const SectionExpedisi = ({ address, listExpedisi, diff --git a/src/lib/checkout/components/SectionExpedition.tsx b/src/lib/checkout/components/SectionExpedition.tsx new file mode 100644 index 00000000..d2dd5763 --- /dev/null +++ b/src/lib/checkout/components/SectionExpedition.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { GetRatesCourierBiteship } from '../api/ExpedisiList'; +import { useQuery } from 'react-query'; + +export default function SectionExpedition() { + let destination = {} + let items = {} + const fetchExpedition = async () => await GetRatesCourierBiteship({destination, items}); + const {data : coursers, isLoading} = useQuery('expedition-'+destination, fetchExpedition) + + return
SectionExpedition
; +} \ 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..1c17258d --- /dev/null +++ b/src/lib/checkout/stores/useAdress.js @@ -0,0 +1,6 @@ +import { create } from "zustand"; + +export const useAddress = create((set) => ({ + address: {}, + setAddress: (address) => set({ address }), +})); \ No newline at end of file -- cgit v1.2.3 From 7966f67569d01c25f7a337962d7d0bb1a0c57808 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Wed, 13 Nov 2024 14:46:59 +0700 Subject: get couries, mapping couries and service --- src/core/api/biteShip.js | 20 +- src/lib/checkout/api/ExpedisiList.js | 18 +- src/lib/checkout/api/getRatesCourier.js | 22 ++ src/lib/checkout/components/Checkout.jsx | 53 +++-- src/lib/checkout/components/SectionExpedition.tsx | 256 +++++++++++++++++++++- src/lib/checkout/stores/useAdress.js | 23 +- src/pages/api/biteship-service.js | 24 ++ 7 files changed, 363 insertions(+), 53 deletions(-) create mode 100644 src/lib/checkout/api/getRatesCourier.js create mode 100644 src/pages/api/biteship-service.js diff --git a/src/core/api/biteShip.js b/src/core/api/biteShip.js index 9e9e8567..f18421d8 100644 --- a/src/core/api/biteShip.js +++ b/src/core/api/biteShip.js @@ -2,23 +2,27 @@ import axios from 'axios'; const biteShipAPI = async (method, url, body = {}) => { try { - const key = process.env.NEXT_PUBLIC_BITE_SHIP_KEY; - let axiosParameter = { + const key = process.env.NEXT_PUBLIC_BITSEHIP_KEY; + const baseUrl = process.env.NEXT_PUBLIC_BITE_SHIP_HOST; + + const axiosParameter = { method, - url: process.env.NEXT_PUBLIC_BITE_SHIP_HOST + url, - headers: { Authorization: key, 'Content-Type': 'application/json' }, + url: baseUrl + url, + headers: { + Authorization: `Bearer ${key}`, // Tambahkan "Bearer " di depan key + 'Content-Type': 'application/json', + }, + data: body, // Tidak perlu JSON.stringify }; - if (Object.keys(body).length > 0) - axiosParameter.data = JSON.stringify(body); const data = await axios(axiosParameter); - return { success: true, data: data}; + return { success: true, data: data }; } catch (error) { console.log(error); return { success: false, - data : {} + data: {}, }; } }; diff --git a/src/lib/checkout/api/ExpedisiList.js b/src/lib/checkout/api/ExpedisiList.js index 110295b7..67ef93e2 100644 --- a/src/lib/checkout/api/ExpedisiList.js +++ b/src/lib/checkout/api/ExpedisiList.js @@ -1,23 +1,7 @@ import odooApi from '@/core/api/odooApi'; -import axios from 'axios'; -import biteShipAPI from '../../../core/api/biteShip'; - const ExpedisiList = async () => { const dataExpedisi = await odooApi('GET', '/api/v1/courier'); return dataExpedisi; }; -const GetRatesCourierBiteship = async ({ destination, items }) => { - const couriers = process.env.NEXT_PUBLIC_BITESHIP_CODE_COURIERS; - let body = { - destination, - couriers: couriers, - items - }; - - const featch = await biteShipAPI('POST', '/v1/rates/couriers', body); - - return featch; -}; - -export { GetRatesCourierBiteship, ExpedisiList} ; +export default ExpedisiList; diff --git a/src/lib/checkout/api/getRatesCourier.js b/src/lib/checkout/api/getRatesCourier.js new file mode 100644 index 00000000..8db02d50 --- /dev/null +++ b/src/lib/checkout/api/getRatesCourier.js @@ -0,0 +1,22 @@ +import axios from "axios"; +import biteShipAPI from "../../../core/api/biteShip"; + +const GetRatesCourierBiteship = async ({ destination, items }) => { + const couriers = process.env.NEXT_PUBLIC_BITESHIP_CODE_COURIERS; + let body = { + ...destination, + couriers: 'gojek, grab, deliveree, lalamove, jne, tiki, ninja, lion, rara, sicepat, jnt, pos, idexpress, rpx, wahana, jdl, pos, anteraja, sap, paxel, borzo', + items: items, + }; + + const response = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/biteship-service?method=POST&url=/v1/rates/couriers&body=` + JSON.stringify(body)); + + // const featch = await biteShipAPI('POST', '/v1/rates/couriers', body); + console.log('ini featch', response); + + + return response; +}; + + +export default GetRatesCourierBiteship \ No newline at end of file diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 6da27745..b4c311a9 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -1,4 +1,4 @@ -import { Button, Skeleton, Spinner } from '@chakra-ui/react'; +import { Skeleton, Spinner } from '@chakra-ui/react'; import { BanknotesIcon, ChevronLeftIcon, @@ -28,10 +28,12 @@ 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 { MapPinIcon } from 'lucide-react'; +import { getVoucher } from '../api/getVoucher'; +import { useAddress } from '../stores/useAdress'; +import SectionExpedition from './SectionExpedition'; const SELF_PICKUP_ID = 32; @@ -57,11 +59,15 @@ const Checkout = () => { }) ); - const [selectedAddress, setSelectedAddress] = useState({ - shipping: null, - invoicing: null, - }); - const [addresses, setAddresses] = useState(null); + const { + selectedAddress, + setSelectedAddress, + addresses, + setAddresses, + setAddressMaps, + setCoordinate, + setPostalCode, + } = useAddress(); useEffect(() => { if (!auth) return; @@ -91,10 +97,20 @@ 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); @@ -1340,6 +1356,7 @@ const Checkout = () => { /> )} + {products && } ( {address.street}, {address?.city?.name}

- - {address.addressMap ? : } + + {address.addressMap ? ( + + ) : ( + + )}
)} -
+
); const SectionValidation = ({ address }) => @@ -1647,11 +1674,9 @@ const SectionValidation = ({ address }) => ); const SectionExpedisiBiteship = ({ listExpedisi, setSelectedExpedisi }) => ( - <> - + <> ); - const SectionExpedisi = ({ address, listExpedisi, diff --git a/src/lib/checkout/components/SectionExpedition.tsx b/src/lib/checkout/components/SectionExpedition.tsx index d2dd5763..a6e05893 100644 --- a/src/lib/checkout/components/SectionExpedition.tsx +++ b/src/lib/checkout/components/SectionExpedition.tsx @@ -1,12 +1,248 @@ -import React from 'react'; -import { GetRatesCourierBiteship } from '../api/ExpedisiList'; +import React, { useEffect, useState } from 'react'; import { useQuery } from 'react-query'; +import GetRatesCourierBiteship from '../api/getRatesCourier'; +import { useAddress } from '../stores/useAdress'; +import axios, { AxiosResponse } from 'axios'; +import { useForm, Controller } from 'react-hook-form'; +import { AnimatePresence, motion } from 'framer-motion'; +import { Spinner } from '@chakra-ui/react'; -export default function SectionExpedition() { - let destination = {} - let items = {} - const fetchExpedition = async () => await GetRatesCourierBiteship({destination, items}); - const {data : coursers, isLoading} = useQuery('expedition-'+destination, fetchExpedition) - - return
SectionExpedition
; -} \ No newline at end of file +function mappingItems(products) { + return products?.map((item) => ({ + name: item.parent.name, + description: `${item.code} - ${item.name}`, + value: item.price.priceDiscount, + weight: item.weight, + quantity: item.quantity, + })); +} + +function mappingCourier(couriers) { + return couriers?.reduce((result, item) => { + const { courier_code, courier_service_code } = item; + + // Jika courier_code belum ada di result, buat objek baru untuknya + if (!result[courier_code]) { + result[courier_code] = { + courier_name: item.courier_name, + courier_code: courier_code, + service_type: { + [courier_service_code]: { + service_name: item.courier_service_name, + duration: item.duration, + shipment_duration: item.duration, + price: item.price, + service_type: item.service_type, + description: item.description, + }, + }, + }; + } else { + result[courier_code].service_type[courier_service_code] = { + service_name: item.courier_service_name, + duration: item.duration, + shipment_duration: item.duration, + price: item.price, + service_type: item.service_type, + description: item.description, + }; + } + + return result; + }, {}); +} + +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; + }; + }; +} + +export default function SectionExpedition({ products }) { + const { addressMaps, coordinate, postalCode } = useAddress(); + const { control, handleSubmit } = useForm(); + const [selectedCourier, setSelectedCourier] = useState(''); + const [serviceOptions, setServiceOptions] = useState([]); + + 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: 12440, + destination_postal_code: postalCode, + }; + } + + const fetchExpedition = async () => { + let body = { + ...destination, + couriers: + 'gojek, grab, deliveree, lalamove, jne, tiki, ninja, lion, rara, sicepat, jnt, pos, idexpress, rpx, wahana, jdl, pos, anteraja, sap, paxel, borzo', + items: items, + }; + try { + const response = await axios.get(`/api/biteship-service`, { + params: { body: JSON.stringify(body) }, + }); + return response; + } catch (error) { + console.error('Failed to fetch expedition rates:', error); + } + }; + + // const fetchExpedition = async () => await GetRatesCourierBiteship({ destination, items }); + const { data, isLoading } = useQuery( + ['expedition', JSON.stringify(destination), JSON.stringify(items)], + fetchExpedition, + { + enabled: Boolean(Object.keys(destination).length) && items?.length > 0, + staleTime: Infinity, + cacheTime: Infinity, + } + ); + + const couriers: CourierService = mappingCourier(data?.data?.pricing); + + console.log('couriers', couriers); + + const onCourierChange = (e) => { + const courier = e.target.value; + console.log('courier', courier); + setSelectedCourier(courier); + // Menentukan layanan berdasarkan pengiriman yang dipilih + if (courier && couriers[courier]) { + setServiceOptions(Object.values(couriers[courier]?.service_type)); + } else { + setServiceOptions([]); + } + }; + + console.log( + 'serviceOptions', + couriers[selectedCourier]?.service_type, + selectedCourier + ); + const onSubmit = (data) => { + console.log(data); + }; + + return ( + + {/* Dropdown untuk memilih jenis pengiriman */} +
+
+
Pilih Ekspedisi:
+
+
+ + + + {isLoading && ( + + + + )} + +
+ {/* {checkoutValidation && ( + + *silahkan pilih expedisi + + )} */} +
+ {/* */} +
+ {/* {checkWeigth == true && ( +

+ Mohon maaf, pengiriman hanya tersedia untuk self pickup karena + terdapat barang yang belum diatur beratnya. Mohon atur berat barang + dengan menghubungi admin melalui{' '} + + tautan ini + +

+ )} */} +
+ + {selectedCourier && ( +
+
+
Tipe Layanan Ekspedisi:
+
+ +
+
+
+ )} + + ); +} diff --git a/src/lib/checkout/stores/useAdress.js b/src/lib/checkout/stores/useAdress.js index 1c17258d..5274ecfe 100644 --- a/src/lib/checkout/stores/useAdress.js +++ b/src/lib/checkout/stores/useAdress.js @@ -1,6 +1,21 @@ -import { create } from "zustand"; +import { create } from 'zustand'; export const useAddress = create((set) => ({ - address: {}, - setAddress: (address) => set({ address }), -})); \ No newline at end of file + 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/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 }); + } +} -- cgit v1.2.3 From b18d45c560d57c788d3646cf6be0beb6381ec0f7 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Thu, 14 Nov 2024 10:53:54 +0700 Subject: biteship --- src/lib/checkout/components/Checkout.jsx | 4 - src/lib/checkout/components/SectionExpedition.jsx | 304 ++++++++++++++++++++++ src/lib/checkout/components/SectionExpedition.tsx | 248 ------------------ 3 files changed, 304 insertions(+), 252 deletions(-) create mode 100644 src/lib/checkout/components/SectionExpedition.jsx delete mode 100644 src/lib/checkout/components/SectionExpedition.tsx diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index b0f8f884..152b84ed 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -1674,10 +1674,6 @@ const SectionValidation = ({ address }) => ); -const SectionExpedisiBiteship = ({ listExpedisi, setSelectedExpedisi }) => ( - <> -); - const SectionExpedisi = ({ address, listExpedisi, diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx new file mode 100644 index 00000000..ead65a6f --- /dev/null +++ b/src/lib/checkout/components/SectionExpedition.jsx @@ -0,0 +1,304 @@ +import React, { useEffect, useState } from 'react'; +import { useQuery } from 'react-query'; +import GetRatesCourierBiteship from '../api/getRatesCourier'; +import { useAddress } from '../stores/useAdress'; +import axios, { AxiosResponse } from 'axios'; +import { useForm, Controller } from 'react-hook-form'; +import { AnimatePresence, motion } from 'framer-motion'; +import { Spinner } from '@chakra-ui/react'; + +import currencyFormat from '@/core/utils/currencyFormat'; + +function mappingItems(products) { + return products?.map((item) => ({ + name: item.parent.name, + description: `${item.code} - ${item.name}`, + value: item.price.priceDiscount, + weight: item.weight, + quantity: item.quantity, + })); +} + +function mappingCourier(couriers) { + return couriers?.reduce((result, item) => { + const { courier_code, courier_service_code } = item; + + // Jika courier_code belum ada di result, buat objek baru untuknya + if (!result[courier_code]) { + result[courier_code] = { + courier_name: item.courier_name, + courier_code: courier_code, + service_type: { + [courier_service_code]: { + service_name: item.courier_service_name, + duration: item.duration, + shipment_duration: item.duration, + price: item.price, + service_type: item.service_type, + description: item.description, + }, + }, + }; + } else { + result[courier_code].service_type[courier_service_code] = { + service_name: item.courier_service_name, + duration: item.duration, + shipment_duration: item.duration, + price: item.price, + service_type: item.service_type, + description: item.description, + }; + } + + return result; + }, {}); +} + +// interface CourierService { +// courier_name: string; +// courier_code: string; +// service_type: { +// [key: string]: { +// service_name: string; +// duration: number; +// shipment_duration: number; +// price: number; +// service_type: string; +// description: string; +// }; +// }; +// } + +// interface ServiceOption { +// service_name: string; +// duration: number; +// shipment_duration: number; +// price: number; +// service_type: string; +// description: string; +// } + +export default function SectionExpedition({ products }) { + const { addressMaps, coordinate, postalCode } = useAddress(); + const { control, handleSubmit } = useForm(); + const [selectedCourier, setSelectedCourier] = useState(''); + const [serviceOptions, setServiceOptions] = useState([]); + const [selectedService, setSelectedService] = useState(null); + const [isOpen, setIsOpen] = useState(false); + + 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: 12440, + destination_postal_code: postalCode, + }; + } + + const fetchExpedition = async () => { + let body = { + ...destination, + couriers: + 'gojek,grab,deliveree,lalamove,jne,tiki,ninja,lion,rara,sicepat,jnt,pos,idexpress,rpx,wahana,jdl,pos,anteraja,sap,paxel,borzo', + items: items, + }; + try { + const response = await axios.get(`/api/biteship-service`, { + params: { body: JSON.stringify(body) }, + }); + return response; + } catch (error) { + console.error('Failed to fetch expedition rates:', error); + } + }; + + // const fetchExpedition = async () => await GetRatesCourierBiteship({ destination, items }); + const { data, isLoading } = useQuery( + ['expedition', JSON.stringify(destination), JSON.stringify(items)], + fetchExpedition, + { + enabled: Boolean(Object.keys(destination).length) && items?.length > 0, + staleTime: Infinity, + cacheTime: Infinity, + } + ); + + const couriers = mappingCourier(data?.data?.pricing); + + console.log('couriers', couriers); + + const onCourierChange = (e) => { + const courier = e.target.value; + console.log('courier', courier); + setSelectedCourier(courier); + // Menentukan layanan berdasarkan pengiriman yang dipilih + if (courier && couriers[courier]) { + setServiceOptions(Object.values(couriers[courier]?.service_type)); + } else { + setServiceOptions([]); + } + }; + + const onSubmit = (data) => { + console.log(data); + }; + + const handleSelect = (service) => { + setSelectedService(service); + setIsOpen(false); + }; + + return ( +
+ {/* Dropdown untuk memilih jenis pengiriman */} +
+
+
Pilih Ekspedisi:
+
+
+ + + + {isLoading && ( + + + + )} + +
+ {/* {checkoutValidation && ( + + *silahkan pilih expedisi + + )} */} +
+ {/* */} +
+ {/* {checkWeigth == true && ( +

+ Mohon maaf, pengiriman hanya tersedia untuk self pickup karena + terdapat barang yang belum diatur beratnya. Mohon atur berat barang + dengan menghubungi admin melalui{' '} + + tautan ini + +

+ )} */} +
+ + {selectedCourier && ( +
+
+
Tipe Layanan Ekspedisi:
+
+
+ {/* Custom Select Input Field */} +
setIsOpen(!isOpen)} + > + {selectedService ? ( +
+ {selectedService.service_name} + + {currencyFormat(selectedService.price)} + +
+ ) : ( + + Pilih layanan pengiriman + + )} +
+ + {/* Dropdown Options */} + {isOpen && ( +
+ {serviceOptions.map((service) => ( +
handleSelect(service)} + className='flex justify-between p-2 items-center hover:bg-gray-100 cursor-pointer' + > +
+

+ {service.service_name} +

+

+ Estimasi Tiba {service.duration} +

+
+ {currencyFormat(service.price)} +
+ ))} +
+ )} +
+ {/* */} +
+
+
+ )} +
+ ); +} diff --git a/src/lib/checkout/components/SectionExpedition.tsx b/src/lib/checkout/components/SectionExpedition.tsx deleted file mode 100644 index a6e05893..00000000 --- a/src/lib/checkout/components/SectionExpedition.tsx +++ /dev/null @@ -1,248 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useQuery } from 'react-query'; -import GetRatesCourierBiteship from '../api/getRatesCourier'; -import { useAddress } from '../stores/useAdress'; -import axios, { AxiosResponse } from 'axios'; -import { useForm, Controller } from 'react-hook-form'; -import { AnimatePresence, motion } from 'framer-motion'; -import { Spinner } from '@chakra-ui/react'; - -function mappingItems(products) { - return products?.map((item) => ({ - name: item.parent.name, - description: `${item.code} - ${item.name}`, - value: item.price.priceDiscount, - weight: item.weight, - quantity: item.quantity, - })); -} - -function mappingCourier(couriers) { - return couriers?.reduce((result, item) => { - const { courier_code, courier_service_code } = item; - - // Jika courier_code belum ada di result, buat objek baru untuknya - if (!result[courier_code]) { - result[courier_code] = { - courier_name: item.courier_name, - courier_code: courier_code, - service_type: { - [courier_service_code]: { - service_name: item.courier_service_name, - duration: item.duration, - shipment_duration: item.duration, - price: item.price, - service_type: item.service_type, - description: item.description, - }, - }, - }; - } else { - result[courier_code].service_type[courier_service_code] = { - service_name: item.courier_service_name, - duration: item.duration, - shipment_duration: item.duration, - price: item.price, - service_type: item.service_type, - description: item.description, - }; - } - - return result; - }, {}); -} - -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; - }; - }; -} - -export default function SectionExpedition({ products }) { - const { addressMaps, coordinate, postalCode } = useAddress(); - const { control, handleSubmit } = useForm(); - const [selectedCourier, setSelectedCourier] = useState(''); - const [serviceOptions, setServiceOptions] = useState([]); - - 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: 12440, - destination_postal_code: postalCode, - }; - } - - const fetchExpedition = async () => { - let body = { - ...destination, - couriers: - 'gojek, grab, deliveree, lalamove, jne, tiki, ninja, lion, rara, sicepat, jnt, pos, idexpress, rpx, wahana, jdl, pos, anteraja, sap, paxel, borzo', - items: items, - }; - try { - const response = await axios.get(`/api/biteship-service`, { - params: { body: JSON.stringify(body) }, - }); - return response; - } catch (error) { - console.error('Failed to fetch expedition rates:', error); - } - }; - - // const fetchExpedition = async () => await GetRatesCourierBiteship({ destination, items }); - const { data, isLoading } = useQuery( - ['expedition', JSON.stringify(destination), JSON.stringify(items)], - fetchExpedition, - { - enabled: Boolean(Object.keys(destination).length) && items?.length > 0, - staleTime: Infinity, - cacheTime: Infinity, - } - ); - - const couriers: CourierService = mappingCourier(data?.data?.pricing); - - console.log('couriers', couriers); - - const onCourierChange = (e) => { - const courier = e.target.value; - console.log('courier', courier); - setSelectedCourier(courier); - // Menentukan layanan berdasarkan pengiriman yang dipilih - if (courier && couriers[courier]) { - setServiceOptions(Object.values(couriers[courier]?.service_type)); - } else { - setServiceOptions([]); - } - }; - - console.log( - 'serviceOptions', - couriers[selectedCourier]?.service_type, - selectedCourier - ); - const onSubmit = (data) => { - console.log(data); - }; - - return ( -
- {/* Dropdown untuk memilih jenis pengiriman */} -
-
-
Pilih Ekspedisi:
-
-
- - - - {isLoading && ( - - - - )} - -
- {/* {checkoutValidation && ( - - *silahkan pilih expedisi - - )} */} -
- {/* */} -
- {/* {checkWeigth == true && ( -

- Mohon maaf, pengiriman hanya tersedia untuk self pickup karena - terdapat barang yang belum diatur beratnya. Mohon atur berat barang - dengan menghubungi admin melalui{' '} - - tautan ini - -

- )} */} -
- - {selectedCourier && ( -
-
-
Tipe Layanan Ekspedisi:
-
- -
-
-
- )} -
- ); -} -- cgit v1.2.3 From 0d4278bd482d2ec2563b29cb3597eb8c7227a2d7 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Fri, 15 Nov 2024 13:32:26 +0700 Subject: add hook state --- src/lib/checkout/components/Checkout.jsx | 13 ------------- src/lib/checkout/stores/stateCheckout.js | 11 +++++++++++ 2 files changed, 11 insertions(+), 13 deletions(-) create mode 100644 src/lib/checkout/stores/stateCheckout.js diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 152b84ed..4dd715d7 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -662,19 +662,6 @@ const Checkout = () => { )}
- {/* {!loadingVoucher && - listVouchers?.length === 1 && - listVoucherShippings?.length === 1} - { -
-
-

Tidak ada voucher tersedia

-

- Maaf, saat ini tidak ada voucher yang tersedia. -

-
-
- } */} {listVoucherShippings && listVoucherShippings?.length > 0 && (
diff --git a/src/lib/checkout/stores/stateCheckout.js b/src/lib/checkout/stores/stateCheckout.js new file mode 100644 index 00000000..5d2904dd --- /dev/null +++ b/src/lib/checkout/stores/stateCheckout.js @@ -0,0 +1,11 @@ +import { create } from "zustand"; + +export const stateCheckout = create((set) => ({ + checkWeigth : false, + hasFlashSale : false, + checkoutValidation : false, + setCheckWeigth : (checkWeigth) => set({ checkWeigth }), + setHasFlashSale : (hasFlashSale) => set({ hasFlashSale }), + setCheckoutValidation : (checkoutValidation) => set({ checkoutValidation }), + +})) \ No newline at end of file -- cgit v1.2.3 From d078c6adfd896b59f14e2a5116ecb977256674fa Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Tue, 19 Nov 2024 16:51:54 +0700 Subject: biteships --- src/lib/checkout/components/Checkout.jsx | 11 +- src/lib/checkout/components/SectionExpedition.jsx | 216 +++++++++++----------- src/lib/checkout/stores/stateCheckout.js | 9 +- 3 files changed, 121 insertions(+), 115 deletions(-) diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 33cfaa87..92a94834 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -34,6 +34,7 @@ import ExpedisiList from '../api/ExpedisiList'; import { getVoucher } from '../api/getVoucher'; import { useAddress } from '../stores/useAdress'; import SectionExpedition from './SectionExpedition'; +import { useCheckout } from '../stores/stateCheckout'; const SELF_PICKUP_ID = 32; @@ -125,7 +126,6 @@ const Checkout = () => { } }, [addresses]); - const [products, setProducts] = useState(null); const [totalWeight, setTotalWeight] = useState(0); const [priceCheck, setPriceCheck] = useState(false); const [listExpedisi, setExpedisi] = useState([]); @@ -133,8 +133,6 @@ const Checkout = () => { const [selectedExpedisi, setSelectedExpedisi] = useState(0); const [selectedCarrierId, setselectedCarrierId] = useState(0); const [selectedCarrier, setselectedCarrier] = useState(0); - const [biayaKirim, setBiayaKirim] = useState(0); - const [checkWeigth, setCheckWeight] = useState(false); const [selectedServiceType, setSelectedServiceType] = useState(null); const [selectedExpedisiService, setselectedExpedisiService] = useState(null); const [etd, setEtd] = useState(null); @@ -149,11 +147,11 @@ const Checkout = () => { const [findCodeVoucher, SetFindVoucher] = useState(null); const [selisihHargaCode, SetSelisihHargaCode] = useState(null); const [buttonTerapkan, SetButtonTerapkan] = useState(false); - const [checkoutValidation, setCheckoutValidation] = useState(false); const [loadingVoucher, setLoadingVoucher] = useState(true); const [loadingRajaOngkir, setLoadingRajaOngkir] = useState(false); const [grandTotal, setGrandTotal] = useState(0); - const [hasFlashSale, setHasFlashSale] = useState(false); + + const {checkWeigth, setCheckWeight, hasFlashSale, setHasFlashSale, checkoutValidation, setCheckoutValidation, biayaKirim, setBiayaKirim, products, setProducts} = useCheckout(); const expedisiValidation = useRef(null); @@ -571,6 +569,9 @@ const Checkout = () => { cartCheckout?.discountVoucher + (cartCheckout?.discountVoucherShipping || 0); + + console.log('etd', etd, calculateEstimatedArrival(etd), splitDuration(etd)); + return ( <> ({ name: item.parent.name, description: `${item.code} - ${item.name}`, value: item.price.priceDiscount, - weight: item.weight, + weight: item.weight * 1000 * item.quantity, quantity: item.quantity, + canInstant: item.availableQuantity > item.quantity ? true : false, })); } -function mappingCourier(couriers) { +function mappingCourier(couriers, notIncludeInstant = false) { return couriers?.reduce((result, item) => { const { courier_code, courier_service_code } = item; + if ( + notIncludeInstant && + ['hours'].includes(item.shipment_duration_unit.toLowerCase()) + ) { + return result; + } // Jika courier_code belum ada di result, buat objek baru untuknya if (!result[courier_code]) { @@ -54,6 +61,10 @@ function mappingCourier(couriers) { }, {}); } +function hasCanInstantFalse(items) { + return items.some((item) => item.canInstant === false); +} + // interface CourierService { // courier_name: string; // courier_code: string; @@ -86,8 +97,13 @@ export default function SectionExpedition({ products }) { const [selectedService, setSelectedService] = useState(null); const [isOpen, setIsOpen] = useState(false); + const { checkWeigth, checkoutValidation } = useCheckout(); + let destination = {}; let items = mappingItems(products); + + let notIncludeInstant = hasCanInstantFalse(items); + console.log('instanCourier', items); if (addressMaps) { destination = { origin_latitude: -6.3031123, @@ -118,27 +134,31 @@ export default function SectionExpedition({ products }) { } }; - // const fetchExpedition = async () => await GetRatesCourierBiteship({ destination, items }); const { data, isLoading } = useQuery( ['expedition', JSON.stringify(destination), JSON.stringify(items)], fetchExpedition, { - enabled: Boolean(Object.keys(destination).length) && items?.length > 0, + enabled: + Boolean(Object.keys(destination).length) && + items?.length > 0 && + !checkWeigth, staleTime: Infinity, cacheTime: Infinity, } ); - const couriers = mappingCourier(data?.data?.pricing); + console.log('data', data); - console.log('couriers', couriers); + const couriers = mappingCourier(data?.data?.pricing, true) || null; const onCourierChange = (e) => { + setIsOpen(false); const courier = e.target.value; - console.log('courier', courier); + setSelectedService(null); setSelectedCourier(courier); + console.log('courier', courier); // Menentukan layanan berdasarkan pengiriman yang dipilih - if (courier && couriers[courier]) { + if (courier && courier !== '0' && courier !== '32' && couriers[courier]) { setServiceOptions(Object.values(couriers[courier]?.service_type)); } else { setServiceOptions([]); @@ -156,7 +176,6 @@ export default function SectionExpedition({ products }) { return (
- {/* Dropdown untuk memilih jenis pengiriman */}
Pilih Ekspedisi:
@@ -167,8 +186,8 @@ export default function SectionExpedition({ products }) { onChange={onCourierChange} required > - - + + {couriers && Object.values(couriers)?.map((expedisi) => (
- {/* {checkoutValidation && ( - - *silahkan pilih expedisi - - )} */} + {checkoutValidation && ( + + *silahkan pilih expedisi + + )}
- {/* */} +
- {/* {checkWeigth == true && ( -

- Mohon maaf, pengiriman hanya tersedia untuk self pickup karena - terdapat barang yang belum diatur beratnya. Mohon atur berat barang - dengan menghubungi admin melalui{' '} - - tautan ini - -

- )} */} + {checkWeigth == true && ( +

+ Mohon maaf, pengiriman hanya tersedia untuk self pickup karena + terdapat barang yang belum diatur beratnya. Mohon atur berat barang + dengan menghubungi admin melalui{' '} + + tautan ini + +

+ )}
- {selectedCourier && ( -
-
-
Tipe Layanan Ekspedisi:
-
-
- {/* Custom Select Input Field */} -
setIsOpen(!isOpen)} - > - {selectedService ? ( -
- {selectedService.service_name} - - {currencyFormat(selectedService.price)} + {selectedCourier && + selectedCourier !== '32' && + selectedCourier !== '0' && ( +
+
+
Tipe Layanan Ekspedisi:
+
+
+ {/* Custom Select Input Field */} +
setIsOpen(!isOpen)} + > + {selectedService ? ( +
+ {selectedService.service_name} + + {currencyFormat(selectedService.price)} + +
+ ) : ( + + Pilih layanan pengiriman + )} +
+ + {/* Dropdown Options */} + {isOpen && ( +
+ {serviceOptions.map((service) => ( +
handleSelect(service)} + className='flex justify-between p-2 items-center hover:bg-gray-100 cursor-pointer' + > +
+

+ {service.service_name} +

+

+ Estimasi Tiba {service.duration} +

+
+ + {currencyFormat(service.price)} + +
+ ))}
- ) : ( - - Pilih layanan pengiriman - )}
- - {/* Dropdown Options */} - {isOpen && ( -
- {serviceOptions.map((service) => ( -
handleSelect(service)} - className='flex justify-between p-2 items-center hover:bg-gray-100 cursor-pointer' - > -
-

- {service.service_name} -

-

- Estimasi Tiba {service.duration} -

-
- {currencyFormat(service.price)} -
- ))} -
- )}
- {/* */}
-
- )} + )} ); } diff --git a/src/lib/checkout/stores/stateCheckout.js b/src/lib/checkout/stores/stateCheckout.js index 5d2904dd..97b0c64e 100644 --- a/src/lib/checkout/stores/stateCheckout.js +++ b/src/lib/checkout/stores/stateCheckout.js @@ -1,11 +1,16 @@ import { create } from "zustand"; -export const stateCheckout = create((set) => ({ +export const useCheckout = create((set) => ({ + products : null, checkWeigth : false, hasFlashSale : false, checkoutValidation : false, - setCheckWeigth : (checkWeigth) => set({ checkWeigth }), + biayaKirim : 0, + etd : null, + setCheckWeight : (checkWeigth) => set({ checkWeigth }), setHasFlashSale : (hasFlashSale) => set({ hasFlashSale }), setCheckoutValidation : (checkoutValidation) => set({ checkoutValidation }), + setBiayaKirim : (biayaKirim) => set({ biayaKirim }), + setProducts : (products) => set({ products }), })) \ No newline at end of file -- cgit v1.2.3 From 821d218ff687a585c99937948989408541b596e4 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Tue, 26 Nov 2024 09:46:07 +0700 Subject: filter biteship courier sesuikan dengan courier dari odoo --- src/lib/checkout/components/Checkout.jsx | 185 ++++++++++------------ src/lib/checkout/components/SectionExpedition.jsx | 118 +++++++++----- src/lib/checkout/stores/stateCheckout.js | 12 ++ src/lib/checkout/utils/functionCheckouit.js | 79 +++++++++ 4 files changed, 250 insertions(+), 144 deletions(-) create mode 100644 src/lib/checkout/utils/functionCheckouit.js diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 92a94834..38e11038 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -35,6 +35,7 @@ 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; @@ -128,15 +129,14 @@ const Checkout = () => { 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 [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); @@ -151,7 +151,7 @@ const Checkout = () => { const [loadingRajaOngkir, setLoadingRajaOngkir] = useState(false); const [grandTotal, setGrandTotal] = useState(0); - const {checkWeigth, setCheckWeight, hasFlashSale, setHasFlashSale, checkoutValidation, setCheckoutValidation, biayaKirim, setBiayaKirim, products, setProducts} = useCheckout(); + const {checkWeigth, setCheckWeight, hasFlashSale, setHasFlashSale, checkoutValidation, setCheckoutValidation, biayaKirim, products, setProducts, etd, unit, selectedCourier, selectedService, listExpedisi, setExpedisi} = useCheckout(); const expedisiValidation = useRef(null); @@ -290,6 +290,7 @@ const Checkout = () => { window.onpopstate = handlePopState; + return () => { window.onpopstate = null; }; @@ -312,70 +313,70 @@ const Checkout = () => { setHasFlashSale(hasFlashSale); }, [cartCheckout]); - useEffect(() => { - setCheckoutValidation(false); - const loadServiceRajaOngkir = async () => { - setLoadingRajaOngkir(true); - const body = { - origin: 2127, - destination: selectedAddress.shipping.rajaongkirCityId, - weight: totalWeight, - courier: selectedCarrier, - originType: 'subdistrict', - destinationType: 'subdistrict', - }; - setBiayaKirim(0); - const dataService = await axios( - '/api/rajaongkir-service?body=' + JSON.stringify(body) - ); - setLoadingRajaOngkir(false); - setListServiceExpedisi(dataService.data[0].costs); - if (dataService.data[0].costs[0]) { - setBiayaKirim(dataService.data[0].costs[0]?.cost[0].value); - setselectedExpedisiService( - dataService.data[0].costs[0]?.description + - '-' + - dataService.data[0].costs[0]?.service - ); - setEtd(dataService.data[0].costs[0]?.cost[0].etd); - toast.success('Harap pilih tipe layanan pengiriman'); - } else { - toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'); - } - }; - if (selectedCarrier != 0 && selectedCarrier != 1 && totalWeight > 0) { - loadServiceRajaOngkir(); - } else { - setListServiceExpedisi(); - setBiayaKirim(0); - setselectedExpedisiService(); - setEtd(); - } - }, [selectedCarrier, selectedAddress, totalWeight]); - - useEffect(() => { - 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]); + // 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); @@ -403,7 +404,7 @@ const Checkout = () => { }); return; } - if (selectedExpedisi === 0) { + if (selectedCourier === 0 || !selectedCourier) { setCheckoutValidation(true); if (expedisiValidation.current) { const position = expedisiValidation.current.getBoundingClientRect(); @@ -414,7 +415,11 @@ const Checkout = () => { } return; } - if (selectedCarrier != 1 && biayaKirim == 0) { + if(!selectedService){ + toast.error('Harap pilih tipe layanan pengiriman'); + return; + } + if (selectedCourier != 1 && biayaKirim == 0) { toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'); return; } @@ -432,15 +437,14 @@ const Checkout = () => { user_id: auth.id, order_line: JSON.stringify(productOrder), delivery_amount: biayaKirim, - carrier_id: selectedCarrierId, - estimated_arrival_days: splitDuration(etd), - delivery_service_type: selectedExpedisiService, + carrier_id: selectedCourier, + estimated_arrival_days: getToDate(etd, unit), + delivery_service_type: selectedService?.service_type, flash_sale: hasFlashSale, // dibuat negasi untuk ngetest kebalikan nilai false voucher: activeVoucher, voucher_shipping: activeVoucherShipping, type: 'sale_order', }; - if (query) { data.source = 'buy'; } @@ -487,24 +491,6 @@ const Checkout = () => { )}`; } } - - /* const midtrans = async () => { - for (const product of products) deleteItemCart({ productId: product.id }); - if (grandTotal > 0) { - const payment = await axios.post( - `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/midtrans-payment?transactionId=${isCheckouted.id}` - ); - setIsLoading(false); - window.location.href = payment.data.redirectUrl; - } else { - window.location.href = `${ - process.env.NEXT_PUBLIC_SELF_HOST - }/shop/checkout/success?order_id=${isCheckouted.name.replace( - /\//g, - '-' - )}`; - } - };*/ }; const handlingActivateCode = async () => { @@ -569,9 +555,6 @@ const Checkout = () => { cartCheckout?.discountVoucher + (cartCheckout?.discountVoucherShipping || 0); - - console.log('etd', etd, calculateEstimatedArrival(etd), splitDuration(etd)); - return ( <> { )} - { + /> */}
{!!products && @@ -1168,7 +1151,7 @@ const Checkout = () => {
- Biaya Kirim

{etdFix}

+ Biaya Kirim

{formatShipmentRange(etd, unit)}

{currencyFormat( @@ -1367,7 +1350,7 @@ const Checkout = () => { {products && } - { + /> */}
Detail Pesanan
@@ -1474,7 +1457,7 @@ const Checkout = () => {
Biaya Kirim -

{etdFix}

+

{formatShipmentRange(etd, unit)}

{currencyFormat( diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx index 27224e5b..be40a577 100644 --- a/src/lib/checkout/components/SectionExpedition.jsx +++ b/src/lib/checkout/components/SectionExpedition.jsx @@ -1,4 +1,4 @@ -import { Spinner } from '@chakra-ui/react'; +import { Skeleton, Spinner } from '@chakra-ui/react'; import axios from 'axios'; import { AnimatePresence, motion } from 'framer-motion'; import React, { useState } from 'react'; @@ -14,15 +14,25 @@ function mappingItems(products) { name: item.parent.name, description: `${item.code} - ${item.name}`, value: item.price.priceDiscount, - weight: item.weight * 1000 * item.quantity, + weight: item.weight * 1000, quantity: item.quantity, canInstant: item.availableQuantity > item.quantity ? true : false, })); } -function mappingCourier(couriers, notIncludeInstant = false) { +function mappingCourier(couriersOdoo, couriers, notIncludeInstant = false) { + const validCourierMap = couriersOdoo.reduce((acc, courier) => { + acc[courier.label.toLowerCase()] = courier.carrierId; + return acc; + }, {}); + return couriers?.reduce((result, item) => { - const { courier_code, courier_service_code } = 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()) @@ -30,16 +40,20 @@ function mappingCourier(couriers, notIncludeInstant = false) { 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_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, @@ -50,6 +64,8 @@ function mappingCourier(couriers, notIncludeInstant = false) { 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, @@ -92,18 +108,27 @@ function hasCanInstantFalse(items) { export default function SectionExpedition({ products }) { const { addressMaps, coordinate, postalCode } = useAddress(); const { control, handleSubmit } = useForm(); - const [selectedCourier, setSelectedCourier] = useState(''); const [serviceOptions, setServiceOptions] = useState([]); - const [selectedService, setSelectedService] = useState(null); const [isOpen, setIsOpen] = useState(false); + const [onFocusSelectedCourier, setOnFocuseSelectedCourier] = useState(false); - const { checkWeigth, checkoutValidation } = useCheckout(); + const { + checkWeigth, + checkoutValidation, + setBiayaKirim, + setUnit, + setEtd, + selectedCourier, + setSelectedCourier, + selectedService, + setSelectedService, + listExpedisi, + } = useCheckout(); let destination = {}; let items = mappingItems(products); let notIncludeInstant = hasCanInstantFalse(items); - console.log('instanCourier', items); if (addressMaps) { destination = { origin_latitude: -6.3031123, @@ -112,7 +137,7 @@ export default function SectionExpedition({ products }) { }; } else if (postalCode) { destination = { - origin_postal_code: 12440, + origin_postal_code: 14440, destination_postal_code: postalCode, }; } @@ -141,23 +166,26 @@ export default function SectionExpedition({ products }) { enabled: Boolean(Object.keys(destination).length) && items?.length > 0 && - !checkWeigth, + !checkWeigth && + onFocusSelectedCourier, staleTime: Infinity, cacheTime: Infinity, } ); - console.log('data', data); + const couriers = + mappingCourier(listExpedisi, data?.data?.pricing, notIncludeInstant) || + null; - const couriers = mappingCourier(data?.data?.pricing, true) || null; + console.log('ini scourier', data?.data?.pricing) const onCourierChange = (e) => { setIsOpen(false); const courier = e.target.value; setSelectedService(null); setSelectedCourier(courier); - console.log('courier', courier); - // Menentukan layanan berdasarkan pengiriman yang dipilih + // setSelectedCourierId(Object.values(couriers[courier]?.courier_id_odoo); + setBiayaKirim(0); if (courier && courier !== '0' && courier !== '32' && couriers[courier]) { setServiceOptions(Object.values(couriers[courier]?.service_type)); } else { @@ -165,12 +193,15 @@ export default function SectionExpedition({ products }) { } }; - const onSubmit = (data) => { + const onSubmit = (data) => {1 console.log(data); }; const handleSelect = (service) => { setSelectedService(service); + setBiayaKirim(service?.price); + setEtd(service?.shipment_range); + setUnit(service?.shipment_unit); setIsOpen(false); }; @@ -184,37 +215,38 @@ export default function SectionExpedition({ products }) { - - - {isLoading && ( - - - + + + )} - +
{checkoutValidation && ( diff --git a/src/lib/checkout/stores/stateCheckout.js b/src/lib/checkout/stores/stateCheckout.js index 97b0c64e..4fefbbed 100644 --- a/src/lib/checkout/stores/stateCheckout.js +++ b/src/lib/checkout/stores/stateCheckout.js @@ -7,10 +7,22 @@ export const useCheckout = create((set) => ({ checkoutValidation : false, biayaKirim : 0, etd : null, + unit : null, + selectedCourier : null, + selectedCourierId : null, + selectedService : null, + listExpedisi : [], setCheckWeight : (checkWeigth) => set({ checkWeigth }), setHasFlashSale : (hasFlashSale) => set({ hasFlashSale }), setCheckoutValidation : (checkoutValidation) => set({ checkoutValidation }), setBiayaKirim : (biayaKirim) => set({ biayaKirim }), setProducts : (products) => set({ products }), + setEtd : (etd) => set({ etd }), + setUnit : (unit) => set({ unit }), + setSelectedCourier : (selectedCourier) => set({ selectedCourier }), + setSelectedService : (selectedService) => set({ selectedService }), + setSelectedCourierId : (selectedCourierId) => set({ selectedCourierId }), + setExpedisi : (listExpedisi) => set({ listExpedisi }) + })) \ No newline at end of file diff --git a/src/lib/checkout/utils/functionCheckouit.js b/src/lib/checkout/utils/functionCheckouit.js new file mode 100644 index 00000000..b299f289 --- /dev/null +++ b/src/lib/checkout/utils/functionCheckouit.js @@ -0,0 +1,79 @@ +export function formatShipmentRange( + shipmentDurationRange, + shipmentDurationUnit +) { + if (!shipmentDurationRange || !shipmentDurationUnit) { + return ''; + } + let minRange, maxRange; + + // Cek apakah durasi berupa range atau angka tunggal + if (shipmentDurationRange.includes('-')) { + [minRange, maxRange] = shipmentDurationRange.split(' - ').map(Number); + } else { + minRange = maxRange = Number(shipmentDurationRange); // Jika angka tunggal + } + + const start = new Date(); // Tanggal saat ini + + let minDate, maxDate; + + // Hitung estimasi berdasarkan unit waktu + if (shipmentDurationUnit === 'days' || shipmentDurationUnit === 'day') { + minDate = new Date(start); + minDate.setDate(start.getDate() + (minRange + 3)); + + maxDate = new Date(start); + maxDate.setDate(start.getDate() + (maxRange + 3)); + } else if (shipmentDurationUnit === 'hours') { + minDate = new Date(start.getTime() + (minRange + 3) * 60 * 60 * 1000); + maxDate = new Date(start.getTime() + (maxRange + 3) * 60 * 60 * 1000); + } else { + throw new Error("Unsupported unit. Please use 'days' or 'hours'."); + } + + console.log('min max', minDate, maxDate); + + const minDateStr = formatDate(minDate); + const maxDateStr = formatDate(maxDate); + if (minDateStr === maxDateStr) { + return `Estimasi tiba ${minDateStr}`; + } + return `Estimasi tiba ${minDateStr} - ${maxDateStr}`; +} + +export function getToDate(shipmentDurationRange, shipmentDurationUnit) { + if (!shipmentDurationRange || !shipmentDurationUnit) { + return ''; + } + const start = new Date(); // Tanggal saat ini + + let maxRange; + + // Cek apakah durasi berupa range atau angka tunggal + if (shipmentDurationRange.includes('-')) { + [, maxRange] = shipmentDurationRange.split(' - ').map(Number); + } else { + maxRange = Number(shipmentDurationRange); // Jika angka tunggal + } + + let maxDate; + + // Hitung estimasi berdasarkan unit waktu + if (shipmentDurationUnit === 'days' || shipmentDurationUnit === 'day') { + maxDate = new Date(start); + maxDate.setDate(start.getDate() + (maxRange + 3)); + } else if (shipmentDurationUnit === 'hours') { + maxDate = new Date(start.getTime() + (maxRange + 3) * 60 * 60 * 1000); + } else { + throw new Error("Unsupported unit. Please use 'days' or 'hours'."); + } + + return formatDate(maxDate); +} + +function formatDate(date) { + const day = date.getDate(); + const month = date.toLocaleString('default', { month: 'short' }); + return `${day} ${month}`; +} -- cgit v1.2.3 From d1592286eef165533c21d52aec70dbb703cdcfd3 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Wed, 18 Dec 2024 15:03:52 +0700 Subject: feedback uat --- src/lib/address/components/CreateAddress.jsx | 6 +- src/lib/address/components/EditAddress.jsx | 71 ++++++++++++++++++++++- src/lib/checkout/components/Checkout.jsx | 2 +- src/lib/checkout/components/SectionExpedition.jsx | 24 +++++--- src/lib/maps/components/PinPointMap.jsx | 6 +- src/pages/my/address/[id]/edit.jsx | 2 +- 6 files changed, 96 insertions(+), 15 deletions(-) diff --git a/src/lib/address/components/CreateAddress.jsx b/src/lib/address/components/CreateAddress.jsx index 404143f9..5f041eb2 100644 --- a/src/lib/address/components/CreateAddress.jsx +++ b/src/lib/address/components/CreateAddress.jsx @@ -45,6 +45,7 @@ const CreateAddress = () => { addressMaps, selectedPosition, detailAddress, + setAddressMaps, } = useMaps(); useEffect(() => { @@ -56,6 +57,7 @@ const CreateAddress = () => { state.label.includes(detailAddress?.province) ); setValue('state', selectedState?.value); + setValue('street', detailAddress?.street); } }, [detailAddress, setValue]); @@ -70,10 +72,10 @@ const CreateAddress = () => { setState(dataState); }; loadState(); + setAddressMaps(''); }, []); const watchState = watch('state'); - console.log(watchState); useEffect(() => { setValue('city', ''); if (watchState) { @@ -228,7 +230,7 @@ const CreateAddress = () => { {addressMaps ? (
- {' '} + setPinedMaps(true)} />{' '} {addressMaps}
) : ( diff --git a/src/lib/address/components/EditAddress.jsx b/src/lib/address/components/EditAddress.jsx index 0b3b0aa3..7675db9d 100644 --- a/src/lib/address/components/EditAddress.jsx +++ b/src/lib/address/components/EditAddress.jsx @@ -42,18 +42,33 @@ const EditAddress = ({ id, defaultValues }) => { const [districts, setDistricts] = useState([]); const [subDistricts, setSubDistricts] = useState([]); const [pinedMaps, setPinedMaps] = useState(false); - const [tempAddress, setTempAddress] = useState(getValues('address_maps')); + const [tempAddress, setTempAddress] = useState(getValues('addressMap')); const { addressMaps, selectedPosition, detailAddress } = useMaps(); + console.log('ini adalah',); + useEffect(() => { if (addressMaps) { setTempAddress(addressMaps); - setValue('address_map', addressMaps); + setValue('addressMap', addressMaps); setValue('longtitude', selectedPosition.lng); setValue('latitude', selectedPosition.lat); } }, [addressMaps, selectedPosition, setValue]); + useEffect(() => { + if (Object.keys(detailAddress).length > 0) { + setValue('zip', detailAddress.postalCode); + const selectedState = states.find( + (state) => + detailAddress?.province.includes(state.label) || + state.label.includes(detailAddress?.province) + ); + setValue('state', selectedState?.value); + setValue('street', detailAddress?.route); + } + }, [detailAddress, setValue]); + useEffect(() => { const loadProfile = async () => { const dataProfile = await addressApi({ id: auth.parentId }); @@ -101,6 +116,21 @@ const EditAddress = ({ id, defaultValues }) => { } }, [watchState, setValue, getValues]); + useEffect(() => { + if (Object.keys(detailAddress).length > 0) { + const selectedCities = cities.find( + (city) => + detailAddress?.district + .toLowerCase() + .includes(city.label.toLowerCase()) || + city.label + .toLowerCase() + .includes(detailAddress?.district.toLowerCase()) + ); + setValue('city', selectedCities?.value); + } + }, [cities, detailAddress, setValue]); + const watchCity = watch('city'); useEffect(() => { setValue('district', ''); @@ -122,6 +152,23 @@ const EditAddress = ({ id, defaultValues }) => { } }, [watchCity, setValue, getValues]); + useEffect(() => { + if (Object.keys(detailAddress).length > 0) { + const selectedDistrict = districts.find( + (district) => + detailAddress.subDistrict + .toLowerCase() + .includes(district.label.toLowerCase()) || + district.label + .toLowerCase() + .includes(detailAddress.subDistrict.toLowerCase()) + ); + setValue('district', selectedDistrict?.value); + } + }, [districts, detailAddress, setValue]); + + + const watchDistrict = watch('district'); useEffect(() => { setValue('subDistrict', ''); @@ -145,6 +192,24 @@ const EditAddress = ({ id, defaultValues }) => { loadSubDistricts(); } }, [watchDistrict, setValue, getValues]); + + + useEffect(() => { + if (Object.keys(detailAddress).length > 0) { + const selectedSubDistrict = subDistricts.find( + (district) => + detailAddress.village + .toLowerCase() + .includes(district.label.toLowerCase()) || + district.label + .toLowerCase() + .includes(detailAddress.village.toLowerCase()) + ); + + setValue('subDistrict', selectedSubDistrict?.value); + } + }, [subDistricts, detailAddress, setValue]); + const onSubmitHandler = async (values) => { const data = { ...values, @@ -228,7 +293,7 @@ const EditAddress = ({ id, defaultValues }) => { {tempAddress ? (
- {' '} + setPinedMaps(true)} />{' '} {tempAddress}
) : ( diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 38e11038..0471dc6c 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -430,7 +430,7 @@ const Checkout = () => { available_quantity: product?.availableQuantity, })); let data = { - // partner_shipping_id: auth.partnerId, + // partner_shipping_id: auth.partnerId,, // partner_invoice_id: auth.partnerId, partner_shipping_id: selectedAddress?.shipping?.id || auth.partnerId, partner_invoice_id: selectedAddress?.invoicing?.id || auth.partnerId, diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx index be40a577..2098d5b5 100644 --- a/src/lib/checkout/components/SectionExpedition.jsx +++ b/src/lib/checkout/components/SectionExpedition.jsx @@ -8,6 +8,7 @@ import { useAddress } from '../stores/useAdress'; import currencyFormat from '@/core/utils/currencyFormat'; import { useCheckout } from '../stores/stateCheckout'; +import { formatShipmentRange } from '../utils/functionCheckouit'; function mappingItems(products) { return products?.map((item) => ({ @@ -32,7 +33,6 @@ function mappingCourier(couriersOdoo, couriers, notIncludeInstant = false) { return result; // Jika tidak ada, lewati item ini } - if ( notIncludeInstant && ['hours'].includes(item.shipment_duration_unit.toLowerCase()) @@ -47,7 +47,7 @@ function mappingCourier(couriersOdoo, couriers, notIncludeInstant = false) { result[courier_code] = { courier_name: item.courier_name, courier_code: courier_code, - courier_id_odoo : carrierId, + courier_id_odoo: carrierId, service_type: { [courier_service_code]: { service_name: item.courier_service_name, @@ -177,7 +177,7 @@ export default function SectionExpedition({ products }) { mappingCourier(listExpedisi, data?.data?.pricing, notIncludeInstant) || null; - console.log('ini scourier', data?.data?.pricing) + console.log('ini scourier', data?.data?.pricing); const onCourierChange = (e) => { setIsOpen(false); @@ -193,7 +193,8 @@ export default function SectionExpedition({ products }) { } }; - const onSubmit = (data) => {1 + const onSubmit = (data) => { + 1; console.log(data); }; @@ -292,7 +293,10 @@ export default function SectionExpedition({ products }) {
{selectedService.service_name} - {currencyFormat(selectedService.price)} + {currencyFormat( + Math.round(parseInt(selectedService?.price * 1.1) / 1000) * + 1000 + )}
) : ( @@ -316,11 +320,17 @@ export default function SectionExpedition({ products }) { {service.service_name}

- Estimasi Tiba {service.duration} + {formatShipmentRange( + service.shipment_range, + service.shipment_unit + )}

- {currencyFormat(service.price)} + {currencyFormat( + Math.round(parseInt(service?.price * 1.1) / 1000) * + 1000 + )}
))} diff --git a/src/lib/maps/components/PinPointMap.jsx b/src/lib/maps/components/PinPointMap.jsx index 201fdeb4..acff5d67 100644 --- a/src/lib/maps/components/PinPointMap.jsx +++ b/src/lib/maps/components/PinPointMap.jsx @@ -66,6 +66,7 @@ const PinpointLocation = () => { if (data.results[0]) { const addressComponents = data.results[0].address_components; const details = { + route : getAddressComponent(addressComponents, 'route')+' '+getAddressComponent(addressComponents, 'street_number')+' '+getAddressComponent(addressComponents, 'administrative_area_level_7')+' '+getAddressComponent(addressComponents, 'administrative_area_level_6'), province: getAddressComponent( addressComponents, 'administrative_area_level_1' @@ -85,7 +86,6 @@ const PinpointLocation = () => { postalCode: getAddressComponent(addressComponents, 'postal_code'), }; setDetailAddress(details); - console.log(details); setTempAddress(data.results[0].formatted_address); } } catch (error) { @@ -115,10 +115,14 @@ const PinpointLocation = () => { alert('Silahkan pilih lokasi terlebih dahulu'); return; } + // console.log('tempPosition', tempPosition.lat); + getAddress(tempPosition.lat, tempPosition.lng); setSelectedPosition(tempPosition); setAddressMaps(tempAddress); }; + console.log('set selected position',selectedPosition); + return (

Tentukan Pinpoint Lokasi

diff --git a/src/pages/my/address/[id]/edit.jsx b/src/pages/my/address/[id]/edit.jsx index 26cde5e1..006785d9 100644 --- a/src/pages/my/address/[id]/edit.jsx +++ b/src/pages/my/address/[id]/edit.jsx @@ -47,7 +47,7 @@ export async function getServerSideProps(context) { business_name: '', longtitude: address?.longtitude || 0, latitude: address?.latitude || 0, - addressMap: address?.address_map || '', + addressMap: address?.addressMap || '', }; return { props: { id, defaultValues } }; -- cgit v1.2.3 From 5559770e0b0e94b68de1e31bf2be5c978362821d Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Tue, 31 Dec 2024 09:01:10 +0700 Subject: logo and mapping courier --- src/lib/checkout/components/Checkout.jsx | 1 + src/lib/checkout/components/SectionExpedition.jsx | 214 ++++++++++++++++------ 2 files changed, 158 insertions(+), 57 deletions(-) diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 0471dc6c..99c7b40a 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -279,6 +279,7 @@ const Checkout = () => { value: expedisi.id, label: expedisi.name, carrierId: expedisi.deliveryCarrierId, + logo : expedisi.image, })); setExpedisi(dataExpedisi); }; diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx index 2098d5b5..8b59d210 100644 --- a/src/lib/checkout/components/SectionExpedition.jsx +++ b/src/lib/checkout/components/SectionExpedition.jsx @@ -1,7 +1,7 @@ import { Skeleton, Spinner } from '@chakra-ui/react'; import axios from 'axios'; import { AnimatePresence, motion } from 'framer-motion'; -import React, { useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import { useQuery } from 'react-query'; import { useAddress } from '../stores/useAdress'; @@ -9,6 +9,8 @@ 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'; function mappingItems(products) { return products?.map((item) => ({ @@ -21,6 +23,57 @@ function mappingItems(products) { })); } +function reverseMappingCourier(couriersOdoo, couriers) { + // 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_name.toLowerCase(); + + 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: item.service_type, + 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; @@ -110,7 +163,9 @@ export default function SectionExpedition({ products }) { const { control, handleSubmit } = useForm(); const [serviceOptions, setServiceOptions] = useState([]); const [isOpen, setIsOpen] = useState(false); + const [selectedE, setIsOpenCourier] = useState(false); const [onFocusSelectedCourier, setOnFocuseSelectedCourier] = useState(false); + const [couriers, setCouriers] = useState(null); const { checkWeigth, @@ -173,22 +228,31 @@ export default function SectionExpedition({ products }) { } ); - const couriers = - mappingCourier(listExpedisi, data?.data?.pricing, notIncludeInstant) || - null; + console.log('ini response', data); - console.log('ini scourier', data?.data?.pricing); + useEffect(() => { + if (data) { + const couriers = reverseMappingCourier(listExpedisi, data?.data?.pricing); + setCouriers(couriers); + console.log('ini scourier', couriers); + } + }, [data]); - const onCourierChange = (e) => { + const onCourierChange = (code) => { setIsOpen(false); - const courier = e.target.value; + setOnFocuseSelectedCourier(false); + const courier = code; setSelectedService(null); - setSelectedCourier(courier); - // setSelectedCourierId(Object.values(couriers[courier]?.courier_id_odoo); setBiayaKirim(0); - if (courier && courier !== '0' && courier !== '32' && couriers[courier]) { - setServiceOptions(Object.values(couriers[courier]?.service_type)); + if (courier !== 0 && courier !== 32) { + if (courier.courier) { + setSelectedCourier(courier.courier.courier_code); + setServiceOptions(Object.values(courier.courier.service_type)); + } else { + toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'); + } } else { + setSelectedCourier(courier === 32 ? 'SELF PICKUP' : null); setServiceOptions([]); } }; @@ -198,6 +262,11 @@ export default function SectionExpedition({ products }) { console.log(data); }; + const handleOnFocuse = (value) => { + setOnFocuseSelectedCourier(!value); + setIsOpen(false); + }; + const handleSelect = (service) => { setSelectedService(service); setBiayaKirim(service?.price); @@ -206,48 +275,79 @@ export default function SectionExpedition({ products }) { setIsOpen(false); }; + console.log('ini selectedCourier', selectedCourier); + return (
Pilih Ekspedisi:
-
-
- +
+
+
+
+
+ {/* Custom Select Input Field */} +
handleOnFocuse(onFocusSelectedCourier)} + > + {selectedCourier ? ( +
+ {selectedCourier} +
+ ) : ( + Pilih Expedisi + )} +
+ + {/* Dropdown Options */} + {onFocusSelectedCourier && ( +
+ {!isLoading ? ( + <> +
onCourierChange(32)} + className='flex justify-between p-2 items-center hover:bg-gray-100 cursor-pointer' + > +
+

SELF PICKUP

+
+
+ {couriers?.map((courier) => ( +
onCourierChange(courier)} + className='flex justify-between p-2 items-center hover:bg-gray-100 cursor-pointer' + > +
+

+ {courier?.label} +

+
+ + {courier?.courier?.courier_name} + +
+ ))} + + ) : ( + <> + + + + )} +
+ )} +
+
+
{checkoutValidation && ( @@ -276,9 +376,9 @@ export default function SectionExpedition({ products }) { )}
- {selectedCourier && - selectedCourier !== '32' && - selectedCourier !== '0' && ( + {serviceOptions.length > 0 && selectedCourier && + selectedCourier !== 32 && + selectedCourier !== 0 && (
Tipe Layanan Ekspedisi:
@@ -294,8 +394,9 @@ export default function SectionExpedition({ products }) { {selectedService.service_name} {currencyFormat( - Math.round(parseInt(selectedService?.price * 1.1) / 1000) * - 1000 + Math.round( + parseInt(selectedService?.price * 1.1) / 1000 + ) * 1000 )}
@@ -305,8 +406,6 @@ export default function SectionExpedition({ products }) { )}
- - {/* Dropdown Options */} {isOpen && (
{serviceOptions.map((service) => ( @@ -328,8 +427,9 @@ export default function SectionExpedition({ products }) {
{currencyFormat( - Math.round(parseInt(service?.price * 1.1) / 1000) * - 1000 + Math.round( + parseInt(service?.price * 1.1) / 1000 + ) * 1000 )}
-- cgit v1.2.3 From bd4cdf2125f717875ba90e03893b319dd962f753 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Sat, 18 Jan 2025 10:37:49 +0700 Subject: bittesip --- src/lib/checkout/components/Checkout.jsx | 48 +++++++++++++++++------ src/lib/checkout/components/SectionExpedition.jsx | 16 +++++--- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 99c7b40a..6479881d 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -44,11 +44,11 @@ const { getProductsCheckout } = require('../api/checkoutApi'); function convertToInternational(number) { if (typeof number !== 'string') { - throw new Error("Input harus berupa string"); + throw new Error('Input harus berupa string'); } if (number.startsWith('08')) { - return '+62' + number.slice(2); + return '+62' + number.slice(2); } return number; @@ -151,7 +151,23 @@ const Checkout = () => { const [loadingRajaOngkir, setLoadingRajaOngkir] = useState(false); const [grandTotal, setGrandTotal] = useState(0); - const {checkWeigth, setCheckWeight, hasFlashSale, setHasFlashSale, checkoutValidation, setCheckoutValidation, biayaKirim, products, setProducts, etd, unit, selectedCourier, selectedService, listExpedisi, setExpedisi} = useCheckout(); + const { + checkWeigth, + setCheckWeight, + hasFlashSale, + setHasFlashSale, + checkoutValidation, + setCheckoutValidation, + biayaKirim, + products, + setProducts, + etd, + unit, + selectedCourier, + selectedService, + listExpedisi, + setExpedisi, + } = useCheckout(); const expedisiValidation = useRef(null); @@ -279,7 +295,7 @@ const Checkout = () => { value: expedisi.id, label: expedisi.name, carrierId: expedisi.deliveryCarrierId, - logo : expedisi.image, + logo: expedisi.image, })); setExpedisi(dataExpedisi); }; @@ -291,7 +307,6 @@ const Checkout = () => { window.onpopstate = handlePopState; - return () => { window.onpopstate = null; }; @@ -416,7 +431,7 @@ const Checkout = () => { } return; } - if(!selectedService){ + if (!selectedService) { toast.error('Harap pilih tipe layanan pengiriman'); return; } @@ -471,10 +486,12 @@ const Checkout = () => { gtag('set', 'user_data', { email: auth.email, - phone_number: convertToInternational(auth.mobile) ?? convertToInternational(auth.phone), + phone_number: + convertToInternational(auth.mobile) ?? + convertToInternational(auth.phone), }); - gtag('config', 'AW-954540379', { ' allow_enhanced_conversions':true } ) ; + gtag('config', 'AW-954540379', { ' allow_enhanced_conversions': true }); for (const product of products) deleteItemCart({ productId: product.id }); if (grandTotal > 0) { @@ -1152,7 +1169,10 @@ const Checkout = () => {
- Biaya Kirim

{formatShipmentRange(etd, unit)}

+ Biaya Kirim{' '} +

+ {formatShipmentRange(etd, unit)} +

{currencyFormat( @@ -1458,7 +1478,9 @@ const Checkout = () => {
Biaya Kirim -

{formatShipmentRange(etd, unit)}

+

+ {formatShipmentRange(etd, unit)} +

{currencyFormat( @@ -1629,7 +1651,7 @@ const SectionAddress = ({ address, label, url }) => (

{address.street}, {address?.city?.name}

-
+
( {address.addressMap ? ( ) : ( - + + + )}
diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx index 8b59d210..5b4f6bfc 100644 --- a/src/lib/checkout/components/SectionExpedition.jsx +++ b/src/lib/checkout/components/SectionExpedition.jsx @@ -228,13 +228,10 @@ export default function SectionExpedition({ products }) { } ); - console.log('ini response', data); - useEffect(() => { if (data) { const couriers = reverseMappingCourier(listExpedisi, data?.data?.pricing); setCouriers(couriers); - console.log('ini scourier', couriers); } }, [data]); @@ -249,7 +246,15 @@ export default function SectionExpedition({ products }) { setSelectedCourier(courier.courier.courier_code); setServiceOptions(Object.values(courier.courier.service_type)); } else { - toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'); + 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.' + ); + } } } else { setSelectedCourier(courier === 32 ? 'SELF PICKUP' : null); @@ -376,7 +381,8 @@ export default function SectionExpedition({ products }) { )}
- {serviceOptions.length > 0 && selectedCourier && + {serviceOptions.length > 0 && + selectedCourier && selectedCourier !== 32 && selectedCourier !== 0 && (
-- cgit v1.2.3 From c26a0d026886e6f70ea3487b9d83a54d20b9c1e4 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Tue, 28 Jan 2025 09:46:11 +0700 Subject: biteship --- src/lib/checkout/api/checkoutApi.js | 11 +++++ src/lib/checkout/components/Checkout.jsx | 5 +- src/lib/checkout/components/SectionExpedition.jsx | 59 +++++++++++++++++++---- src/lib/checkout/stores/stateCheckout.js | 4 +- src/lib/checkout/utils/functionCheckouit.js | 31 ++++++++---- 5 files changed, 89 insertions(+), 21 deletions(-) diff --git a/src/lib/checkout/api/checkoutApi.js b/src/lib/checkout/api/checkoutApi.js index fd982fff..9d326b10 100644 --- a/src/lib/checkout/api/checkoutApi.js +++ b/src/lib/checkout/api/checkoutApi.js @@ -18,3 +18,14 @@ 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 + ) + + console.log('ini sla - data', dataSLA); + return dataSLA +} diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 5f630799..3ad833ec 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -168,6 +168,7 @@ const Checkout = () => { selectedService, listExpedisi, setExpedisi, + productSla } = useCheckout(); const expedisiValidation = useRef(null); @@ -1171,7 +1172,7 @@ const Checkout = () => {
Biaya Kirim{' '}

- {formatShipmentRange(etd, unit)} + {formatShipmentRange(etd, unit, productSla)}

@@ -1479,7 +1480,7 @@ const Checkout = () => {
Biaya Kirim

- {formatShipmentRange(etd, unit)} + {formatShipmentRange(etd, unit, productSla)}

diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx index 5b4f6bfc..d1844204 100644 --- a/src/lib/checkout/components/SectionExpedition.jsx +++ b/src/lib/checkout/components/SectionExpedition.jsx @@ -12,6 +12,9 @@ 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, @@ -19,16 +22,19 @@ function mappingItems(products) { value: item.price.priceDiscount, weight: item.weight * 1000, quantity: item.quantity, - canInstant: item.availableQuantity > item.quantity ? true : false, })); } -function reverseMappingCourier(couriersOdoo, couriers) { +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_name.toLowerCase(); + if (!includeInstant && ['hours'].includes(item.shipment_duration_unit.toLowerCase())) { + return acc; + } + if (!acc[key]) { acc[key] = { courier_name: item.courier_name, @@ -166,6 +172,7 @@ export default function SectionExpedition({ products }) { const [selectedE, setIsOpenCourier] = useState(false); const [onFocusSelectedCourier, setOnFocuseSelectedCourier] = useState(false); const [couriers, setCouriers] = useState(null); + const [slaProducts, setSlaProducts] = useState(null); const { checkWeigth, @@ -178,12 +185,13 @@ export default function SectionExpedition({ products }) { selectedService, setSelectedService, listExpedisi, + productSla, + setProductSla } = useCheckout(); let destination = {}; let items = mappingItems(products); - let notIncludeInstant = hasCanInstantFalse(items); if (addressMaps) { destination = { origin_latitude: -6.3031123, @@ -197,6 +205,32 @@ export default function SectionExpedition({ products }) { }; } + const fetchSlaProducts = async () => { + try { + const ids = products.map((p) => p.id).join(',') + const res = await odooApi('GET', `/api/v1/product/variants/sla?ids=${ids}`) + + setSlaProducts(res); + } catch (error) { + console.error('Failed to fetch expedition rates:', error); + } + }; + + useEffect(() => { + fetchSlaProducts(); + }, []); + + useEffect(() => { + if (slaProducts) { + let productSla = slaProducts?.slaDuration + if(slaProducts.slaUnit === 'jam') { + productSla = 1 + } + setProductSla(productSla); + } + console.log('ini slaProducts', slaProducts, productSla); + }, [slaProducts]); + const fetchExpedition = async () => { let body = { ...destination, @@ -230,7 +264,8 @@ export default function SectionExpedition({ products }) { useEffect(() => { if (data) { - const couriers = reverseMappingCourier(listExpedisi, data?.data?.pricing); + const instant = slaProducts?.includeInstant || false; + const couriers = reverseMappingCourier(listExpedisi, data?.data?.pricing, instant); setCouriers(couriers); } }, [data]); @@ -246,10 +281,15 @@ export default function SectionExpedition({ products }) { setSelectedCourier(courier.courier.courier_code); setServiceOptions(Object.values(courier.courier.service_type)); } else { - if ((courier.label === 'GRAB' || courier.label === 'GOJEK') && !addressMaps) { + 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.' - ) + '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.' @@ -280,7 +320,7 @@ export default function SectionExpedition({ products }) { setIsOpen(false); }; - console.log('ini selectedCourier', selectedCourier); + console.log('ini selectedCourier', couriers); return ( @@ -427,7 +467,8 @@ export default function SectionExpedition({ products }) {

{formatShipmentRange( service.shipment_range, - service.shipment_unit + service.shipment_unit, + productSla )}

diff --git a/src/lib/checkout/stores/stateCheckout.js b/src/lib/checkout/stores/stateCheckout.js index 4fefbbed..52210d7f 100644 --- a/src/lib/checkout/stores/stateCheckout.js +++ b/src/lib/checkout/stores/stateCheckout.js @@ -12,6 +12,7 @@ export const useCheckout = create((set) => ({ selectedCourierId : null, selectedService : null, listExpedisi : [], + productSla : null, setCheckWeight : (checkWeigth) => set({ checkWeigth }), setHasFlashSale : (hasFlashSale) => set({ hasFlashSale }), setCheckoutValidation : (checkoutValidation) => set({ checkoutValidation }), @@ -22,7 +23,8 @@ export const useCheckout = create((set) => ({ setSelectedCourier : (selectedCourier) => set({ selectedCourier }), setSelectedService : (selectedService) => set({ selectedService }), setSelectedCourierId : (selectedCourierId) => set({ selectedCourierId }), - setExpedisi : (listExpedisi) => set({ listExpedisi }) + setExpedisi : (listExpedisi) => set({ listExpedisi }), + setProductSla : (productSla) => set({ productSla }) })) \ No newline at end of file diff --git a/src/lib/checkout/utils/functionCheckouit.js b/src/lib/checkout/utils/functionCheckouit.js index b299f289..a6e6c337 100644 --- a/src/lib/checkout/utils/functionCheckouit.js +++ b/src/lib/checkout/utils/functionCheckouit.js @@ -1,17 +1,27 @@ +import { m } from 'framer-motion'; +import { min } from 'moment/moment'; + export function formatShipmentRange( shipmentDurationRange, - shipmentDurationUnit + shipmentDurationUnit, + productSLA ) { if (!shipmentDurationRange || !shipmentDurationUnit) { return ''; } let minRange, maxRange; + console.log('ini masuk', shipmentDurationRange); + // 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 = maxRange = Number(shipmentDurationRange); // Jika angka tunggal + minRange = Number(shipmentDurationRange); // Jika angka tunggal + maxRange = Number(shipmentDurationRange) + 3; } const start = new Date(); // Tanggal saat ini @@ -21,19 +31,22 @@ export function formatShipmentRange( // Hitung estimasi berdasarkan unit waktu if (shipmentDurationUnit === 'days' || shipmentDurationUnit === 'day') { minDate = new Date(start); - minDate.setDate(start.getDate() + (minRange + 3)); + minDate.setDate(start.getDate() + (minRange + productSLA)); maxDate = new Date(start); - maxDate.setDate(start.getDate() + (maxRange + 3)); + maxDate.setDate(start.getDate() + (maxRange + productSLA)); } else if (shipmentDurationUnit === 'hours') { - minDate = new Date(start.getTime() + (minRange + 3) * 60 * 60 * 1000); - maxDate = new Date(start.getTime() + (maxRange + 3) * 60 * 60 * 1000); + 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'."); } - console.log('min max', minDate, maxDate); - const minDateStr = formatDate(minDate); const maxDateStr = formatDate(maxDate); if (minDateStr === maxDateStr) { @@ -60,7 +73,7 @@ export function getToDate(shipmentDurationRange, shipmentDurationUnit) { let maxDate; // Hitung estimasi berdasarkan unit waktu - if (shipmentDurationUnit === 'days' || shipmentDurationUnit === 'day') { + if (shipmentDurationUnit === 'days' || shipmentDurationUnit === 'day') { maxDate = new Date(start); maxDate.setDate(start.getDate() + (maxRange + 3)); } else if (shipmentDurationUnit === 'hours') { -- cgit v1.2.3 From fdb488651e864cf30f09fe2337187241ef649177 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Sat, 1 Feb 2025 11:04:29 +0700 Subject: function check holidays --- src/lib/checkout/components/SectionExpedition.jsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx index d1844204..a00858e5 100644 --- a/src/lib/checkout/components/SectionExpedition.jsx +++ b/src/lib/checkout/components/SectionExpedition.jsx @@ -80,6 +80,14 @@ function reverseMappingCourier(couriersOdoo, couriers, includeInstant = false) { }); } + +function isHiloday(){ + const today = new Date(); + const day = today.getDay(); + + return day === 6 || day === 7 +} + function mappingCourier(couriersOdoo, couriers, notIncludeInstant = false) { const validCourierMap = couriersOdoo.reduce((acc, courier) => { acc[courier.label.toLowerCase()] = courier.carrierId; -- cgit v1.2.3 From 58601bc17b6f0516eee9c36aa36e39f1dea3ad77 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Wed, 26 Feb 2025 10:47:58 +0700 Subject: bitehsip --- src/core/api/odooApi.js | 36 +++++++++++++------ src/lib/checkout/components/Checkout.jsx | 14 +++++--- src/lib/checkout/components/SectionExpedition.jsx | 23 ++++-------- src/lib/checkout/utils/functionCheckouit.js | 4 +-- src/lib/treckingAwb/component/Manifest.jsx | 43 +++++++++++++++++++++-- 5 files changed, 85 insertions(+), 35 deletions(-) diff --git a/src/core/api/odooApi.js b/src/core/api/odooApi.js index 504d097a..2bff42e2 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/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 3ad833ec..1a7fdc03 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -165,6 +165,7 @@ const Checkout = () => { etd, unit, selectedCourier, + selectedCourierId, selectedService, listExpedisi, setExpedisi, @@ -447,6 +448,10 @@ const Checkout = () => { quantity: product.quantity, available_quantity: product?.availableQuantity, })); + 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_invoice_id: auth.partnerId, @@ -455,8 +460,9 @@ const Checkout = () => { user_id: auth.id, order_line: JSON.stringify(productOrder), delivery_amount: biayaKirim, - carrier_id: selectedCourier, - estimated_arrival_days: getToDate(etd, unit), + 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, @@ -469,6 +475,8 @@ const Checkout = () => { if (poNumber.current.value) data.po_number = poNumber.current.value; if (typeof file !== 'undefined') data.po_file = await getFileBase64(file); + console.log('ini data', data); + const isCheckouted = await checkoutApi({ data }); if (!isCheckouted?.id) { @@ -1297,7 +1305,6 @@ const Checkout = () => { className='flex-1 btn-yellow' onClick={checkout} disabled={ - isLoading || !products || products?.length == 0 || priceCheck || @@ -1603,7 +1610,6 @@ const Checkout = () => { className='w-full btn-yellow mt-4' onClick={checkout} disabled={ - isLoading || !products || products?.length == 0 || priceCheck || diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx index a00858e5..b261cae0 100644 --- a/src/lib/checkout/components/SectionExpedition.jsx +++ b/src/lib/checkout/components/SectionExpedition.jsx @@ -49,7 +49,7 @@ function reverseMappingCourier(couriersOdoo, couriers, includeInstant = false) { shipment_range: item.shipment_duration_range, shipment_unit: item.shipment_duration_unit, price: item.price, - service_type: item.service_type, + service_type: courier_service_code, description: item.description, }; @@ -81,13 +81,6 @@ function reverseMappingCourier(couriersOdoo, couriers, includeInstant = false) { } -function isHiloday(){ - const today = new Date(); - const day = today.getDay(); - - return day === 6 || day === 7 -} - function mappingCourier(couriersOdoo, couriers, notIncludeInstant = false) { const validCourierMap = couriersOdoo.reduce((acc, courier) => { acc[courier.label.toLowerCase()] = courier.carrierId; @@ -144,9 +137,6 @@ function mappingCourier(couriersOdoo, couriers, notIncludeInstant = false) { }, {}); } -function hasCanInstantFalse(items) { - return items.some((item) => item.canInstant === false); -} // interface CourierService { // courier_name: string; @@ -181,6 +171,7 @@ export default function SectionExpedition({ products }) { const [onFocusSelectedCourier, setOnFocuseSelectedCourier] = useState(false); const [couriers, setCouriers] = useState(null); const [slaProducts, setSlaProducts] = useState(null); + const [addHolidays, setAddHolidays] = useState(0); const { checkWeigth, @@ -194,7 +185,8 @@ export default function SectionExpedition({ products }) { setSelectedService, listExpedisi, productSla, - setProductSla + setProductSla, + setSelectedCourierId } = useCheckout(); let destination = {}; @@ -217,7 +209,6 @@ export default function SectionExpedition({ products }) { try { const ids = products.map((p) => p.id).join(',') const res = await odooApi('GET', `/api/v1/product/variants/sla?ids=${ids}`) - setSlaProducts(res); } catch (error) { console.error('Failed to fetch expedition rates:', error); @@ -230,7 +221,7 @@ export default function SectionExpedition({ products }) { useEffect(() => { if (slaProducts) { - let productSla = slaProducts?.slaDuration + let productSla = slaProducts?.slaTotal if(slaProducts.slaUnit === 'jam') { productSla = 1 } @@ -282,11 +273,13 @@ export default function SectionExpedition({ products }) { setIsOpen(false); setOnFocuseSelectedCourier(false); const courier = code; + console.log('ini courier', courier, couriers); 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 ( @@ -328,8 +321,6 @@ export default function SectionExpedition({ products }) { setIsOpen(false); }; - console.log('ini selectedCourier', couriers); - return (
diff --git a/src/lib/checkout/utils/functionCheckouit.js b/src/lib/checkout/utils/functionCheckouit.js index a6e6c337..a95e6fb4 100644 --- a/src/lib/checkout/utils/functionCheckouit.js +++ b/src/lib/checkout/utils/functionCheckouit.js @@ -21,7 +21,7 @@ export function formatShipmentRange( // } } else { minRange = Number(shipmentDurationRange); // Jika angka tunggal - maxRange = Number(shipmentDurationRange) + 3; + maxRange = Number(shipmentDurationRange); } const start = new Date(); // Tanggal saat ini @@ -82,7 +82,7 @@ export function getToDate(shipmentDurationRange, shipmentDurationUnit) { throw new Error("Unsupported unit. Please use 'days' or 'hours'."); } - return formatDate(maxDate); + return maxDate.getDate(); } function formatDate(date) { diff --git a/src/lib/treckingAwb/component/Manifest.jsx b/src/lib/treckingAwb/component/Manifest.jsx index 87e01e38..a0df6ee9 100644 --- a/src/lib/treckingAwb/component/Manifest.jsx +++ b/src/lib/treckingAwb/component/Manifest.jsx @@ -7,6 +7,18 @@ import { toast } from 'react-hot-toast'; import ImageNext from 'next/image'; import { list } from 'postcss'; + +function capitalizeFirstLetter(str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + + +function capitalizeWords(str) { + return str.split(' ').map(word => capitalizeFirstLetter(word)).join(' '); +} + + + const Manifest = ({ idAWB, closePopup }) => { const [manifests, setManifests] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -101,6 +113,16 @@ const Manifest = ({ idAWB, closePopup }) => {

Sedang Dikirim

)} + {manifests?.status === 'cancelled' && ( +
+

Di Batalkan

+
+ )} + {manifests?.status === 'on_hold' && ( +
+

Ditunda Sementara

+
+ )} {manifests?.status === 'pending' && (

Pending

@@ -112,12 +134,18 @@ const Manifest = ({ idAWB, closePopup }) => { Estimasi tiba pada{' '} ({manifests?.eta}) -

+

Dikirim Menggunakan{' '} {manifests?.deliveryOrder.carrier}

+

+ Tipe Service {' '} + + {manifests?.deliveryOrder.service} + +

{manifests?.waybillNumber && (

No. Resi

@@ -184,11 +212,20 @@ const Manifest = ({ idAWB, closePopup }) => { - {manifests.delivered == true && index == 0 && ( + {manifests.delivered == true && + index == 0 && + manifests.isBiteship == false && ( +

+ Sudah Sampai +

+ )} + {manifests.isBiteship == true && (

- Sudah Sampai + {capitalizeWords(manifest.status)}

)}

-- cgit v1.2.3 From 00ba9833becbea5b25c6aafcb95b327d8adef4e3 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Mon, 3 Mar 2025 14:41:44 +0700 Subject: delete comment --- src/lib/checkout/components/Checkout.jsx | 65 -------------------------------- 1 file changed, 65 deletions(-) diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 1a7fdc03..d5c370bf 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -332,71 +332,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); -- cgit v1.2.3 From f88f457fd1b91298ea8a7f9f396e49660a81e276 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Mon, 17 Mar 2025 09:06:47 +0700 Subject: handle bug service type kurir --- src/lib/checkout/api/checkoutApi.js | 2 - src/lib/checkout/components/Checkout.jsx | 8 ++-- src/lib/checkout/components/SectionExpedition.jsx | 47 ++++++++++++++++------- src/lib/checkout/utils/functionCheckouit.js | 2 +- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/lib/checkout/api/checkoutApi.js b/src/lib/checkout/api/checkoutApi.js index 9d326b10..c30d9631 100644 --- a/src/lib/checkout/api/checkoutApi.js +++ b/src/lib/checkout/api/checkoutApi.js @@ -25,7 +25,5 @@ export const getProductsSla = async ({data}) => { `/api/v1/product/variants/sla`, data ) - - console.log('ini sla - data', dataSLA); return dataSLA } diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index d5c370bf..fa8d8aea 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -71,7 +71,10 @@ const Checkout = () => { source: query, voucher: activeVoucher, voucher_shipping: activeVoucherShipping, - }) + }), + { + keepPreviousData: true, // Menjaga data sebelumnya sampai data baru tersedia + } ); const { @@ -205,6 +208,7 @@ const Checkout = () => { SetFindVoucher(1); return; } + dataVoucher.forEach((addNewLine) => { if (addNewLine.applyType !== 'shipping') { @@ -410,8 +414,6 @@ const Checkout = () => { if (poNumber.current.value) data.po_number = poNumber.current.value; if (typeof file !== 'undefined') data.po_file = await getFileBase64(file); - console.log('ini data', data); - const isCheckouted = await checkoutApi({ data }); if (!isCheckouted?.id) { diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx index b261cae0..22d8df32 100644 --- a/src/lib/checkout/components/SectionExpedition.jsx +++ b/src/lib/checkout/components/SectionExpedition.jsx @@ -31,7 +31,10 @@ function reverseMappingCourier(couriersOdoo, couriers, includeInstant = false) { const { courier_name, courier_code, courier_service_code } = item; const key = courier_name.toLowerCase(); - if (!includeInstant && ['hours'].includes(item.shipment_duration_unit.toLowerCase())) { + if ( + !includeInstant && + ['hours'].includes(item.shipment_duration_unit.toLowerCase()) + ) { return acc; } @@ -80,7 +83,6 @@ function reverseMappingCourier(couriersOdoo, couriers, includeInstant = false) { }); } - function mappingCourier(couriersOdoo, couriers, notIncludeInstant = false) { const validCourierMap = couriersOdoo.reduce((acc, courier) => { acc[courier.label.toLowerCase()] = courier.carrierId; @@ -137,7 +139,6 @@ function mappingCourier(couriersOdoo, couriers, notIncludeInstant = false) { }, {}); } - // interface CourierService { // courier_name: string; // courier_code: string; @@ -172,6 +173,7 @@ export default function SectionExpedition({ products }) { const [couriers, setCouriers] = useState(null); const [slaProducts, setSlaProducts] = useState(null); const [addHolidays, setAddHolidays] = useState(0); + const [savedServiceOptions, setSavedServiceOptions] = useState([]); const { checkWeigth, @@ -186,7 +188,7 @@ export default function SectionExpedition({ products }) { listExpedisi, productSla, setProductSla, - setSelectedCourierId + setSelectedCourierId, } = useCheckout(); let destination = {}; @@ -207,8 +209,15 @@ export default function SectionExpedition({ products }) { const fetchSlaProducts = async () => { try { - const ids = products.map((p) => p.id).join(',') - const res = await odooApi('GET', `/api/v1/product/variants/sla?ids=${ids}`) + 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); @@ -221,13 +230,12 @@ export default function SectionExpedition({ products }) { useEffect(() => { if (slaProducts) { - let productSla = slaProducts?.slaTotal - if(slaProducts.slaUnit === 'jam') { - productSla = 1 + let productSla = slaProducts?.slaTotal; + if (slaProducts.slaUnit === 'jam') { + productSla = 1; } setProductSla(productSla); } - console.log('ini slaProducts', slaProducts, productSla); }, [slaProducts]); const fetchExpedition = async () => { @@ -264,7 +272,11 @@ export default function SectionExpedition({ products }) { useEffect(() => { if (data) { const instant = slaProducts?.includeInstant || false; - const couriers = reverseMappingCourier(listExpedisi, data?.data?.pricing, instant); + const couriers = reverseMappingCourier( + listExpedisi, + data?.data?.pricing, + instant + ); setCouriers(couriers); } }, [data]); @@ -273,13 +285,12 @@ export default function SectionExpedition({ products }) { setIsOpen(false); setOnFocuseSelectedCourier(false); const courier = code; - console.log('ini courier', courier, couriers); setSelectedService(null); setBiayaKirim(0); if (courier !== 0 && courier !== 32) { if (courier.courier) { setSelectedCourier(courier.courier.courier_code); - setSelectedCourierId(courier.carrierId) + setSelectedCourierId(courier.carrierId); setServiceOptions(Object.values(courier.courier.service_type)); } else { if ( @@ -296,6 +307,7 @@ export default function SectionExpedition({ products }) { 'Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.' ); } + setServiceOptions([]); } } else { setSelectedCourier(courier === 32 ? 'SELF PICKUP' : null); @@ -321,6 +333,12 @@ export default function SectionExpedition({ products }) { setIsOpen(false); }; + useEffect(() => { + if (serviceOptions.length > 0) { + setSavedServiceOptions(serviceOptions); + } +}, [serviceOptions]); + return (

@@ -420,7 +438,8 @@ export default function SectionExpedition({ products }) { )}
- {serviceOptions.length > 0 && + {(serviceOptions.length > 0 || + selectedService )&& selectedCourier && selectedCourier !== 32 && selectedCourier !== 0 && ( diff --git a/src/lib/checkout/utils/functionCheckouit.js b/src/lib/checkout/utils/functionCheckouit.js index a95e6fb4..a7fa8c5a 100644 --- a/src/lib/checkout/utils/functionCheckouit.js +++ b/src/lib/checkout/utils/functionCheckouit.js @@ -11,7 +11,7 @@ export function formatShipmentRange( } let minRange, maxRange; - console.log('ini masuk', shipmentDurationRange); + console.log('ini masuk format shipment range', shipmentDurationRange, shipmentDurationUnit, productSLA); // Cek apakah durasi berupa range atau angka tunggal if (shipmentDurationRange.includes('-')) { -- cgit v1.2.3 From e25b9c18c192277863727ce3707e51e4aabee3d3 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Fri, 11 Apr 2025 14:51:37 +0700 Subject: manifest --- src/lib/treckingAwb/component/Manifest.jsx | 180 ++++++++++++++++------------- 1 file changed, 98 insertions(+), 82 deletions(-) diff --git a/src/lib/treckingAwb/component/Manifest.jsx b/src/lib/treckingAwb/component/Manifest.jsx index a0df6ee9..fa998bd3 100644 --- a/src/lib/treckingAwb/component/Manifest.jsx +++ b/src/lib/treckingAwb/component/Manifest.jsx @@ -7,18 +7,17 @@ import { toast } from 'react-hot-toast'; import ImageNext from 'next/image'; import { list } from 'postcss'; - function capitalizeFirstLetter(str) { return str.charAt(0).toUpperCase() + str.slice(1); } - function capitalizeWords(str) { - return str.split(' ').map(word => capitalizeFirstLetter(word)).join(' '); + return str + .split(' ') + .map((word) => capitalizeFirstLetter(word)) + .join(' '); } - - const Manifest = ({ idAWB, closePopup }) => { const [manifests, setManifests] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -63,7 +62,7 @@ const Manifest = ({ idAWB, closePopup }) => { setManifests(list); setIsLoading(false); }; - + console.log(manifests); useEffect(() => { if (idAWB) { getManifest(); @@ -97,7 +96,7 @@ const Manifest = ({ idAWB, closePopup }) => { {!isLoading && ( @@ -118,7 +117,7 @@ const Manifest = ({ idAWB, closePopup }) => {

Di Batalkan

)} - {manifests?.status === 'on_hold' && ( + {manifests?.status === 'on_hold' && (

Ditunda Sementara

@@ -129,28 +128,15 @@ const Manifest = ({ idAWB, closePopup }) => {
)}
+
-

- Estimasi tiba pada{' '} - ({manifests?.eta}) -

-

- Dikirim Menggunakan{' '} - - {manifests?.deliveryOrder.carrier} - -

-

- Tipe Service {' '} - - {manifests?.deliveryOrder.service} - -

- {manifests?.waybillNumber && ( -
-

No. Resi

-
-

{manifests?.waybillNumber}

+
+
+ Nomor Resi +
+ + : {manifests?.waybillNumber}{' '} +
- )} +
+ Kurir + + {' '} + : {manifests?.deliveryOrder.carrier} + +
+
+ Jenis Service + + {' '} + : {manifests?.deliveryOrder.service} + +
+
+ Tanggal Dikirim + : {manifests?.deliveredDate} +
+
+ Estimasi Tiba + + :{' '} + + {manifests?.eta} + + +
+

+ {manifests?.isDelay && ( + + )}
-
    - {manifests?.manifests?.map((manifest, index) => ( - <> -
  1. - {manifests.delivered == true && index == 0 ? ( -
    - -
    - ) : ( -
    - )} - {manifests.delivered != true && ( +
      + {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) + : ''; + + return ( +
    1. + {/* Kolom 1: Tanggal + Jam */} +
      + {formatCustomDate(manifest.datetime)} +
      + + {/* Kolom 2: Bullet/Poin */} +
      - )} +
      - - {manifests.delivered == true && - index == 0 && - manifests.isBiteship == false && ( -

      - Sudah Sampai -

      - )} - {manifests.isBiteship == true && ( -

      - {capitalizeWords(manifest.status)} + {/* Kolom 3: Status dan Deskripsi */} +

      +

      + {manifests?.deliveryOrder.carrier != 'Self Pick Up' ? capitalizeWords(manifest.status) : ''}

      - )} -

      - {manifest.description} -

      +

      +

    2. - - ))} + ); + })}
    -- cgit v1.2.3 From 900372920d521ee940b141646381d363d487e4d0 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Mon, 14 Apr 2025 16:47:04 +0700 Subject: transaction --- src/lib/transaction/components/Transaction.jsx | 89 +++++++++++++--------- .../treckingAwb/component/InformationSection.jsx | 73 ++++++++++++++++++ src/lib/treckingAwb/component/Manifest.jsx | 71 +---------------- 3 files changed, 127 insertions(+), 106 deletions(-) create mode 100644 src/lib/treckingAwb/component/InformationSection.jsx diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx index 2ca7d386..62743df3 100644 --- a/src/lib/transaction/components/Transaction.jsx +++ b/src/lib/transaction/components/Transaction.jsx @@ -41,6 +41,7 @@ import { useRouter } from 'next/router'; import { gtagPurchase } from '@/core/utils/googleTag'; import { deleteItemCart } from '@/core/utils/cart'; import axios from 'axios'; +import InformationSection from '../../treckingAwb/component/InformationSection'; const Transaction = ({ id }) => { const PPN = process.env.NEXT_PUBLIC_PPN; const router = useRouter(); @@ -208,7 +209,9 @@ const Transaction = ({ id }) => {

    {currencyFormat(transaction.data?.amountUntaxed)}

-

PPN {((PPN - 1) * 100).toFixed(0)}%

+

+ PPN {((PPN - 1) * 100).toFixed(0)}% +

{currencyFormat(transaction.data?.amountTax)}

@@ -279,6 +282,8 @@ const Transaction = ({ id }) => { } }; + console.log('ini transaction', transaction.data); + return ( transaction.data?.name && ( <> @@ -731,52 +736,58 @@ const Transaction = ({ id }) => {
-
- Informasi Pelanggan -
-
-
-
Detail Pelanggan
- +
+
+
+ Informasi Pelanggan +
+
+
Detail Pelanggan
+ +
-
-
- Pengiriman + Informasi Pengiriman
{transaction?.data?.pickings.length == 0 && (
Belum ada pengiriman
)} -
- {transaction?.data?.pickings?.map((airway) => ( - - ))} -
+ {/*
*/} + {transaction?.data?.pickings?.map((airway) => ( +
+ +
+ // + ))} + {/*
*/}
+
+ +
Invoice @@ -976,7 +987,9 @@ const Transaction = ({ id }) => { {currencyFormat(transaction.data?.amountUntaxed)}
-
PPN {((PPN - 1) * 100).toFixed(0)}%
+
+ PPN {((PPN - 1) * 100).toFixed(0)}% +
{currencyFormat(transaction.data?.amountTax)}
diff --git a/src/lib/treckingAwb/component/InformationSection.jsx b/src/lib/treckingAwb/component/InformationSection.jsx new file mode 100644 index 00000000..41386683 --- /dev/null +++ b/src/lib/treckingAwb/component/InformationSection.jsx @@ -0,0 +1,73 @@ +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 ( +
+
+
+ Nomor Resi +
+ : {manifests?.waybillNumber} + +
+
+
+ Kurir + + {' '} + : {manifests?.deliveryOrder.carrier} + +
+
+ Jenis Service + + {' '} + : {manifests?.deliveryOrder.service} + +
+
+ Tanggal Dikirim + : {manifests?.deliveredDate} +
+
+ Estimasi Tiba + + :{' '} + {manifests?.eta} + +
+
+
+ ); +}; + +export default InformationSection; diff --git a/src/lib/treckingAwb/component/Manifest.jsx b/src/lib/treckingAwb/component/Manifest.jsx index fa998bd3..e2251e3e 100644 --- a/src/lib/treckingAwb/component/Manifest.jsx +++ b/src/lib/treckingAwb/component/Manifest.jsx @@ -6,6 +6,7 @@ import { useEffect, useState } from 'react'; import { toast } from 'react-hot-toast'; import ImageNext from 'next/image'; import { list } from 'postcss'; +import InformationSection from './InformationSection'; function capitalizeFirstLetter(str) { return str.charAt(0).toUpperCase() + str.slice(1); @@ -71,16 +72,6 @@ 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 - }; - return ( <> {isLoading && ( @@ -129,64 +120,8 @@ const Manifest = ({ idAWB, closePopup }) => { )}
-
-
-
- Nomor Resi -
- - : {manifests?.waybillNumber}{' '} - - -
-
-
- Kurir - - {' '} - : {manifests?.deliveryOrder.carrier} - -
-
- Jenis Service - - {' '} - : {manifests?.deliveryOrder.service} - -
-
- Tanggal Dikirim - : {manifests?.deliveredDate} -
-
- Estimasi Tiba - - :{' '} - - {manifests?.eta} - - -
-
-
+ +
{manifests?.isDelay && (
Date: Wed, 16 Apr 2025 15:03:12 +0700 Subject: fixing --- src/lib/address/components/CreateAddress.jsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/lib/address/components/CreateAddress.jsx b/src/lib/address/components/CreateAddress.jsx index 8c51dd89..cb5b364f 100644 --- a/src/lib/address/components/CreateAddress.jsx +++ b/src/lib/address/components/CreateAddress.jsx @@ -47,7 +47,6 @@ const CreateAddress = () => { detailAddress, setAddressMaps, } = useMaps(); - useEffect(() => { if (detailAddress) { setValue('zip', detailAddress.postalCode); @@ -92,15 +91,15 @@ const CreateAddress = () => { }, [watchState, setValue]); useEffect(() => { - if (detailAddress) { + if (detailAddress && Object.keys(detailAddress).length > 0) { const selectedCities = cities.find( (city) => - detailAddress.district + detailAddress?.district .toLowerCase() .includes(city.label.toLowerCase()) || city.label .toLowerCase() - .includes(detailAddress.district.toLowerCase()) + .includes(detailAddress?.district.toLowerCase()) ); setValue('city', selectedCities?.value); } @@ -141,7 +140,7 @@ const CreateAddress = () => { }, [watchCity, setValue]); useEffect(() => { - if (detailAddress) { + if (detailAddress && Object.keys(detailAddress).length > 0) { const selectedDistrict = districts.find( (district) => detailAddress.subDistrict @@ -174,7 +173,7 @@ const CreateAddress = () => { }, [watchDistrict, setValue]); useEffect(() => { - if (detailAddress) { + if (detailAddress && Object.keys(detailAddress).length > 0) { const selectedSubDistrict = subDistricts.find( (district) => detailAddress.village -- cgit v1.2.3 From 0aa0d458e668520ef96ccf7ecb35bf84a585b279 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Mon, 21 Apr 2025 13:05:57 +0700 Subject: fedback renca --- src/lib/address/components/CreateAddress.jsx | 3 ++- src/lib/maps/components/PinPointMap.jsx | 3 ++- src/lib/maps/stores/useMaps.js | 2 ++ src/lib/transaction/components/Transaction.jsx | 16 ++++++++++++++-- src/lib/treckingAwb/component/InformationSection.jsx | 2 +- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/lib/address/components/CreateAddress.jsx b/src/lib/address/components/CreateAddress.jsx index cb5b364f..fbb01fcc 100644 --- a/src/lib/address/components/CreateAddress.jsx +++ b/src/lib/address/components/CreateAddress.jsx @@ -40,12 +40,13 @@ const CreateAddress = () => { const [districts, setDistricts] = useState([]); const [subDistricts, setSubDistricts] = useState([]); const [filteredTypes, setFilteredTypes] = useState(types); // State to manage filtered types - const [pinedMaps, setPinedMaps] = useState(false); const { addressMaps, selectedPosition, detailAddress, setAddressMaps, + pinedMaps, + setPinedMaps } = useMaps(); useEffect(() => { if (detailAddress) { diff --git a/src/lib/maps/components/PinPointMap.jsx b/src/lib/maps/components/PinPointMap.jsx index acff5d67..acd5ab92 100644 --- a/src/lib/maps/components/PinPointMap.jsx +++ b/src/lib/maps/components/PinPointMap.jsx @@ -26,7 +26,7 @@ const PinpointLocation = () => { libraries: ['places'], }); - const { addressMaps, setAddressMaps, selectedPosition, setSelectedPosition, setDetailAddress } = + const { addressMaps, setAddressMaps, selectedPosition, setSelectedPosition, setDetailAddress, setPinedMaps } = useMaps(); const [tempAddress, setTempAddress] = useState(''); @@ -119,6 +119,7 @@ const PinpointLocation = () => { getAddress(tempPosition.lat, tempPosition.lng); setSelectedPosition(tempPosition); setAddressMaps(tempAddress); + setPinedMaps(false) }; console.log('set selected position',selectedPosition); diff --git a/src/lib/maps/stores/useMaps.js b/src/lib/maps/stores/useMaps.js index 83f476bc..4daf7f62 100644 --- a/src/lib/maps/stores/useMaps.js +++ b/src/lib/maps/stores/useMaps.js @@ -9,7 +9,9 @@ export const useMaps = create((set) => ({ selectedPosition: center, addressMaps: '', detailAddress: {}, + pinedMaps : false, setSelectedPosition: (position) => set({ selectedPosition: position }), setAddressMaps: (addressMaps) => set({ addressMaps }), setDetailAddress: (detailAddress) => set({ detailAddress }), + setPinedMaps: (pinedMaps) => set({pinedMaps}) })); \ No newline at end of file diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx index 62743df3..3853ea79 100644 --- a/src/lib/transaction/components/Transaction.jsx +++ b/src/lib/transaction/components/Transaction.jsx @@ -42,6 +42,7 @@ import { gtagPurchase } from '@/core/utils/googleTag'; import { deleteItemCart } from '@/core/utils/cart'; 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(); @@ -759,8 +760,19 @@ const Transaction = ({ id }) => { )} {/*
*/} {transaction?.data?.pickings?.map((airway) => ( -
- +
+ +
+ +
//
Persiapan Barang
-
{sla?.sla_date}
+ {isLoading && ( +
+ +
+ )} + {!isLoading &&
{sla?.sla_date}
}
); diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index 4667e086..1581f33d 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -42,6 +42,7 @@ const ProductDetail = ({ product }: Props) => { setIsApproval, isApproval, setSelectedVariant, + setSla, } = useProductDetail(); useEffect(() => { diff --git a/src-migrate/modules/product-detail/hook/useVariant.ts b/src-migrate/modules/product-detail/hook/useVariant.ts new file mode 100644 index 00000000..18451f7e --- /dev/null +++ b/src-migrate/modules/product-detail/hook/useVariant.ts @@ -0,0 +1,18 @@ +import { useQuery } from "react-query" +import { number } from "zod" +import { getVariantById, getVariantSLA } from "~/services/productVariant" + +interface Props { + variantId : number +} +const useVariant = ({variantId}:Props) => { + const fetchVariant = async () => await getVariantSLA(variantId ) + const {data, isLoading, refetch} = useQuery(variantId ? `variant-${variantId}` : '', fetchVariant, + { + enabled: !!variantId, + }) + + return {slaVariant: data, isLoading, refetch} +} + +export default useVariant \ No newline at end of file diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx index 22d8df32..b017c82e 100644 --- a/src/lib/checkout/components/SectionExpedition.jsx +++ b/src/lib/checkout/components/SectionExpedition.jsx @@ -29,7 +29,7 @@ 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_name.toLowerCase(); + const key = courier_code.toLowerCase(); if ( !includeInstant && @@ -165,14 +165,11 @@ function mappingCourier(couriersOdoo, couriers, notIncludeInstant = false) { export default function SectionExpedition({ products }) { const { addressMaps, coordinate, postalCode } = useAddress(); - const { control, handleSubmit } = useForm(); const [serviceOptions, setServiceOptions] = useState([]); const [isOpen, setIsOpen] = useState(false); - const [selectedE, setIsOpenCourier] = useState(false); const [onFocusSelectedCourier, setOnFocuseSelectedCourier] = useState(false); const [couriers, setCouriers] = useState(null); const [slaProducts, setSlaProducts] = useState(null); - const [addHolidays, setAddHolidays] = useState(0); const [savedServiceOptions, setSavedServiceOptions] = useState([]); const { @@ -270,8 +267,8 @@ export default function SectionExpedition({ products }) { ); useEffect(() => { + const instant = slaProducts?.includeInstant || false; if (data) { - const instant = slaProducts?.includeInstant || false; const couriers = reverseMappingCourier( listExpedisi, data?.data?.pricing, @@ -279,7 +276,7 @@ export default function SectionExpedition({ products }) { ); setCouriers(couriers); } - }, [data]); + }, [data, slaProducts]); const onCourierChange = (code) => { setIsOpen(false); @@ -315,11 +312,6 @@ export default function SectionExpedition({ products }) { } }; - const onSubmit = (data) => { - 1; - console.log(data); - }; - const handleOnFocuse = (value) => { setOnFocuseSelectedCourier(!value); setIsOpen(false); @@ -340,7 +332,7 @@ export default function SectionExpedition({ products }) { }, [serviceOptions]); return ( - +
Pilih Ekspedisi:
diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx index 3853ea79..867de577 100644 --- a/src/lib/transaction/components/Transaction.jsx +++ b/src/lib/transaction/components/Transaction.jsx @@ -283,8 +283,6 @@ const Transaction = ({ id }) => { } }; - console.log('ini transaction', transaction.data); - return ( transaction.data?.name && ( <> @@ -764,13 +762,19 @@ const Transaction = ({ id }) => { key={airway?.id} className='border border-gray_r-6 rounded p-3' > - +
-
diff --git a/src/lib/treckingAwb/component/InformationSection.jsx b/src/lib/treckingAwb/component/InformationSection.jsx index 87fb05f4..bb37d4ae 100644 --- a/src/lib/treckingAwb/component/InformationSection.jsx +++ b/src/lib/treckingAwb/component/InformationSection.jsx @@ -1,7 +1,7 @@ import { useState } from "react"; import toast from "react-hot-toast"; -const InformationSection = ({ manifests, source = "tracking" }) => { +const InformationSection = ({ manifests}) => { const [copied, setCopied] = useState(false); const handleCopyClick = () => { @@ -44,14 +44,14 @@ const InformationSection = ({ manifests, source = "tracking" }) => { Kurir {' '} - : {manifests?.deliveryOrder.carrier} + : {manifests?.deliveryOrder?.carrier}
Jenis Service {' '} - : {manifests?.deliveryOrder.service} + : {manifests?.deliveryOrder?.service}
-- cgit v1.2.3 From 5077cf5ac59e15529de1abab43b4a49a4722bd2d Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Mon, 28 Apr 2025 08:46:44 +0700 Subject: handlig cache sla pengiriman --- src-migrate/modules/product-detail/components/Information.tsx | 6 +++++- src/lib/checkout/components/SectionExpedition.jsx | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src-migrate/modules/product-detail/components/Information.tsx b/src-migrate/modules/product-detail/components/Information.tsx index d1f1e852..a7a58cbc 100644 --- a/src-migrate/modules/product-detail/components/Information.tsx +++ b/src-migrate/modules/product-detail/components/Information.tsx @@ -66,10 +66,14 @@ const Information = ({ product }: Props) => { }, [selectedVariant]); useEffect(() => { + if (isLoading){ + setSla(null); + } if (slaVariant) { setSla(slaVariant); } - }, [slaVariant]); + }, [slaVariant, isLoading]); + const handleOnChange = (vals: any) => { setDisableFilter(true); diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx index b017c82e..40084048 100644 --- a/src/lib/checkout/components/SectionExpedition.jsx +++ b/src/lib/checkout/components/SectionExpedition.jsx @@ -32,8 +32,8 @@ function reverseMappingCourier(couriersOdoo, couriers, includeInstant = false) { const key = courier_code.toLowerCase(); if ( - !includeInstant && - ['hours'].includes(item.shipment_duration_unit.toLowerCase()) + !includeInstant && (['hours'].includes(item.shipment_duration_unit.toLowerCase()) || item.service_type == 'same_day') + ) { return acc; } -- cgit v1.2.3 From ca05a70e98e9066882de6394ffbd89db7af2cb9d Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Tue, 29 Apr 2025 09:14:44 +0700 Subject: add button live tracking --- src/lib/treckingAwb/component/Manifest.jsx | 44 +++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/lib/treckingAwb/component/Manifest.jsx b/src/lib/treckingAwb/component/Manifest.jsx index e2251e3e..acb86f57 100644 --- a/src/lib/treckingAwb/component/Manifest.jsx +++ b/src/lib/treckingAwb/component/Manifest.jsx @@ -7,6 +7,7 @@ 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); @@ -19,6 +20,19 @@ function capitalizeWords(str) { .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); const [isLoading, setIsLoading] = useState(false); @@ -63,7 +77,7 @@ const Manifest = ({ idAWB, closePopup }) => { setManifests(list); setIsLoading(false); }; - console.log(manifests); + useEffect(() => { if (idAWB) { getManifest(); @@ -72,6 +86,8 @@ const Manifest = ({ idAWB, closePopup }) => { } }, [idAWB]); + const isLiveTracking = mappingLiveTracking(manifests?.deliveryOrder?.carrier.toLowerCase(), manifests?.waybillNumber) + return ( <> {isLoading && ( @@ -121,7 +137,7 @@ const Manifest = ({ idAWB, closePopup }) => {
- +
{manifests?.isDelay && (
{ : ''; return ( -
  • +
  • {/* Kolom 1: Tanggal + Jam */}
    {formatCustomDate(manifest.datetime)} @@ -173,7 +192,9 @@ const Manifest = ({ idAWB, closePopup }) => { {/* Kolom 3: Status dan Deskripsi */}

    - {manifests?.deliveryOrder.carrier != 'Self Pick Up' ? capitalizeWords(manifest.status) : ''} + {manifests?.deliveryOrder.carrier != 'Self Pick Up' + ? capitalizeWords(manifest.status) + : ''}

    { })}

    +
    + { + isLiveTracking && + manifests?.status === 'shipment' && ( + + Live Tracking + + ) + } +
    )} -- cgit v1.2.3 From 7d4445bb9bad3d6c945503086a07bd882536e5f6 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 19 May 2025 11:02:19 +0700 Subject: fix unresponsive cart select --- src-migrate/modules/cart/components/ItemSelect.tsx | 170 ++++++--- src-migrate/modules/cart/components/Summary.tsx | 219 ++++++++++-- src-migrate/modules/cart/stores/useCartStore.ts | 135 ++++++- src-migrate/pages/shop/cart/index.tsx | 395 +++++++++++++++------ src-migrate/utils/cart.js | 290 +++++++++++++++ src/lib/checkout/components/Checkout.jsx | 5 +- 6 files changed, 1019 insertions(+), 195 deletions(-) create mode 100644 src-migrate/utils/cart.js diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx index d4a1b537..733ee64d 100644 --- a/src-migrate/modules/cart/components/ItemSelect.tsx +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -1,56 +1,138 @@ -import { Checkbox, Spinner } from '@chakra-ui/react' -import React, { useState } from 'react' - -import { getAuth } from '~/libs/auth' -import { CartItem } from '~/types/cart' -import { upsertUserCart } from '~/services/cart' - -import { useCartStore } from '../stores/useCartStore' +import { Checkbox } from '@chakra-ui/react'; +import React, { useState, useCallback, useEffect } from 'react'; +import { getAuth } from '~/libs/auth'; +import { CartItem } from '~/types/cart'; +import { upsertUserCart } from '~/services/cart'; +import { useCartStore } from '../stores/useCartStore'; +import { toast } from 'react-hot-toast'; +import { + getSelectedItemsFromCookie, + updateSelectedItemInCookie, +} from '~/utils/cart'; type Props = { - item: CartItem -} + item: CartItem; +}; const CartItemSelect = ({ item }: Props) => { - const auth = getAuth() - const { updateCartItem, cart } = useCartStore() + const auth = getAuth(); + const { updateCartItem, cart, loadCart } = useCartStore(); + const [isUpdating, setIsUpdating] = useState(false); + const [localSelected, setLocalSelected] = useState(item.selected); + + // Initialize local state from cookie or server + useEffect(() => { + if (isUpdating) return; // Skip if we're currently updating + + // Check cookie first + const selectedItems = getSelectedItemsFromCookie(); + const storedState = selectedItems[item.id]; + + if (storedState !== undefined) { + // Only update local state if it differs from current state + if (localSelected !== storedState) { + setLocalSelected(storedState); + } + + // If cookie state differs from server state and we're not in the middle of an update, + // synchronize the item state with cookie + if (storedState !== item.selected) { + // Update cart item silently to match cookie + if (cart) { + const updatedCartItems = cart.products.map((cartItem) => + cartItem.id === item.id + ? { ...cartItem, selected: storedState } + : cartItem + ); + + const updatedCart = { ...cart, products: updatedCartItems }; + updateCartItem(updatedCart); + } + } + } else { + // Fall back to server state if no cookie exists + setLocalSelected(item.selected); + + // Save this state to cookie for future use + updateSelectedItemInCookie(item.id, item.selected); + } + }, [item.id, item.selected, localSelected, cart, updateCartItem, isUpdating]); + + const handleChange = useCallback( + async (e: React.ChangeEvent) => { + if (typeof auth !== 'object' || !cart || isUpdating) { + return; + } + + const newSelectedState = e.target.checked; + + // Update local state immediately for responsiveness + setLocalSelected(newSelectedState); + setIsUpdating(true); + + try { + // Update cookie immediately + updateSelectedItemInCookie(item.id, newSelectedState); + + // Update cart state immediately for UI responsiveness + const updatedCartItems = cart.products.map((cartItem) => + cartItem.id === item.id + ? { ...cartItem, selected: newSelectedState } + : cartItem + ); + + const updatedCart = { ...cart, products: updatedCartItems }; + updateCartItem(updatedCart); + + // Save to server + await upsertUserCart({ + userId: auth.id, + type: item.cart_type, + id: item.id, + qty: item.quantity, + selected: newSelectedState, + }); - const [isLoad, setIsLoad] = useState(false) + // Reload cart to ensure consistency + await loadCart(auth.id); + } catch (error) { + console.error('Failed to update item selection:', error); + toast.error('Gagal memperbarui pilihan barang'); - const handleChange = async (e: React.ChangeEvent) => { - if (typeof auth !== 'object' || !cart) return - - setIsLoad(true); - const updatedCartItems = cart.products.map(cartItem => - cartItem.id === item.id - ? { ...cartItem, selected: e.target.checked } - : cartItem - ); + // Revert local state on error + setLocalSelected(!newSelectedState); - // Update the entire cart - const updatedCart = { ...cart, products: updatedCartItems }; - updateCartItem(updatedCart); + // Update cookie back + updateSelectedItemInCookie(item.id, !newSelectedState); - setIsLoad(false); - } + // Reload cart to get server state + loadCart(auth.id); + } finally { + setIsUpdating(false); + } + }, + [auth, cart, item, isUpdating, updateCartItem, loadCart] + ); return ( -
    - {isLoad && ( - - )} - - {!isLoad && ( - - )} +
    +
    - ) -} + ); +}; -export default CartItemSelect \ No newline at end of file +export default CartItemSelect; diff --git a/src-migrate/modules/cart/components/Summary.tsx b/src-migrate/modules/cart/components/Summary.tsx index 0af5ab18..b4fbab6b 100644 --- a/src-migrate/modules/cart/components/Summary.tsx +++ b/src-migrate/modules/cart/components/Summary.tsx @@ -1,20 +1,19 @@ -import style from '../styles/summary.module.css' - -import React from 'react' -import formatCurrency from '~/libs/formatCurrency' -import clsxm from '~/libs/clsxm' -import { Skeleton } from '@chakra-ui/react' -import _ from 'lodash' +import style from '../styles/summary.module.css'; +import React, { useEffect, useState, useMemo } from 'react'; +import formatCurrency from '~/libs/formatCurrency'; +import clsxm from '~/libs/clsxm'; +import { Skeleton, Box, useColorModeValue, Text } from '@chakra-ui/react'; type Props = { - total?: number - discount?: number - subtotal?: number - tax?: number - shipping?: number - grandTotal?: number - isLoaded: boolean -} + total?: number; + discount?: number; + subtotal?: number; + tax?: number; + shipping?: number; + grandTotal?: number; + isLoaded: boolean; + products?: any[]; // Added to detect changes in selected products +}; const CartSummary = ({ total, @@ -24,53 +23,203 @@ const CartSummary = ({ shipping, grandTotal, isLoaded = false, + products = [], }: Props) => { - const PPN : number = process.env.NEXT_PUBLIC_PPN ? parseFloat(process.env.NEXT_PUBLIC_PPN) : 0; - return ( - <> -
    Ringkasan Pesanan
    + const PPN: number = process.env.NEXT_PUBLIC_PPN + ? parseFloat(process.env.NEXT_PUBLIC_PPN) + : 0; + const [isMounted, setIsMounted] = useState(false); + + // Local state to store calculated values + const [summaryValues, setSummaryValues] = useState({ + subtotal: 0, + discount: 0, + total: 0, + tax: 0, + shipping: 0, + grandTotal: 0, + }); + + const bgHighlight = useColorModeValue('red.50', 'red.900'); + + // This fixes hydration issues by ensuring the component only renders fully after mounting + useEffect(() => { + setIsMounted(true); + }, []); + + // Calculate summary based on products whenever products change + useMemo(() => { + if (!products || products.length === 0) return; + + // Only count selected products + const selectedProducts = products.filter((product) => product.selected); + + // Calculate values based on selected products + let calculatedSubtotal = 0; + let calculatedDiscount = 0; + + selectedProducts.forEach((product) => { + // Get raw price and discount from product + const productBasePrice = product.price?.price || 0; + const productQty = product.quantity || 1; + const productDiscountedPrice = + product.price?.price_discount || productBasePrice; + const productDiscount = productBasePrice - productDiscountedPrice; + + calculatedSubtotal += productBasePrice * productQty; + calculatedDiscount += productDiscount * productQty; + }); + + const calculatedTotal = calculatedSubtotal - calculatedDiscount; + const calculatedTax = calculatedTotal * (PPN - 1); + const calculatedShipping = shipping || 0; + const calculatedGrandTotal = + calculatedTotal + calculatedTax + calculatedShipping; + + // If calculated values are different from props, use calculated ones + const shouldUpdateValues = + Math.abs((subtotal || 0) - calculatedSubtotal) > 0.01 || + Math.abs((discount || 0) - calculatedDiscount) > 0.01 || + Math.abs((total || 0) - calculatedTotal) > 0.01 || + Math.abs((tax || 0) - calculatedTax) > 0.01 || + Math.abs((grandTotal || 0) - calculatedGrandTotal) > 0.01; -
    + if (shouldUpdateValues && isLoaded) { + setSummaryValues({ + subtotal: calculatedSubtotal, + discount: calculatedDiscount, + total: calculatedTotal, + tax: calculatedTax, + shipping: calculatedShipping, + grandTotal: calculatedGrandTotal, + }); + } else if (isLoaded) { + // Use values from props when available + setSummaryValues({ + subtotal: subtotal || 0, + discount: discount || 0, + total: total || 0, + tax: tax || 0, + shipping: shipping || 0, + grandTotal: grandTotal || 0, + }); + } + }, [ + products, + isLoaded, + subtotal, + discount, + total, + tax, + shipping, + grandTotal, + PPN, + ]); + + // Update local values whenever props change + useEffect(() => { + if (isLoaded) { + setSummaryValues({ + subtotal: subtotal || 0, + discount: discount || 0, + total: total || 0, + tax: tax || 0, + shipping: shipping || 0, + grandTotal: grandTotal || 0, + }); + } + }, [isLoaded, subtotal, discount, total, tax, shipping, grandTotal]); + + if (!isMounted) { + return ( + + + Ringkasan Pesanan + + {Array(6) + .fill(0) + .map((_, index) => ( + + ))} + + ); + } + + // Use local state for rendering to ensure responsiveness + const { + subtotal: displaySubtotal, + discount: displayDiscount, + total: displayTotal, + tax: displayTax, + shipping: displayShipping, + grandTotal: displayGrandTotal, + } = summaryValues; + + return ( + + + Ringkasan Pesanan +
    Total Belanja - Rp {formatCurrency(subtotal || 0)} + + Rp {formatCurrency(displaySubtotal)} + Total Diskon - - Rp {formatCurrency(discount || 0)} + + - Rp {formatCurrency(displayDiscount)} +
    Subtotal - Rp {formatCurrency(total || 0)} + Rp {formatCurrency(displayTotal)} - Tax {((PPN - 1) * 100).toFixed(0)}% - Rp {formatCurrency(tax || 0)} + + Tax {((PPN - 1) * 100).toFixed(0)}% + + Rp {formatCurrency(displayTax)} Biaya Kirim - Rp {formatCurrency(shipping || 0)} + + Rp {formatCurrency(displayShipping)} +
    - - - Grand Total - - Rp {formatCurrency(grandTotal || 0)} + + + + Grand Total + + + Rp {formatCurrency(displayGrandTotal)} + +
    - - ) -} + + ); +}; -export default CartSummary \ No newline at end of file +export default CartSummary; diff --git a/src-migrate/modules/cart/stores/useCartStore.ts b/src-migrate/modules/cart/stores/useCartStore.ts index e7d2cdd3..d211304a 100644 --- a/src-migrate/modules/cart/stores/useCartStore.ts +++ b/src-migrate/modules/cart/stores/useCartStore.ts @@ -1,6 +1,15 @@ import { create } from 'zustand'; import { CartItem, CartProps } from '~/types/cart'; import { getUserCart } from '~/services/cart'; +import { + syncCartWithCookie, + getCartDataFromCookie, + getSelectedItemsFromCookie, + updateSelectedItemInCookie, + setAllSelectedInCookie, + removeCartItemsFromCookie, + forceResetAllSelectedItems, +} from '~/utils/cart'; type State = { cart: CartProps | null; @@ -17,6 +26,8 @@ type State = { type Action = { loadCart: (userId: number) => Promise; updateCartItem: (updateCart: CartProps) => void; + syncCartWithCookieAndUpdate: (cart: CartProps) => { needsUpdate: boolean }; + forceResetSelection: () => void; }; export const useCartStore = create((set, get) => ({ @@ -29,34 +40,140 @@ export const useCartStore = create((set, get) => ({ tax: 0, grandTotal: 0, }, + loadCart: async (userId) => { if (get().isLoadCart === true) return; set({ isLoadCart: true }); - const cart: CartProps = (await getUserCart(userId)) as CartProps; - set({ cart }); - set({ isLoadCart: false }); + try { + // Fetch cart from API + const cart: CartProps = (await getUserCart(userId)) as CartProps; - const summary = computeSummary(cart); - set({ summary }); + // Sync with cookie and get updated data if needed + const { needsUpdate } = get().syncCartWithCookieAndUpdate(cart); + + // If no update needed from cookie, just set the cart directly + if (!needsUpdate) { + set({ cart }); + } + + // Update summary with current cart + const summary = computeSummary(get().cart!); + set({ summary }); + } catch (error) { + console.error('Failed to load cart from API:', error); + + // Fallback to cookie if API fails + try { + const cartData = getCartDataFromCookie(); + if (Object.keys(cartData).length > 0) { + // Transform cart data from cookie to expected format + const products = Object.values(cartData).map((item) => ({ + cart_id: item.cart_id, + id: item.id, + cart_type: item.cart_type, + product_id: item.product?.id, + product_name: item.product?.name, + program_line_id: item.program_line?.id, + program_line_name: item.program_line?.name, + quantity: item.quantity, + selected: item.selected, + price: item.price, + package_price: item.package_price, + source: item.source, + })); + + const fallbackCart: CartProps = { + product_total: products.length, + products, + }; + + set({ cart: fallbackCart }); + const summary = computeSummary(fallbackCart); + set({ summary }); + } + } catch (cookieError) { + console.error('Failed to fallback to cookie:', cookieError); + } + } finally { + set({ isLoadCart: false }); + } }, + updateCartItem: (updatedCart) => { const cart = get().cart; if (!cart) return; set({ cart: updatedCart }); + + // Sync updated cart with cookie + syncCartWithCookie(updatedCart); + const summary = computeSummary(updatedCart); set({ summary }); }, + syncCartWithCookieAndUpdate: (cart) => { + if (!cart) return { needsUpdate: false }; + + // Sync cart with cookie + const result = syncCartWithCookie(cart); + + // If we need to update the cart based on cookie data + if (result.needsUpdate && cart.products) { + // Create updated cart with selections from cookie + const selectedItems = getSelectedItemsFromCookie(); + + const updatedCart = { + ...cart, + products: cart.products.map((item) => ({ + ...item, + selected: + selectedItems[item.id] !== undefined + ? selectedItems[item.id] + : item.selected, + })), + }; + + // Update the store + set({ cart: updatedCart }); + } + + return result; + }, + + forceResetSelection: () => { + const { cart } = get(); + if (!cart) return; + + // Reset all selections in cookie + forceResetAllSelectedItems(); + + // Update the cart in state + const updatedCart = { + ...cart, + products: cart.products.map((item) => ({ + ...item, + selected: false, + })), + }; + + set({ cart: updatedCart }); + + // Update summary + const summary = computeSummary(updatedCart); + set({ summary }); + }, })); const computeSummary = (cart: CartProps) => { let subtotal = 0; let discount = 0; - const PPN: number = process.env.NEXT_PUBLIC_PPN ? parseFloat(process.env.NEXT_PUBLIC_PPN) : 0; - + const PPN: number = process.env.NEXT_PUBLIC_PPN + ? parseFloat(process.env.NEXT_PUBLIC_PPN) + : 0; + for (const item of cart?.products) { if (!item.selected) continue; @@ -74,5 +191,5 @@ const computeSummary = (cart: CartProps) => { let tax = grandTotal - total; // let grandTotal = total + tax; - return { subtotal, discount, total, grandTotal, tax }; -}; \ No newline at end of file + return { subtotal, discount, total, grandTotal, tax }; +}; diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx index 24baa933..475a4259 100644 --- a/src-migrate/pages/shop/cart/index.tsx +++ b/src-migrate/pages/shop/cart/index.tsx @@ -1,8 +1,16 @@ import style from './cart.module.css'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; -import { Button, Checkbox, Spinner, Tooltip } from '@chakra-ui/react'; +import { + Button, + Checkbox, + Spinner, + Tooltip, + Text, + Box, + Flex, +} from '@chakra-ui/react'; import { toast } from 'react-hot-toast'; import { useRouter } from 'next/router'; import { getAuth } from '~/libs/auth'; @@ -14,26 +22,125 @@ import clsxm from '~/libs/clsxm'; import useDevice from '@/core/hooks/useDevice'; import CartSummaryMobile from '~/modules/cart/components/CartSummaryMobile'; import Image from '~/components/ui/image'; -import { CartItem } from '~/types/cart'; import { deleteUserCart, upsertUserCart } from '~/services/cart'; import { Trash2Icon } from 'lucide-react'; import { useProductCartContext } from '@/contexts/ProductCartContext'; +import { + getSelectedItemsFromCookie, + setSelectedItemsToCookie, + syncSelectedItemsWithCookie, + setAllSelectedInCookie, + removeSelectedItemsFromCookie, + forceResetAllSelectedItems, +} from '~/utils/cart'; const CartPage = () => { const router = useRouter(); const auth = getAuth(); const [isStepApproval, setIsStepApproval] = useState(false); - const [isSelectedAll, setIsSelectedAll] = useState(false); - const [isButtonChek, setIsButtonChek] = useState(false); - const [buttonSelectNow, setButtonSelectNow] = useState(true); const [isLoad, setIsLoad] = useState(false); const [isLoadDelete, setIsLoadDelete] = useState(false); const { loadCart, cart, summary, updateCartItem } = useCartStore(); - const useDivvice = useDevice(); + const device = useDevice(); const { setRefreshCart } = useProductCartContext(); const [isTop, setIsTop] = useState(true); - const [hasChanged, setHasChanged] = useState(false); - const prevCartRef = useRef(null); + const [isUpdating, setIsUpdating] = useState(false); + const [isStateMismatch, setIsStateMismatch] = useState(false); + + // Function to check if cart state is inconsistent + const checkCartStateMismatch = () => { + if (!cart || !cart.products || isUpdating) return false; + + try { + // Ambil status selected dari cookie + const selectedItems = getSelectedItemsFromCookie(); + + // Periksa ketidaksesuaian antara UI dan cookie + // 1. Periksa item yang selected di UI tapi tidak di cookie + for (const product of cart.products) { + const cookieState = selectedItems[product.id]; + + // Jika ada di cookie tapi tidak sama dengan UI + if (cookieState !== undefined && cookieState !== product.selected) { + return true; + } + + // Jika tidak ada di cookie tapi selected di UI + if (cookieState === undefined && product.selected) { + return true; + } + } + + // 2. Periksa item yang selected di cookie tapi tidak ada di cart + for (const productId in selectedItems) { + const isSelected = selectedItems[productId]; + if (isSelected) { + // Cek apakah product id ini ada di cart + const productExists = cart.products.some( + (p) => p.id.toString() === productId.toString() + ); + if (!productExists) { + // Ada item selected di cookie yang tidak ada di cart + return true; + } + } + } + + return false; + } catch (error) { + console.error('Error checking cart state mismatch:', error); + return false; + } + }; // Function to reset all selected items when state is inconsistent + const handleResetSelections = () => { + if (!cart) return; + + setIsUpdating(true); + try { + // Use the forceResetSelection function from the store + useCartStore.getState().forceResetSelection(); + + // Set state back to normal + setIsStateMismatch(false); + + // Give visual feedback + toast.success('Semua pilihan telah direset'); + + // Optional: Sync with server if needed + if (typeof auth === 'object') { + const updatePromises = cart.products.map((item) => + upsertUserCart({ + userId: auth.id, + type: item.cart_type, + id: item.id, + qty: item.quantity, + selected: false, + }) + ); + + Promise.all(updatePromises) + .then(() => loadCart(auth.id)) + .catch((error) => { + console.error('Error updating selections to server:', error); + }) + .finally(() => setIsUpdating(false)); + } else { + setIsUpdating(false); + } + } catch (error) { + console.error('Error resetting selections:', error); + setIsUpdating(false); + toast.error('Gagal mereset pilihan'); + } + }; + + // Check for state inconsistency + useEffect(() => { + if (!cart || !cart.products || isUpdating) return; + + const hasMismatch = checkCartStateMismatch(); + setIsStateMismatch(hasMismatch); + }, [cart, isUpdating]); useEffect(() => { const handleScroll = () => { @@ -47,40 +154,35 @@ const CartPage = () => { }, []); useEffect(() => { - if (typeof auth === 'object' && !cart) { - loadCart(auth.id); - setIsStepApproval(auth?.feature?.soApproval); - } - }, [auth, loadCart, cart, isButtonChek]); + const loadCartWithStorage = async () => { + if (typeof auth === 'object' && !cart) { + await loadCart(auth.id); + setIsStepApproval(auth?.feature?.soApproval); - useEffect(() => { - if (typeof auth === 'object' && !cart) { - loadCart(auth.id); - setIsStepApproval(auth?.feature?.soApproval); - } - }, [auth, loadCart, cart, isButtonChek]); + // Sync selected items with server data using cookies + if (cart?.products) { + const { items, needsUpdate } = syncSelectedItemsWithCookie( + cart.products + ); - useEffect(() => { - const hasSelectedChanged = () => { - if (prevCartRef.current && cart) { - const prevCart = prevCartRef.current; - return cart.products.some( - (item, index) => - prevCart[index] && prevCart[index].selected !== item.selected - ); + // If there's a mismatch between cookie and server data, update the UI + if (needsUpdate) { + const updatedCart = { + ...cart, + products: cart.products.map((item) => ({ + ...item, + selected: + items[item.id] !== undefined ? items[item.id] : item.selected, + })), + }; + updateCartItem(updatedCart); + } + } } - return false; }; - if (hasSelectedChanged()) { - setHasChanged(true); - // Perform necessary actions here if selection has changed - } else { - setHasChanged(false); - } - - prevCartRef.current = cart ? [...cart.products] : null; - }, [cart]); + loadCartWithStorage(); + }, [auth, cart]); const hasSelectedPromo = useMemo(() => { if (!cart) return false; @@ -103,38 +205,24 @@ const CartPage = () => { const hasSelectedAll = useMemo(() => { if (!cart || !Array.isArray(cart.products)) return false; - return cart.products.every((item) => item.selected); + return ( + cart.products.length > 0 && cart.products.every((item) => item.selected) + ); }, [cart]); - useEffect(() => { - const updateCartItems = async () => { - if (typeof auth === 'object' && cart) { - const upsertPromises = cart.products.map((item) => - upsertUserCart({ - userId: auth.id, - type: item.cart_type, - id: item.id, - qty: item.quantity, - selected: item.selected, - }) - ); - try { - await Promise.all(upsertPromises); - await loadCart(auth.id); - } catch (error) { - console.error('Failed to update cart items:', error); - } - } - }; - - updateCartItems(); - }, [hasChanged]); - const handleCheckout = () => { + if (isUpdating || isLoadDelete) { + toast.error('Harap tunggu pembaruan selesai'); + return; + } router.push('/shop/checkout'); }; const handleQuotation = () => { + if (isUpdating || isLoadDelete) { + toast.error('Harap tunggu pembaruan selesai'); + return; + } if (hasSelectedPromo || !hasSelected) { toast.error('Maaf, Barang promo tidak dapat dibuat quotation'); } else { @@ -143,20 +231,62 @@ const CartPage = () => { }; const handleChange = async (e: React.ChangeEvent) => { - if (cart) { + if (cart && !isUpdating && typeof auth === 'object') { + const newSelectedState = !hasSelectedAll; + + // Update UI immediately const updatedCart = { ...cart, products: cart.products.map((item) => ({ ...item, - selected: !hasSelectedAll, + selected: newSelectedState, })), }; updateCartItem(updatedCart); - if (hasSelectedAll) { - setIsSelectedAll(false); - } else { - setIsSelectedAll(true); + + // Get all product IDs in cart + const productIds = cart.products.map((item) => item.id); + + // Update cookies immediately for responsive UI + setAllSelectedInCookie(productIds, newSelectedState); + + setIsUpdating(true); + + try { + // Update all items on server in background + const updatePromises = cart.products.map((item) => + upsertUserCart({ + userId: auth.id, + type: item.cart_type, + id: item.id, + qty: item.quantity, + selected: newSelectedState, + }) + ); + + await Promise.all(updatePromises); + await loadCart(auth.id); + } catch (error) { + console.error('Error updating select all:', error); + + // Revert changes on error + const revertedCart = { + ...cart, + products: cart.products.map((item) => ({ + ...item, + selected: !newSelectedState, + })), + }; + + updateCartItem(revertedCart); + + // Revert cookies + setAllSelectedInCookie(productIds, !newSelectedState); + + toast.error('Gagal memperbarui pilihan'); + } finally { + setIsUpdating(false); } } }; @@ -165,14 +295,25 @@ const CartPage = () => { if (typeof auth !== 'object' || !cart) return; setIsLoadDelete(true); - for (const item of cart.products) { - if (item.selected === true) { + try { + const itemsToDelete = cart.products.filter((item) => item.selected); + const itemIdsToDelete = itemsToDelete.map((item) => item.id); + + for (const item of itemsToDelete) { await deleteUserCart(auth.id, [item.cart_id]); - await loadCart(auth.id); } + + // Remove deleted items from cookie + removeSelectedItemsFromCookie(itemIdsToDelete); + + await loadCart(auth.id); + setRefreshCart(true); + } catch (error) { + console.error('Failed to delete cart items:', error); + toast.error('Gagal menghapus item'); + } finally { + setIsLoadDelete(false); } - setIsLoadDelete(false); - setRefreshCart(true); }; return ( @@ -180,27 +321,45 @@ const CartPage = () => {
    -

    Keranjang Belanja

    +
    +

    Keranjang Belanja

    + {isStateMismatch && ( + + )} +
    +
    -
    +
    - {isLoad && } - {!isLoad && ( - - )} +

    {hasSelectedAll ? 'Uncheck all' : 'Select all'}

    -
    +
    { bg='#fadede' variant='outline' colorScheme='red' - w='full' - isDisabled={!hasSelected} + w='auto' + size={device.isMobile ? 'sm' : 'md'} + isDisabled={!hasSelected || isUpdating} onClick={handleDelete} > {isLoadDelete && } @@ -223,19 +383,19 @@ const CartPage = () => {
    -
    +
    {!cart && }
    -
    +
    {cart?.products?.map((item) => ( ))} {cart?.products?.length === 0 && ( -
    +
    Empty Cart {
    -
    - {useDivvice.isMobile && ( - - )} - {!useDivvice.isMobile && ( - +
    + {device.isMobile ? ( + + ) : ( + )}
    { } > @@ -301,14 +477,21 @@ const CartPage = () => { label={clsxm({ 'Tidak ada item yang dipilih': !hasSelected, 'Terdapat item yang tidak ada harga': hasSelectNoPrice, + 'Harap tunggu pembaruan selesai': isUpdating, })} > diff --git a/src-migrate/utils/cart.js b/src-migrate/utils/cart.js new file mode 100644 index 00000000..431ff530 --- /dev/null +++ b/src-migrate/utils/cart.js @@ -0,0 +1,290 @@ +// cart-cookie-utils.js +import Cookies from 'js-cookie'; + +// Constants +const CART_ITEMS_COOKIE = 'cart_data'; +const SELECTED_ITEMS_COOKIE = 'cart_selected_items'; +const COOKIE_EXPIRY_DAYS = 7; // Cookie akan berlaku selama 7 hari + +/** + * Mengambil data cart lengkap dari cookie + * @returns {Object} Object dengan key cart_id dan value cart item data lengkap + */ +export const getCartDataFromCookie = () => { + try { + const storedData = Cookies.get(CART_ITEMS_COOKIE); + return storedData ? JSON.parse(storedData) : {}; + } catch (error) { + console.error('Error reading cart data from cookie:', error); + return {}; + } +}; + +/** + * Menyimpan data cart lengkap ke cookie + * @param {Object} cartData Object dengan key cart_id dan value cart item data lengkap + */ +export const setCartDataToCookie = (cartData) => { + try { + Cookies.set(CART_ITEMS_COOKIE, JSON.stringify(cartData), { + expires: COOKIE_EXPIRY_DAYS, + path: '/', + sameSite: 'strict', + }); + } catch (error) { + console.error('Error saving cart data to cookie:', error); + } +}; + +/** + * Mengambil state selected items dari cookie + * @returns {Object} Object dengan key product id dan value boolean selected status + */ +export const getSelectedItemsFromCookie = () => { + try { + const storedItems = Cookies.get(SELECTED_ITEMS_COOKIE); + return storedItems ? JSON.parse(storedItems) : {}; + } catch (error) { + console.error('Error reading selected items from cookie:', error); + return {}; + } +}; + +/** + * Menyimpan state selected items ke cookie + * @param {Object} items Object dengan key product id dan value boolean selected status + */ +export const setSelectedItemsToCookie = (items) => { + try { + Cookies.set(SELECTED_ITEMS_COOKIE, JSON.stringify(items), { + expires: COOKIE_EXPIRY_DAYS, + path: '/', + sameSite: 'strict', + }); + } catch (error) { + console.error('Error saving selected items to cookie:', error); + } +}; + +/** + * Transform cart items dari format API ke format yang lebih simpel untuk disimpan di cookie + * @param {Array} cartItems Array cart items dari API + * @returns {Object} Object dengan key cart_id dan value cart item data + */ +export const transformCartItemsForCookie = (cartItems) => { + if (!cartItems || !Array.isArray(cartItems)) return {}; + + const cartData = {}; + + cartItems.forEach((item) => { + // Skip items yang tidak memiliki cart_id + if (!item.cart_id) return; + + cartData[item.cart_id] = { + id: item.id, + cart_id: item.cart_id, + cart_type: item.cart_type, + product: item.product_id + ? { + id: item.product_id, + name: item.product_name || '', + } + : null, + program_line: item.program_line_id + ? { + id: item.program_line_id, + name: item.program_line_name || '', + } + : null, + quantity: item.quantity, + selected: item.selected, + price: item.price, + package_price: item.package_price, + source: item.source || 'add_to_cart', + }; + }); + + return cartData; +}; + +/** + * Sinkronisasi cart data dan selected items dari server dengan cookie + * @param {Object} cart Cart object dari API + * @returns {Object} Object yang berisi updated cartData dan selectedItems + */ +export const syncCartWithCookie = (cart) => { + try { + if (!cart || !cart.products) return { needsUpdate: false }; + + // Transform data dari API ke format cookie + const serverCartData = transformCartItemsForCookie(cart.products); + + // Ambil data lama dari cookie + const existingCartData = getCartDataFromCookie(); + + // Ambil selected status dari cookie + const selectedItems = getSelectedItemsFromCookie(); + + // Gabungkan data cart, prioritaskan data server + const mergedCartData = { ...existingCartData, ...serverCartData }; + + // Periksa apakah ada perbedaan status selected + let needsUpdate = false; + + // Update selected status berdasarkan cookie jika ada + for (const cartId in mergedCartData) { + const item = mergedCartData[cartId]; + if (item.id && selectedItems[item.id] !== undefined) { + // Jika status di cookie berbeda dengan di cart + if (item.selected !== selectedItems[item.id]) { + needsUpdate = true; + item.selected = selectedItems[item.id]; + } + } else if (item.id) { + // Jika tidak ada di cookie, tambahkan dari cart + selectedItems[item.id] = item.selected; + } + } + + // Simpan kembali ke cookie + setCartDataToCookie(mergedCartData); + setSelectedItemsToCookie(selectedItems); + + return { + cartData: mergedCartData, + selectedItems, + needsUpdate, + }; + } catch (error) { + console.error('Error syncing cart with cookie:', error); + return { needsUpdate: false }; + } +}; + +/** + * Update selected status item di cookie + * @param {number} productId ID produk + * @param {boolean} isSelected Status selected baru + */ +export const updateSelectedItemInCookie = (productId, isSelected) => { + try { + const selectedItems = getSelectedItemsFromCookie(); + selectedItems[productId] = isSelected; + setSelectedItemsToCookie(selectedItems); + + // Update juga di cart data + const cartData = getCartDataFromCookie(); + + for (const cartId in cartData) { + const item = cartData[cartId]; + if (item.id === productId) { + item.selected = isSelected; + } + } + + setCartDataToCookie(cartData); + + return { selectedItems, cartData }; + } catch (error) { + console.error('Error updating selected item in cookie:', error); + return {}; + } +}; + +/** + * Set semua item menjadi selected atau unselected di cookie + * @param {Array} productIds Array product IDs + * @param {boolean} isSelected Status selected baru + */ +export const setAllSelectedInCookie = (productIds, isSelected) => { + try { + const selectedItems = getSelectedItemsFromCookie(); + + productIds.forEach((id) => { + if (id) selectedItems[id] = isSelected; + }); + + setSelectedItemsToCookie(selectedItems); + + // Update juga di cart data + const cartData = getCartDataFromCookie(); + + for (const cartId in cartData) { + if (productIds.includes(cartData[cartId].id)) { + cartData[cartId].selected = isSelected; + } + } + + setCartDataToCookie(cartData); + + return { selectedItems, cartData }; + } catch (error) { + console.error('Error setting all selected in cookie:', error); + return {}; + } +}; + +/** + * Hapus item dari cookie + * @param {Array} cartIds Array cart IDs untuk dihapus + */ +export const removeCartItemsFromCookie = (cartIds) => { + try { + const cartData = getCartDataFromCookie(); + const selectedItems = getSelectedItemsFromCookie(); + const productIdsToRemove = []; + + // Hapus item dari cartData dan catat product IDs + cartIds.forEach((cartId) => { + if (cartData[cartId]) { + if (cartData[cartId].id) { + productIdsToRemove.push(cartData[cartId].id); + } + delete cartData[cartId]; + } + }); + + // Hapus dari selectedItems + productIdsToRemove.forEach((productId) => { + if (selectedItems[productId] !== undefined) { + delete selectedItems[productId]; + } + }); + + // Simpan kembali ke cookie + setCartDataToCookie(cartData); + setSelectedItemsToCookie(selectedItems); + + return { cartData, selectedItems }; + } catch (error) { + console.error('Error removing cart items from cookie:', error); + return {}; + } +}; + +/** + * Force reset semua selected items ke unselected state + */ +export const forceResetAllSelectedItems = () => { + try { + const cartData = getCartDataFromCookie(); + const selectedItems = {}; + + // Reset semua selected status di cartData + for (const cartId in cartData) { + cartData[cartId].selected = false; + if (cartData[cartId].id) { + selectedItems[cartData[cartId].id] = false; + } + } + + // Simpan kembali ke cookie + setCartDataToCookie(cartData); + setSelectedItemsToCookie(selectedItems); + + return { cartData, selectedItems }; + } catch (error) { + console.error('Error resetting all selected items:', error); + return {}; + } +}; diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 5256a328..4120df2c 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -1106,7 +1106,10 @@ const Checkout = () => {
    Jika mengalami kesulitan dalam melakukan pembelian di website - Indoteknik. Hubungi kami disini + Indoteknik.{' '} + + Hubungi kami disini +
    -- cgit v1.2.3 From 09cebc9020c4f1995a73305187bc1576e339d183 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 22 May 2025 10:05:09 +0700 Subject: disable button when updating checkboxes and change summary design --- src-migrate/modules/cart/components/ItemSelect.tsx | 43 +++++++-- src-migrate/modules/cart/components/Summary.tsx | 28 ++---- src-migrate/pages/shop/cart/index.tsx | 98 +++++++++++++------- src-migrate/utils/cart.js | 102 ++++++++++++++++++++- src-migrate/utils/checkBoxState.js | 89 ++++++++++++++++++ src/lib/checkout/components/Checkout.jsx | 5 +- 6 files changed, 297 insertions(+), 68 deletions(-) create mode 100644 src-migrate/utils/checkBoxState.js diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx index 733ee64d..70b656ec 100644 --- a/src-migrate/modules/cart/components/ItemSelect.tsx +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -8,6 +8,7 @@ import { toast } from 'react-hot-toast'; import { getSelectedItemsFromCookie, updateSelectedItemInCookie, + checkboxUpdateState, } from '~/utils/cart'; type Props = { @@ -19,6 +20,20 @@ const CartItemSelect = ({ item }: Props) => { const { updateCartItem, cart, loadCart } = useCartStore(); const [isUpdating, setIsUpdating] = useState(false); const [localSelected, setLocalSelected] = useState(item.selected); + const [isGlobalUpdating, setIsGlobalUpdating] = useState(false); + + // Subscribe to global checkbox update state + useEffect(() => { + const handleUpdateStateChange = (isUpdating) => { + setIsGlobalUpdating(isUpdating); + }; + + checkboxUpdateState.addListener(handleUpdateStateChange); + + return () => { + checkboxUpdateState.removeListener(handleUpdateStateChange); + }; + }, []); // Initialize local state from cookie or server useEffect(() => { @@ -54,7 +69,7 @@ const CartItemSelect = ({ item }: Props) => { setLocalSelected(item.selected); // Save this state to cookie for future use - updateSelectedItemInCookie(item.id, item.selected); + updateSelectedItemInCookie(item.id, item.selected, false); // don't notify for initial sync } }, [item.id, item.selected, localSelected, cart, updateCartItem, isUpdating]); @@ -70,9 +85,12 @@ const CartItemSelect = ({ item }: Props) => { setLocalSelected(newSelectedState); setIsUpdating(true); + // Start the update - notify global state with this checkbox's ID + checkboxUpdateState.startUpdate(item.id); + try { - // Update cookie immediately - updateSelectedItemInCookie(item.id, newSelectedState); + // The cookie update is now handled inside the function with notification + updateSelectedItemInCookie(item.id, newSelectedState, false); // We already started above // Update cart state immediately for UI responsiveness const updatedCartItems = cart.products.map((cartItem) => @@ -91,6 +109,7 @@ const CartItemSelect = ({ item }: Props) => { id: item.id, qty: item.quantity, selected: newSelectedState, + purchase_tax_id: item.purchase_tax_id || null, // Ensure null for numeric fields }); // Reload cart to ensure consistency @@ -102,18 +121,26 @@ const CartItemSelect = ({ item }: Props) => { // Revert local state on error setLocalSelected(!newSelectedState); - // Update cookie back - updateSelectedItemInCookie(item.id, !newSelectedState); + // Revert cookie change + updateSelectedItemInCookie(item.id, !newSelectedState, false); // Reload cart to get server state loadCart(auth.id); } finally { setIsUpdating(false); + + // End the update - notify global state with this checkbox's ID + checkboxUpdateState.endUpdate(item.id); } }, [auth, cart, item, isUpdating, updateCartItem, loadCart] ); + // Determine if THIS specific checkbox should be disabled - only disable + // if this specific checkbox is updating + const isDisabled = + isUpdating || checkboxUpdateState.isCheckboxUpdating(item.id); + return (
    { size='lg' isChecked={localSelected} onChange={handleChange} - isDisabled={isUpdating} - opacity={isUpdating ? 0.5 : 1} - cursor={isUpdating ? 'not-allowed' : 'pointer'} + isDisabled={isDisabled} + opacity={isDisabled ? 0.5 : 1} + cursor={isDisabled ? 'not-allowed' : 'pointer'} _disabled={{ opacity: 0.5, cursor: 'not-allowed', diff --git a/src-migrate/modules/cart/components/Summary.tsx b/src-migrate/modules/cart/components/Summary.tsx index b4fbab6b..68db6323 100644 --- a/src-migrate/modules/cart/components/Summary.tsx +++ b/src-migrate/modules/cart/components/Summary.tsx @@ -40,8 +40,6 @@ const CartSummary = ({ grandTotal: 0, }); - const bgHighlight = useColorModeValue('red.50', 'red.900'); - // This fixes hydration issues by ensuring the component only renders fully after mounting useEffect(() => { setIsMounted(true); @@ -156,15 +154,7 @@ const CartSummary = ({ } = summaryValues; return ( - +
    Ringkasan Pesanan @@ -208,17 +198,15 @@ const CartSummary = ({
    - - - Grand Total - - - Rp {formatCurrency(displayGrandTotal)} - - + + Grand Total + + + Rp {formatCurrency(displayGrandTotal)} +
    - +
    ); }; diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx index 475a4259..eefe8d09 100644 --- a/src-migrate/pages/shop/cart/index.tsx +++ b/src-migrate/pages/shop/cart/index.tsx @@ -32,8 +32,12 @@ import { setAllSelectedInCookie, removeSelectedItemsFromCookie, forceResetAllSelectedItems, + checkboxUpdateState, } from '~/utils/cart'; +// Special ID for the "select all" checkbox +const SELECT_ALL_ID = 'select_all_checkbox'; + const CartPage = () => { const router = useRouter(); const auth = getAuth(); @@ -46,6 +50,22 @@ const CartPage = () => { const [isTop, setIsTop] = useState(true); const [isUpdating, setIsUpdating] = useState(false); const [isStateMismatch, setIsStateMismatch] = useState(false); + const [isAnyCheckboxUpdating, setIsAnyCheckboxUpdating] = useState(false); + + // Subscribe to checkbox update state changes + useEffect(() => { + const handleUpdateStateChange = (isUpdating) => { + setIsAnyCheckboxUpdating(isUpdating); + }; + + // Add listener for checkbox update state changes + checkboxUpdateState.addListener(handleUpdateStateChange); + + // Cleanup listener on component unmount + return () => { + checkboxUpdateState.removeListener(handleUpdateStateChange); + }; + }, []); // Function to check if cart state is inconsistent const checkCartStateMismatch = () => { @@ -91,11 +111,14 @@ const CartPage = () => { console.error('Error checking cart state mismatch:', error); return false; } - }; // Function to reset all selected items when state is inconsistent + }; + + // Function to reset all selected items when state is inconsistent const handleResetSelections = () => { if (!cart) return; setIsUpdating(true); + try { // Use the forceResetSelection function from the store useCartStore.getState().forceResetSelection(); @@ -115,6 +138,7 @@ const CartPage = () => { id: item.id, qty: item.quantity, selected: false, + purchase_tax_id: item.purchase_tax_id || null, // Fix integer field issue }) ); @@ -123,7 +147,9 @@ const CartPage = () => { .catch((error) => { console.error('Error updating selections to server:', error); }) - .finally(() => setIsUpdating(false)); + .finally(() => { + setIsUpdating(false); + }); } else { setIsUpdating(false); } @@ -211,7 +237,7 @@ const CartPage = () => { }, [cart]); const handleCheckout = () => { - if (isUpdating || isLoadDelete) { + if (isUpdating || isLoadDelete || isAnyCheckboxUpdating) { toast.error('Harap tunggu pembaruan selesai'); return; } @@ -219,7 +245,7 @@ const CartPage = () => { }; const handleQuotation = () => { - if (isUpdating || isLoadDelete) { + if (isUpdating || isLoadDelete || isAnyCheckboxUpdating) { toast.error('Harap tunggu pembaruan selesai'); return; } @@ -234,6 +260,12 @@ const CartPage = () => { if (cart && !isUpdating && typeof auth === 'object') { const newSelectedState = !hasSelectedAll; + // Set updating flag + setIsUpdating(true); + + // Notify checkbox update state system with the special select all ID + checkboxUpdateState.startUpdate(SELECT_ALL_ID); + // Update UI immediately const updatedCart = { ...cart, @@ -249,9 +281,7 @@ const CartPage = () => { const productIds = cart.products.map((item) => item.id); // Update cookies immediately for responsive UI - setAllSelectedInCookie(productIds, newSelectedState); - - setIsUpdating(true); + setAllSelectedInCookie(productIds, newSelectedState, false); // We're already notifying try { // Update all items on server in background @@ -262,6 +292,7 @@ const CartPage = () => { id: item.id, qty: item.quantity, selected: newSelectedState, + purchase_tax_id: item.purchase_tax_id || null, // Fix integer field issue }) ); @@ -282,11 +313,14 @@ const CartPage = () => { updateCartItem(revertedCart); // Revert cookies - setAllSelectedInCookie(productIds, !newSelectedState); + setAllSelectedInCookie(productIds, !newSelectedState, false); toast.error('Gagal memperbarui pilihan'); } finally { setIsUpdating(false); + + // End update notification + checkboxUpdateState.endUpdate(SELECT_ALL_ID); } } }; @@ -295,6 +329,8 @@ const CartPage = () => { if (typeof auth !== 'object' || !cart) return; setIsLoadDelete(true); + checkboxUpdateState.startUpdate('delete_operation'); // Use special ID for delete + try { const itemsToDelete = cart.products.filter((item) => item.selected); const itemIdsToDelete = itemsToDelete.map((item) => item.id); @@ -313,9 +349,18 @@ const CartPage = () => { toast.error('Gagal menghapus item'); } finally { setIsLoadDelete(false); + checkboxUpdateState.endUpdate('delete_operation'); } }; + // Check if buttons should be disabled + const areButtonsDisabled = + isUpdating || isLoadDelete || isAnyCheckboxUpdating; + + // Only disable the select all checkbox if it specifically is updating + const isSelectAllDisabled = + isUpdating || checkboxUpdateState.isCheckboxUpdating(SELECT_ALL_ID); + return ( <>
    { >

    Keranjang Belanja

    - {isStateMismatch && ( - - )}
    @@ -346,9 +381,9 @@ const CartPage = () => { size='lg' isChecked={hasSelectedAll} onChange={handleChange} - isDisabled={isUpdating || isLoadDelete} - opacity={isUpdating ? 0.5 : 1} - cursor={isUpdating ? 'not-allowed' : 'pointer'} + isDisabled={isSelectAllDisabled} + opacity={isSelectAllDisabled ? 0.5 : 1} + cursor={isSelectAllDisabled ? 'not-allowed' : 'pointer'} _disabled={{ opacity: 0.5, cursor: 'not-allowed', @@ -363,6 +398,7 @@ const CartPage = () => { @@ -477,21 +510,18 @@ const CartPage = () => { label={clsxm({ 'Tidak ada item yang dipilih': !hasSelected, 'Terdapat item yang tidak ada harga': hasSelectNoPrice, - 'Harap tunggu pembaruan selesai': isUpdating, + 'Harap tunggu pembaruan selesai': areButtonsDisabled, })} > diff --git a/src-migrate/utils/cart.js b/src-migrate/utils/cart.js index 431ff530..f474cbde 100644 --- a/src-migrate/utils/cart.js +++ b/src-migrate/utils/cart.js @@ -1,5 +1,6 @@ // cart-cookie-utils.js import Cookies from 'js-cookie'; +import checkboxUpdateState from './checkBoxState'; // Constants const CART_ITEMS_COOKIE = 'cart_data'; @@ -165,9 +166,19 @@ export const syncCartWithCookie = (cart) => { * Update selected status item di cookie * @param {number} productId ID produk * @param {boolean} isSelected Status selected baru + * @param {boolean} notifyUpdate Whether to notify checkbox update state (default: true) */ -export const updateSelectedItemInCookie = (productId, isSelected) => { +export const updateSelectedItemInCookie = ( + productId, + isSelected, + notifyUpdate = true +) => { try { + // Notify checkbox update state if requested + if (notifyUpdate) { + checkboxUpdateState.startUpdate(); + } + const selectedItems = getSelectedItemsFromCookie(); selectedItems[productId] = isSelected; setSelectedItemsToCookie(selectedItems); @@ -188,6 +199,11 @@ export const updateSelectedItemInCookie = (productId, isSelected) => { } catch (error) { console.error('Error updating selected item in cookie:', error); return {}; + } finally { + // End update notification if requested + if (notifyUpdate) { + checkboxUpdateState.endUpdate(); + } } }; @@ -195,9 +211,19 @@ export const updateSelectedItemInCookie = (productId, isSelected) => { * Set semua item menjadi selected atau unselected di cookie * @param {Array} productIds Array product IDs * @param {boolean} isSelected Status selected baru + * @param {boolean} notifyUpdate Whether to notify checkbox update state (default: true) */ -export const setAllSelectedInCookie = (productIds, isSelected) => { +export const setAllSelectedInCookie = ( + productIds, + isSelected, + notifyUpdate = true +) => { try { + // Notify checkbox update state if requested + if (notifyUpdate) { + checkboxUpdateState.startUpdate(); + } + const selectedItems = getSelectedItemsFromCookie(); productIds.forEach((id) => { @@ -221,6 +247,11 @@ export const setAllSelectedInCookie = (productIds, isSelected) => { } catch (error) { console.error('Error setting all selected in cookie:', error); return {}; + } finally { + // End update notification if requested + if (notifyUpdate) { + checkboxUpdateState.endUpdate(); + } } }; @@ -262,11 +293,38 @@ export const removeCartItemsFromCookie = (cartIds) => { } }; +/** + * Hapus item selected dari cookie berdasarkan product IDs + * @param {Array} productIds Array product IDs untuk dihapus + */ +export const removeSelectedItemsFromCookie = (productIds) => { + try { + const selectedItems = getSelectedItemsFromCookie(); + + // Hapus dari selectedItems + productIds.forEach((productId) => { + if (selectedItems[productId] !== undefined) { + delete selectedItems[productId]; + } + }); + + // Simpan kembali ke cookie + setSelectedItemsToCookie(selectedItems); + + return { selectedItems }; + } catch (error) { + console.error('Error removing selected items from cookie:', error); + return {}; + } +}; + /** * Force reset semua selected items ke unselected state */ export const forceResetAllSelectedItems = () => { try { + checkboxUpdateState.startUpdate(); + const cartData = getCartDataFromCookie(); const selectedItems = {}; @@ -286,5 +344,45 @@ export const forceResetAllSelectedItems = () => { } catch (error) { console.error('Error resetting all selected items:', error); return {}; + } finally { + checkboxUpdateState.endUpdate(); } }; + +/** + * Sync selected items between cookie and cart data + * @param {Array} cartProducts Products array from cart + */ +export const syncSelectedItemsWithCookie = (cartProducts) => { + try { + if (!cartProducts || !Array.isArray(cartProducts)) { + return { items: {}, needsUpdate: false }; + } + + const selectedItems = getSelectedItemsFromCookie(); + let needsUpdate = false; + + // Check if we need to update any items based on cookie values + cartProducts.forEach((product) => { + if (product.id && selectedItems[product.id] !== undefined) { + if (product.selected !== selectedItems[product.id]) { + needsUpdate = true; + } + } else if (product.id) { + // If not in cookie, add with current value + selectedItems[product.id] = product.selected; + } + }); + + // Update the cookie with the latest values + setSelectedItemsToCookie(selectedItems); + + return { items: selectedItems, needsUpdate }; + } catch (error) { + console.error('Error syncing selected items with cookie:', error); + return { items: {}, needsUpdate: false }; + } +}; + +// Export the checkbox update state for use in components +export { checkboxUpdateState }; diff --git a/src-migrate/utils/checkBoxState.js b/src-migrate/utils/checkBoxState.js new file mode 100644 index 00000000..0c58321f --- /dev/null +++ b/src-migrate/utils/checkBoxState.js @@ -0,0 +1,89 @@ +// ~/modules/cart/utils/checkboxUpdateState.js + +/** + * Enhanced state manager for checkbox updates + * Tracks both global update state and individual checkbox update states + */ + +// Track the number of ongoing updates +let updateCount = 0; +let listeners = []; + +// Track which checkboxes are currently updating by ID +let updatingCheckboxIds = new Set(); + +const checkboxUpdateState = { + // Check if any checkboxes are currently updating (for buttons) + isUpdating: () => updateCount > 0, + + // Check if a specific checkbox is updating (for disabling just that checkbox) + isCheckboxUpdating: (itemId) => updatingCheckboxIds.has(itemId.toString()), + + // Start an update for a specific checkbox + startUpdate: (itemId = null) => { + updateCount++; + + // If an item ID is provided, mark it as updating + if (itemId !== null) { + updatingCheckboxIds.add(itemId.toString()); + } + + notifyListeners(); + return updateCount; + }, + + // End an update for a specific checkbox + endUpdate: (itemId = null) => { + updateCount = Math.max(0, updateCount - 1); + + // If an item ID is provided, remove it from updating set + if (itemId !== null) { + updatingCheckboxIds.delete(itemId.toString()); + } + + notifyListeners(); + return updateCount; + }, + + // Reset the update counter and clear all updating checkboxes + reset: () => { + updateCount = 0; + updatingCheckboxIds.clear(); + notifyListeners(); + }, + + // Get IDs of all checkboxes currently updating (for debugging) + getUpdatingCheckboxIds: () => [...updatingCheckboxIds], + + // Add a listener function to be called when update state changes + addListener: (callback) => { + if (typeof callback === 'function') { + listeners.push(callback); + + // Immediately call with current state + callback(updateCount > 0); + } + }, + + // Remove a listener + removeListener: (callback) => { + listeners = listeners.filter((listener) => listener !== callback); + }, + + // Get current counter (for debugging) + getUpdateCount: () => updateCount, +}; + +// Private function to notify all listeners of state changes +function notifyListeners() { + const isUpdating = updateCount > 0; + listeners.forEach((listener) => { + try { + listener(isUpdating); + } catch (error) { + console.error('Error in checkbox update state listener:', error); + } + }); +} + +export default checkboxUpdateState; diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 4120df2c..5256a328 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -1106,10 +1106,7 @@ const Checkout = () => {
    Jika mengalami kesulitan dalam melakukan pembelian di website - Indoteknik.{' '} - - Hubungi kami disini - + Indoteknik. Hubungi kami disini
    -- cgit v1.2.3 From 4904573845478e7e9648735d008153728870a123 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 23 May 2025 09:37:46 +0700 Subject: fix cookie not updating when delete an item --- src-migrate/modules/cart/components/ItemAction.tsx | 139 ++++++++++++++------- src-migrate/modules/cart/components/ItemSelect.tsx | 2 +- src-migrate/pages/shop/cart/index.tsx | 36 +++++- src-migrate/utils/cart.js | 68 +++++++--- 4 files changed, 174 insertions(+), 71 deletions(-) diff --git a/src-migrate/modules/cart/components/ItemAction.tsx b/src-migrate/modules/cart/components/ItemAction.tsx index 7220e362..eea0cbe9 100644 --- a/src-migrate/modules/cart/components/ItemAction.tsx +++ b/src-migrate/modules/cart/components/ItemAction.tsx @@ -1,74 +1,121 @@ -import style from '../styles/item-action.module.css' +import style from '../styles/item-action.module.css'; -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react'; -import { Spinner, Tooltip } from '@chakra-ui/react' -import { MinusIcon, PlusIcon, Trash2Icon } from 'lucide-react' +import { Spinner, Tooltip } from '@chakra-ui/react'; +import { MinusIcon, PlusIcon, Trash2Icon } from 'lucide-react'; -import { CartItem } from '~/types/cart' -import { getAuth } from '~/libs/auth' -import { deleteUserCart, upsertUserCart } from '~/services/cart' +import { CartItem } from '~/types/cart'; +import { getAuth } from '~/libs/auth'; +import { deleteUserCart, upsertUserCart } from '~/services/cart'; -import { useDebounce } from 'usehooks-ts' -import { useCartStore } from '../stores/useCartStore' -import { useProductCartContext } from '@/contexts/ProductCartContext' +import { useDebounce } from 'usehooks-ts'; +import { useCartStore } from '../stores/useCartStore'; +import { useProductCartContext } from '@/contexts/ProductCartContext'; +import { + removeSelectedItemsFromCookie, + removeCartItemsFromCookie, +} from '~/utils/cart'; +import { toast } from 'react-hot-toast'; type Props = { - item: CartItem -} + item: CartItem; +}; const CartItemAction = ({ item }: Props) => { - const auth = getAuth() - const { setRefreshCart } = useProductCartContext() - const [isLoadDelete, setIsLoadDelete] = useState(false) - const [isLoadQuantity, setIsLoadQuantity] = useState(false) + const auth = getAuth(); + const { setRefreshCart } = useProductCartContext(); + const [isLoadDelete, setIsLoadDelete] = useState(false); + const [isLoadQuantity, setIsLoadQuantity] = useState(false); - const [quantity, setQuantity] = useState(item.quantity) + const [quantity, setQuantity] = useState(item.quantity); - const { loadCart } = useCartStore() + const { loadCart, cart, updateCartItem } = useCartStore(); // TAMBAHKAN cart dan updateCartItem - const limitQty = item.limit_qty?.transaction || 0 + const limitQty = item.limit_qty?.transaction || 0; + // PERBAIKI FUNCTION INI const handleDelete = async () => { - if (typeof auth !== 'object') return - - setIsLoadDelete(true) - await deleteUserCart(auth.id, [item.cart_id]) - await loadCart(auth.id) - setIsLoadDelete(false) - setRefreshCart(true) - } - - const decreaseQty = () => { setQuantity((quantity) => quantity -= 1) } - const increaseQty = () => { setQuantity((quantity) => quantity += 1) } - const debounceQty = useDebounce(quantity, 1000) + if (typeof auth !== 'object') return; + + setIsLoadDelete(true); + + try { + // Step 1: Delete from server + await deleteUserCart(auth.id, [item.cart_id]); + + // Step 2: Clean up cookies IMMEDIATELY + removeSelectedItemsFromCookie([item.id]); + removeCartItemsFromCookie([item.cart_id]); + + // Step 3: Update local cart state optimistically + if (cart) { + const updatedProducts = cart.products.filter( + (product) => product.id !== item.id + ); + const updatedCart = { + ...cart, + products: updatedProducts, + product_total: updatedProducts.length, + }; + updateCartItem(updatedCart); + } + + // Step 4: Reload from server to ensure consistency + await loadCart(auth.id); + setRefreshCart(true); + + toast.success('Item berhasil dihapus'); + } catch (error) { + console.error('Failed to delete cart item:', error); + toast.error('Gagal menghapus item'); + + // Reload on error + await loadCart(auth.id); + } finally { + setIsLoadDelete(false); + } + }; + + const decreaseQty = () => { + setQuantity((quantity) => (quantity -= 1)); + }; + const increaseQty = () => { + setQuantity((quantity) => (quantity += 1)); + }; + const debounceQty = useDebounce(quantity, 1000); + useEffect(() => { - if (isNaN(debounceQty)) setQuantity(1) - if (limitQty > 0 && debounceQty > limitQty) setQuantity(limitQty) - }, [debounceQty, limitQty]) + if (isNaN(debounceQty)) setQuantity(1); + if (limitQty > 0 && debounceQty > limitQty) setQuantity(limitQty); + }, [debounceQty, limitQty]); useEffect(() => { const updateCart = async () => { - if (typeof auth !== 'object' || isNaN(debounceQty)) return + if (typeof auth !== 'object' || isNaN(debounceQty)) return; - setIsLoadQuantity(true) + setIsLoadQuantity(true); await upsertUserCart({ userId: auth.id, type: item.cart_type, id: item.id, qty: debounceQty, selected: item.selected, - }) - await loadCart(auth.id) - setIsLoadQuantity(false) - } - updateCart() + }); + await loadCart(auth.id); + setIsLoadQuantity(false); + }; + updateCart(); //eslint-disable-next-line react-hooks/exhaustive-deps - }, [debounceQty]) + }, [debounceQty]); return (
    - @@ -106,7 +153,7 @@ const CartItemAction = ({ item }: Props) => {
    - ) -} + ); +}; -export default CartItemAction \ No newline at end of file +export default CartItemAction; diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx index 70b656ec..f580f81d 100644 --- a/src-migrate/modules/cart/components/ItemSelect.tsx +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -109,7 +109,7 @@ const CartItemSelect = ({ item }: Props) => { id: item.id, qty: item.quantity, selected: newSelectedState, - purchase_tax_id: item.purchase_tax_id || null, // Ensure null for numeric fields + // purchase_tax_id: item.purchase_tax_id || null, // Ensure null for numeric fields }); // Reload cart to ensure consistency diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx index eefe8d09..798ad318 100644 --- a/src-migrate/pages/shop/cart/index.tsx +++ b/src-migrate/pages/shop/cart/index.tsx @@ -329,24 +329,52 @@ const CartPage = () => { if (typeof auth !== 'object' || !cart) return; setIsLoadDelete(true); - checkboxUpdateState.startUpdate('delete_operation'); // Use special ID for delete + checkboxUpdateState.startUpdate('delete_operation'); try { const itemsToDelete = cart.products.filter((item) => item.selected); const itemIdsToDelete = itemsToDelete.map((item) => item.id); + const cartIdsToDelete = itemsToDelete.map((item) => item.cart_id); + // Step 1: Delete from server first for (const item of itemsToDelete) { await deleteUserCart(auth.id, [item.cart_id]); } - // Remove deleted items from cookie - removeSelectedItemsFromCookie(itemIdsToDelete); + // Step 2: Update local cart state immediately (optimistic update) + const updatedProducts = cart.products.filter((item) => !item.selected); + const updatedCart = { + ...cart, + products: updatedProducts, + product_total: updatedProducts.length, + }; + updateCartItem(updatedCart); - await loadCart(auth.id); + // Step 3: Clean up cookies AFTER state update + removeSelectedItemsFromCookie(itemIdsToDelete); + removeCartItemsFromCookie(cartIdsToDelete); + + // Step 4: Reload from server to ensure consistency (but don't wait for it to complete UI update) + loadCart(auth.id) + .then(() => { + console.log('Cart reloaded from server'); + }) + .catch((error) => { + console.error('Error reloading cart:', error); + // If reload fails, at least we have the optimistic update + }); + + // Step 5: Trigger context refresh setRefreshCart(true); + + // Success feedback + toast.success('Item berhasil dihapus'); } catch (error) { console.error('Failed to delete cart items:', error); toast.error('Gagal menghapus item'); + + // If deletion failed, reload cart to restore proper state + loadCart(auth.id); } finally { setIsLoadDelete(false); checkboxUpdateState.endUpdate('delete_operation'); diff --git a/src-migrate/utils/cart.js b/src-migrate/utils/cart.js index f474cbde..1ddc5446 100644 --- a/src-migrate/utils/cart.js +++ b/src-migrate/utils/cart.js @@ -297,26 +297,10 @@ export const removeCartItemsFromCookie = (cartIds) => { * Hapus item selected dari cookie berdasarkan product IDs * @param {Array} productIds Array product IDs untuk dihapus */ -export const removeSelectedItemsFromCookie = (productIds) => { - try { - const selectedItems = getSelectedItemsFromCookie(); - - // Hapus dari selectedItems - productIds.forEach((productId) => { - if (selectedItems[productId] !== undefined) { - delete selectedItems[productId]; - } - }); - - // Simpan kembali ke cookie - setSelectedItemsToCookie(selectedItems); - - return { selectedItems }; - } catch (error) { - console.error('Error removing selected items from cookie:', error); - return {}; - } -}; +/** + * Hapus item selected dari cookie berdasarkan product IDs dan juga hapus dari cart data + * @param {Array} productIds Array product IDs untuk dihapus + */ /** * Force reset semua selected items ke unselected state @@ -386,3 +370,47 @@ export const syncSelectedItemsWithCookie = (cartProducts) => { // Export the checkbox update state for use in components export { checkboxUpdateState }; + +/** + * Hapus item selected dari cookie berdasarkan product IDs dan juga hapus dari cart data + * @param {Array} productIds Array product IDs untuk dihapus + */ +/** + * Hapus item selected dari cookie berdasarkan product IDs dan juga hapus dari cart data + * @param {Array} productIds Array product IDs untuk dihapus + */ +export const removeSelectedItemsFromCookie = (productIds) => { + try { + const selectedItems = getSelectedItemsFromCookie(); + const cartData = getCartDataFromCookie(); + const cartIdsToRemove = []; + + // Find cart IDs that match the product IDs + for (const cartId in cartData) { + if (productIds.includes(cartData[cartId].id)) { + cartIdsToRemove.push(cartId); + } + } + + // Remove from selectedItems + productIds.forEach((productId) => { + if (selectedItems[productId] !== undefined) { + delete selectedItems[productId]; + } + }); + + // Remove from cartData + cartIdsToRemove.forEach((cartId) => { + delete cartData[cartId]; + }); + + // Save both cookies + setSelectedItemsToCookie(selectedItems); + setCartDataToCookie(cartData); + + return { selectedItems, cartData }; + } catch (error) { + console.error('Error removing selected items from cookie:', error); + return {}; + } +}; -- cgit v1.2.3 From 582f00294ba924b105c789b43e6e92beaf99260f Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 23 May 2025 10:31:38 +0700 Subject: remove checkboxstate utils --- src-migrate/utils/checkBoxState.js | 89 -------------------------------------- 1 file changed, 89 deletions(-) delete mode 100644 src-migrate/utils/checkBoxState.js diff --git a/src-migrate/utils/checkBoxState.js b/src-migrate/utils/checkBoxState.js deleted file mode 100644 index 0c58321f..00000000 --- a/src-migrate/utils/checkBoxState.js +++ /dev/null @@ -1,89 +0,0 @@ -// ~/modules/cart/utils/checkboxUpdateState.js - -/** - * Enhanced state manager for checkbox updates - * Tracks both global update state and individual checkbox update states - */ - -// Track the number of ongoing updates -let updateCount = 0; -let listeners = []; - -// Track which checkboxes are currently updating by ID -let updatingCheckboxIds = new Set(); - -const checkboxUpdateState = { - // Check if any checkboxes are currently updating (for buttons) - isUpdating: () => updateCount > 0, - - // Check if a specific checkbox is updating (for disabling just that checkbox) - isCheckboxUpdating: (itemId) => updatingCheckboxIds.has(itemId.toString()), - - // Start an update for a specific checkbox - startUpdate: (itemId = null) => { - updateCount++; - - // If an item ID is provided, mark it as updating - if (itemId !== null) { - updatingCheckboxIds.add(itemId.toString()); - } - - notifyListeners(); - return updateCount; - }, - - // End an update for a specific checkbox - endUpdate: (itemId = null) => { - updateCount = Math.max(0, updateCount - 1); - - // If an item ID is provided, remove it from updating set - if (itemId !== null) { - updatingCheckboxIds.delete(itemId.toString()); - } - - notifyListeners(); - return updateCount; - }, - - // Reset the update counter and clear all updating checkboxes - reset: () => { - updateCount = 0; - updatingCheckboxIds.clear(); - notifyListeners(); - }, - - // Get IDs of all checkboxes currently updating (for debugging) - getUpdatingCheckboxIds: () => [...updatingCheckboxIds], - - // Add a listener function to be called when update state changes - addListener: (callback) => { - if (typeof callback === 'function') { - listeners.push(callback); - - // Immediately call with current state - callback(updateCount > 0); - } - }, - - // Remove a listener - removeListener: (callback) => { - listeners = listeners.filter((listener) => listener !== callback); - }, - - // Get current counter (for debugging) - getUpdateCount: () => updateCount, -}; - -// Private function to notify all listeners of state changes -function notifyListeners() { - const isUpdating = updateCount > 0; - listeners.forEach((listener) => { - try { - listener(isUpdating); - } catch (error) { - console.error('Error in checkbox update state listener:', error); - } - }); -} - -export default checkboxUpdateState; -- cgit v1.2.3 From 31e27d92a1965f02b644a7d905366d7180d33c36 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 26 May 2025 10:38:39 +0700 Subject: add checkboxstate --- src-migrate/utils/checkBoxState.js | 89 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src-migrate/utils/checkBoxState.js diff --git a/src-migrate/utils/checkBoxState.js b/src-migrate/utils/checkBoxState.js new file mode 100644 index 00000000..0c58321f --- /dev/null +++ b/src-migrate/utils/checkBoxState.js @@ -0,0 +1,89 @@ +// ~/modules/cart/utils/checkboxUpdateState.js + +/** + * Enhanced state manager for checkbox updates + * Tracks both global update state and individual checkbox update states + */ + +// Track the number of ongoing updates +let updateCount = 0; +let listeners = []; + +// Track which checkboxes are currently updating by ID +let updatingCheckboxIds = new Set(); + +const checkboxUpdateState = { + // Check if any checkboxes are currently updating (for buttons) + isUpdating: () => updateCount > 0, + + // Check if a specific checkbox is updating (for disabling just that checkbox) + isCheckboxUpdating: (itemId) => updatingCheckboxIds.has(itemId.toString()), + + // Start an update for a specific checkbox + startUpdate: (itemId = null) => { + updateCount++; + + // If an item ID is provided, mark it as updating + if (itemId !== null) { + updatingCheckboxIds.add(itemId.toString()); + } + + notifyListeners(); + return updateCount; + }, + + // End an update for a specific checkbox + endUpdate: (itemId = null) => { + updateCount = Math.max(0, updateCount - 1); + + // If an item ID is provided, remove it from updating set + if (itemId !== null) { + updatingCheckboxIds.delete(itemId.toString()); + } + + notifyListeners(); + return updateCount; + }, + + // Reset the update counter and clear all updating checkboxes + reset: () => { + updateCount = 0; + updatingCheckboxIds.clear(); + notifyListeners(); + }, + + // Get IDs of all checkboxes currently updating (for debugging) + getUpdatingCheckboxIds: () => [...updatingCheckboxIds], + + // Add a listener function to be called when update state changes + addListener: (callback) => { + if (typeof callback === 'function') { + listeners.push(callback); + + // Immediately call with current state + callback(updateCount > 0); + } + }, + + // Remove a listener + removeListener: (callback) => { + listeners = listeners.filter((listener) => listener !== callback); + }, + + // Get current counter (for debugging) + getUpdateCount: () => updateCount, +}; + +// Private function to notify all listeners of state changes +function notifyListeners() { + const isUpdating = updateCount > 0; + listeners.forEach((listener) => { + try { + listener(isUpdating); + } catch (error) { + console.error('Error in checkbox update state listener:', error); + } + }); +} + +export default checkboxUpdateState; -- cgit v1.2.3 From 8ef5d44ff4aaf3f8826ffbb28e4466451a750af1 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 26 May 2025 11:52:18 +0700 Subject: push --- src-migrate/utils/checkBoxState.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-migrate/utils/checkBoxState.js b/src-migrate/utils/checkBoxState.js index 0c58321f..8e65ea66 100644 --- a/src-migrate/utils/checkBoxState.js +++ b/src-migrate/utils/checkBoxState.js @@ -13,7 +13,7 @@ let listeners = []; let updatingCheckboxIds = new Set(); const checkboxUpdateState = { - // Check if any checkboxes are currently updating (for buttons) + // Check if any checkboxes are currently updating (for buttons quotation and checkout) isUpdating: () => updateCount > 0, // Check if a specific checkbox is updating (for disabling just that checkbox) -- cgit v1.2.3 From 1a247903bf7bb87e0a43b4e5e338ea67ec90e6de Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 26 May 2025 14:39:39 +0700 Subject: cleaning code --- src-migrate/modules/cart/components/ItemSelect.tsx | 4 ++-- src-migrate/utils/cart.js | 7 +++---- src-migrate/utils/checkBoxState.js | 20 +++++++++----------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx index f580f81d..00c7be43 100644 --- a/src-migrate/modules/cart/components/ItemSelect.tsx +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -68,8 +68,8 @@ const CartItemSelect = ({ item }: Props) => { // Fall back to server state if no cookie exists setLocalSelected(item.selected); - // Save this state to cookie for future use - updateSelectedItemInCookie(item.id, item.selected, false); // don't notify for initial sync + // Save state to cookie for future + updateSelectedItemInCookie(item.id, item.selected, false); } }, [item.id, item.selected, localSelected, cart, updateCartItem, isUpdating]); diff --git a/src-migrate/utils/cart.js b/src-migrate/utils/cart.js index 1ddc5446..ebd771e5 100644 --- a/src-migrate/utils/cart.js +++ b/src-migrate/utils/cart.js @@ -117,7 +117,7 @@ export const syncCartWithCookie = (cart) => { try { if (!cart || !cart.products) return { needsUpdate: false }; - // Transform data dari API ke format cookie + // Transform data API ke cookie const serverCartData = transformCartItemsForCookie(cart.products); // Ambil data lama dari cookie @@ -126,7 +126,7 @@ export const syncCartWithCookie = (cart) => { // Ambil selected status dari cookie const selectedItems = getSelectedItemsFromCookie(); - // Gabungkan data cart, prioritaskan data server + // Gabungkan data cart, (prioritize data server) const mergedCartData = { ...existingCartData, ...serverCartData }; // Periksa apakah ada perbedaan status selected @@ -142,12 +142,11 @@ export const syncCartWithCookie = (cart) => { item.selected = selectedItems[item.id]; } } else if (item.id) { - // Jika tidak ada di cookie, tambahkan dari cart selectedItems[item.id] = item.selected; } } - // Simpan kembali ke cookie + // Simpan ke cookie setCartDataToCookie(mergedCartData); setSelectedItemsToCookie(selectedItems); diff --git a/src-migrate/utils/checkBoxState.js b/src-migrate/utils/checkBoxState.js index 8e65ea66..2b527f36 100644 --- a/src-migrate/utils/checkBoxState.js +++ b/src-migrate/utils/checkBoxState.js @@ -1,5 +1,3 @@ -// ~/modules/cart/utils/checkboxUpdateState.js - /** * Enhanced state manager for checkbox updates * Tracks both global update state and individual checkbox update states @@ -53,17 +51,17 @@ const checkboxUpdateState = { }, // Get IDs of all checkboxes currently updating (for debugging) - getUpdatingCheckboxIds: () => [...updatingCheckboxIds], + // getUpdatingCheckboxIds: () => [...updatingCheckboxIds], - // Add a listener function to be called when update state changes - addListener: (callback) => { - if (typeof callback === 'function') { - listeners.push(callback); + // // Add a listener function to be called when update state changes + // addListener: (callback) => { + // if (typeof callback === 'function') { + // listeners.push(callback); - // Immediately call with current state - callback(updateCount > 0); - } - }, + // // Immediately call with current state + // callback(updateCount > 0); + // } + // }, // Remove a listener removeListener: (callback) => { -- cgit v1.2.3 From cca6d803fc4db729865def23004ab1c4bd279e24 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 26 May 2025 15:10:41 +0700 Subject: error checkboxstate --- src-migrate/utils/checkBoxState.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src-migrate/utils/checkBoxState.js b/src-migrate/utils/checkBoxState.js index 2b527f36..8f7236c3 100644 --- a/src-migrate/utils/checkBoxState.js +++ b/src-migrate/utils/checkBoxState.js @@ -53,15 +53,15 @@ const checkboxUpdateState = { // Get IDs of all checkboxes currently updating (for debugging) // getUpdatingCheckboxIds: () => [...updatingCheckboxIds], - // // Add a listener function to be called when update state changes - // addListener: (callback) => { - // if (typeof callback === 'function') { - // listeners.push(callback); - - // // Immediately call with current state - // callback(updateCount > 0); - // } - // }, + // Add a listener function to be called when update state changes + addListener: (callback) => { + if (typeof callback === 'function') { + listeners.push(callback); + + // Immediately call with current state + callback(updateCount > 0); + } + }, // Remove a listener removeListener: (callback) => { -- cgit v1.2.3 From 3feaad9127ff429b27f0eb69fa6ea539de2f2e8c Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 26 May 2025 20:00:17 +0700 Subject: Cleaning code --- src-migrate/modules/cart/components/ItemAction.tsx | 125 ++++++- src-migrate/modules/cart/components/ItemSelect.tsx | 76 ++--- src-migrate/modules/cart/stores/useCartStore.ts | 198 ++++++----- src-migrate/pages/shop/cart/index.tsx | 380 ++++++++------------- src-migrate/utils/cart.js | 40 +++ src-migrate/utils/checkBoxState.js | 141 ++++---- 6 files changed, 477 insertions(+), 483 deletions(-) diff --git a/src-migrate/modules/cart/components/ItemAction.tsx b/src-migrate/modules/cart/components/ItemAction.tsx index eea0cbe9..4dcebd9e 100644 --- a/src-migrate/modules/cart/components/ItemAction.tsx +++ b/src-migrate/modules/cart/components/ItemAction.tsx @@ -15,7 +15,11 @@ import { useProductCartContext } from '@/contexts/ProductCartContext'; import { removeSelectedItemsFromCookie, removeCartItemsFromCookie, + quantityUpdateState, + getCartDataFromCookie, + setCartDataToCookie, } from '~/utils/cart'; + import { toast } from 'react-hot-toast'; type Props = { @@ -30,25 +34,24 @@ const CartItemAction = ({ item }: Props) => { const [quantity, setQuantity] = useState(item.quantity); - const { loadCart, cart, updateCartItem } = useCartStore(); // TAMBAHKAN cart dan updateCartItem + const { loadCart, cart, updateCartItem } = useCartStore(); const limitQty = item.limit_qty?.transaction || 0; - // PERBAIKI FUNCTION INI const handleDelete = async () => { if (typeof auth !== 'object') return; setIsLoadDelete(true); try { - // Step 1: Delete from server + // Delete from server await deleteUserCart(auth.id, [item.cart_id]); - // Step 2: Clean up cookies IMMEDIATELY + // Clean up cookies immediately removeSelectedItemsFromCookie([item.id]); removeCartItemsFromCookie([item.cart_id]); - // Step 3: Update local cart state optimistically + // Update local cart state optimistically if (cart) { const updatedProducts = cart.products.filter( (product) => product.id !== item.id @@ -61,7 +64,7 @@ const CartItemAction = ({ item }: Props) => { updateCartItem(updatedCart); } - // Step 4: Reload from server to ensure consistency + // Reload from server and refresh context await loadCart(auth.id); setRefreshCart(true); @@ -77,12 +80,47 @@ const CartItemAction = ({ item }: Props) => { } }; + const updateQuantityInCookie = (productId, cartId, newQuantity) => { + try { + const cartData = getCartDataFromCookie(); + let itemFound = false; + + // Find item by cart_id key or search within objects + if (cartData[cartId]) { + cartData[cartId].quantity = newQuantity; + itemFound = true; + } else { + // Search by product id or cart_id within objects + for (const key in cartData) { + const item = cartData[key]; + if (item.id === productId || item.cart_id === cartId) { + item.quantity = newQuantity; + itemFound = true; + break; + } + } + } + + if (itemFound) { + setCartDataToCookie(cartData); + return true; + } + + return false; + } catch (error) { + console.error('Error updating quantity in cookie:', error); + return false; + } + }; + const decreaseQty = () => { setQuantity((quantity) => (quantity -= 1)); }; + const increaseQty = () => { setQuantity((quantity) => (quantity += 1)); }; + const debounceQty = useDebounce(quantity, 1000); useEffect(() => { @@ -93,18 +131,73 @@ const CartItemAction = ({ item }: Props) => { useEffect(() => { const updateCart = async () => { if (typeof auth !== 'object' || isNaN(debounceQty)) return; + if (debounceQty === item.quantity) return; + quantityUpdateState.startUpdate(item.id); setIsLoadQuantity(true); - await upsertUserCart({ - userId: auth.id, - type: item.cart_type, - id: item.id, - qty: debounceQty, - selected: item.selected, - }); - await loadCart(auth.id); - setIsLoadQuantity(false); + + try { + // Update cookie immediately for responsive UI + updateQuantityInCookie(item.id, item.cart_id, debounceQty); + + // Update local cart state optimistically + if (cart) { + const updatedProducts = cart.products.map((product) => + product.id === item.id + ? { ...product, quantity: debounceQty } + : product + ); + const updatedCart = { + ...cart, + products: updatedProducts, + }; + updateCartItem(updatedCart); + } + + // Send update to server + await upsertUserCart({ + userId: auth.id, + type: item.cart_type, + id: item.id, + qty: debounceQty, + selected: item.selected, + }); + + // Reload from server to ensure consistency + await loadCart(auth.id); + + // Re-update cookie if server reload overwrote it + const currentCookieData = getCartDataFromCookie(); + let needsReUpdate = false; + + for (const key in currentCookieData) { + const cookieItem = currentCookieData[key]; + if ( + (cookieItem.id === item.id || + cookieItem.cart_id === item.cart_id) && + cookieItem.quantity !== debounceQty + ) { + needsReUpdate = true; + break; + } + } + + if (needsReUpdate) { + updateQuantityInCookie(item.id, item.cart_id, debounceQty); + } + } catch (error) { + console.error('Error updating quantity:', error); + toast.error('Gagal mengupdate quantity'); + + // Revert changes on error + updateQuantityInCookie(item.id, item.cart_id, item.quantity); + loadCart(auth.id); + } finally { + setIsLoadQuantity(false); + quantityUpdateState.endUpdate(item.id); + } }; + updateCart(); //eslint-disable-next-line react-hooks/exhaustive-deps }, [debounceQty]); @@ -156,4 +249,4 @@ const CartItemAction = ({ item }: Props) => { ); }; -export default CartItemAction; +export default CartItemAction; \ No newline at end of file diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx index 00c7be43..8dbfe2bc 100644 --- a/src-migrate/modules/cart/components/ItemSelect.tsx +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -20,124 +20,98 @@ const CartItemSelect = ({ item }: Props) => { const { updateCartItem, cart, loadCart } = useCartStore(); const [isUpdating, setIsUpdating] = useState(false); const [localSelected, setLocalSelected] = useState(item.selected); - const [isGlobalUpdating, setIsGlobalUpdating] = useState(false); // Subscribe to global checkbox update state useEffect(() => { const handleUpdateStateChange = (isUpdating) => { - setIsGlobalUpdating(isUpdating); + // This component doesn't need to react to global state changes + // Individual checkboxes are managed independently }; checkboxUpdateState.addListener(handleUpdateStateChange); - - return () => { - checkboxUpdateState.removeListener(handleUpdateStateChange); - }; + return () => checkboxUpdateState.removeListener(handleUpdateStateChange); }, []); - // Initialize local state from cookie or server + // Sync local state with cookie and server data useEffect(() => { - if (isUpdating) return; // Skip if we're currently updating + if (isUpdating) return; - // Check cookie first const selectedItems = getSelectedItemsFromCookie(); const storedState = selectedItems[item.id]; if (storedState !== undefined) { - // Only update local state if it differs from current state + // Update local state if cookie differs if (localSelected !== storedState) { setLocalSelected(storedState); } - // If cookie state differs from server state and we're not in the middle of an update, - // synchronize the item state with cookie - if (storedState !== item.selected) { - // Update cart item silently to match cookie - if (cart) { - const updatedCartItems = cart.products.map((cartItem) => - cartItem.id === item.id - ? { ...cartItem, selected: storedState } - : cartItem - ); - - const updatedCart = { ...cart, products: updatedCartItems }; - updateCartItem(updatedCart); - } + // Sync cart state with cookie if needed + if (storedState !== item.selected && cart) { + const updatedCartItems = cart.products.map((cartItem) => + cartItem.id === item.id + ? { ...cartItem, selected: storedState } + : cartItem + ); + updateCartItem({ ...cart, products: updatedCartItems }); } } else { - // Fall back to server state if no cookie exists + // Initialize cookie with server state setLocalSelected(item.selected); - - // Save state to cookie for future updateSelectedItemInCookie(item.id, item.selected, false); } }, [item.id, item.selected, localSelected, cart, updateCartItem, isUpdating]); const handleChange = useCallback( async (e: React.ChangeEvent) => { - if (typeof auth !== 'object' || !cart || isUpdating) { - return; - } + if (typeof auth !== 'object' || !cart || isUpdating) return; const newSelectedState = e.target.checked; - // Update local state immediately for responsiveness + // Update local state immediately setLocalSelected(newSelectedState); setIsUpdating(true); - - // Start the update - notify global state with this checkbox's ID checkboxUpdateState.startUpdate(item.id); try { - // The cookie update is now handled inside the function with notification - updateSelectedItemInCookie(item.id, newSelectedState, false); // We already started above + // Update cookie immediately for responsive UI + updateSelectedItemInCookie(item.id, newSelectedState, false); - // Update cart state immediately for UI responsiveness + // Update cart state optimistically const updatedCartItems = cart.products.map((cartItem) => cartItem.id === item.id ? { ...cartItem, selected: newSelectedState } : cartItem ); + updateCartItem({ ...cart, products: updatedCartItems }); - const updatedCart = { ...cart, products: updatedCartItems }; - updateCartItem(updatedCart); - - // Save to server + // Send update to server await upsertUserCart({ userId: auth.id, type: item.cart_type, id: item.id, qty: item.quantity, selected: newSelectedState, - // purchase_tax_id: item.purchase_tax_id || null, // Ensure null for numeric fields + purchase_tax_id: item.purchase_tax_id || null, }); - // Reload cart to ensure consistency + // Reload cart for consistency await loadCart(auth.id); } catch (error) { console.error('Failed to update item selection:', error); toast.error('Gagal memperbarui pilihan barang'); - // Revert local state on error + // Revert changes on error setLocalSelected(!newSelectedState); - - // Revert cookie change updateSelectedItemInCookie(item.id, !newSelectedState, false); - - // Reload cart to get server state loadCart(auth.id); } finally { setIsUpdating(false); - - // End the update - notify global state with this checkbox's ID checkboxUpdateState.endUpdate(item.id); } }, [auth, cart, item, isUpdating, updateCartItem, loadCart] ); - // Determine if THIS specific checkbox should be disabled - only disable - // if this specific checkbox is updating const isDisabled = isUpdating || checkboxUpdateState.isCheckboxUpdating(item.id); diff --git a/src-migrate/modules/cart/stores/useCartStore.ts b/src-migrate/modules/cart/stores/useCartStore.ts index d211304a..dc47b011 100644 --- a/src-migrate/modules/cart/stores/useCartStore.ts +++ b/src-migrate/modules/cart/stores/useCartStore.ts @@ -5,9 +5,6 @@ import { syncCartWithCookie, getCartDataFromCookie, getSelectedItemsFromCookie, - updateSelectedItemInCookie, - setAllSelectedInCookie, - removeCartItemsFromCookie, forceResetAllSelectedItems, } from '~/utils/cart'; @@ -26,8 +23,8 @@ type State = { type Action = { loadCart: (userId: number) => Promise; updateCartItem: (updateCart: CartProps) => void; - syncCartWithCookieAndUpdate: (cart: CartProps) => { needsUpdate: boolean }; forceResetSelection: () => void; + clearCart: () => void; }; export const useCartStore = create((set, get) => ({ @@ -42,154 +39,151 @@ export const useCartStore = create((set, get) => ({ }, loadCart: async (userId) => { - if (get().isLoadCart === true) return; + if (get().isLoadCart) return; set({ isLoadCart: true }); + try { - // Fetch cart from API const cart: CartProps = (await getUserCart(userId)) as CartProps; - // Sync with cookie and get updated data if needed - const { needsUpdate } = get().syncCartWithCookieAndUpdate(cart); - - // If no update needed from cookie, just set the cart directly - if (!needsUpdate) { + // Sync with cookie data + const syncResult = syncCartWithCookie(cart); + + if (syncResult?.needsUpdate && cart.products) { + const selectedItems = getSelectedItemsFromCookie(); + + const updatedCart = { + ...cart, + products: cart.products.map((item) => ({ + ...item, + selected: + selectedItems[item.id] !== undefined + ? selectedItems[item.id] + : item.selected, + })), + }; + + set({ cart: updatedCart }); + } else { set({ cart }); } - // Update summary with current cart + // Update summary const summary = computeSummary(get().cart!); set({ summary }); } catch (error) { - console.error('Failed to load cart from API:', error); - - // Fallback to cookie if API fails - try { - const cartData = getCartDataFromCookie(); - if (Object.keys(cartData).length > 0) { - // Transform cart data from cookie to expected format - const products = Object.values(cartData).map((item) => ({ - cart_id: item.cart_id, - id: item.id, - cart_type: item.cart_type, - product_id: item.product?.id, - product_name: item.product?.name, - program_line_id: item.program_line?.id, - program_line_name: item.program_line?.name, - quantity: item.quantity, - selected: item.selected, - price: item.price, - package_price: item.package_price, - source: item.source, - })); - - const fallbackCart: CartProps = { - product_total: products.length, - products, - }; - - set({ cart: fallbackCart }); - const summary = computeSummary(fallbackCart); - set({ summary }); - } - } catch (cookieError) { - console.error('Failed to fallback to cookie:', cookieError); - } + console.error('Failed to load cart:', error); + + // Fallback to cookie data + await handleFallbackFromCookie(); } finally { set({ isLoadCart: false }); } }, updateCartItem: (updatedCart) => { - const cart = get().cart; - if (!cart) return; - set({ cart: updatedCart }); - - // Sync updated cart with cookie syncCartWithCookie(updatedCart); const summary = computeSummary(updatedCart); set({ summary }); }, - syncCartWithCookieAndUpdate: (cart) => { - if (!cart) return { needsUpdate: false }; - - // Sync cart with cookie - const result = syncCartWithCookie(cart); - - // If we need to update the cart based on cookie data - if (result.needsUpdate && cart.products) { - // Create updated cart with selections from cookie - const selectedItems = getSelectedItemsFromCookie(); - - const updatedCart = { - ...cart, - products: cart.products.map((item) => ({ - ...item, - selected: - selectedItems[item.id] !== undefined - ? selectedItems[item.id] - : item.selected, - })), - }; - - // Update the store - set({ cart: updatedCart }); - } - - return result; - }, - forceResetSelection: () => { const { cart } = get(); if (!cart) return; - // Reset all selections in cookie forceResetAllSelectedItems(); - // Update the cart in state const updatedCart = { ...cart, - products: cart.products.map((item) => ({ - ...item, - selected: false, - })), + products: cart.products.map((item) => ({ ...item, selected: false })), }; set({ cart: updatedCart }); - // Update summary const summary = computeSummary(updatedCart); set({ summary }); }, + + clearCart: () => { + set({ + cart: null, + summary: { + subtotal: 0, + discount: 0, + total: 0, + tax: 0, + grandTotal: 0, + }, + }); + }, })); +// Helper function for cookie fallback +const handleFallbackFromCookie = async () => { + try { + const cartData = getCartDataFromCookie(); + + if (Object.keys(cartData).length === 0) return; + + const products = Object.values(cartData).map(transformCookieItemToProduct); + + const fallbackCart: CartProps = { + product_total: products.length, + products, + }; + + useCartStore.setState({ cart: fallbackCart }); + + const summary = computeSummary(fallbackCart); + useCartStore.setState({ summary }); + } catch (error) { + console.error('Cookie fallback failed:', error); + } +}; + +// Helper function to transform cookie item to product format +const transformCookieItemToProduct = (item: any): CartItem => ({ + cart_id: item.cart_id, + id: item.id, + cart_type: item.cart_type, + product_id: item.product?.id, + product_name: item.product?.name, + program_line_id: item.program_line?.id, + program_line_name: item.program_line?.name, + quantity: item.quantity, + selected: item.selected, + price: item.price, + package_price: item.package_price, + source: item.source, +}); + +// Helper function to compute cart summary const computeSummary = (cart: CartProps) => { + if (!cart?.products) { + return { subtotal: 0, discount: 0, total: 0, grandTotal: 0, tax: 0 }; + } + + const PPN = parseFloat(process.env.NEXT_PUBLIC_PPN || '0'); let subtotal = 0; let discount = 0; - const PPN: number = process.env.NEXT_PUBLIC_PPN - ? parseFloat(process.env.NEXT_PUBLIC_PPN) - : 0; - - for (const item of cart?.products) { + for (const item of cart.products) { if (!item.selected) continue; - let price = 0; - if (item.cart_type === 'promotion') - price = (item?.package_price || 0) * item.quantity; - else if (item.cart_type === 'product') - price = item.price.price * item.quantity; + const price = + item.cart_type === 'promotion' + ? (item?.package_price || 0) * item.quantity + : item.price.price * item.quantity; subtotal += price; discount += price - item.price.price_discount * item.quantity; } - let total = subtotal - discount; - let grandTotal = total * PPN; - let tax = grandTotal - total; - // let grandTotal = total + tax; + + const total = subtotal - discount; + const grandTotal = total * (1 + PPN); + const tax = grandTotal - total; return { subtotal, discount, total, grandTotal, tax }; -}; +}; \ No newline at end of file diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx index 798ad318..03854d79 100644 --- a/src-migrate/pages/shop/cart/index.tsx +++ b/src-migrate/pages/shop/cart/index.tsx @@ -2,15 +2,7 @@ import style from './cart.module.css'; import React, { useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; -import { - Button, - Checkbox, - Spinner, - Tooltip, - Text, - Box, - Flex, -} from '@chakra-ui/react'; +import { Button, Checkbox, Spinner, Tooltip } from '@chakra-ui/react'; import { toast } from 'react-hot-toast'; import { useRouter } from 'next/router'; import { getAuth } from '~/libs/auth'; @@ -27,171 +19,65 @@ import { Trash2Icon } from 'lucide-react'; import { useProductCartContext } from '@/contexts/ProductCartContext'; import { getSelectedItemsFromCookie, - setSelectedItemsToCookie, syncSelectedItemsWithCookie, setAllSelectedInCookie, removeSelectedItemsFromCookie, - forceResetAllSelectedItems, + removeCartItemsFromCookie, checkboxUpdateState, + quantityUpdateState, } from '~/utils/cart'; -// Special ID for the "select all" checkbox const SELECT_ALL_ID = 'select_all_checkbox'; const CartPage = () => { const router = useRouter(); const auth = getAuth(); const [isStepApproval, setIsStepApproval] = useState(false); - const [isLoad, setIsLoad] = useState(false); const [isLoadDelete, setIsLoadDelete] = useState(false); const { loadCart, cart, summary, updateCartItem } = useCartStore(); const device = useDevice(); const { setRefreshCart } = useProductCartContext(); const [isTop, setIsTop] = useState(true); const [isUpdating, setIsUpdating] = useState(false); - const [isStateMismatch, setIsStateMismatch] = useState(false); const [isAnyCheckboxUpdating, setIsAnyCheckboxUpdating] = useState(false); + const [isAnyQuantityUpdating, setIsAnyQuantityUpdating] = useState(false); - // Subscribe to checkbox update state changes + // Subscribe to update state changes useEffect(() => { - const handleUpdateStateChange = (isUpdating) => { + const handleCheckboxUpdate = (isUpdating) => setIsAnyCheckboxUpdating(isUpdating); - }; + const handleQuantityUpdate = (isUpdating) => + setIsAnyQuantityUpdating(isUpdating); - // Add listener for checkbox update state changes - checkboxUpdateState.addListener(handleUpdateStateChange); + checkboxUpdateState.addListener(handleCheckboxUpdate); + quantityUpdateState.addListener(handleQuantityUpdate); - // Cleanup listener on component unmount return () => { - checkboxUpdateState.removeListener(handleUpdateStateChange); + checkboxUpdateState.removeListener(handleCheckboxUpdate); + quantityUpdateState.removeListener(handleQuantityUpdate); }; }, []); - // Function to check if cart state is inconsistent - const checkCartStateMismatch = () => { - if (!cart || !cart.products || isUpdating) return false; - - try { - // Ambil status selected dari cookie - const selectedItems = getSelectedItemsFromCookie(); - - // Periksa ketidaksesuaian antara UI dan cookie - // 1. Periksa item yang selected di UI tapi tidak di cookie - for (const product of cart.products) { - const cookieState = selectedItems[product.id]; - - // Jika ada di cookie tapi tidak sama dengan UI - if (cookieState !== undefined && cookieState !== product.selected) { - return true; - } - - // Jika tidak ada di cookie tapi selected di UI - if (cookieState === undefined && product.selected) { - return true; - } - } - - // 2. Periksa item yang selected di cookie tapi tidak ada di cart - for (const productId in selectedItems) { - const isSelected = selectedItems[productId]; - if (isSelected) { - // Cek apakah product id ini ada di cart - const productExists = cart.products.some( - (p) => p.id.toString() === productId.toString() - ); - if (!productExists) { - // Ada item selected di cookie yang tidak ada di cart - return true; - } - } - } - - return false; - } catch (error) { - console.error('Error checking cart state mismatch:', error); - return false; - } - }; - - // Function to reset all selected items when state is inconsistent - const handleResetSelections = () => { - if (!cart) return; - - setIsUpdating(true); - - try { - // Use the forceResetSelection function from the store - useCartStore.getState().forceResetSelection(); - - // Set state back to normal - setIsStateMismatch(false); - - // Give visual feedback - toast.success('Semua pilihan telah direset'); - - // Optional: Sync with server if needed - if (typeof auth === 'object') { - const updatePromises = cart.products.map((item) => - upsertUserCart({ - userId: auth.id, - type: item.cart_type, - id: item.id, - qty: item.quantity, - selected: false, - purchase_tax_id: item.purchase_tax_id || null, // Fix integer field issue - }) - ); - - Promise.all(updatePromises) - .then(() => loadCart(auth.id)) - .catch((error) => { - console.error('Error updating selections to server:', error); - }) - .finally(() => { - setIsUpdating(false); - }); - } else { - setIsUpdating(false); - } - } catch (error) { - console.error('Error resetting selections:', error); - setIsUpdating(false); - toast.error('Gagal mereset pilihan'); - } - }; - - // Check for state inconsistency - useEffect(() => { - if (!cart || !cart.products || isUpdating) return; - - const hasMismatch = checkCartStateMismatch(); - setIsStateMismatch(hasMismatch); - }, [cart, isUpdating]); - + // Handle scroll for sticky header styling useEffect(() => { - const handleScroll = () => { - setIsTop(window.scrollY < 200); - }; + const handleScroll = () => setIsTop(window.scrollY < 200); window.addEventListener('scroll', handleScroll); - return () => { - window.removeEventListener('scroll', handleScroll); - }; + return () => window.removeEventListener('scroll', handleScroll); }, []); + // Initialize cart and sync with cookies useEffect(() => { - const loadCartWithStorage = async () => { + const initializeCart = async () => { if (typeof auth === 'object' && !cart) { await loadCart(auth.id); setIsStepApproval(auth?.feature?.soApproval); - // Sync selected items with server data using cookies if (cart?.products) { const { items, needsUpdate } = syncSelectedItemsWithCookie( cart.products ); - // If there's a mismatch between cookie and server data, update the UI if (needsUpdate) { const updatedCart = { ...cart, @@ -207,37 +93,47 @@ const CartPage = () => { } }; - loadCartWithStorage(); - }, [auth, cart]); + initializeCart(); + }, [auth, cart, loadCart, updateCartItem]); + // Computed values const hasSelectedPromo = useMemo(() => { - if (!cart) return false; - return cart?.products?.some( - (item) => item.cart_type === 'promotion' && item.selected + return ( + cart?.products?.some( + (item) => item.cart_type === 'promotion' && item.selected + ) || false ); }, [cart]); const hasSelected = useMemo(() => { - if (!cart) return false; - return cart?.products?.some((item) => item.selected); + return cart?.products?.some((item) => item.selected) || false; }, [cart]); const hasSelectNoPrice = useMemo(() => { - if (!cart) return false; - return cart?.products?.some( - (item) => item.selected && item.price.price_discount === 0 + return ( + cart?.products?.some( + (item) => item.selected && item.price.price_discount === 0 + ) || false ); }, [cart]); const hasSelectedAll = useMemo(() => { - if (!cart || !Array.isArray(cart.products)) return false; - return ( - cart.products.length > 0 && cart.products.every((item) => item.selected) - ); + if (!cart?.products?.length) return false; + return cart.products.every((item) => item.selected); }, [cart]); + // Button states + const areButtonsDisabled = + isUpdating || + isLoadDelete || + isAnyCheckboxUpdating || + isAnyQuantityUpdating; + const isSelectAllDisabled = + isUpdating || checkboxUpdateState.isCheckboxUpdating(SELECT_ALL_ID); + + // Handlers const handleCheckout = () => { - if (isUpdating || isLoadDelete || isAnyCheckboxUpdating) { + if (areButtonsDisabled) { toast.error('Harap tunggu pembaruan selesai'); return; } @@ -245,7 +141,7 @@ const CartPage = () => { }; const handleQuotation = () => { - if (isUpdating || isLoadDelete || isAnyCheckboxUpdating) { + if (areButtonsDisabled) { toast.error('Harap tunggu pembaruan selesai'); return; } @@ -256,16 +152,14 @@ const CartPage = () => { } }; - const handleChange = async (e: React.ChangeEvent) => { - if (cart && !isUpdating && typeof auth === 'object') { - const newSelectedState = !hasSelectedAll; + const handleSelectAll = async (e: React.ChangeEvent) => { + if (!cart || isUpdating || typeof auth !== 'object') return; - // Set updating flag - setIsUpdating(true); - - // Notify checkbox update state system with the special select all ID - checkboxUpdateState.startUpdate(SELECT_ALL_ID); + const newSelectedState = !hasSelectedAll; + setIsUpdating(true); + checkboxUpdateState.startUpdate(SELECT_ALL_ID); + try { // Update UI immediately const updatedCart = { ...cart, @@ -274,54 +168,47 @@ const CartPage = () => { selected: newSelectedState, })), }; - updateCartItem(updatedCart); - // Get all product IDs in cart + // Update cookies const productIds = cart.products.map((item) => item.id); + setAllSelectedInCookie(productIds, newSelectedState, false); + + // Update server + const updatePromises = cart.products.map((item) => + upsertUserCart({ + userId: auth.id, + type: item.cart_type, + id: item.id, + qty: item.quantity, + selected: newSelectedState, + purchase_tax_id: item.purchase_tax_id || null, + }) + ); - // Update cookies immediately for responsive UI - setAllSelectedInCookie(productIds, newSelectedState, false); // We're already notifying - - try { - // Update all items on server in background - const updatePromises = cart.products.map((item) => - upsertUserCart({ - userId: auth.id, - type: item.cart_type, - id: item.id, - qty: item.quantity, - selected: newSelectedState, - purchase_tax_id: item.purchase_tax_id || null, // Fix integer field issue - }) - ); - - await Promise.all(updatePromises); - await loadCart(auth.id); - } catch (error) { - console.error('Error updating select all:', error); - - // Revert changes on error - const revertedCart = { - ...cart, - products: cart.products.map((item) => ({ - ...item, - selected: !newSelectedState, - })), - }; - - updateCartItem(revertedCart); - - // Revert cookies - setAllSelectedInCookie(productIds, !newSelectedState, false); - - toast.error('Gagal memperbarui pilihan'); - } finally { - setIsUpdating(false); + await Promise.all(updatePromises); + await loadCart(auth.id); + } catch (error) { + console.error('Error updating select all:', error); + toast.error('Gagal memperbarui pilihan'); - // End update notification - checkboxUpdateState.endUpdate(SELECT_ALL_ID); - } + // Revert on error + const revertedCart = { + ...cart, + products: cart.products.map((item) => ({ + ...item, + selected: !newSelectedState, + })), + }; + updateCartItem(revertedCart); + setAllSelectedInCookie( + cart.products.map((item) => item.id), + !newSelectedState, + false + ); + } finally { + setIsUpdating(false); + checkboxUpdateState.endUpdate(SELECT_ALL_ID); } }; @@ -336,12 +223,12 @@ const CartPage = () => { const itemIdsToDelete = itemsToDelete.map((item) => item.id); const cartIdsToDelete = itemsToDelete.map((item) => item.cart_id); - // Step 1: Delete from server first + // Delete from server for (const item of itemsToDelete) { await deleteUserCart(auth.id, [item.cart_id]); } - // Step 2: Update local cart state immediately (optimistic update) + // Update local state optimistically const updatedProducts = cart.products.filter((item) => !item.selected); const updatedCart = { ...cart, @@ -350,30 +237,20 @@ const CartPage = () => { }; updateCartItem(updatedCart); - // Step 3: Clean up cookies AFTER state update + // Clean up cookies removeSelectedItemsFromCookie(itemIdsToDelete); removeCartItemsFromCookie(cartIdsToDelete); - // Step 4: Reload from server to ensure consistency (but don't wait for it to complete UI update) - loadCart(auth.id) - .then(() => { - console.log('Cart reloaded from server'); - }) - .catch((error) => { - console.error('Error reloading cart:', error); - // If reload fails, at least we have the optimistic update - }); + // Reload from server + loadCart(auth.id).catch((error) => + console.error('Error reloading cart:', error) + ); - // Step 5: Trigger context refresh setRefreshCart(true); - - // Success feedback toast.success('Item berhasil dihapus'); } catch (error) { console.error('Failed to delete cart items:', error); toast.error('Gagal menghapus item'); - - // If deletion failed, reload cart to restore proper state loadCart(auth.id); } finally { setIsLoadDelete(false); @@ -381,16 +258,41 @@ const CartPage = () => { } }; - // Check if buttons should be disabled - const areButtonsDisabled = - isUpdating || isLoadDelete || isAnyCheckboxUpdating; + // Tooltip messages + const getTooltipMessage = () => { + if (isAnyQuantityUpdating) return 'Harap tunggu update quantity selesai'; + if (isAnyCheckboxUpdating) return 'Harap tunggu pembaruan checkbox selesai'; + if (isLoadDelete) return 'Harap tunggu penghapusan selesai'; + if (isUpdating) return 'Harap tunggu pembaruan selesai'; + return ''; + }; - // Only disable the select all checkbox if it specifically is updating - const isSelectAllDisabled = - isUpdating || checkboxUpdateState.isCheckboxUpdating(SELECT_ALL_ID); + const getQuotationTooltip = () => { + const baseMessage = getTooltipMessage(); + if (baseMessage) return baseMessage; + if (hasSelectedPromo) return 'Barang promo tidak dapat dibuat quotation'; + if (!hasSelected) return 'Tidak ada item yang dipilih'; + return ''; + }; + + const getCheckoutTooltip = () => { + const baseMessage = getTooltipMessage(); + if (baseMessage) return baseMessage; + if (!hasSelected) return 'Tidak ada item yang dipilih'; + if (hasSelectNoPrice) return 'Terdapat item yang tidak ada harga'; + return ''; + }; + + const getDeleteTooltip = () => { + const baseMessage = getTooltipMessage(); + if (baseMessage) return baseMessage; + if (!hasSelected) return 'Tidak ada item yang dipilih'; + return ''; + }; return ( <> + {/* Sticky Header */}
    { colorScheme='red' size='lg' isChecked={hasSelectedAll} - onChange={handleChange} + onChange={handleSelectAll} isDisabled={isSelectAllDisabled} opacity={isSelectAllDisabled ? 0.5 : 1} cursor={isSelectAllDisabled ? 'not-allowed' : 'pointer'} @@ -422,13 +324,9 @@ const CartPage = () => { {hasSelectedAll ? 'Uncheck all' : 'Select all'}

    +
    - +
    + {/* Main Content */}
    @@ -485,6 +384,8 @@ const CartPage = () => { )}
    + + {/* Cart Summary */}
    { : style['summary-buttons'] } > - + + {!isStepApproval && ( - + @@ -562,4 +452,4 @@ const CartPage = () => { ); }; -export default CartPage; +export default CartPage; \ No newline at end of file diff --git a/src-migrate/utils/cart.js b/src-migrate/utils/cart.js index ebd771e5..4bdee49a 100644 --- a/src-migrate/utils/cart.js +++ b/src-migrate/utils/cart.js @@ -413,3 +413,43 @@ export const removeSelectedItemsFromCookie = (productIds) => { return {}; } }; + +class QuantityUpdateState { + constructor() { + this.updateItems = new Set(); + this.listeners = new Set(); + } + + startUpdate(itemId) { + this.updateItems.add(itemId); + this.notifyListeners(); + } + + endUpdate(itemId) { + this.updateItems.delete(itemId); + this.notifyListeners(); + } + + isAnyQuantityUpdating() { + return this.updateItems.size > 0; + } + + isItemUpdating(itemId) { + return this.updateItems.has(itemId); + } + + addListener(callback) { + this.listeners.add(callback); + } + + removeListener(callback) { + this.listeners.delete(callback); + } + + notifyListeners() { + const isUpdating = this.isAnyQuantityUpdating(); + this.listeners.forEach(callback => callback(isUpdating)); + } +} + +export const quantityUpdateState = new QuantityUpdateState(); \ No newline at end of file diff --git a/src-migrate/utils/checkBoxState.js b/src-migrate/utils/checkBoxState.js index 8f7236c3..9568c321 100644 --- a/src-migrate/utils/checkBoxState.js +++ b/src-migrate/utils/checkBoxState.js @@ -1,87 +1,90 @@ /** - * Enhanced state manager for checkbox updates - * Tracks both global update state and individual checkbox update states + * State manager for checkbox updates + * Tracks global and individual checkbox update states */ +class CheckboxUpdateState { + constructor() { + this.updateCount = 0; + this.listeners = new Set(); + this.updatingCheckboxIds = new Set(); + } + + // Global update state (for buttons quotation and checkout) + isUpdating() { + return this.updateCount > 0; + } + + // Individual checkbox state + isCheckboxUpdating(itemId) { + return this.updatingCheckboxIds.has(String(itemId)); + } + + // Start update + startUpdate(itemId = null) { + this.updateCount++; -// Track the number of ongoing updates -let updateCount = 0; -let listeners = []; - -// Track which checkboxes are currently updating by ID -let updatingCheckboxIds = new Set(); - -const checkboxUpdateState = { - // Check if any checkboxes are currently updating (for buttons quotation and checkout) - isUpdating: () => updateCount > 0, - - // Check if a specific checkbox is updating (for disabling just that checkbox) - isCheckboxUpdating: (itemId) => updatingCheckboxIds.has(itemId.toString()), - - // Start an update for a specific checkbox - startUpdate: (itemId = null) => { - updateCount++; - - // If an item ID is provided, mark it as updating if (itemId !== null) { - updatingCheckboxIds.add(itemId.toString()); + this.updatingCheckboxIds.add(String(itemId)); } - notifyListeners(); - return updateCount; - }, + this.notifyListeners(); + return this.updateCount; + } - // End an update for a specific checkbox - endUpdate: (itemId = null) => { - updateCount = Math.max(0, updateCount - 1); + // End update + endUpdate(itemId = null) { + this.updateCount = Math.max(0, this.updateCount - 1); - // If an item ID is provided, remove it from updating set if (itemId !== null) { - updatingCheckboxIds.delete(itemId.toString()); + this.updatingCheckboxIds.delete(String(itemId)); } - notifyListeners(); - return updateCount; - }, - - // Reset the update counter and clear all updating checkboxes - reset: () => { - updateCount = 0; - updatingCheckboxIds.clear(); - notifyListeners(); - }, + this.notifyListeners(); + return this.updateCount; + } - // Get IDs of all checkboxes currently updating (for debugging) - // getUpdatingCheckboxIds: () => [...updatingCheckboxIds], + // Reset all states + reset() { + this.updateCount = 0; + this.updatingCheckboxIds.clear(); + this.notifyListeners(); + } - // Add a listener function to be called when update state changes - addListener: (callback) => { + // Listener management + addListener(callback) { if (typeof callback === 'function') { - listeners.push(callback); - - // Immediately call with current state - callback(updateCount > 0); - } - }, - - // Remove a listener - removeListener: (callback) => { - listeners = listeners.filter((listener) => listener !== callback); - }, - - // Get current counter (for debugging) - getUpdateCount: () => updateCount, -}; - -// Private function to notify all listeners of state changes -function notifyListeners() { - const isUpdating = updateCount > 0; - listeners.forEach((listener) => { - try { - listener(isUpdating); - } catch (error) { - console.error('Error in checkbox update state listener:', error); + this.listeners.add(callback); + // Immediate callback with current state + callback(this.isUpdating()); } - }); + } + + removeListener(callback) { + this.listeners.delete(callback); + } + + // Debug helpers + getUpdateCount() { + return this.updateCount; + } + + getUpdatingCheckboxIds() { + return [...this.updatingCheckboxIds]; + } + + // Private method to notify listeners + notifyListeners() { + const isUpdating = this.isUpdating(); + + this.listeners.forEach((listener) => { + try { + listener(isUpdating); + } catch (error) { + console.error('Checkbox update state listener error:', error); + } + }); + } } +const checkboxUpdateState = new CheckboxUpdateState(); export default checkboxUpdateState; -- cgit v1.2.3 From 2732c04b36f98a25895826b28003b1e2c56ad952 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 27 May 2025 09:05:10 +0700 Subject: remove purchase_tax_id and vendor_id --- src-migrate/modules/cart/components/ItemSelect.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx index 8dbfe2bc..72ab49aa 100644 --- a/src-migrate/modules/cart/components/ItemSelect.tsx +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -91,7 +91,8 @@ const CartItemSelect = ({ item }: Props) => { id: item.id, qty: item.quantity, selected: newSelectedState, - purchase_tax_id: item.purchase_tax_id || null, + // purchase_tax_id: item.purchase_tax_id, + // vendor_id: item.vendor_id }); // Reload cart for consistency -- cgit v1.2.3 From ce6b3f5fa848403b46c0ffa41a354213c5bd0d84 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 27 May 2025 11:57:54 +0700 Subject: highlight nomor resi and center delivered status --- src/lib/transaction/components/Transaction.jsx | 39 ++++++++++++++++---------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx index 48bea2bf..709fa5d4 100644 --- a/src/lib/transaction/components/Transaction.jsx +++ b/src/lib/transaction/components/Transaction.jsx @@ -208,7 +208,9 @@ const Transaction = ({ id }) => {

    {currencyFormat(transaction.data?.amountUntaxed)}

    -

    PPN {((PPN - 1) * 100).toFixed(0)}%

    +

    + PPN {((PPN - 1) * 100).toFixed(0)}% +

    {currencyFormat(transaction.data?.amountTax)}

    @@ -406,9 +408,9 @@ const Transaction = ({ id }) => {
    - +
    - {transaction.data?.expectedReadyToShip} + {transaction.data?.expectedReadyToShip}
    @@ -437,13 +439,13 @@ const Transaction = ({ id }) => { onClick={() => setIdAWB(airway?.id)} >
    - +

    {airway?.name}

    + No Resi : {airway?.trackingNumber || '-'}{' '} -

    {airway?.name}

    -
    +
    {airway?.delivered ? 'Pesanan Tiba' : 'Sedang Dikirim'}
    @@ -620,15 +622,18 @@ const Transaction = ({ id }) => { )}
    -
    -
    - +
    +
    + {transaction?.data?.name}
    -
    - Estimasi Barang Siap: {transaction?.data?.expectedReadyToShip} +
    + Estimasi Barang Siap:{' '} + + {transaction?.data?.expectedReadyToShip} +
    @@ -770,13 +775,15 @@ const Transaction = ({ id }) => { onClick={() => setIdAWB(airway?.id)} >
    - +

    + {airway?.name} +

    + No Resi : {airway?.trackingNumber || '-'}{' '} -

    {airway?.name}

    -
    +
    {airway?.delivered ? 'Pesanan Tiba' : 'Sedang Dikirim'} @@ -986,7 +993,9 @@ const Transaction = ({ id }) => { {currencyFormat(transaction.data?.amountUntaxed)}
    -
    PPN {((PPN - 1) * 100).toFixed(0)}%
    +
    + PPN {((PPN - 1) * 100).toFixed(0)}% +
    {currencyFormat(transaction.data?.amountTax)}
    -- cgit v1.2.3 From 242ea685141c3b62f031d991709776e0ccb54000 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 27 May 2025 14:27:34 +0700 Subject: hide picking except bu out --- src/lib/shipment/components/Shipments.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 = () => { Tanggal No. Resi - No. Pengiriman + No. Dokumen Sales Order Purchase Order Expedisi -- cgit v1.2.3 From d1a018ce300e1b59374db77459cdd6296f9b822e Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 28 May 2025 15:11:12 +0700 Subject: make detail invoice similar to detail transaction --- src/lib/invoice/components/Invoice.jsx | 158 +++++++++++++++++++++------------ 1 file changed, 99 insertions(+), 59 deletions(-) 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 (
    - ) + ); } - 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 && ( <>
    - {invoice.data?.name} + + {invoice.data?.name} + {invoice.data?.amountResidual > 0 ? ( Belum Lunas @@ -68,13 +74,18 @@ const Invoice = ({ id }) => { {invoice.data?.paymentTerm} - {invoice.data?.amountResidual > 0 && invoice.invoiceDate != invoice.invoiceDateDue && ( - - {invoice.data?.invoiceDateDue} - - )} - {invoice.data?.sales} - {invoice.data?.invoiceDate} + {invoice.data?.amountResidual > 0 && + invoice.invoiceDate != invoice.invoiceDateDue && ( + + {invoice.data?.invoiceDateDue} + + )} + + {invoice.data?.sales} + + + {invoice.data?.invoiceDate} +

    Invoice

    -

    Invoice & Faktur Pajak

    +

    + Invoice & Faktur Pajak +

    - {invoice?.data?.name} + + {invoice?.data?.name} + {invoice?.data?.amountResidual > 0 ? (
    Belum Lunas
    ) : ( @@ -180,14 +199,16 @@ const Invoice = ({ id }) => {
    -
    Rincian Pembelian
    +
    + Rincian Pembelian +
    - + {/* */} @@ -229,13 +250,22 @@ const Invoice = ({ id }) => { - + {/* */} + - ))} @@ -244,20 +274,30 @@ const Invoice = ({ id }) => {
    Subtotal
    -
    {currencyFormat(totalAmount)}
    +
    + {currencyFormat( + totalAmount - totalDiscountAmount - taxAmount + )} +
    -
    Total Diskon
    + {/*
    Total Diskon
    - {currencyFormat(-totalDiscountAmount)} + {currencyFormat(totalDiscountAmount)} +
    */} +
    + PPN {((PPN - 1) * 100).toFixed(0)}% +
    +
    + {currencyFormat( + invoice.data?.amountTotal - + invoice.data?.amountTotal / PPN + )}
    Grand Total
    {currencyFormat(invoice.data?.amountTotal)}
    - -
    PPN {((PPN - 1) * 100).toFixed(0)}% (Incl.)
    -
    {currencyFormat(invoice.data?.amountTotal - totalAmount)}
    @@ -265,14 +305,14 @@ const Invoice = ({ id }) => { ) - ) -} + ); +}; const DescriptionRow = ({ children, label }) => (
    {label} {children}
    -) +); -export default Invoice +export default Invoice; -- cgit v1.2.3 From 3f992f62c54e09254c48d653c2cd138df1cbd8e2 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 31 May 2025 14:33:24 +0700 Subject: fix error deployment --- src-migrate/modules/cart/components/ItemAction.tsx | 65 ++++++----- src-migrate/modules/cart/components/ItemSelect.tsx | 22 ++-- src-migrate/modules/cart/stores/useCartStore.ts | 98 +++++++++++------ src-migrate/pages/shop/cart/index.tsx | 121 ++++++++++----------- 4 files changed, 177 insertions(+), 129 deletions(-) diff --git a/src-migrate/modules/cart/components/ItemAction.tsx b/src-migrate/modules/cart/components/ItemAction.tsx index 4dcebd9e..b06e8e75 100644 --- a/src-migrate/modules/cart/components/ItemAction.tsx +++ b/src-migrate/modules/cart/components/ItemAction.tsx @@ -22,11 +22,11 @@ import { import { toast } from 'react-hot-toast'; -type Props = { +interface Props { item: CartItem; -}; +} -const CartItemAction = ({ item }: Props) => { +const CartItemAction: React.FC = ({ item }) => { const auth = getAuth(); const { setRefreshCart } = useProductCartContext(); const [isLoadDelete, setIsLoadDelete] = useState(false); @@ -36,9 +36,9 @@ const CartItemAction = ({ item }: Props) => { const { loadCart, cart, updateCartItem } = useCartStore(); - const limitQty = item.limit_qty?.transaction || 0; + const limitQty: number = item.limit_qty?.transaction || 0; - const handleDelete = async () => { + const handleDelete = async (): Promise => { if (typeof auth !== 'object') return; setIsLoadDelete(true); @@ -80,25 +80,29 @@ const CartItemAction = ({ item }: Props) => { } }; - const updateQuantityInCookie = (productId, cartId, newQuantity) => { + const updateQuantityInCookie = ( + productId: number, + cartId: string | number, + newQuantity: number + ): boolean => { try { - const cartData = getCartDataFromCookie(); + const cartData = getCartDataFromCookie() as Record; let itemFound = false; + const cartIdString = String(cartId); // Find item by cart_id key or search within objects - if (cartData[cartId]) { - cartData[cartId].quantity = newQuantity; + if (cartData[cartIdString]) { + cartData[cartIdString].quantity = newQuantity; itemFound = true; } else { // Search by product id or cart_id within objects - for (const key in cartData) { + Object.keys(cartData).forEach((key) => { const item = cartData[key]; - if (item.id === productId || item.cart_id === cartId) { + if (item.id === productId || String(item.cart_id) === cartIdString) { item.quantity = newQuantity; itemFound = true; - break; } - } + }); } if (itemFound) { @@ -113,12 +117,12 @@ const CartItemAction = ({ item }: Props) => { } }; - const decreaseQty = () => { - setQuantity((quantity) => (quantity -= 1)); + const decreaseQty = (): void => { + setQuantity((prevQuantity) => prevQuantity - 1); }; - const increaseQty = () => { - setQuantity((quantity) => (quantity += 1)); + const increaseQty = (): void => { + setQuantity((prevQuantity) => prevQuantity + 1); }; const debounceQty = useDebounce(quantity, 1000); @@ -129,7 +133,7 @@ const CartItemAction = ({ item }: Props) => { }, [debounceQty, limitQty]); useEffect(() => { - const updateCart = async () => { + const updateCart = async (): Promise => { if (typeof auth !== 'object' || isNaN(debounceQty)) return; if (debounceQty === item.quantity) return; @@ -167,20 +171,22 @@ const CartItemAction = ({ item }: Props) => { await loadCart(auth.id); // Re-update cookie if server reload overwrote it - const currentCookieData = getCartDataFromCookie(); + const currentCookieData = getCartDataFromCookie() as Record< + string, + any + >; let needsReUpdate = false; - for (const key in currentCookieData) { + Object.keys(currentCookieData).forEach((key) => { const cookieItem = currentCookieData[key]; if ( (cookieItem.id === item.id || - cookieItem.cart_id === item.cart_id) && + String(cookieItem.cart_id) === String(item.cart_id)) && cookieItem.quantity !== debounceQty ) { needsReUpdate = true; - break; } - } + }); if (needsReUpdate) { updateQuantityInCookie(item.id, item.cart_id, debounceQty); @@ -202,6 +208,13 @@ const CartItemAction = ({ item }: Props) => { //eslint-disable-next-line react-hooks/exhaustive-deps }, [debounceQty]); + const handleQuantityInputChange = ( + e: React.ChangeEvent + ): void => { + const value = parseInt(e.target.value); + setQuantity(isNaN(value) ? 1 : value); + }; + return (
    +
    + Kurir + : {manifests?.products} +
    Kurir -- cgit v1.2.3 From 2e507ccbc92f18a0d7f8ebc59112a0337a0bd678 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Wed, 11 Jun 2025 13:40:45 +0700 Subject: Pinpoint done --- package.json | 4 +- src/core/api/odooApi.js | 2 +- src/lib/address/api/editPartnerApi.js | 12 +++ src/lib/address/components/EditAddress.jsx | 150 +++++++++++++++++------------ src/lib/maps/components/PinPointMap.jsx | 139 ++++++++++++++------------ 5 files changed, 183 insertions(+), 124 deletions(-) create mode 100644 src/lib/address/api/editPartnerApi.js diff --git a/package.json b/package.json index 805980b7..54d37f49 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@ramonak/react-progress-bar": "^5.3.0", "@react-email/components": "^0.0.2", "@react-email/render": "^0.0.6", - "@react-google-maps/api": "^2.20.3", + "@react-google-maps/api": "^2.20.6", "@tailwindcss/line-clamp": "^0.4.2", "axios": "^1.1.3", "browser-image-compression": "^2.0.2", @@ -27,6 +27,7 @@ "classnames": "^2.3.2", "clsx": "^2.0.0", "cookies-next": "^2.1.1", + "cors": "^2.8.5", "flowbite": "^1.6.4", "framer-motion": "^7.10.3", "lodash-contrib": "^4.1200.1", @@ -64,6 +65,7 @@ }, "devDependencies": { "@tailwindcss/typography": "^0.5.9", + "@types/google.maps": "^3.58.1", "@types/react": "^18.2.31", "@types/react-dom": "^18.2.14", "@types/react-google-recaptcha": "^2.1.7", diff --git a/src/core/api/odooApi.js b/src/core/api/odooApi.js index 2bff42e2..ab3dedb0 100644 --- a/src/core/api/odooApi.js +++ b/src/core/api/odooApi.js @@ -65,7 +65,7 @@ const odooApi = async (method, url, data = {}, headers = {}) => { 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); 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/EditAddress.jsx b/src/lib/address/components/EditAddress.jsx index 9f038b74..cfb35fbd 100644 --- a/src/lib/address/components/EditAddress.jsx +++ b/src/lib/address/components/EditAddress.jsx @@ -8,6 +8,7 @@ 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'; @@ -20,6 +21,7 @@ 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(); @@ -41,12 +43,14 @@ const EditAddress = ({ id, defaultValues }) => { const [cities, setCities] = useState([]); const [districts, setDistricts] = useState([]); const [subDistricts, setSubDistricts] = useState([]); - const [pinedMaps, setPinedMaps] = useState(false); const [tempAddress, setTempAddress] = useState(getValues('addressMap')); - const { addressMaps, selectedPosition, detailAddress } = useMaps(); - - console.log('ini adalah',); + const { addressMaps, + selectedPosition, + detailAddress, + pinedMaps, + setPinedMaps } = useMaps(); + useEffect(() => { if (addressMaps) { setTempAddress(addressMaps); @@ -55,20 +59,20 @@ const EditAddress = ({ id, defaultValues }) => { 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) + state.label.includes(detailAddress?.province) ); setValue('state', selectedState?.value); - setValue('street', detailAddress?.route); + setValue('street', detailAddress?.street); } }, [detailAddress, setValue]); - + useEffect(() => { const loadProfile = async () => { const dataProfile = await addressApi({ id: auth.partnerId }); @@ -85,7 +89,7 @@ const EditAddress = ({ id, defaultValues }) => { }; if (auth) loadProfile(); }, [auth?.parentId]); - + useEffect(() => { const loadStates = async () => { let dataStates = await stateApi({ tempo: false }); @@ -97,7 +101,7 @@ const EditAddress = ({ id, defaultValues }) => { }; loadStates(); }, []); - + const watchState = watch('state'); useEffect(() => { setValue('city', ''); @@ -118,7 +122,7 @@ const EditAddress = ({ id, defaultValues }) => { loadCities(); } }, [watchState, setValue, getValues]); - + useEffect(() => { if (Object.keys(detailAddress).length > 0) { const selectedCities = cities.find( @@ -126,23 +130,23 @@ const EditAddress = ({ id, defaultValues }) => { detailAddress?.district .toLowerCase() .includes(city.label.toLowerCase()) || - city.label + 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, + ); + 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'); @@ -154,7 +158,7 @@ const EditAddress = ({ id, defaultValues }) => { loadDistricts(); } }, [watchCity, setValue, getValues]); - + useEffect(() => { if (Object.keys(detailAddress).length > 0) { const selectedDistrict = districts.find( @@ -169,9 +173,9 @@ const EditAddress = ({ id, defaultValues }) => { setValue('district', selectedDistrict?.value); } }, [districts, detailAddress, setValue]); - - - + + + const watchDistrict = watch('district'); useEffect(() => { if (watchDistrict) { @@ -204,23 +208,29 @@ const EditAddress = ({ id, defaultValues }) => { detailAddress.village .toLowerCase() .includes(district.label.toLowerCase()) || - district.label + district.label .toLowerCase() .includes(detailAddress.village.toLowerCase()) - ); + ); - setValue('subDistrict', selectedSubDistrict?.value); + 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, @@ -230,37 +240,57 @@ const EditAddress = ({ id, defaultValues }) => { } try { const address = await editAddressApi({ id, data }); - let dataAlamat; - let isUpdated = true; - if (auth.company) { - if (auth?.partnerId == id) { - dataAlamat = { - id_user: auth.partnerId, - alamat_lengkap_text: values.alamat_wajib_pajak, - street: values.street, - }; - isUpdated = await odooApi( - 'PUT', - `/api/v1/partner/${auth.parentId}`, - dataAlamat - ); - } + 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, + }; + + const isUpdated = await editPartnerApi({ + id: auth.partnerId, + data: dataAlamat, + }); + + console.log('Response isUpdated:', isUpdated); } - // if (isUpdated?.id) { - if (address?.id && (auth.company && auth?.partnerId == id ? isUpdated?.id : true)) { + + // Validasi kondisi sukses + const isSuccess = !!address?.id; + + if (isSuccess) { toast.success('Berhasil mengubah alamat'); router.back(); } else { - toast.error('Terjadi kesalahan internal'); - router.back(); + const errorMsg = + address?.message || + isUpdated?.message || + 'Gagal memperbarui alamat, silakan coba lagi.'; + toast.error(errorMsg); } } catch (error) { - console.log(error); - toast.error('Terjadi kesalahan internal'); - router.back(); + 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 ? isUpdated?.id : true) { diff --git a/src/lib/maps/components/PinPointMap.jsx b/src/lib/maps/components/PinPointMap.jsx index acd5ab92..fde1f36c 100644 --- a/src/lib/maps/components/PinPointMap.jsx +++ b/src/lib/maps/components/PinPointMap.jsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useRef } from 'react'; +import React, { useState, useCallback, useRef, useEffect } from 'react'; import { GoogleMap, useJsApiLoader, @@ -8,49 +8,46 @@ import { import { useMaps } from '../stores/useMaps'; import { LocateFixed, MapPinIcon } from 'lucide-react'; import { Button } from '@chakra-ui/react'; -import { useForm } from 'react-hook-form'; const containerStyle = { width: '100%', height: '400px', }; -const center = { - lat: -6.2, // Default latitude (Jakarta) - lng: 106.816666, // Default longitude (Jakarta) +const defaultCenter = { + lat: -6.2, + lng: 106.816666, }; const PinpointLocation = () => { const { isLoaded } = useJsApiLoader({ - googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_API_KEY, // Pastikan API key ada di .env.local + googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_API_KEY, libraries: ['places'], }); - const { addressMaps, setAddressMaps, selectedPosition, setSelectedPosition, setDetailAddress, setPinedMaps } = - useMaps(); + const { + addressMaps, + setAddressMaps, + selectedPosition, + setSelectedPosition, + setDetailAddress, + setPinedMaps, + } = useMaps(); const [tempAddress, setTempAddress] = useState(''); - const [tempPosition, setTempPosition] = useState(center); - const { setValue } = useForm(); + const [tempPosition, setTempPosition] = useState(defaultCenter); + const [markerIcon, setMarkerIcon] = useState(null); const autocompleteRef = useRef(null); - const onMapClick = useCallback((event) => { - const lat = event.latLng.lat(); - const lng = event.latLng.lng(); - setTempPosition({ lat, lng }); - getAddress(lat, lng); - }, []); - - const handlePlaceSelect = () => { - const place = autocompleteRef.current.getPlace(); - if (place && place.geometry) { - const lat = place.geometry.location.lat(); - const lng = place.geometry.location.lng(); - setTempPosition({ lat, lng }); - setTempAddress(place.formatted_address); + 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), + }); } - }; + }, [isLoaded]); const getAddressComponent = (components, type) => { const component = components.find((comp) => comp.types.includes(type)); @@ -63,43 +60,59 @@ const PinpointLocation = () => { `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 = { - route : getAddressComponent(addressComponents, 'route')+' '+getAddressComponent(addressComponents, 'street_number')+' '+getAddressComponent(addressComponents, 'administrative_area_level_7')+' '+getAddressComponent(addressComponents, 'administrative_area_level_6'), - province: getAddressComponent( - addressComponents, - 'administrative_area_level_1' - ), - district: getAddressComponent( - addressComponents, - 'administrative_area_level_2' - ), - subDistrict: getAddressComponent( - addressComponents, - 'administrative_area_level_3' - ), - village: getAddressComponent( - addressComponents, - 'administrative_area_level_4' - ), + 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(data.results[0].formatted_address); + 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; - setTempPosition({ lat, lng }); + const newPosition = { lat, lng }; + setTempPosition(newPosition); getAddress(lat, lng); }, (error) => { @@ -115,18 +128,16 @@ const PinpointLocation = () => { alert('Silahkan pilih lokasi terlebih dahulu'); return; } - // console.log('tempPosition', tempPosition.lat); - getAddress(tempPosition.lat, tempPosition.lng); + setSelectedPosition(tempPosition); setAddressMaps(tempAddress); - setPinedMaps(false) + setPinedMaps(false); }; - console.log('set selected position',selectedPosition); - return (

    Tentukan Pinpoint Lokasi

    +
    {isLoaded ? ( { zoom={15} onClick={onMapClick} > - onMapClick(e)} - icon={{ - url: 'https://maps.google.com/mapfiles/ms/icons/red-pushpin.png', - scaledSize: new window.google.maps.Size(40, 40), - }} - /> + {markerIcon && ( + { + const lat = e.latLng.lat(); + const lng = e.latLng.lng(); + const newPosition = { lat, lng }; + setTempPosition(newPosition); + getAddress(lat, lng); + }} + icon={markerIcon} + /> + )} ) : (

    Loading map...

    @@ -169,16 +185,15 @@ const PinpointLocation = () => {

    PinPoint :

    - - + +
    -- cgit v1.2.3 From 43bed6bf9ebe559e32e7572ea17278b5f3b0897c Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 11 Jun 2025 16:31:35 +0700 Subject: fix checkout --- src/lib/checkout/components/Checkout.jsx | 416 ++++++++++++------------------- 1 file changed, 154 insertions(+), 262 deletions(-) diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 0dc025f5..f6889558 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -28,15 +28,10 @@ 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 { useAddress } from '../stores/useAdress'; -import SectionExpedition from './SectionExpedition'; -import { useCheckout } from '../stores/stateCheckout'; -import { formatShipmentRange, getToDate } from '../utils/functionCheckouit'; const SELF_PICKUP_ID = 32; const { checkoutApi } = require('../api/checkoutApi'); @@ -73,28 +68,14 @@ const Checkout = () => { source: query, voucher: activeVoucher, voucher_shipping: activeVoucherShipping, - }), - //biteship - { - 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(); + const [selectedAddress, setSelectedAddress] = useState({ + shipping: null, + invoicing: null, + }); + const [addresses, setAddresses] = useState(null); useEffect(() => { if (!auth) return; @@ -124,37 +105,26 @@ 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 [products, setProducts] = useState(null); const [totalWeight, setTotalWeight] = useState(0); const [priceCheck, setPriceCheck] = useState(false); - // const [listExpedisi, setExpedisi] = useState([]); + const [listExpedisi, setExpedisi] = useState([]); const [listserviceExpedisi, setListServiceExpedisi] = useState([]); const [selectedExpedisi, setSelectedExpedisi] = useState(0); const [selectedCarrierId, setselectedCarrierId] = useState(0); const [selectedCarrier, setselectedCarrier] = useState(0); - //new release - // const [biayaKirim, setBiayaKirim] = useState(0); - // const [checkWeigth, setCheckWeight] = useState(false); + 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); @@ -165,31 +135,12 @@ const Checkout = () => { const [findCodeVoucher, SetFindVoucher] = useState(null); const [selisihHargaCode, SetSelisihHargaCode] = useState(null); const [buttonTerapkan, SetButtonTerapkan] = useState(false); - // const [checkoutValidation, setCheckoutValidation] = 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); const voucher = async () => { @@ -253,7 +204,6 @@ const Checkout = () => { return; } - dataVoucher.forEach((addNewLine) => { if (addNewLine.applyType !== 'shipping') { // Mencari voucher dalam listVouchers @@ -346,7 +296,6 @@ const Checkout = () => { value: expedisi.id, label: expedisi.name, carrierId: expedisi.deliveryCarrierId, - logo: expedisi.image, })); setExpedisi(dataExpedisi); }; @@ -363,38 +312,38 @@ 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; - // }; + 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; @@ -432,71 +381,70 @@ const Checkout = () => { setHasFlashSale(hasFlashSale); }, [cartCheckout]); - // useEffect(() => { - // setCheckoutValidation(false); - // const loadServiceRajaOngkir = async () => { - // setLoadingRajaOngkir(true); - // const body = { - // origin: 2127, - // destination: selectedAddress.shipping.rajaongkirCityId, - // weight: totalWeight, - // courier: selectedCarrier, - // originType: 'subdistrict', - // destinationType: 'subdistrict', - // }; - // setBiayaKirim(0); - // const dataService = await axios( - // '/api/rajaongkir-service?body=' + JSON.stringify(body) - // ); - // setLoadingRajaOngkir(false); - // setListServiceExpedisi(dataService.data[0].costs); - // if (dataService.data[0].costs[0]) { - // setBiayaKirim(dataService.data[0].costs[0]?.cost[0].value); - // setselectedExpedisiService( - // dataService.data[0].costs[0]?.description + - // '-' + - // dataService.data[0].costs[0]?.service - // ); - // setEtd(dataService.data[0].costs[0]?.cost[0].etd); - // toast.success('Harap pilih tipe layanan pengiriman'); - // } else { - // toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'); - // } - // }; - // if (selectedCarrier != 0 && selectedCarrier != 1 && totalWeight > 0) { - // loadServiceRajaOngkir(); - // } else { - // setListServiceExpedisi(); - // setBiayaKirim(0); - // setselectedExpedisiService(); - // setEtd(); - // } - // }, [selectedCarrier, selectedAddress, totalWeight]); - // - // useEffect(() => { - // 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]); - // + 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); @@ -524,7 +472,7 @@ const Checkout = () => { }); return; } - if (selectedCourier === 0 || !selectedCourier) { + if (selectedExpedisi === 0) { setCheckoutValidation(true); if (expedisiValidation.current) { const position = expedisiValidation.current.getBoundingClientRect(); @@ -535,20 +483,7 @@ const Checkout = () => { } return; } - //new release - // if (!selectedService) { - toast.error('Harap pilih tipe layanan pengiriman'); - return; - } - if (selectedCourier != 1 && biayaKirim == 0) { - // toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'); - // return; - // } - // if (!selectedService) { - // toast.error('Harap pilih tipe layanan pengiriman'); - // return; - // } - if (selectedCourier != 1 && biayaKirim == 0) { + if (selectedCarrier != 1 && biayaKirim == 0) { toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'); return; } @@ -558,39 +493,23 @@ const Checkout = () => { quantity: product.quantity, available_quantity: product?.availableQuantity, })); - 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: selectedCourierId, - // estimated_arrival_days_start : parseInt(eta_courier_start) + parseInt(productSla), - // estimated_arrival_days: parseInt(eta_courier) + parseInt(productSla), - // If NaN conver to 0 (int) - estimated_arrival_days_start: (isNaN(parseInt(eta_courier_start)) ? 0 : parseInt(eta_courier_start)) + - (isNaN(parseInt(productSla)) ? 0 : parseInt(productSla)), - - estimated_arrival_days: (isNaN(parseInt(eta_courier)) ? 0 : parseInt(eta_courier)) + - (isNaN(parseInt(productSla)) ? 0 : parseInt(productSla)), - - delivery_service_type: selectedService?.service_type, - // New release - // 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, + carrier_id: selectedCarrierId, + estimated_arrival_days: splitDuration(etd), + delivery_service_type: selectedExpedisiService, flash_sale: hasFlashSale, // dibuat negasi untuk ngetest kebalikan nilai false voucher: activeVoucher, voucher_shipping: activeVoucherShipping, type: 'sale_order', }; + if (query) { data.source = 'buy'; } @@ -598,8 +517,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; @@ -1240,22 +1159,21 @@ const Checkout = () => { )} - - {/*new-relase*/} - {/**/} - {/* */} - {/* */} + + + +
    {!!products && @@ -1344,10 +1262,7 @@ const Checkout = () => {
    - Biaya Kirim{' '} -

    - {formatShipmentRange(etd, unit, productSla)} -

    + Biaya Kirim

    {etdFix}

    {currencyFormat( @@ -1471,8 +1386,7 @@ const Checkout = () => { className='flex-1 btn-yellow' onClick={checkout} disabled={ - //new release - // isLoading || + isLoading || !products || products?.length == 0 || priceCheck || @@ -1544,10 +1458,9 @@ const Checkout = () => { /> )} - {products && } - - {/* + { */} + />
    Detail Pesanan
    @@ -1656,9 +1569,7 @@ const Checkout = () => {
    Biaya Kirim -

    - {formatShipmentRange(etd, unit, productSla)} -

    +

    {etdFix}

    {currencyFormat( @@ -1780,8 +1691,7 @@ const Checkout = () => { className='w-full btn-yellow mt-4' onClick={checkout} disabled={ - // new-relase - // isLoading || + isLoading || !products || products?.length == 0 || priceCheck || @@ -1830,32 +1740,14 @@ const SectionAddress = ({ address, label, url }) => (

    {address.street}, {address?.city?.name}

    -
    - - {address.addressMap ? ( - - ) : ( - - - - )} -
    )}
    ); const SectionValidation = ({ address }) => - //new release - // address?.stateId == 0 || - // (address?.rajaongkirCityId == 0 && ( - address?.stateId == 0 && ( + address?.stateId == 0 || + (address?.rajaongkirCityId == 0 && (
    Mohon untuk memperbarui alamat Anda dengan mengklik tombol di bawah ini.{' '} @@ -1869,17 +1761,17 @@ const SectionValidation = ({ address }) =>
    - ); + )); const SectionExpedisi = ({ - address, - listExpedisi, - setSelectedExpedisi, - checkWeigth, - checkoutValidation, - expedisiValidation, - loadingRajaOngkir, -}) => + address, + listExpedisi, + setSelectedExpedisi, + checkWeigth, + checkoutValidation, + expedisiValidation, + loadingRajaOngkir, + }) => address?.rajaongkirCityId > 0 && (
    @@ -1931,9 +1823,9 @@ const SectionExpedisi = ({ )}
    {checkWeigth == true && ( -- cgit v1.2.3 From 337e7a189efacbe696f4512130278952977b2da2 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 11 Jun 2025 16:37:19 +0700 Subject: fix checkout --- src/lib/checkout/components/Checkout.jsx | 327 ++++++++++--------------------- 1 file changed, 105 insertions(+), 222 deletions(-) diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index f6889558..668c7ac0 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,7 +374,11 @@ const Checkout = () => { } return; } - if (selectedCarrier != 1 && biayaKirim == 0) { + if (!selectedService) { + toast.error('Harap pilih tipe layanan pengiriman'); + return; + } + if (selectedCourier != 1 && biayaKirim == 0) { toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'); return; } @@ -493,23 +388,27 @@ const Checkout = () => { quantity: product.quantity, available_quantity: product?.availableQuantity, })); + 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'; } @@ -570,24 +469,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 +642,6 @@ const Checkout = () => { )}
    - {/* {!loadingVoucher && - listVouchers?.length === 1 && - listVoucherShippings?.length === 1} - { -
    -
    -

    Tidak ada voucher tersedia

    -

    - Maaf, saat ini tidak ada voucher yang tersedia. -

    -
    -
    - } */} {listVoucherShippings && listVoucherShippings?.length > 0 && (
    @@ -1159,8 +1027,8 @@ const Checkout = () => { )} - - + {/* { + /> */}
    {!!products && @@ -1255,14 +1123,15 @@ const Checkout = () => {
    {currencyFormat(cartCheckout?.subtotal)}
    -
    - PPN {((PPN - 1) * 100).toFixed(0)}% -
    +
    PPN {((PPN - 1) * 100).toFixed(0)}%
    {currencyFormat(cartCheckout?.tax)}
    - Biaya Kirim

    {etdFix}

    + Biaya Kirim{' '} +

    + {formatShipmentRange(etd, unit, productSla)} +

    {currencyFormat( @@ -1386,7 +1255,6 @@ const Checkout = () => { className='flex-1 btn-yellow' onClick={checkout} disabled={ - isLoading || !products || products?.length == 0 || priceCheck || @@ -1458,9 +1326,10 @@ const Checkout = () => { /> )} + {products && } - - + {/* { + /> */}
    Detail Pesanan
    @@ -1561,15 +1430,15 @@ const Checkout = () => {
    {currencyFormat(cartCheckout?.subtotal)}
    -
    - PPN {((PPN - 1) * 100).toFixed(0)}% -
    +
    PPN {((PPN - 1) * 100).toFixed(0)}%
    {currencyFormat(cartCheckout?.tax)}
    Biaya Kirim -

    {etdFix}

    +

    + {formatShipmentRange(etd, unit, productSla)} +

    {currencyFormat( @@ -1691,7 +1560,6 @@ const Checkout = () => { className='w-full btn-yellow mt-4' onClick={checkout} disabled={ - isLoading || !products || products?.length == 0 || priceCheck || @@ -1740,14 +1608,29 @@ const SectionAddress = ({ address, label, url }) => (

    {address.street}, {address?.city?.name}

    +
    + + {address.addressMap ? ( + + ) : ( + + + + )} +
    )}
    ); const SectionValidation = ({ address }) => - address?.stateId == 0 || - (address?.rajaongkirCityId == 0 && ( + address?.stateId == 0 && (
    Mohon untuk memperbarui alamat Anda dengan mengklik tombol di bawah ini.{' '} @@ -1761,7 +1644,7 @@ const SectionValidation = ({ address }) =>
    - )); + ); const SectionExpedisi = ({ address, -- cgit v1.2.3 From e0e0729ee57d2f9b1188a0604e3cc4a51317b0ed Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Thu, 12 Jun 2025 11:00:00 +0700 Subject: fix pinpoint --- src/lib/address/components/CreateAddress.jsx | 18 +++++++++------ src/lib/address/components/EditAddress.jsx | 21 +++++++++++++----- src/lib/maps/components/PinPointMap.jsx | 24 +++++++++++++++----- src/lib/maps/stores/useMaps.js | 33 ++++++++++++++++++++-------- 4 files changed, 69 insertions(+), 27 deletions(-) diff --git a/src/lib/address/components/CreateAddress.jsx b/src/lib/address/components/CreateAddress.jsx index fbb01fcc..35c966c7 100644 --- a/src/lib/address/components/CreateAddress.jsx +++ b/src/lib/address/components/CreateAddress.jsx @@ -212,7 +212,7 @@ const CreateAddress = () => { <> setPinedMaps(false)} > @@ -227,16 +227,20 @@ const CreateAddress = () => {
    - + {addressMaps ? ( -
    - setPinedMaps(true)} />{' '} +
    + {addressMaps}
    ) : ( - + Pin Koordinat Alamat )}
    diff --git a/src/lib/address/components/EditAddress.jsx b/src/lib/address/components/EditAddress.jsx index cfb35fbd..743bb4c8 100644 --- a/src/lib/address/components/EditAddress.jsx +++ b/src/lib/address/components/EditAddress.jsx @@ -311,7 +311,12 @@ const EditAddress = ({ id, defaultValues }) => { close={() => setPinedMaps(false)} >
    - + +
    @@ -327,16 +332,20 @@ const EditAddress = ({ id, defaultValues }) => {
    - + {tempAddress ? (
    - setPinedMaps(true)} />{' '} + {tempAddress}
    ) : ( - + Pin Koordinat Alamat )}
    diff --git a/src/lib/maps/components/PinPointMap.jsx b/src/lib/maps/components/PinPointMap.jsx index fde1f36c..c46d838a 100644 --- a/src/lib/maps/components/PinPointMap.jsx +++ b/src/lib/maps/components/PinPointMap.jsx @@ -19,7 +19,7 @@ const defaultCenter = { lng: 106.816666, }; -const PinpointLocation = () => { +const PinpointLocation = ({ initialLatitude, initialLongitude, initialAddress }) => { const { isLoaded } = useJsApiLoader({ googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_API_KEY, libraries: ['places'], @@ -34,8 +34,15 @@ const PinpointLocation = () => { setPinedMaps, } = useMaps(); - const [tempAddress, setTempAddress] = useState(''); - const [tempPosition, setTempPosition] = useState(defaultCenter); + 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); @@ -47,7 +54,12 @@ const PinpointLocation = () => { scaledSize: new window.google.maps.Size(25, 40), }); } - }, [isLoaded]); + + // 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)); @@ -147,6 +159,8 @@ const PinpointLocation = () => { setTempAddress(e.target.value)} style={{ width: '100%', padding: '8px' }} /> @@ -209,4 +223,4 @@ const PinpointLocation = () => { ); }; -export default PinpointLocation; +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 index 4daf7f62..c57a05ad 100644 --- a/src/lib/maps/stores/useMaps.js +++ b/src/lib/maps/stores/useMaps.js @@ -6,12 +6,27 @@ const center = { }; export const useMaps = create((set) => ({ - selectedPosition: center, - addressMaps: '', - detailAddress: {}, - pinedMaps : false, - setSelectedPosition: (position) => set({ selectedPosition: position }), - setAddressMaps: (addressMaps) => set({ addressMaps }), - setDetailAddress: (detailAddress) => set({ detailAddress }), - setPinedMaps: (pinedMaps) => set({pinedMaps}) - })); \ No newline at end of file + // 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 }), +})); -- cgit v1.2.3 From 8ca6c0aa1b2a578332ff1c3706f58530f549352e Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 12 Jun 2025 18:36:58 +0700 Subject: (miqdad) fix icon color pin pont --- src/lib/address/components/EditAddress.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/address/components/EditAddress.jsx b/src/lib/address/components/EditAddress.jsx index 743bb4c8..217be867 100644 --- a/src/lib/address/components/EditAddress.jsx +++ b/src/lib/address/components/EditAddress.jsx @@ -335,14 +335,14 @@ const EditAddress = ({ id, defaultValues }) => { {tempAddress ? (
    - {tempAddress}
    ) : ( Pin Koordinat Alamat -- cgit v1.2.3 From 5754fd0a95bc72f0e97e6af2d246f4d14a45bf9f Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 16 Jun 2025 11:14:28 +0700 Subject: (andri) fix self pick up checkout --- src/lib/checkout/components/Checkout.jsx | 34 ++++++++++++++++------- src/lib/checkout/components/SectionExpedition.jsx | 1 + 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 668c7ac0..aad3d66d 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -374,13 +374,17 @@ const Checkout = () => { } return; } - if (!selectedService) { - toast.error('Harap pilih tipe layanan pengiriman'); - return; - } - if (selectedCourier != 1 && biayaKirim == 0) { - toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'); - return; + 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) => ({ @@ -388,9 +392,19 @@ const Checkout = () => { quantity: product.quantity, available_quantity: product?.availableQuantity, })); - let estimated_courier = etd.split('-').map(Number); - let eta_courier = Math.max(...estimated_courier); - let eta_courier_start = Math.min(...estimated_courier); + + 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,, diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx index 40084048..fdad04ce 100644 --- a/src/lib/checkout/components/SectionExpedition.jsx +++ b/src/lib/checkout/components/SectionExpedition.jsx @@ -308,6 +308,7 @@ export default function SectionExpedition({ products }) { } } else { setSelectedCourier(courier === 32 ? 'SELF PICKUP' : null); + setSelectedCourierId(courier); setServiceOptions([]); } }; -- cgit v1.2.3 From 6415e9c36b00c38089d2676a2e95ab7a165c99ea Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 16 Jun 2025 16:31:44 +0700 Subject: fix tidak bisa checkout barang bundling --- src/lib/checkout/components/SectionExpedition.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx index fdad04ce..7a02c6e9 100644 --- a/src/lib/checkout/components/SectionExpedition.jsx +++ b/src/lib/checkout/components/SectionExpedition.jsx @@ -17,7 +17,8 @@ import { getProductsSla } from '../api/checkoutApi'; function mappingItems(products) { return products?.map((item) => ({ - name: item.parent.name, + // name: item.parent.name || item?.name || 'Unknown Product', + name: item?.name, description: `${item.code} - ${item.name}`, value: item.price.priceDiscount, weight: item.weight * 1000, -- cgit v1.2.3 From 762d7dad3420df8fa0fdaf3ca4484f37b2d5c5a7 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Wed, 18 Jun 2025 11:15:22 +0700 Subject: fix pengajuan tempo --- src/lib/pengajuan-tempo/component/FinishTempo.jsx | 14 ++++++++++---- src/lib/pengajuan-tempo/component/PengajuanTempo.jsx | 2 +- src/pages/pengajuan-tempo/[status].jsx | 6 ------ 3 files changed, 11 insertions(+), 11 deletions(-) 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'}
    - {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'}
    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/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: {} }; } -- cgit v1.2.3 From 3c1258e0687063e05e67982c618f00154e234605 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Fri, 20 Jun 2025 09:12:32 +0700 Subject: add top banner mobile --- .../components/elements/Navbar/NavbarMobile.jsx | 4 +- .../components/elements/Navbar/TopBannerMobile.jsx | 72 ++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 src/core/components/elements/Navbar/TopBannerMobile.jsx 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 ( - +
    Nama Produk Jumlah HargaDiskonDiskonSubtotal
    {product.quantity}{currencyFormat(product.price.price)} + {currencyFormat( + product.price.priceDiscount - taxAmount + )} + {product.price.discountPercentage > 0 ? `${product.price.discountPercentage}%` : ''} + + {currencyFormat( + product.price.priceDiscount * product.quantity - + taxAmount + )} {currencyFormat(product.price.priceDiscount * product.quantity)}