summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/address/components/CreateAddress.jsx107
-rw-r--r--src/lib/address/components/EditAddress.jsx331
-rw-r--r--src/lib/auth/api/checkParentStatusApi.js13
-rw-r--r--src/lib/auth/api/editPersonalProfileApi.js2
-rw-r--r--src/lib/auth/api/switchAccountApi.js2
-rw-r--r--src/lib/auth/components/SwitchAccount.jsx71
-rw-r--r--src/lib/category/components/Breadcrumb.jsx224
-rw-r--r--src/lib/checkout/api/checkoutApi.js10
-rw-r--r--src/lib/checkout/components/Checkout.jsx107
-rw-r--r--src/lib/checkout/components/FinishCheckout.jsx19
-rw-r--r--src/lib/checkout/components/SectionExpedition.jsx152
-rw-r--r--src/lib/checkout/components/SectionQuotationExpedition.jsx19
-rw-r--r--src/lib/form/components/Merchant.jsx2
-rw-r--r--src/lib/maps/components/PinPointMap.jsx95
-rw-r--r--src/lib/maps/stores/useMaps.js49
-rw-r--r--src/lib/pengajuan-tempo/api/createPengajuanTempoApi.js2
-rw-r--r--src/lib/pengajuan-tempo/component/FinishTempo.jsx130
-rw-r--r--src/lib/pengajuan-tempo/component/PengajuanTempo.jsx1
-rw-r--r--src/lib/product/components/Product/ProductDesktopVariant.jsx136
-rw-r--r--src/lib/product/components/Product/ProductMobileVariant.jsx188
-rw-r--r--src/lib/product/components/ProductCard.jsx2
-rw-r--r--src/lib/product/components/ProductFilterDesktop.jsx348
-rw-r--r--src/lib/product/components/ProductSearch.jsx78
-rw-r--r--src/lib/quotation/components/Quotation.jsx6
-rw-r--r--src/lib/transaction/components/Transaction.jsx576
-rw-r--r--src/lib/transaction/components/TransactionStatusBadge.jsx4
-rw-r--r--src/lib/transaction/components/Transactions.jsx934
-rw-r--r--src/lib/treckingAwb/component/Manifest.jsx13
28 files changed, 2080 insertions, 1541 deletions
diff --git a/src/lib/address/components/CreateAddress.jsx b/src/lib/address/components/CreateAddress.jsx
index 963a19aa..2879fb3d 100644
--- a/src/lib/address/components/CreateAddress.jsx
+++ b/src/lib/address/components/CreateAddress.jsx
@@ -48,19 +48,16 @@ const CreateAddress = () => {
pinedMaps,
setPinedMaps
} = useMaps();
- useEffect(() => {
- if (detailAddress) {
- setValue('zip', detailAddress.postalCode);
- const selectedState = states.find(
- (state) =>
- detailAddress?.province.includes(state.label) ||
- state.label.includes(detailAddress?.province)
- );
- setValue('state', selectedState?.value);
- setValue('street', detailAddress?.street);
-
- }
- }, [detailAddress, setValue]);
+
+ const resetPin = useMaps((state) => state.resetPin);
+ const [showValidationPopup, setShowValidationPopup] = useState(false);
+ const [popupMessage, setPopupMessage] = useState("");
+ const [selectedCityName, setSelectedCityName] = useState("");
+ const [normalizedDistrict, setNormalizedDistrict] = useState("");
+
+ useEffect(() => {
+ resetPin();
+ }, [resetPin]);
useEffect(() => {
const loadState = async () => {
@@ -75,6 +72,20 @@ const CreateAddress = () => {
setAddressMaps('');
}, []);
+ useEffect(() => {
+ if (detailAddress) {
+ setValue('zip', detailAddress.postalCode);
+ const selectedState = states.find(
+ (state) =>
+ (detailAddress?.province || "").includes(state.label) ||
+ state.label.includes(detailAddress?.province)
+ );
+ setValue('state', selectedState?.value);
+ setValue('street', detailAddress?.street);
+
+ }
+ }, [detailAddress, setValue]);
+
const watchState = watch('state');
useEffect(() => {
setValue('city', '');
@@ -91,19 +102,6 @@ const CreateAddress = () => {
}
}, [watchState, setValue]);
- useEffect(() => {
- if (detailAddress && Object.keys(detailAddress).length > 0) {
- const selectedCities = cities.find(
- (city) =>
- city.label.toLowerCase() === detailAddress?.district.toLowerCase()
- ) || cities.find(
- (city) =>
- detailAddress?.district.toLowerCase().includes(city.label.toLowerCase()) ||
- city.label.toLowerCase().includes(detailAddress?.district.toLowerCase())
- );
- setValue('city', selectedCities?.value);
- }
- }, [cities, detailAddress, setValue]);
useEffect(() => {
if (addresses) {
@@ -123,6 +121,29 @@ const CreateAddress = () => {
}
}, [auth]);
+ const normalizeName = (name = "") => {
+ return name
+ .toLowerCase()
+ .replace(/\bkabupaten\b/gi, "")
+ .replace(/\bkota\b/gi, "")
+ .trim();
+ };
+
+ useEffect(() => {
+ if (detailAddress?.district) {
+ const normalizedDistrict = normalizeName(detailAddress.district);
+
+ const selectedCity = cities.find((city) => {
+ const normalizedCity = normalizeName(city.label);
+ return normalizedCity === normalizedDistrict;
+ });
+
+ if (selectedCity) {
+ setValue("city", selectedCity.value);
+ }
+ }
+ }, [cities, detailAddress, setValue]);
+
const watchCity = watch('city');
useEffect(() => {
setValue('district', '');
@@ -188,7 +209,23 @@ const CreateAddress = () => {
}
}, [subDistricts, detailAddress, setValue]);
+
+
+ // console.log(selectedCityName, '=', normalizedDistrict);
const onSubmitHandler = async (values) => {
+ if (detailAddress) {
+ const cityName = normalizeName(
+ cities.find((c) => c.value === watch("city"))?.label || ""
+ );
+ const districtName = normalizeName(detailAddress?.district || "");
+ console.log(cityName, '=', districtName);
+
+ if (cityName && cityName !== districtName) {
+ setPopupMessage("Titik Koordinat tidak sesuai dengan Kota yang dipilih");
+ setShowValidationPopup(true);
+ return;
+ }
+ }
const data = {
...values,
state_id: values.state,
@@ -219,6 +256,24 @@ const CreateAddress = () => {
<PinPointMap />
</div>
</BottomPopup>
+ <BottomPopup
+ active={showValidationPopup}
+ close={() => setShowValidationPopup(false)}
+ >
+ <div className="leading-7 text-gray_r-12/80 text-center">
+ {popupMessage}
+ </div>
+
+ <div className="flex justify-center mt-6">
+ <button
+ className="btn-solid-red w-full md:w-auto"
+ type="button"
+ onClick={() => setShowValidationPopup(false)}
+ >
+ OK
+ </button>
+ </div>
+ </BottomPopup>
<div className='max-w-none md:container mx-auto flex p-0 md:py-10'>
<div className='hidden md:block w-3/12 pr-4'>
<Menu />
diff --git a/src/lib/address/components/EditAddress.jsx b/src/lib/address/components/EditAddress.jsx
index deaa8a3e..75f1a89a 100644
--- a/src/lib/address/components/EditAddress.jsx
+++ b/src/lib/address/components/EditAddress.jsx
@@ -1,6 +1,6 @@
import { yupResolver } from '@hookform/resolvers/yup';
import { useRouter } from 'next/router';
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useMemo } from 'react';
import * as Yup from 'yup';
import cityApi from '../api/cityApi';
import { Controller, useForm } from 'react-hook-form';
@@ -44,22 +44,92 @@ const EditAddress = ({ id, defaultValues }) => {
const [districts, setDistricts] = useState([]);
const [subDistricts, setSubDistricts] = useState([]);
const [tempAddress, setTempAddress] = useState(getValues('addressMap'));
- const { addressMaps,
+ const resetPin = useMaps((state) => state.resetPin);
+ const setSelectedPosition = useMaps((state) => state.setSelectedPosition);
+ const setAddressMaps = useMaps((state) => state.setAddressMaps);
+ const setDetailAddress = useMaps((state) => state.setDetailAddress);
+ const [showValidationPopup, setShowValidationPopup] = useState(false);
+ const [popupMessage, setPopupMessage] = useState("");
+ const [selectedCityName, setSelectedCityName] = useState("");
+ const [normalizedDistrict, setNormalizedDistrict] = useState("");
+
+ const {
+ addressMaps,
selectedPosition,
detailAddress,
pinedMaps,
- setPinedMaps } = useMaps();
+ setPinedMaps,
+ getDefaultCenter, // penting untuk deteksi default center
+ } = useMaps();
+
+ const normalizeName = (name = "") => {
+ return name
+ .toLowerCase()
+ .replace(/\bkabupaten\b/gi, "")
+ .replace(/\bkota\b/gi, "")
+ .trim();
+ };
-
+ // Helper: cek apakah benar2 sudah PIN (bukan default center & ada addressMaps)
+ const isPinned = useMemo(() => {
+ if (!selectedPosition) return false;
+
+ // pastikan selalu cast ke number
+ const lat = Number(selectedPosition.lat);
+ const lng = Number(selectedPosition.lng);
+
+ // kalau hasil cast bukan angka valid
+ if (isNaN(lat) || isNaN(lng)) return false;
+
+ const dc =
+ typeof getDefaultCenter === "function"
+ ? getDefaultCenter()
+ : { lat: -6.2, lng: 106.816666 };
+
+ const nearDefault =
+ Math.abs(lat - dc.lat) < 1e-4 &&
+ Math.abs(lng - dc.lng) < 1e-4;
+
+ return Boolean(addressMaps) && !nearDefault;
+ }, [selectedPosition, addressMaps, getDefaultCenter]);
+
+ // Hanya isi addressMap & lat/lng di form kalau SUDAH PIN
useEffect(() => {
- if (addressMaps) {
- setTempAddress(addressMaps);
- setValue('addressMap', addressMaps);
- setValue('longtitude', selectedPosition.lng);
- setValue('latitude', selectedPosition.lat);
+ // cek kalau form punya koordinat lama
+ const lat = getValues("latitude");
+ const lng = getValues("longtitude");
+ const oldAddress = getValues("addressMap");
+
+ if (lat && lng) {
+ setTempAddress(oldAddress);
+ setValue("addressMap", oldAddress);
+
+ // kalau store punya setter untuk koordinat/alamat:
+ if (typeof setSelectedPosition === "function") {
+ setSelectedPosition({ lat: Number(lat), lng: Number(lng) });
+ }
+ if (typeof setAddressMaps === "function") {
+ setAddressMaps(oldAddress);
+ }
}
- }, [addressMaps, selectedPosition, setValue]);
-
+ }, [setSelectedPosition, setAddressMaps, getValues, setValue]);
+
+ useEffect(() => {
+ const addr = getValues("addressMap");
+
+ if (!addr || addr.trim() === "") {
+ resetPin();
+ } else {
+ setAddressMaps(addr);
+
+ const lat = getValues("latitude");
+ const lng = getValues("longtitude");
+ if (lat && lng) {
+ setSelectedPosition({ lat: Number(lat), lng: Number(lng) });
+ }
+ }
+ }, [getValues, resetPin, setAddressMaps, setSelectedPosition]);
+
useEffect(() => {
if (Object.keys(detailAddress).length > 0) {
setValue('zip', detailAddress.postalCode);
@@ -72,7 +142,7 @@ const EditAddress = ({ id, defaultValues }) => {
setValue('street', detailAddress?.street);
}
}, [detailAddress, setValue]);
-
+
useEffect(() => {
const loadProfile = async () => {
const dataProfile = await addressApi({ id: auth.partnerId });
@@ -83,13 +153,40 @@ const EditAddress = ({ id, defaultValues }) => {
setValue('alamat_wajib_pajak', dataProfile.alamatWajibPajak);
setValue('alamat_bisnis', dataProfile.alamatBisnis);
setValue('business_name', dataProfile.name);
- setValue('city', dataProfile.city?.id);
- setValue('district', dataProfile.district?.id);
- setValue('subDistrict', dataProfile.subDistrict?.id);
};
if (auth) loadProfile();
- }, [auth?.parentId]);
-
+ }, [auth?.parentId, setValue]);
+
+ // Isi ZIP/Prov dari detailAddress (JANGAN isi street)
+useEffect(() => {
+ const zip = getValues("zip");
+ const province = getValues("state");
+ const street = getValues("street");
+
+ // set zip dari DB kalau kosong
+ if (!zip && defaultValues?.zip) {
+ setValue("zip", defaultValues.zip);
+ }
+
+ // set state dari DB kalau kosong
+ if (!province && defaultValues?.state) {
+ const selectedState = states.find(
+ (state) =>
+ defaultValues.state.includes(state.label) ||
+ state.label.includes(defaultValues.state)
+ );
+ if (selectedState) {
+ setValue("state", selectedState.value);
+ }
+ }
+
+ // set street dari DB kalau kosong
+ if (!street && defaultValues?.street) {
+ setValue("street", defaultValues.street);
+ }
+ }, [states, setValue, getValues, defaultValues]);
+
+
useEffect(() => {
const loadStates = async () => {
let dataStates = await stateApi({ tempo: false });
@@ -101,7 +198,7 @@ const EditAddress = ({ id, defaultValues }) => {
};
loadStates();
}, []);
-
+
const watchState = watch('state');
useEffect(() => {
setValue('city', '');
@@ -122,35 +219,46 @@ const EditAddress = ({ id, defaultValues }) => {
loadCities();
}
}, [watchState, setValue, getValues]);
-
+
useEffect(() => {
- if (Object.keys(detailAddress).length > 0) {
- const selectedCities = cities.find(
- (city) =>
- city.label.toLowerCase() === detailAddress?.district.toLowerCase()
- ) || cities.find(
+ if (!isPinned) return;
+
+ if (getValues("city")) return;
+
+ if (Object.keys(detailAddress || {}).length > 0) {
+ const selectedCities =
+ cities.find(
+ (city) =>
+ city.label.toLowerCase() === detailAddress?.district?.toLowerCase()
+ ) ||
+ cities.find(
(city) =>
- detailAddress?.district.toLowerCase().includes(city.label.toLowerCase()) ||
- city.label.toLowerCase().includes(detailAddress?.district.toLowerCase())
+ detailAddress?.district
+ ?.toLowerCase()
+ .includes(city.label.toLowerCase()) ||
+ city.label
+ .toLowerCase()
+ .includes(detailAddress?.district?.toLowerCase())
);
- setValue('city', selectedCities?.value);
- }
- }, [cities, detailAddress, setValue]);
-
- const watchCity = watch('city');
- useEffect(() => {
- if (watchCity) {
- // setValue('district', '');
- const loadDistricts = async () => {
- let dataDistricts = await districtApi({ cityId: watchCity });
- dataDistricts = dataDistricts.map((district) => ({
- value: district.id,
- label: district.name,
+
+ if (selectedCities) {
+ setValue("city", selectedCities.value);
+ }
+ }
+ }, [cities, detailAddress, isPinned, getValues, setValue]);
+
+ const watchCity = watch('city');
+ useEffect(() => {
+ if (watchCity) {
+ 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');
if (oldDistrict) {
- // setValue('district', oldDistrict);
setValue('oldDistrict', '');
}
};
@@ -159,6 +267,9 @@ const EditAddress = ({ id, defaultValues }) => {
}, [watchCity, setValue, getValues]);
useEffect(() => {
+ if (!isPinned) return; // skip kalau belum pin
+ if (getValues("district")) return;
+
if (Object.keys(detailAddress).length > 0) {
const selectedDistrict = districts.find(
(district) =>
@@ -173,12 +284,10 @@ const EditAddress = ({ id, defaultValues }) => {
}
}, [districts, detailAddress, setValue]);
-
-
+
const watchDistrict = watch('district');
useEffect(() => {
if (watchDistrict) {
- // setValue('subDistrict', '');
const loadSubDistricts = async () => {
let dataSubDistricts = await subDistrictApi({
districtId: watchDistrict,
@@ -199,8 +308,10 @@ const EditAddress = ({ id, defaultValues }) => {
}
}, [watchDistrict, setValue, getValues]);
-
useEffect(() => {
+ if (!isPinned) return; // skip kalau belum pin
+ if (getValues("subDistrict")) return;
+
if (Object.keys(detailAddress).length > 0) {
const selectedSubDistrict = subDistricts.find(
(district) =>
@@ -221,8 +332,39 @@ const EditAddress = ({ id, defaultValues }) => {
setValue('id', id);
}
}, [id, setValue]);
-
+
const onSubmitHandler = async (values) => {
+ if (addressMaps) {
+ if (!detailAddress){
+ if (defaultValues?.oldCity !== values.city) {
+ setPopupMessage("Titik Koordinat tidak sesuai dengan Kota yang dipilih");
+ setShowValidationPopup(true);
+ // console.log(detailAddress)
+ return;
+ }
+ }
+ if (detailAddress) {
+ const cityName = normalizeName(
+ cities.find((c) => c.value === watch("city"))?.label || ""
+ );
+ const districtName = normalizeName(detailAddress?.district || "");
+ // console.log(cityName, '=', districtName);
+
+ if (cityName && cityName !== districtName) {
+ setPopupMessage("Titik Koordinat tidak sesuai dengan Kota yang dipilih");
+ setShowValidationPopup(true);
+ return;
+ }
+ }
+ }
+ // if(!addressMaps && detailAddress){
+ // if (selectedCityName && selectedCityName !== detailAddress?.district?.toLowerCase()) {
+ // setPopupMessage("Titik Koordinat tidak sesuai dengan Kota yang dipilih 3");
+ // setShowValidationPopup(true);
+ // return;
+ // }
+ // }
+
const data = {
...values,
phone: values.mobile,
@@ -230,20 +372,32 @@ const EditAddress = ({ id, defaultValues }) => {
city_id: parseInt(values.city, 10),
district_id: parseInt(values.district, 10),
sub_district_id: parseInt(values.subDistrict, 10),
- longtitude: selectedPosition?.lng,
- latitude: selectedPosition?.lat,
- address_map: addressMaps,
};
+
+
+ if (isPinned) {
+ data.longtitude = selectedPosition?.lng;
+ data.latitude = selectedPosition?.lat;
+ data.address_map = addressMaps || values.addressMap;
+ data.use_pin = true;
+ } else {
+ data.use_pin = false;
+ // pastikan tidak ada nilai default center yang ikut terkirim
+ delete data.longtitude;
+ delete data.latitude;
+ delete data.address_map;
+ }
+
if (!auth.company) {
data.alamat_lengkap_text = values.street;
}
+
try {
const address = await editAddressApi({ id, data });
console.log('Response address:', address);
let isUpdated = null;
- // Jika company dan partnerId sama dengan id, maka update data alamat wajib pajak
const isCompanyEditingSelf = auth.company && auth.partnerId == id;
if (isCompanyEditingSelf) {
@@ -260,15 +414,13 @@ const EditAddress = ({ id, defaultValues }) => {
mobile: values.mobile,
};
- const isUpdated = await editPartnerApi({
+ const isUpdatedRes = await editPartnerApi({
id: auth.partnerId,
data: dataAlamat,
});
-
- console.log('Response isUpdated:', isUpdated);
+ console.log('Response isUpdated:', isUpdatedRes);
}
- // Validasi kondisi sukses
const isSuccess = !!address?.id;
if (isSuccess) {
@@ -286,21 +438,10 @@ const EditAddress = ({ id, defaultValues }) => {
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) {
- // toast.success('Berhasil mengubah alamat');
- // router.back();
- // } else {
- // toast.error('Terjadi kesalahan internal');
- // router.back();
- // }
+ // console.log('ini adalah', dataProfile);
};
-
+ // console.log('ini adalah', detailAddress);
return (
<>
<BottomPopup
@@ -310,12 +451,29 @@ const EditAddress = ({ id, defaultValues }) => {
close={() => setPinedMaps(false)}
>
<div className='flex mt-4'>
- <PinPointMap
- initialLatitude={selectedPosition?.lat}
- initialLongitude={selectedPosition?.lng}
- initialAddress={tempAddress}
- />
+ <PinPointMap
+ initialLatitude={getValues('latitude')}
+ initialLongitude={getValues('longtitude')}
+ initialAddress={getValues('addressMap')}
+ />
+ </div>
+ </BottomPopup>
+ <BottomPopup
+ active={showValidationPopup}
+ close={() => setShowValidationPopup(false)}
+ >
+ <div className="leading-7 text-gray_r-12/80 text-center">
+ {popupMessage}
+ </div>
+ <div className="flex justify-center mt-6">
+ <button
+ className="btn-solid-red w-full md:w-auto"
+ type="button"
+ onClick={() => setShowValidationPopup(false)}
+ >
+ OK
+ </button>
</div>
</BottomPopup>
<div className='max-w-none md:container mx-auto flex p-0 md:py-10'>
@@ -332,17 +490,30 @@ const EditAddress = ({ id, defaultValues }) => {
<form onSubmit={handleSubmit(onSubmitHandler)}>
<div className='mb-4 items-start'>
<label className='form-label mb-2'>Koordinat Alamat</label>
- {tempAddress ? (
+ {addressMaps ? (
<div className='flex gap-x-2 items-center'>
- <button type='button' className="flex items-center justify-center me-3 p-2 badge-solid-red text-white rounded-full hover:bg-red-500 transition">
- <MapPinIcon class='h-6 w-6' onClick={() => setPinedMaps(true)} />{' '}
- </button>
- <span> {tempAddress} </span>
+ <button
+ type='button'
+ className='flex items-center justify-center me-3 p-2 badge-solid-red text-white rounded-full hover:bg-red-500 transition'
+ >
+ <MapPinIcon
+ className='h-6 w-6'
+ onClick={() => setPinedMaps(true)}
+ />
+ </button>
+ <span> {addressMaps} </span>
</div>
) : (
- <Button variant='plain' style={{ padding: 0 }} onClick={() => setPinedMaps(true)}>
- <button type='button' className="flex items-center justify-center me-3 p-2 badge-solid-red text-white rounded-full hover:bg-red-500 transition">
- <MapPinIcon className="h-6 w-6" />
+ <Button
+ variant='plain'
+ style={{ padding: 0 }}
+ onClick={() => setPinedMaps(true)}
+ >
+ <button
+ type='button'
+ className='flex items-center justify-center me-3 p-2 badge-solid-red text-white rounded-full hover:bg-red-500 transition'
+ >
+ <MapPinIcon className='h-6 w-6' />
</button>
Pin Koordinat Alamat
</Button>
@@ -530,4 +701,4 @@ const types = [
{ value: 'other', label: 'Other Address' },
];
-export default EditAddress;
+export default EditAddress; \ No newline at end of file
diff --git a/src/lib/auth/api/checkParentStatusApi.js b/src/lib/auth/api/checkParentStatusApi.js
new file mode 100644
index 00000000..aa2eb1b6
--- /dev/null
+++ b/src/lib/auth/api/checkParentStatusApi.js
@@ -0,0 +1,13 @@
+import odooApi from '@/core/api/odooApi';
+import { getAuth } from '@/core/utils/auth';
+
+const checkParentStatusApi = async () => {
+ const auth = getAuth();
+ const checkParentStatus = await odooApi(
+ 'GET',
+ `/api/v1/user/${auth.partnerId}/parent_status`
+ );
+ return checkParentStatus;
+};
+
+export default checkParentStatusApi; \ No newline at end of file
diff --git a/src/lib/auth/api/editPersonalProfileApi.js b/src/lib/auth/api/editPersonalProfileApi.js
index 39cd44c1..90bda114 100644
--- a/src/lib/auth/api/editPersonalProfileApi.js
+++ b/src/lib/auth/api/editPersonalProfileApi.js
@@ -3,7 +3,7 @@ import { getAuth } from '@/core/utils/auth'
const editPersonalProfileApi = async ({ data }) => {
const auth = getAuth()
- const dataProfile = await odooApi('PUT', `/api/v1/user/${auth.id}`, data)
+ const dataProfile = await odooApi('POST', `/api/v1/user/${auth.id}`, data)
return dataProfile
}
diff --git a/src/lib/auth/api/switchAccountApi.js b/src/lib/auth/api/switchAccountApi.js
index 79ca2553..f62693f6 100644
--- a/src/lib/auth/api/switchAccountApi.js
+++ b/src/lib/auth/api/switchAccountApi.js
@@ -4,7 +4,7 @@ import { getAuth } from '@/core/utils/auth';
const switchAccountApi = async ({ data }) => {
const auth = getAuth();
const switchAccount = await odooApi(
- 'PUT',
+ 'POST',
`/api/v1/user/${auth.partnerId}/switch`,
data
);
diff --git a/src/lib/auth/components/SwitchAccount.jsx b/src/lib/auth/components/SwitchAccount.jsx
index 46e57348..840758c9 100644
--- a/src/lib/auth/components/SwitchAccount.jsx
+++ b/src/lib/auth/components/SwitchAccount.jsx
@@ -13,9 +13,10 @@ import { useRegisterStore } from '~/modules/register/stores/useRegisterStore.ts'
import { registerUser } from '~/services/auth';
import { useMutation } from 'react-query';
import { isValid } from 'zod';
+import Spinner from "@/core/components/elements/Spinner/LogoSpinner";
import useDevice from '@/core/hooks/useDevice';
import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
-const SwitchAccount = ({ company_type }) => {
+const SwitchAccount = ({ company_type, setIsAprove, setUbahAkun }) => {
const { isDesktop, isMobile } = useDevice();
const auth = useAuth();
const [isOpen, setIsOpen] = useState(true);
@@ -27,6 +28,8 @@ const SwitchAccount = ({ company_type }) => {
const [selectedValue, setSelectedValue] = useState('PKP');
const [buttonSubmitClick, setButtonSubmitClick] = useState(false);
const [changeConfirmation, setChangeConfirmation] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isLoadingPopup, setIsLoadingPopup] = useState(false);
const { register, setValue, handleSubmit } = useForm({
defaultValues: {
email: '',
@@ -131,35 +134,46 @@ const SwitchAccount = ({ company_type }) => {
setIsPKP(false);
}
};
- const onSubmitHandler = async (values) => {
- toast.loading('Mengubah status akun...');
+ const onSubmitHandler = async () => {
+ setChangeConfirmation(false); // Tutup popup konfirmasi
+ setIsLoadingPopup(true); // Munculkan popup loading
+
updateForm('parent_id', `${auth.parentId}`);
- setChangeConfirmation(false);
- // let data = { ...form, id: `${auth.partnerId}` };
+
const data = form;
if (!isFormValid) {
setNotValid(true);
setButtonSubmitClick(!buttonSubmitClick);
+ toast.error('Form belum valid. Mohon periksa kembali input Anda.');
+ setIsLoadingPopup(false);
return;
- } else {
- setButtonSubmitClick(!buttonSubmitClick);
- setNotValid(false);
}
- // if (!values.password) delete data.password;
- const isUpdated = await switchAccountApi({ data });
- if (isUpdated?.switch === 'Pending') {
- // setAuth(isUpdated.user);
- // setValue('password', '');
- toast.success('Berhasil mengubah akun', { duration: 1500 });
- setTimeout(() => {
- window.location.reload();
- }, 1500);
- return;
+ try {
+ const isUpdated = await switchAccountApi({ data });
+
+ if (isUpdated?.switch === 'pending') {
+ toast.success('Berhasil mengajukan ubah akun', { duration: 1500 });
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('autoCheckProfile', 'true');
+ }
+ setTimeout(() => {
+ setIsAprove('pending');
+ setUbahAkun('pending');
+ window.location.reload();
+ }, 1500);
+ } else {
+ toast.error('Gagal mengubah akun. Silakan coba lagi nanti atau hubungi admin jika masalah tetap terjadi.');
+ setIsLoadingPopup(false);
+ }
+ } catch (error) {
+ console.error(error);
+ toast.error('Terjadi kesalahan saat menghubungi server, silahkan cek internet Anda atau hubungi admin Indoteknik.');
+ setIsLoadingPopup(false);
}
- toast.error('Terjadi kesalahan internal');
};
+
const onSubmitHandlerCancel = async (values) => {
window.location.reload();
};
@@ -172,26 +186,37 @@ const SwitchAccount = ({ company_type }) => {
title='Ubah profil Bisnis'
>
<div className='leading-7 text-gray_r-12/80'>
- Anda yakin akan merubah profil bisnis anda dari INDIVIDU menjadi{' '}
- {selectedValue}?
+ Anda yakin akan merubah profil bisnis anda dari INDIVIDU menjadi {selectedValue}?
</div>
<div className='flex mt-6 gap-x-4 md:justify-end'>
<button
- className='btn-solid-red flex-1 md:flex-none'
+ className='btn-solid-red flex-1 md:flex-none flex items-center justify-center'
type='button'
onClick={onSubmitHandler}
+ disabled={isLoading}
>
- Ya, Ubah
+ {isLoading && <Spinner className="w-4 h-4 mr-2 text-white" />}
+ {isLoading ? 'Menyimpan...' : 'Ya, Ubah'}
</button>
<button
className='btn-light flex-1 md:flex-none'
type='button'
onClick={() => setChangeConfirmation(false)}
+ disabled={isLoading}
>
Batal
</button>
</div>
</BottomPopup>
+ <BottomPopup active={isLoadingPopup} close="">
+ <div className="leading-7 text-gray_r-12/80 flex justify-center">
+ Mengubah status akun...
+ </div>
+ <div className="container flex justify-center my-4">
+ <Spinner width={48} height={48} />
+ </div>
+ </BottomPopup>
+
{/* <div type='button' className='ml-4 flex items-center text-left w-full'>
<div
className={`flex ${
diff --git a/src/lib/category/components/Breadcrumb.jsx b/src/lib/category/components/Breadcrumb.jsx
index 127904ee..50557c3e 100644
--- a/src/lib/category/components/Breadcrumb.jsx
+++ b/src/lib/category/components/Breadcrumb.jsx
@@ -1,56 +1,188 @@
-import odooApi from '@/core/api/odooApi'
-import { createSlug } from '@/core/utils/slug'
+import odooApi from '@/core/api/odooApi';
+import { createSlug } from '@/core/utils/slug';
import {
Breadcrumb as ChakraBreadcrumb,
BreadcrumbItem,
BreadcrumbLink,
- Skeleton
-} from '@chakra-ui/react'
-import Link from 'next/link'
-import React from 'react'
-import { useQuery } from 'react-query'
-
-/**
- * Render a breadcrumb component.
- *
- * @param {object} categoryId - The ID of the category.
- * @return {JSX.Element} The breadcrumb component.
- */
+ Skeleton,
+} from '@chakra-ui/react';
+import Link from 'next/link';
+import React from 'react';
+import { useQuery } from 'react-query';
+import useDevice from '@/core/hooks/useDevice';
+
const Breadcrumb = ({ categoryId }) => {
const breadcrumbs = useQuery(
- `category-breadcrumbs/${categoryId}`,
- async () => await odooApi('GET', `/api/v1/category/${categoryId}/category-breadcrumb`)
- )
-
- return (
- <div className='container mx-auto py-4 md:py-6'>
- <Skeleton isLoaded={!breadcrumbs.isLoading} className='w-2/3'>
- <ChakraBreadcrumb>
- <BreadcrumbItem>
- <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'>
- Home
- </BreadcrumbLink>
- </BreadcrumbItem>
-
- {breadcrumbs.data?.map((category, index) => (
- <BreadcrumbItem key={index} isCurrentPage={index === breadcrumbs.data.length - 1}>
- {index === breadcrumbs.data.length - 1 ? (
- <BreadcrumbLink className='whitespace-nowrap'>{category.name}</BreadcrumbLink>
- ) : (
+ ['category-breadcrumbs', categoryId],
+ async () =>
+ await odooApi('GET', `/api/v1/category/${categoryId}/category-breadcrumb`)
+ );
+ const { isDesktop, isMobile } = useDevice();
+
+ const items = breadcrumbs.data ?? [];
+ const lastIdx = items.length - 1;
+
+ if (isDesktop) {
+ return (
+ <div className='container mx-auto py-4 md:py-6'>
+ <Skeleton isLoaded={!breadcrumbs.isLoading} className='w-2/3'>
+ <ChakraBreadcrumb
+ spacing='8px'
+ sx={{
+ '& ol': {
+ display: 'flex',
+ flexWrap: { base: 'wrap', md: 'nowrap' },
+ alignItems: 'center',
+ },
+ '& li': { display: 'inline-flex', alignItems: 'center' },
+ '& li:not(:last-of-type)': {
+ flex: '0 0 auto',
+ whiteSpace: 'nowrap',
+ },
+ '& li:last-of-type': {
+ flex: '1 1 auto',
+ minWidth: 0,
+ },
+ }}
+ >
+ {/* Home */}
+ <BreadcrumbItem>
+ <BreadcrumbLink as={Link} href='/' className='!text-danger-500'>
+ Home
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+
+ {/* Categories */}
+ {items.map((category, index) => {
+ const isLast = index === lastIdx;
+ return (
+ <BreadcrumbItem key={index} isCurrentPage={isLast}>
+ {isLast ? (
+ <BreadcrumbLink className='block whitespace-normal break-words md:whitespace-nowrap'>
+ {category.name}
+ </BreadcrumbLink>
+ ) : (
+ <BreadcrumbLink
+ as={Link}
+ href={createSlug(
+ '/shop/category/',
+ category.name,
+ category.id
+ )}
+ className='!text-danger-500'
+ >
+ {category.name}
+ </BreadcrumbLink>
+ )}
+ </BreadcrumbItem>
+ );
+ })}
+ </ChakraBreadcrumb>
+ </Skeleton>
+ </div>
+ );
+ }
+
+ if (isMobile) {
+ const items = breadcrumbs.data ?? [];
+ const n = items.length;
+ const lastCat = n >= 1 ? items[n - 1] : null; // terakhir (current)
+ const secondLast = n >= 2 ? items[n - 2] : null; // sebelum current
+ const beforeSecond = n >= 3 ? items[n - 3] : null; // sebelum secondLast
+ const hiddenText =
+ n >= 3
+ ? items
+ .slice(0, n - 2)
+ .map((c) => c.name)
+ .join(' / ')
+ : '';
+
+ return (
+ <div className='container mx-auto py-2 mt-2'>
+ <Skeleton isLoaded={!breadcrumbs.isLoading} className='w-full'>
+ <ChakraBreadcrumb
+ separator={<span className='mx-1'>/</span>} // lebih rapat
+ spacing='4px'
+ sx={{
+ '& ol': {
+ display: 'flex',
+ alignItems: 'center',
+ overflow: 'hidden', // untuk ellipsis
+ whiteSpace: 'nowrap', // untuk ellipsis
+ gap: '0', // no extra gap
+ },
+ '& li': { display: 'inline-flex', alignItems: 'center' },
+ '& li:not(:last-of-type)': {
+ flex: '0 0 auto',
+ whiteSpace: 'nowrap',
+ },
+ '& li:last-of-type': {
+ flex: '0 1 auto', // jangan ambil full space biar gak keliatan “space kosong”
+ minWidth: 0,
+ },
+ }}
+ className='text-caption-2 p-0'
+ >
+ {/* Home */}
+ <BreadcrumbItem>
+ <BreadcrumbLink as={Link} href='/' className='!text-danger-500'>
+ Home
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+
+ {/* Jika ada kategori sebelum secondLast, tampilkan '..' (link ke beforeSecond) */}
+ {beforeSecond && (
+ <BreadcrumbItem>
<BreadcrumbLink
as={Link}
- href={createSlug('/shop/category/', category.name, category.id)}
- className='!text-danger-500 whitespace-nowrap'
+ href={createSlug(
+ '/shop/category/',
+ beforeSecond.name,
+ beforeSecond.id
+ )}
+ title={hiddenText}
+ aria-label={`Kembali ke ${beforeSecond.name}`}
+ className='!text-danger-500'
>
- {category.name}
+ ..
</BreadcrumbLink>
- )}
- </BreadcrumbItem>
- ))}
- </ChakraBreadcrumb>
- </Skeleton>
- </div>
- )
-}
-
-export default Breadcrumb
+ </BreadcrumbItem>
+ )}
+
+ {/* secondLast sebagai link (kalau ada) */}
+ {secondLast && (
+ <BreadcrumbItem>
+ <BreadcrumbLink
+ as={Link}
+ href={createSlug(
+ '/shop/category/',
+ secondLast.name,
+ secondLast.id
+ )}
+ className='!text-danger-500'
+ >
+ {secondLast.name}
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+ )}
+
+ {/* lastCat (current) dengan truncate & lebar dibatasi */}
+ {lastCat && (
+ <BreadcrumbItem isCurrentPage>
+ <span
+ className='inline-block truncate align-bottom'
+ style={{ maxWidth: '60vw' }} // batasi lebar supaya gak “makan” baris & keliatan space kosong
+ title={lastCat.name}
+ >
+ {lastCat.name}
+ </span>
+ </BreadcrumbItem>
+ )}
+ </ChakraBreadcrumb>
+ </Skeleton>
+ </div>
+ );
+ }
+};
+
+export default Breadcrumb;
diff --git a/src/lib/checkout/api/checkoutApi.js b/src/lib/checkout/api/checkoutApi.js
index c30d9631..11c7d4c2 100644
--- a/src/lib/checkout/api/checkoutApi.js
+++ b/src/lib/checkout/api/checkoutApi.js
@@ -11,6 +11,16 @@ export const checkoutApi = async ({ data }) => {
return dataCheckout;
};
+export const checkoutQuotation = async (data) => {
+ const auth = getAuth();
+ const qs = new URLSearchParams({ context: 'quotation' }).toString();
+ return odooApi(
+ 'POST',
+ `/api/v1/partner/${auth.partnerId}/sale_order/checkout?${qs}`,
+ data
+ );
+};
+
export const getProductsCheckout = async (query) => {
const queryParam = new URLSearchParams(query);
const userId = getAuth()?.id;
diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx
index d8ede118..6cda069c 100644
--- a/src/lib/checkout/components/Checkout.jsx
+++ b/src/lib/checkout/components/Checkout.jsx
@@ -55,7 +55,9 @@ 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;
@@ -172,7 +174,7 @@ const Checkout = () => {
selectedService,
listExpedisi,
setExpedisi,
- productSla
+ productSla,
} = useCheckout();
const expedisiValidation = useRef(null);
@@ -184,7 +186,7 @@ const Checkout = () => {
let dataVoucher = await getVoucher(auth?.id, {
source: query,
type: 'all,brand',
- partner_id : auth?.partnerId,
+ partner_id: auth?.partnerId,
});
SetListVoucher(dataVoucher);
@@ -210,7 +212,6 @@ const Checkout = () => {
return;
}
-
dataVoucher.forEach((addNewLine) => {
if (addNewLine.applyType !== 'shipping') {
// Mencari voucher dalam listVouchers
@@ -374,17 +375,21 @@ const Checkout = () => {
}
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;
- }
+ 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) => ({
@@ -415,7 +420,8 @@ const Checkout = () => {
order_line: JSON.stringify(productOrder),
delivery_amount: biayaKirim,
carrier_id: selectedCourierId,
- estimated_arrival_days_start : parseInt(eta_courier_start) + parseInt(productSla),
+ 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
@@ -430,8 +436,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;
@@ -1019,10 +1025,10 @@ const Checkout = () => {
</div>
)}
- {selectedCarrierId == SELF_PICKUP_ID && (
+ {selectedCourierId == SELF_PICKUP_ID && (
<PickupAddress label='Alamat Pickup' />
)}
- {selectedCarrierId != SELF_PICKUP_ID && (
+ {selectedCourierId != SELF_PICKUP_ID && (
<Skeleton
isLoaded={!!selectedAddress.invoicing && !!selectedAddress.shipping}
minHeight={320}
@@ -1137,7 +1143,9 @@ const Checkout = () => {
<div>{currencyFormat(cartCheckout?.subtotal)}</div>
</div>
<div className='flex gap-x-2 justify-between'>
- <div className='text-gray_r-11'>PPN {((PPN - 1) * 100).toFixed(0)}%</div>
+ <div className='text-gray_r-11'>
+ PPN {((PPN - 1) * 100).toFixed(0)}%
+ </div>
<div>{currencyFormat(cartCheckout?.tax)}</div>
</div>
<div className='flex gap-x-2 justify-between'>
@@ -1269,10 +1277,7 @@ const Checkout = () => {
className='flex-1 btn-yellow'
onClick={checkout}
disabled={
- !products ||
- products?.length == 0 ||
- priceCheck ||
- hasNoPrice
+ !products || products?.length == 0 || priceCheck || hasNoPrice || isLoading
}
>
{isLoading ? 'Loading...' : 'Lanjut Pembayaran'}
@@ -1317,16 +1322,11 @@ const Checkout = () => {
<div className='flex'>
{' '}
<div className='w-3/4 border border-gray_r-6 rounded bg-white'>
- {selectedCarrierId == SELF_PICKUP_ID && (
+ {selectedCourierId == SELF_PICKUP_ID && (
<PickupAddress label='Alamat Pickup' />
)}
- {selectedCarrierId != SELF_PICKUP_ID && (
- <Skeleton
- isLoaded={
- !!selectedAddress.invoicing && !!selectedAddress.shipping
- }
- minHeight={290}
- >
+ {selectedCourierId != SELF_PICKUP_ID && (
+ <Skeleton isLoaded minHeight={290}>
<SectionAddress
address={selectedAddress.shipping}
label='Alamat Pengiriman'
@@ -1444,7 +1444,9 @@ const Checkout = () => {
<div>{currencyFormat(cartCheckout?.subtotal)}</div>
</div>
<div className='flex gap-x-2 justify-between'>
- <div className='text-gray_r-11'>PPN {((PPN - 1) * 100).toFixed(0)}%</div>
+ <div className='text-gray_r-11'>
+ PPN {((PPN - 1) * 100).toFixed(0)}%
+ </div>
<div>{currencyFormat(cartCheckout?.tax)}</div>
</div>
<div className='flex gap-x-2 justify-between'>
@@ -1577,7 +1579,8 @@ const Checkout = () => {
!products ||
products?.length == 0 ||
priceCheck ||
- hasNoPrice
+ hasNoPrice ||
+ isLoading
}
>
{isLoading ? 'Loading...' : 'Lanjut Pembayaran'}
@@ -1633,8 +1636,14 @@ const SectionAddress = ({ address, label, url }) => (
{address.addressMap ? (
<label>Sudah Pinpoint</label>
) : (
- <Link href={'/my/address/' + address.id + '/edit'} target='_blank' className='cursor-pointer'>
- <label className='text-red-500 cursor-pointer '>Belum Pinpoint</label>
+ <Link
+ href={'/my/address/' + address.id + '/edit'}
+ target='_blank'
+ className='cursor-pointer'
+ >
+ <label className='text-red-500 cursor-pointer '>
+ Belum Pinpoint
+ </label>
</Link>
)}
</div>
@@ -1644,7 +1653,7 @@ const SectionAddress = ({ address, label, url }) => (
);
const SectionValidation = ({ address }) =>
- address?.stateId == 0 && (
+ address?.stateId === 0 && (
<BottomPopup active={true} title='Update Alamat'>
<div className='leading-7 text-gray_r-12/80'>
Mohon untuk memperbarui alamat Anda dengan mengklik tombol di bawah ini.{' '}
@@ -1661,14 +1670,14 @@ const SectionValidation = ({ address }) =>
);
const SectionExpedisi = ({
- address,
- listExpedisi,
- setSelectedExpedisi,
- checkWeigth,
- checkoutValidation,
- expedisiValidation,
- loadingRajaOngkir,
- }) =>
+ address,
+ listExpedisi,
+ setSelectedExpedisi,
+ checkWeigth,
+ checkoutValidation,
+ expedisiValidation,
+ loadingRajaOngkir,
+}) =>
address?.rajaongkirCityId > 0 && (
<div className='p-4' ref={expedisiValidation}>
<div className='flex justify-between items-center'>
@@ -1720,9 +1729,9 @@ const SectionExpedisi = ({
)}
</div>
<style jsx>{`
- .shake {
- animation: shake 0.4s ease-in-out;
- }
+ .shake {
+ animation: shake 0.4s ease-in-out;
+ }
`}</style>
</div>
{checkWeigth == true && (
diff --git a/src/lib/checkout/components/FinishCheckout.jsx b/src/lib/checkout/components/FinishCheckout.jsx
index d533325e..51837a59 100644
--- a/src/lib/checkout/components/FinishCheckout.jsx
+++ b/src/lib/checkout/components/FinishCheckout.jsx
@@ -55,12 +55,21 @@ const FinishCheckout = ({ query }) => {
<div className='text-title-sm md:text-title-lg text-center font-semibold'>
Terima Kasih atas Pembelian di Indoteknik.com
</div>
- <p className='text-title-sm md:text-title-lg font-semibold my-2'>No. Transaksi: <span className='text-red-500'>{query?.order_id?.replaceAll('-', '/')}</span></p>
+ <p className='text-title-sm md:text-title-lg font-semibold my-2'>
+ No. Transaksi:{' '}
+ <span className='text-red-500'>
+ {query?.order_id?.replaceAll('-', '/')}
+ </span>
+ </p>
<div className='flex flex-col justify-center items-center text-body-2 md:text-body-1 text-center mt-3 px-24 md:px-36 py-4 border-2 gap-y-2 rounded'>
- <p className="text-title-sm md:text-title-xl text-gray-500 mt-1">Estimasi Barang Siap pada Tanggal</p>
- <p className="text-title-sm md:text-title-xl text-red-500 font-semibold my-2">{data?.expectedReadyToShip}</p>
+ <p className='text-title-sm md:text-title-xl text-gray-500 mt-1'>
+ Estimasi Barang Siap pada Tanggal
+ </p>
+ <p className='text-title-sm md:text-title-xl text-red-500 font-semibold my-2'>
+ {data?.expectedReadyToShip}
+ </p>
<Link
- href={`/my/quotations/${data?.id}`}
+ href={`/my/transactions/${data?.id}`}
className='btn-solid-red rounded-md text-base'
>
Cek Detail Transaksi
@@ -74,7 +83,7 @@ const FinishCheckout = ({ query }) => {
</a>{' '}
atau{' '}
<span onClick={sendEmail} className='text-red-500 cursor-pointer'>
- kirim rincian pesanan ulang
+ Kirim ulang rincian pesanan ke Email anda.
</span>
.
</div>
diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx
index 2e92ffbc..66182589 100644
--- a/src/lib/checkout/components/SectionExpedition.jsx
+++ b/src/lib/checkout/components/SectionExpedition.jsx
@@ -18,7 +18,7 @@ import { getProductsSla } from '../api/checkoutApi';
function mappingItems(products) {
return products?.map((item) => ({
// name: item.parent.name || item?.name || 'Unknown Product',
- name: item?.name,
+ name: item?.name,
description: `${item.code} - ${item.name}`,
value: item.price.priceDiscount,
weight: item.weight * 1000,
@@ -27,61 +27,71 @@ function mappingItems(products) {
}
function reverseMappingCourier(couriersOdoo, couriers, includeInstant = false) {
- // Buat peta courier berdasarkan nama courier dari couriers
+ // Bangun peta dari hasil Biteship (pakai dua key: code & name)
const courierMap = couriers.reduce((acc, item) => {
- const { courier_name, courier_code, courier_service_code } = item;
- const key = courier_code.toLowerCase();
-
- if (
- !includeInstant && (['hours'].includes(item.shipment_duration_unit.toLowerCase()) || item.service_type == 'same_day')
-
- ) {
- return acc;
- }
+ const codeKey = (item.courier_code || '').toLowerCase();
+ const nameKey = (item.courier_name || '').toLowerCase();
+
+ const isInstant =
+ (item.shipment_duration_unit || '').toLowerCase() === 'hours' ||
+ (item.service_type || '').toLowerCase() === 'same_day';
+ if (!includeInstant && isInstant) return acc;
+
+ const ensureEntry = (key) => {
+ if (!key) return;
+ if (!acc[key]) {
+ acc[key] = {
+ courier_name: item.courier_name,
+ courier_code: item.courier_code,
+ service_type: {},
+ };
+ }
+ };
- if (!acc[key]) {
- acc[key] = {
- courier_name: item.courier_name,
- courier_code: courier_code,
- service_type: {},
- };
- }
+ ensureEntry(codeKey);
+ ensureEntry(nameKey);
- acc[key].service_type[courier_service_code] = {
+ const svc = {
service_name: item.courier_service_name,
duration: item.duration,
shipment_range: item.shipment_duration_range,
shipment_unit: item.shipment_duration_unit,
price: item.price,
- service_type: courier_service_code,
+ service_type: item.courier_service_code,
description: item.description,
};
+ if (codeKey && acc[codeKey]) {
+ acc[codeKey].service_type[item.courier_service_code] = svc;
+ }
+ if (nameKey && acc[nameKey]) {
+ acc[nameKey].service_type[item.courier_service_code] = svc;
+ }
return acc;
}, {});
- // Iterasi berdasarkan couriersOdoo
- return couriersOdoo.map((courierOdoo) => {
- const courierNameKey = courierOdoo.label.toLowerCase();
- const carrierId = courierOdoo.carrierId;
+ // Petakan Odoo ke map Biteship dan FILTER hanya yg punya layanan
+ return couriersOdoo
+ .map((courierOdoo) => {
+ const key = (courierOdoo.label || '').toLowerCase();
+ const matched = courierMap[key] || null;
- const mappedCourier = courierMap[courierNameKey] || false;
+ if (!matched) return { ...courierOdoo, courier: false };
- if (!mappedCourier) {
return {
...courierOdoo,
- courier: false,
+ courier: {
+ ...matched,
+ courier_id_odoo: courierOdoo.carrierId, // penting: simpan id Odoo di sini
+ },
};
- }
-
- return {
- ...courierOdoo,
- courier: {
- ...mappedCourier,
- courier_id_odoo: carrierId,
- },
- };
- });
+ })
+ .filter(
+ (x) =>
+ x.courier &&
+ x.courier.service_type &&
+ Object.keys(x.courier.service_type).length > 0
+ );
}
function mappingCourier(couriersOdoo, couriers, notIncludeInstant = false) {
@@ -214,7 +224,7 @@ export default function SectionExpedition({ products }) {
let data = {
products: JSON.stringify(productsMapped),
- }
+ };
const res = await odooApi('POST', `/api/v1/product/variants/sla`, data);
setSlaProducts(res);
} catch (error) {
@@ -328,13 +338,13 @@ export default function SectionExpedition({ products }) {
};
useEffect(() => {
- if (serviceOptions.length > 0) {
- setSavedServiceOptions(serviceOptions);
- }
-}, [serviceOptions]);
+ if (serviceOptions.length > 0) {
+ setSavedServiceOptions(serviceOptions);
+ }
+ }, [serviceOptions]);
return (
- <form >
+ <form>
<div className='px-4 py-2'>
<div className='flex justify-between items-center'>
<div className='font-medium'>Pilih Ekspedisi: </div>
@@ -371,27 +381,40 @@ export default function SectionExpedition({ products }) {
<p className='font-semibold'>SELF PICKUP</p>
</div>
</div>
- {couriers?.map((courier) => (
- <div
- key={courier?.courier?.courier_code}
- onClick={() => onCourierChange(courier)}
- className='flex justify-between p-2 items-center hover:bg-gray-100 cursor-pointer'
- >
- <div>
- <p className='font-semibold'>
- {courier?.label}
- </p>
+ {couriers
+ ?.map((c) => c) // sudah ter-filter di reverseMappingCourier, tapi aman buat double-check
+ .filter(
+ (c) =>
+ c.courier &&
+ Object.keys(c.courier.service_type).length > 0
+ )
+ .map((courier) => (
+ <div
+ key={
+ courier?.courier?.courier_code ||
+ courier?.label
+ }
+ onClick={() => onCourierChange(courier)}
+ className='flex justify-between p-2 items-center hover:bg-gray-100 cursor-pointer'
+ >
+ <div>
+ <p className='font-semibold'>
+ {courier?.label}
+ </p>
+ </div>
+ <span className='font-semibold'>
+ <Image
+ src={courier?.logo}
+ alt={
+ courier?.courier?.courier_name ||
+ courier?.label
+ }
+ width={50}
+ height={50}
+ />
+ </span>
</div>
- <span className='font-semibold'>
- <Image
- src={courier?.logo}
- alt={courier?.courier?.courier_name}
- width={50}
- height={50}
- />
- </span>
- </div>
- ))}
+ ))}
</>
) : (
<>
@@ -432,8 +455,7 @@ export default function SectionExpedition({ products }) {
)}
</div>
- {(serviceOptions.length > 0 ||
- selectedService )&&
+ {(serviceOptions.length > 0 || selectedService) &&
selectedCourier &&
selectedCourier !== 32 &&
selectedCourier !== 0 && (
diff --git a/src/lib/checkout/components/SectionQuotationExpedition.jsx b/src/lib/checkout/components/SectionQuotationExpedition.jsx
index b8ea04ef..817cd21b 100644
--- a/src/lib/checkout/components/SectionQuotationExpedition.jsx
+++ b/src/lib/checkout/components/SectionQuotationExpedition.jsx
@@ -239,7 +239,7 @@ export default function SectionExpeditionQuotation({ products }) {
<div className='px-4 py-2'>
<div className='flex justify-between items-center'>
<div className='font-medium'>Pilih Ekspedisi: </div>
- <div className='w-[350px]'>
+ <div className='relative w-[350px]'>
<div
className='w-full p-2 border rounded-lg bg-white cursor-pointer'
onClick={() => setOnFocuseSelectedCourier(!onFocusSelectedCourier)}
@@ -253,7 +253,10 @@ export default function SectionExpeditionQuotation({ products }) {
)}
</div>
{onFocusSelectedCourier && (
- <div className='absolute bg-white border rounded-lg mt-1 shadow-lg z-10 max-h-[200px] overflow-y-auto w-[350px]'>
+ <div
+ className='absolute left-0 top-full mt-1 bg-white border rounded-lg shadow-lg z-50
+ max-h-[200px] overflow-y-auto w-full sm:w-[350px]'
+ >
{!isLoading ? (
<>
<div
@@ -297,8 +300,8 @@ export default function SectionExpeditionQuotation({ products }) {
{checkWeigth && (
<p className='mt-4 text-gray-600'>
- Mohon maaf, pengiriman hanya tersedia untuk self pickup karena ada barang
- yang belum memiliki berat. Silakan hubungi admin via{' '}
+ Mohon maaf, pengiriman hanya tersedia untuk self pickup karena ada
+ barang yang belum memiliki berat. Silakan hubungi admin via{' '}
<a
className='text-blue-600 underline'
href='https://api.whatsapp.com/send?phone=6281717181922'
@@ -316,7 +319,7 @@ export default function SectionExpeditionQuotation({ products }) {
selectedCourier !== 0 && (
<div className='mt-4 flex justify-between'>
<div className='font-medium mb-2'>Tipe Layanan Ekspedisi:</div>
- <div className='relative w-[350px]'>
+ <div className='relative w-full sm:w-[350px]'>
<div
className='p-2 border rounded-lg bg-white cursor-pointer'
onClick={() => setIsOpen(!isOpen)}
@@ -331,11 +334,13 @@ export default function SectionExpeditionQuotation({ products }) {
</span>
</div>
) : (
- <span className='text-gray-500'>Pilih layanan pengiriman</span>
+ <span className='text-gray-500'>
+ Pilih layanan pengiriman
+ </span>
)}
</div>
{isOpen && (
- <div className='absolute bg-white border rounded-lg mt-1 shadow-lg z-10 w-full'>
+ <div className='absolute left-0 top-full mt-1 bg-white border rounded-lg shadow-lg z-50 w-full'>
{serviceOptions.map((service) => (
<div
key={service.service_type}
diff --git a/src/lib/form/components/Merchant.jsx b/src/lib/form/components/Merchant.jsx
index 03b8fc84..b65449a8 100644
--- a/src/lib/form/components/Merchant.jsx
+++ b/src/lib/form/components/Merchant.jsx
@@ -147,7 +147,7 @@ const CreateMerchant = () => {
<p>
Penjualan online adalah hal yang HARUS dilakukan mulai sekarang.
Perubahan dalam banyak industri dan pola pembelian. Gabung dengan
- platform kami dan mulai penjualan lansung di ribuan perusahaan d
+ platform kami dan mulai penjualan langsung di ribuan perusahaan di
seluruh Indonesia.{' '}
</p>
</div>
diff --git a/src/lib/maps/components/PinPointMap.jsx b/src/lib/maps/components/PinPointMap.jsx
index c46d838a..a9ead055 100644
--- a/src/lib/maps/components/PinPointMap.jsx
+++ b/src/lib/maps/components/PinPointMap.jsx
@@ -14,37 +14,54 @@ const containerStyle = {
height: '400px',
};
-const defaultCenter = {
- lat: -6.2,
- lng: 106.816666,
-};
-
-const PinpointLocation = ({ initialLatitude, initialLongitude, initialAddress }) => {
+const PinpointLocation = ({
+ initialLatitude,
+ initialLongitude,
+ initialAddress,
+}) => {
const { isLoaded } = useJsApiLoader({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_API_KEY,
libraries: ['places'],
});
const {
- addressMaps,
setAddressMaps,
selectedPosition,
setSelectedPosition,
setDetailAddress,
setPinedMaps,
+ getDefaultCenter, // âś… ambil default center dari store
} = useMaps();
+
+
const [tempAddress, setTempAddress] = useState(initialAddress || '');
const [tempPosition, setTempPosition] = useState(
- initialLatitude && initialLongitude
+ initialLatitude && initialLongitude
? { lat: parseFloat(initialLatitude), lng: parseFloat(initialLongitude) }
- : selectedPosition.lat && selectedPosition.lng
- ? selectedPosition
- : defaultCenter
+ : selectedPosition?.lat && selectedPosition?.lng
+ ? selectedPosition
+ : getDefaultCenter() // âś… fallback aman untuk view
);
- const [markerIcon, setMarkerIcon] = useState(null);
+ useEffect(() => {
+ if (initialLatitude && initialLongitude) {
+ const lat = parseFloat(initialLatitude);
+ const lng = parseFloat(initialLongitude);
+
+ if (!isNaN(lat) && !isNaN(lng)) {
+ setTempPosition({ lat, lng });
+ // kalau address belum ada, reverse geocode
+ if (!initialAddress) {
+ getAddress(lat, lng);
+ } else {
+ setTempAddress(initialAddress);
+ }
+ }
+ }
+ }, [initialLatitude, initialLongitude, initialAddress]);
+ const [markerIcon, setMarkerIcon] = useState(null);
const autocompleteRef = useRef(null);
useEffect(() => {
@@ -55,7 +72,7 @@ const PinpointLocation = ({ initialLatitude, initialLongitude, initialAddress })
});
}
- // If we have initial coordinates but no address, fetch the address
+ // Jika ada koordinat awal tapi belum ada address → reverse geocode
if (initialLatitude && initialLongitude && !initialAddress) {
getAddress(parseFloat(initialLatitude), parseFloat(initialLongitude));
}
@@ -66,10 +83,11 @@ const PinpointLocation = ({ initialLatitude, initialLongitude, initialAddress })
return component ? component.long_name : '';
};
+ // fill from pin point
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}`
+ `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${process.env.NEXT_PUBLIC_GOOGLE_API_KEY}&language=id`
);
const data = await response.json();
@@ -78,14 +96,31 @@ const PinpointLocation = ({ initialLatitude, initialLongitude, initialAddress })
const formattedAddress = data.results[0].formatted_address;
const details = {
- street:
- getAddressComponent(addressComponents, 'route') +
- ' ' +
+ 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'),
+ getAddressComponent(addressComponents, 'administrative_area_level_4'),
+ getAddressComponent(addressComponents, 'administrative_area_level_3'),
+ getAddressComponent(addressComponents, 'administrative_area_level_2'),
+ getAddressComponent(addressComponents, 'administrative_area_level_1'),
+ getAddressComponent(addressComponents, 'postal_code'),
+ ].filter(Boolean).join(', '),
+ 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'),
};
@@ -136,8 +171,15 @@ const PinpointLocation = ({ initialLatitude, initialLongitude, initialAddress })
const handleSavePinpoint = (event) => {
event.preventDefault();
- if (tempAddress === '') {
- alert('Silahkan pilih lokasi terlebih dahulu');
+
+ // âś… cegah save jika masih di default center (user belum benar2 pilih lokasi)
+ const dc = getDefaultCenter();
+ const isDefault =
+ Math.abs(tempPosition.lat - dc.lat) < 1e-6 &&
+ Math.abs(tempPosition.lng - dc.lng) < 1e-6;
+
+ if (!tempAddress || isDefault) {
+ alert('Silahkan pilih lokasi di peta atau autocomplete terlebih dahulu');
return;
}
@@ -173,13 +215,13 @@ const PinpointLocation = ({ initialLatitude, initialLongitude, initialAddress })
{isLoaded ? (
<GoogleMap
mapContainerStyle={containerStyle}
- center={tempPosition}
+ center={tempPosition || getDefaultCenter()} // âś… aman jika null
zoom={15}
onClick={onMapClick}
>
{markerIcon && (
<Marker
- position={tempPosition}
+ position={tempPosition || getDefaultCenter()} // âś… selalu ada posisi
draggable={true}
onDragEnd={(e) => {
const lat = e.latLng.lat();
@@ -199,7 +241,8 @@ const PinpointLocation = ({ initialLatitude, initialLongitude, initialAddress })
<div style={{ marginTop: '20px' }}>
<Button variant='solid' onClick={handleUseCurrentLocation}>
- <LocateFixed className='h-6 w-6 text-gray-500 mr-2' /> Gunakan Lokasi Saat Ini
+ <LocateFixed className='h-6 w-6 text-gray-500 mr-2' /> Gunakan Lokasi
+ Saat Ini
</Button>
</div>
diff --git a/src/lib/maps/stores/useMaps.js b/src/lib/maps/stores/useMaps.js
index c57a05ad..b02c2ae3 100644
--- a/src/lib/maps/stores/useMaps.js
+++ b/src/lib/maps/stores/useMaps.js
@@ -1,32 +1,57 @@
import { create } from "zustand";
-const center = {
- lat: -6.200000, // Default latitude (Jakarta)
- lng: 106.816666, // Default longitude (Jakarta)
-};
-
-export const useMaps = create((set) => ({
- // State existing
- selectedPosition: center,
+const DEFAULT_CENTER = { lat: -6.2, lng: 106.816666 };
+
+export const useMaps = create((set, get) => ({
+ // ==== STATE ====
+ selectedPosition: null,
addressMaps: '',
detailAddress: {},
pinedMaps: false,
- // State tambahan untuk penyimpanan posisi sementara
+ // posisi sementara (create/edit)
tempPositionCreate: null,
tempPositionEdit: null,
- // Setter existing
+ // ==== SETTERS ====
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 }),
+
+ getDefaultCenter: () => DEFAULT_CENTER,
+
+ isPinned: () => {
+ const p = get().selectedPosition;
+ if (!p) return false;
+
+ // parse string -> number
+ const lat = parseFloat(p.lat);
+ const lng = parseFloat(p.lng);
+
+ // cek kalau bukan angka valid
+ if (isNaN(lat) || isNaN(lng)) return false;
+
+ // cek apakah sama dengan default
+ const isDefault =
+ Math.abs(lat - DEFAULT_CENTER.lat) < 1e-6 &&
+ Math.abs(lng - DEFAULT_CENTER.lng) < 1e-6;
+
+ return !isDefault;
+ },
+
+ resetPin: () => set({
+ selectedPosition: null,
+ addressMaps: '',
+ detailAddress: {},
+ pinedMaps: false,
+ tempPositionCreate: null,
+ tempPositionEdit: null,
+ }),
}));
diff --git a/src/lib/pengajuan-tempo/api/createPengajuanTempoApi.js b/src/lib/pengajuan-tempo/api/createPengajuanTempoApi.js
index af1d6c3a..173287de 100644
--- a/src/lib/pengajuan-tempo/api/createPengajuanTempoApi.js
+++ b/src/lib/pengajuan-tempo/api/createPengajuanTempoApi.js
@@ -11,4 +11,4 @@ const createPengajuanTempoApi = async (data) => {
return dataPengajuanTempo;
};
-export default createPengajuanTempoApi;
+export default createPengajuanTempoApi; \ No newline at end of file
diff --git a/src/lib/pengajuan-tempo/component/FinishTempo.jsx b/src/lib/pengajuan-tempo/component/FinishTempo.jsx
index aacb9ef3..abf218d9 100644
--- a/src/lib/pengajuan-tempo/component/FinishTempo.jsx
+++ b/src/lib/pengajuan-tempo/component/FinishTempo.jsx
@@ -8,13 +8,19 @@ import useAuth from '@/core/hooks/useAuth';
import axios from 'axios';
import { toast } from 'react-hot-toast';
import { ChevronRightIcon, ChevronLeftIcon } from '@heroicons/react/24/outline';
+import { useRouter } from 'next/router';
+import switchAccountProgresApi from '@/lib/auth/api/switchAccountProgresApi.js';
const FinishTempo = ({ query }) => {
const [data, setData] = useState();
+ const [switchStatus, setSwitchStatus] = useState(null);
+ const [loadingStatus, setLoadingStatus] = useState(true);
const [transactionData, setTransactionData] = useState();
const { isDesktop, isMobile } = useDevice();
const auth = useAuth();
const so_order = query?.order_id?.replaceAll('-', '/');
+ const router = useRouter();
+
useEffect(() => {
const fetchData = async () => {
const fetchedData = await odooApi(
@@ -26,8 +32,46 @@ const FinishTempo = ({ query }) => {
fetchData();
}, [query]);
+ useEffect(() => {
+ const fetchSwitchStatus = async () => {
+ try {
+ const progres = await switchAccountProgresApi();
+ setSwitchStatus(progres?.status);
+ } catch (err) {
+ console.error('Gagal cek progres switch account:', err);
+ } finally {
+ setLoadingStatus(false);
+ }
+ };
+ fetchSwitchStatus();
+ }, []);
+
+ // Handler khusus untuk tombol Ubah Akun
+ const handleSwitchAccountClick = () => {
+ if (switchStatus === 'pending') {
+ toast.info('Akun sedang menunggu verifikasi. Tidak dapat mengubah akun saat ini.', { duration: 2500 });
+ return;
+ }
+ localStorage.setItem('autoCheckProfile', 'true');
+ router.push('/my/profile');
+ };
+
+ if (loadingStatus) {
+ return (
+ <div className="container flex flex-col items-center gap-4">
+ <div className="py-20 text-gray-500">Memuat data...</div>
+ </div>
+ );
+ }
+
return (
- <div className='container flex flex-col items-center gap-4'>
+ <div
+ className={`container flex flex-col items-center gap-4 pb-20 ${
+ switchStatus === 'pending'
+ ? 'min-h-[calc(100vh-80px)] md:min-h-[calc(100vh-100px)] lg:min-h-[calc(100vh-120px)]'
+ : 'min-h-screen'
+ }`}
+ >
<div
className={`flex ${
isMobile ? 'w-full' : 'w-2/3'
@@ -40,8 +84,11 @@ const FinishTempo = ({ query }) => {
>
{query?.status == 'finish' &&
'Form Pengajuan Tempo kamu Telah Berhasil Didaftarkan Mohon menunggu hingga Proses Verifikasi Selesai Dilakukan'}
- {query?.status == 'switch-account' &&
- 'Form Pengajuan Tempo Kamu Gagal Dilakukan'}
+ {switchStatus === 'pending'
+ ? 'Form Pengajuan Tempo Kamu Belum Dapat Dilakukan'
+ : query?.status == 'switch-account' &&
+ 'Form Pengajuan Tempo Kamu Gagal Dilakukan'
+ }
{query?.status == 'review' &&
'Pengajuan Tempo dalam Proses Verifikasi'}
{query?.status == 'approve' && 'Pengajuan Tempo Berhasil'}
@@ -85,8 +132,11 @@ const FinishTempo = ({ query }) => {
isMobile ? 'w-full text-sm' : 'w-4/5 text-base'
}`}
>
- {query?.status == 'switch-account' &&
- 'Terima kasih atas minat anda untuk mendaftar Tempo, namun sayangnya akun anda bukan merupakan akun bisnis. Segera ubah akun anda menjadi Bisnis untuk menggunakan fitur ini'}
+ {switchStatus === 'pending'
+ ? 'Proses perubahan ke akun bisnis sedang kami review, mohon menunggu hingga 2x24 jam'
+ : query?.status == 'switch-account' &&
+ 'Terima kasih atas minat anda untuk mendaftar Tempo, namun sayangnya akun anda bukan merupakan akun bisnis. Segera ubah akun anda menjadi Bisnis untuk menggunakan fitur ini'
+ }
{query?.status == 'finish' &&
'Mohon menunggu untuk verifikasi dokumen dan kelengkapan data yang telah anda berikan. Proses approval pembayaran tempo kamu berhasil atau tidak akan diinfokan melalui email perusahaan / email yang mendaftar'}
{query?.status == 'review' &&
@@ -94,23 +144,59 @@ const FinishTempo = ({ query }) => {
{query?.status == 'approve' &&
'Proses pengajuan tempo anda sudah berhasil terdaftar di indoteknik.com. Nikmati pembelian anda di website indoteknik dengan menggunakan pembayaran tempo'}
</div>
- <Link
- href={
- query?.status === 'switch-account'
- ? '/my/profile'
- : query?.status === 'approve'
- ? '/my/tempo/'
- : '/'
- }
- className='btn-solid-red rounded-md text-base flex flex-row items-center justify-center'
- >
- {query?.status === 'switch-account'
- ? 'Ubah Akun'
- : query?.status === 'approve'
- ? 'Lihat Detail Tempo'
- : 'Kembali Ke Beranda'}
- <ChevronRightIcon className='w-5' />
- </Link>
+
+ {switchStatus !== 'pending' && (
+ <hr className="border-gray-300 w-full" />
+ )}
+
+ {/* Video panduan khusus tampil saat status switch-account */}
+ {query?.status === 'switch-account' && switchStatus !== 'pending' && (
+ <div className="w-full max-w-3xl mx-auto px-4 text-center text-gray-700 mb-6">
+ <div className="mb-3 font-medium">
+ <h1
+ className={`text-red-500 text-center font-semibold ${
+ isMobile ? 'text-lg' : 'text-3xl'
+ }`}
+ // Mengganti py-4 dengan my-6 supaya jarak vertikalnya sama dengan hr
+ style={{ marginTop: 24, marginBottom: 24 }}
+ >
+ Video Panduan Pengajuan Tempo
+ </h1>
+ </div>
+ <div className="relative" style={{ paddingTop: '56.25%' /* 16:9 aspect ratio */ }}>
+ <iframe
+ src="https://www.youtube.com/embed/m15f8-eLqUc?si=frNbGnJu1zjINnDT"
+ title="YouTube video player"
+ frameBorder="0"
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
+ allowFullScreen
+ referrerPolicy="strict-origin-when-cross-origin"
+ className="absolute top-0 left-0 w-full h-full rounded-md shadow-lg"
+ ></iframe>
+ </div>
+ </div>
+ )}
+
+ {/* Tombol dengan behavior berbeda jika status switch-account */}
+ {query?.status === 'switch-account' && switchStatus !== 'pending' ? (
+ <button
+ onClick={handleSwitchAccountClick}
+ className="btn-solid-red rounded-md text-base flex flex-row items-center justify-center mb-10"
+ >
+ Ubah Akun
+ <ChevronRightIcon className="w-5" />
+ </button>
+ ) : query?.status !== 'switch-account' && (
+ <Link
+ href={query?.status === 'approve' ? '/my/tempo/' : '/'}
+ className="btn-solid-red rounded-md text-base flex flex-row items-center justify-center mb-10"
+ >
+ {query?.status === 'approve'
+ ? 'Lihat Detail Tempo'
+ : 'Kembali Ke Beranda'}
+ <ChevronRightIcon className="w-5" />
+ </Link>
+ )}
</div>
);
};
diff --git a/src/lib/pengajuan-tempo/component/PengajuanTempo.jsx b/src/lib/pengajuan-tempo/component/PengajuanTempo.jsx
index ae3d97fd..096fe1ed 100644
--- a/src/lib/pengajuan-tempo/component/PengajuanTempo.jsx
+++ b/src/lib/pengajuan-tempo/component/PengajuanTempo.jsx
@@ -40,6 +40,7 @@ const PengajuanTempo = () => {
const { form, errors, validate, updateForm } = usePengajuanTempoStore();
const { control, watch, setValue, setError } = useForm();
const auth = useAuth();
+ console.log('auth', auth);
const router = useRouter();
const [BannerTempo, setBannerTempo] = useState();
const { formDokumen, errorsDokumen, validateDokumen, updateFormDokumen } =
diff --git a/src/lib/product/components/Product/ProductDesktopVariant.jsx b/src/lib/product/components/Product/ProductDesktopVariant.jsx
index de88e5bb..59fa2032 100644
--- a/src/lib/product/components/Product/ProductDesktopVariant.jsx
+++ b/src/lib/product/components/Product/ProductDesktopVariant.jsx
@@ -103,15 +103,6 @@ const ProductDesktopVariant = ({
variantQuantityRefs.current[variantId] = element;
};
- const validQuantity = (quantity) => {
- let isValid = true;
- if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) {
- toast.error('Jumlah barang minimal 1');
- isValid = false;
- }
- return isValid;
- };
-
const handleAddToCart = (variant) => {
if (!auth) {
router.push(`/login?next=/shop/product/${slug}?srsltid=${srsltid}`);
@@ -132,29 +123,36 @@ const ProductDesktopVariant = ({
setAddCartAlert(true);
};
+ const toInt = (v) => {
+ const n = parseInt(String(v ?? '').trim(), 10);
+ return Number.isFinite(n) ? n : 0;
+ };
+
+ const validQuantity = (q) => {
+ if (!Number.isInteger(q) || q < 1) {
+ toast.error('Jumlah barang minimal 1');
+ return false;
+ }
+ return true;
+ };
+
const handleBuy = async (variant) => {
- const quantity = variantQuantityRefs?.current[product.id]?.value;
- let isLoggedIn = typeof auth === 'object';
+ const quantity = Math.max(1, toInt(quantityInput)); // clamp min 1
+ let isLoggedIn = typeof auth === 'object';
if (!isLoggedIn) {
const currentUrl = encodeURIComponent(router.asPath);
await router.push(`/login?next=${currentUrl}`);
- // Tunggu login berhasil, misalnya dengan memantau perubahan status auth.
- const authCheckInterval = setInterval(() => {
- const newAuth = getAuth();
- if (typeof newAuth === 'object') {
- isLoggedIn = true;
- auth = newAuth; // Update nilai auth setelah login
- clearInterval(authCheckInterval);
- }
- }, 500); // Periksa status login setiap 500ms
-
+ // tunggu sampai auth ada
await new Promise((resolve) => {
- const checkLogin = setInterval(() => {
- if (isLoggedIn) {
- clearInterval(checkLogin);
- resolve(null);
+ const t = setInterval(() => {
+ const newAuth = getAuth();
+ if (typeof newAuth === 'object') {
+ isLoggedIn = true;
+ auth = newAuth;
+ clearInterval(t);
+ resolve();
}
}, 500);
});
@@ -162,16 +160,57 @@ const ProductDesktopVariant = ({
if (!validQuantity(quantity)) return;
- updateItemCart({
+ await updateItemCart({
productId: variant,
quantity,
programLineId: null,
selected: true,
source: 'buy',
});
- router.push(`/shop/checkout?source=buy`);
+
+ router.push('/shop/checkout?source=buy');
};
+ // const handleBuy = async (variant) => {
+ // const quantity = variantQuantityRefs?.current[product.id]?.value;
+ // let isLoggedIn = typeof auth === 'object';
+
+ // if (!isLoggedIn) {
+ // const currentUrl = encodeURIComponent(router.asPath);
+ // await router.push(`/login?next=${currentUrl}`);
+
+ // // Tunggu login berhasil, misalnya dengan memantau perubahan status auth.
+ // const authCheckInterval = setInterval(() => {
+ // const newAuth = getAuth();
+ // if (typeof newAuth === 'object') {
+ // isLoggedIn = true;
+ // auth = newAuth; // Update nilai auth setelah login
+ // clearInterval(authCheckInterval);
+ // }
+ // }, 500); // Periksa status login setiap 500ms
+
+ // await new Promise((resolve) => {
+ // const checkLogin = setInterval(() => {
+ // if (isLoggedIn) {
+ // clearInterval(checkLogin);
+ // resolve(null);
+ // }
+ // }, 500);
+ // });
+ // }
+
+ // if (!validQuantity(quantity)) return;
+
+ // updateItemCart({
+ // productId: variant,
+ // quantity,
+ // programLineId: null,
+ // selected: true,
+ // source: 'buy',
+ // });
+ // router.push(`/shop/checkout?source=buy`);
+ // };
+
const handleButton = async (variant) => {
const quantity = quantityInput;
let isLoggedIn = typeof auth === 'object';
@@ -443,37 +482,53 @@ const ProductDesktopVariant = ({
)}
</h3>
)}
- <div className='flex justify-between items-center py-5 px-3'>
+ <div className='flex gap-x-5 items-center py-5'>
<div className='relative flex items-center'>
<button
type='button'
className='absolute left-0 px-2 py-1 h-full text-gray-500'
- onClick={() =>
- setQuantityInput(
- String(Math.max(1, Number(quantityInput) - 1))
- )
- }
+ onClick={() => {
+ const n = parseInt(String(quantityInput), 10);
+ const next = Number.isFinite(n) ? Math.max(1, n - 1) : 1;
+ setQuantityInput(next);
+ }}
>
-
</button>
+
<input
type='number'
id='quantity'
min={1}
+ step={1}
+ inputMode='numeric'
+ pattern='[0-9]*'
value={quantityInput}
- onChange={(e) => setQuantityInput(e.target.value)}
- className=' w-24 h-10 text-center border border-gray-300 rounded focus:outline-none'
+ onChange={(e) => {
+ const raw = e.target.value.trim();
+ const n = parseInt(raw, 10);
+ setQuantityInput(Number.isFinite(n) && n > 0 ? n : 1);
+ }}
+ onKeyDown={(e) => {
+ if (['e', 'E', '+', '-', '.'].includes(e.key))
+ e.preventDefault();
+ }}
+ className='w-24 h-10 text-center border border-gray-300 rounded focus:outline-none'
/>
+
<button
type='button'
className='absolute right-0 px-2 py-1 h-full text-gray-500'
- onClick={() =>
- setQuantityInput(String(Number(quantityInput) + 1))
- }
+ onClick={() => {
+ const n = parseInt(String(quantityInput), 10);
+ const next = (Number.isFinite(n) ? n : 0) + 1;
+ setQuantityInput(next);
+ }}
>
+
</button>
</div>
+
<div>
<Skeleton
isLoaded={!isLoadingSLA}
@@ -510,7 +565,8 @@ const ProductDesktopVariant = ({
<Button
onClick={() => handleAddToCart(product.id)}
className='w-full'
- colorScheme='yellow'
+ colorScheme='red'
+ variant={'outline'}
>
Keranjang
</Button>
@@ -529,7 +585,7 @@ const ProductDesktopVariant = ({
className='w-full border-2 p-2 gap-1 mt-2 hover:bg-slate-100 flex items-center'
>
<ImageNext
- src='/images/writing.png'
+ src='/images/doc_red.svg'
alt='penawaran instan'
className=''
width={25}
diff --git a/src/lib/product/components/Product/ProductMobileVariant.jsx b/src/lib/product/components/Product/ProductMobileVariant.jsx
index de5c3f10..cab8e9be 100644
--- a/src/lib/product/components/Product/ProductMobileVariant.jsx
+++ b/src/lib/product/components/Product/ProductMobileVariant.jsx
@@ -27,6 +27,8 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
let auth = getAuth();
const [quantity, setQuantity] = useState('1');
const [selectedVariant, setSelectedVariant] = useState(product.id);
+ const [quantityInput, setQuantityInput] = useState(String(1));
+ const [qtyPickUp, setQtyPickUp] = useState(0);
const [informationTab, setInformationTab] = useState(
informationTabOptions[0].value
);
@@ -63,30 +65,33 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
}
}, [selectedVariant, product]);
- const validAction = () => {
- let isValid = true;
+ const validAction = (q) => {
if (!selectedVariant) {
toast.error('Pilih varian terlebih dahulu');
- isValid = false;
+ return false;
}
- if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) {
+ if (!Number.isInteger(q) || q < 1) {
toast.error('Jumlah barang minimal 1');
- isValid = false;
+ return false;
}
- return isValid;
+ return true;
};
+ const getQty = () => Math.max(1, toInt(quantityInput));
+
const handleClickCart = async () => {
+ const q = getQty();
+
if (!auth) {
router.push(`/login?next=/shop/product/${slug}?srsltid=${srsltid}`);
return;
}
+ if (!validAction(q)) return;
- if (!validAction()) return;
- gtagAddToCart(activeVariant, quantity);
+ gtagAddToCart(activeVariant, q);
updateItemCart({
productId: product.id,
- quantity,
+ quantity: q,
programLineId: null,
selected: true,
source: null,
@@ -95,37 +100,29 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
};
const handleClickBuy = async () => {
- let isLoggedIn = typeof auth === 'object';
+ const q = getQty();
+ let isLoggedIn = typeof auth === 'object';
if (!isLoggedIn) {
const currentUrl = encodeURIComponent(router.asPath);
await router.push(`/login?next=${currentUrl}`);
-
- // Tunggu login berhasil, misalnya dengan memantau perubahan status auth.
- const authCheckInterval = setInterval(() => {
- const newAuth = getAuth();
- if (typeof newAuth === 'object') {
- isLoggedIn = true;
- auth = newAuth; // Update nilai auth setelah login
- clearInterval(authCheckInterval);
- }
- }, 500); // Periksa status login setiap 500ms
-
await new Promise((resolve) => {
- const checkLogin = setInterval(() => {
- if (isLoggedIn) {
- clearInterval(checkLogin);
+ const t = setInterval(() => {
+ const newAuth = getAuth();
+ if (typeof newAuth === 'object') {
+ auth = newAuth;
+ clearInterval(t);
resolve(null);
}
}, 500);
});
}
- if (!validAction()) return;
+ if (!validAction(q)) return;
updateItemCart({
productId: product.id,
- quantity,
+ quantity: q,
programLineId: null,
selected: true,
source: 'buy',
@@ -133,8 +130,21 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
router.push(`/shop/checkout?source=buy`);
};
+ const toInt = (v) => {
+ const n = parseInt(String(v ?? '').trim(), 10);
+ return Number.isFinite(n) ? n : 0;
+ };
+
+ const validQuantity = (q) => {
+ if (!Number.isInteger(q) || q < 1) {
+ toast.error('Jumlah barang minimal 1');
+ return false;
+ }
+ return true;
+ };
+
const handleButton = (variant) => {
- const quantity = quantityInput;
+ const quantity = Math.max(1, toInt(quantityInput)); // clamp min 1
if (!validQuantity(quantity)) return;
updateItemCart({
@@ -168,9 +178,10 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
return (
<MobileView>
- <div
- className={`px-4 block md:sticky md:top-[150px] md:py-6 fixed bottom-0 left-0 right-0 bg-white p-2 z-10 pb-6 pt-6 rounded-lg shadow-[rgba(0,0,4,0.1)_0px_-4px_4px_0px] `}
- >
+ {/* PRICE & ACTIONS: tetap punyamu, hanya hapus input number lama */}
+ {/* ===== BAR BAWAH (fixed) ===== */}
+ <div className='px-4 fixed bottom-0 left-0 right-0 bg-white z-10 pb-6 pt-4 rounded-t-2xl shadow-[rgba(0,0,4,0.1)_0px_-4px_4px_0px]'>
+ {/* HARGA & PPN (logikamu tetap) */}
{activeVariant.isFlashSale &&
activeVariant?.price?.discountPercentage > 0 ? (
<>
@@ -225,50 +236,105 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
)}
</div>
)}
- <div className=''>
- <div className='mt-4 mb-2'>Jumlah</div>
- <div className='flex gap-x-3'>
- <div className='w-2/12'>
+
+ {/* ⬇️ TAMBAHKAN BLOK INI DI DALAM BAR: STOK & STEPPER */}
+ <div className='grid grid-cols-12 items-center gap-3 mt-3'>
+ <div className='col-span-7'>
+ <div
+ className={`text-[14px] ${
+ product?.sla?.qty < 10 ? 'text-red-600 font-medium' : ''
+ }`}
+ >
+ Stock : {activeVariant?.stock ?? 0}
+ </div>
+ {qtyPickUp > 0 && (
+ <div className='text-[16px] mt-0.5 text-red-500 italic'>
+ * {qtyPickUp} barang bisa di pickup
+ </div>
+ )}
+ </div>
+ <div className='col-span-5 flex justify-end'>
+ <div className='inline-flex items-stretch border rounded-xl overflow-hidden'>
+ <button
+ type='button'
+ className='h-10 w-10 grid place-items-center text-gray-700 hover:bg-gray-100 active:scale-95'
+ onClick={() =>
+ setQuantityInput(
+ String(Math.max(1, Number(quantityInput || 1) - 1))
+ )
+ }
+ aria-label='Kurangi'
+ >
+ <span className='text-2xl leading-none'>–</span>
+ </button>
<input
- name='quantity'
type='number'
- className='form-input'
- value={quantity}
- onChange={(e) => setQuantity(e.target.value)}
+ min={1}
+ value={quantityInput}
+ onChange={(e) => setQuantityInput(e.target.value)}
+ className='h-10 w-16 text-center text-lg outline-none border-x
+ [appearance:textfield]
+ [&::-webkit-outer-spin-button]:appearance-none
+ [&::-webkit-inner-spin-button]:appearance-none'
/>
+ <button
+ type='button'
+ className='h-10 w-10 grid place-items-center text-gray-700 hover:bg-gray-100 active:scale-95'
+ onClick={() =>
+ setQuantityInput(String(Number(quantityInput || 1) + 1))
+ }
+ aria-label='Tambah'
+ >
+ <span className='text-2xl leading-none'>+</span>
+ </button>
</div>
- <button
- type='button'
- className='btn-yellow flex-1'
- onClick={handleClickCart}
- >
- Keranjang
- </button>
- <button
- type='button'
- className='btn-solid-red flex-1'
- onClick={handleClickBuy}
- >
- Beli
- </button>
</div>
+ </div>
+
+ <div className='h4'/>
+ {/* TOMBOL AKSI */}
+ <div className='flex gap-2 mt-3'>
+ {/* Tombol Dokumen */}
<Button
onClick={() => handleButton(product.id)}
- color={'red'}
- colorScheme='white'
- className='w-full border-2 p-2 gap-1 mt-2 hover:bg-slate-100 flex items-center'
+ className='flex items-center justify-center p-2 border-2 hover:bg-slate-100'
+ variant='outline'
+ title='Lihat Dokumen'
>
<ImageNext
- src='/images/writing.png'
- alt='penawaran instan'
- className=''
- width={25}
- height={25}
+ src='/images/doc.svg'
+ width={18}
+ height={18}
+ alt='Dokumen'
/>
- Penawaran Harga Instan
</Button>
+ {/* Container untuk tombol aksi utama */}
+ <div className='flex-1 flex gap-2'>
+ <Button
+ onClick={() =>
+ handleClickCart(product.id, Number(quantityInput || 1))
+ }
+ className='flex-1'
+ colorScheme='red'
+ variant='outline'
+ isDisabled={product.stock === 0}
+ >
+ Keranjang
+ </Button>
+ <Button
+ onClick={() =>
+ handleClickBuy(product.id, Number(quantityInput || 1))
+ }
+ className='flex-1'
+ colorScheme='red'
+ isDisabled={product.stock === 0}
+ >
+ Beli
+ </Button>
+ </div>
</div>
</div>
+
<Image
src={product.image + '?variant=True'}
alt={product.name}
diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx
index a8ed90a4..f4f5882e 100644
--- a/src/lib/product/components/ProductCard.jsx
+++ b/src/lib/product/components/ProductCard.jsx
@@ -73,7 +73,7 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
if (variant == 'vertical') {
return (
- <div className='rounded shadow-sm border border-gray_r-4 bg-white h-[330px] md:h-[380px]'>
+ <div className='rounded shadow-sm border border-gray_r-4 bg-white'>
<Link href={URL.product} className='border-b border-gray_r-4 relative' aria-label='Produk'>
<div className='relative'>
<Image
diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx
index d2ecb4d9..440e1795 100644
--- a/src/lib/product/components/ProductFilterDesktop.jsx
+++ b/src/lib/product/components/ProductFilterDesktop.jsx
@@ -1,7 +1,6 @@
import { useRouter } from 'next/router';
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import _ from 'lodash';
-import { toQuery } from 'lodash-contrib';
import {
Accordion,
AccordionButton,
@@ -9,7 +8,6 @@ import {
AccordionItem,
AccordionPanel,
Box,
- Button,
Checkbox,
Input,
InputGroup,
@@ -17,136 +15,200 @@ import {
Stack,
VStack,
} from '@chakra-ui/react';
-import Image from '@/core/components/elements/Image/Image';
import { formatCurrency } from '@/core/utils/formatValue';
const ProductFilterDesktop = ({
- brands,
- categories,
+ brands, // bisa [{id,name,qty}] atau [{brand,qty}]
+ categories, // [{name, qty}]
prefixUrl,
- defaultBrand = null,
}) => {
const router = useRouter();
- const { query } = router;
- const [order, setOrder] = useState(query?.orderBy);
- const [brandValues, setBrand] = useState(query?.brand?.split(',') || []);
+
+ const [order, setOrder] = useState(router.query?.orderBy);
+ const [brandValues, setBrand] = useState(
+ typeof router.query?.brand === 'string' && router.query.brand
+ ? router.query.brand.split(',').filter(Boolean)
+ : []
+ );
const [categoryValues, setCategory] = useState(
- query?.category?.split(',') || []
+ typeof router.query?.category === 'string' && router.query.category
+ ? router.query.category.split(',').filter(Boolean)
+ : []
);
- const [priceFrom, setPriceFrom] = useState(query?.priceFrom);
- const [priceTo, setPriceTo] = useState(query?.priceTo);
- const [stock, setStock] = useState(query?.stock);
+ const [priceFrom, setPriceFrom] = useState(router.query?.priceFrom ?? '');
+ const [priceTo, setPriceTo] = useState(router.query?.priceTo ?? '');
+ const [stock, setStock] = useState(router.query?.stock ?? null);
const [activeRange, setActiveRange] = useState(null);
- const [activeIndeces, setActiveIndeces] = useState([]);
+
+ const handlePriceKeyDown = (e) => {
+ if (e.key !== 'Enter') return;
+ e.preventDefault();
+ // keluar dari preset kalau user input manual
+ setActiveRange(null);
+
+ // pakai state terkini untuk apply
+ const fromVal = priceFrom === '' ? '' : String(priceFrom);
+ const toVal = priceTo === '' ? '' : String(priceTo);
+
+ applyFilters({ priceFrom: fromVal, priceTo: toVal });
+ };
+
+ // --- normalisasi data brand agar tahan banting ---
+ const normBrands = useMemo(() => {
+ return (brands ?? [])
+ .map((b, i) => ({
+ id: String(b.id ?? b.val ?? b.brand ?? i),
+ name: String(b.name ?? b.brand ?? b.label ?? b.val ?? '').trim(),
+ qty: b.qty ?? b.count,
+ }))
+ .filter((b) => b.name);
+ }, [brands]);
const priceRange = [
- {
- priceFrom: 100000,
- priceTo: 200000,
- },
- {
- priceFrom: 200000,
- priceTo: 300000,
- },
- {
- priceFrom: 300000,
- priceTo: 400000,
- },
- {
- priceFrom: 400000,
- priceTo: 500000,
- },
+ { priceFrom: 100000, priceTo: 200000 },
+ { priceFrom: 200000, priceTo: 300000 },
+ { priceFrom: 300000, priceTo: 400000 },
+ { priceFrom: 400000, priceTo: 500000 },
];
- const indexRange = priceRange.findIndex((range) => {
- return (
- range.priceFrom === parseInt(priceFrom) &&
- range.priceTo == parseInt(priceTo)
- );
- });
-
- const handleCategoriesChange = (event) => {
- const value = event.target.value;
- const isChecked = event.target.checked;
- if (isChecked) {
- setCategory([...categoryValues, value]);
- } else {
- setCategory(categoryValues.filter((val) => val !== value));
- }
- };
- const handleBrandsChange = (event) => {
- const value = event.target.value;
- const isChecked = event.target.checked;
- if (isChecked) {
- setBrand([...brandValues, value]);
- } else {
- setBrand(brandValues.filter((val) => val !== value));
+ const indexRange = priceRange.findIndex(
+ (r) => r.priceFrom === parseInt(priceFrom) && r.priceTo == parseInt(priceTo)
+ );
+
+ const applyFilters = (changes = {}) => {
+ const params = new URLSearchParams();
+
+ // 1) salin SEMUA param yang ada sekarang (jangan hilangkan apapun)
+ Object.entries(router.query).forEach(([k, v]) => {
+ if (v == null) return;
+ if (Array.isArray(v)) {
+ // penting: fq bisa multi-value; gunakan append, bukan join
+ v.forEach((item) => params.append(k, String(item)));
+ } else {
+ params.set(k, String(v));
+ }
+ });
+
+ // 2) baca nilai dasar langsung dari URL (hindari state stale)
+ const arr = (val) =>
+ typeof val === 'string' && val ? val.split(',').filter(Boolean) : [];
+
+ const nextBrand =
+ 'brandValues' in changes ? changes.brandValues : arr(router.query.brand);
+ const nextCategory =
+ 'categoryValues' in changes
+ ? changes.categoryValues
+ : arr(router.query.category);
+ const nextPriceFrom =
+ 'priceFrom' in changes ? changes.priceFrom : router.query.priceFrom ?? '';
+ const nextPriceTo =
+ 'priceTo' in changes ? changes.priceTo : router.query.priceTo ?? '';
+ const nextStock =
+ 'stock' in changes ? changes.stock : router.query.stock ?? null;
+ const nextOrder =
+ 'order' in changes ? changes.order : router.query.orderBy ?? '';
+
+ const setOrDel = (key, val) => {
+ const empty =
+ val == null || val === '' || (Array.isArray(val) && val.length === 0);
+ if (empty) params.delete(key);
+ else params.set(key, Array.isArray(val) ? val.join(',') : String(val));
+ };
+
+ setOrDel('brand', nextBrand);
+ setOrDel('category', nextCategory);
+ setOrDel('priceFrom', nextPriceFrom);
+ setOrDel('priceTo', nextPriceTo);
+ setOrDel('stock', nextStock);
+ setOrDel('orderBy', nextOrder);
+
+ // 3) kalau ada perubahan filter utama → reset page ke 1
+ const changedKeys = Object.keys(changes);
+ const touched = [
+ 'brandValues',
+ 'categoryValues',
+ 'priceFrom',
+ 'priceTo',
+ 'stock',
+ 'order',
+ ];
+ if (changedKeys.some((k) => touched.includes(k))) {
+ params.set('page', '1');
}
+
+ // 4) shallow replace (tanpa reload penuh)
+ const base = router.asPath.split('?')[0];
+ router.replace(`${base}?${params.toString()}`, undefined, {
+ shallow: true,
+ scroll: false,
+ });
};
- const handleReadyStockChange = (event) => {
- const value = event.target.value;
- const isChecked = event.target.checked;
- if (isChecked) {
- setStock(value);
- } else {
- setStock(null);
- }
+ // debounce untuk input harga (biar nggak spam)
+ const debouncedApply = useMemo(() => _.debounce(applyFilters, 350), []); // eslint-disable-line
+ useEffect(() => () => debouncedApply.cancel(), [debouncedApply]);
+
+ // === handlers ===
+ const handleCategoriesChange = (e) => {
+ const { value, checked } = e.target;
+ const next = checked
+ ? [...categoryValues, value]
+ : categoryValues.filter((v) => v !== value);
+ setCategory(next);
+ applyFilters({ categoryValues: next });
};
- const handlePriceFromChange = async (priceFromr, priceTor, index) => {
- await setPriceFrom(priceFromr);
- await setPriceTo(priceTor);
- setActiveRange(index);
+ const handleBrandsChange = (e) => {
+ const { value, checked } = e.target; // value = brand ID/name (string)
+ const next = checked
+ ? [...brandValues, value]
+ : brandValues.filter((v) => v !== value);
+ setBrand(next);
+ applyFilters({ brandValues: next });
};
- const handleSubmit = () => {
- let params = {
- penawaran: router.query.penawaran,
- q: router.query.q,
- orderBy: order,
- brand: brandValues.join(','),
- category: categoryValues.join(','),
- priceFrom,
- priceTo,
- stock: stock,
- };
- params = _.pickBy(params, _.identity);
- params = toQuery(params);
+ const handleReadyStockChange = (e) => {
+ const { checked, value } = e.target;
+ const next = checked ? value : null;
+ setStock(next);
+ applyFilters({ stock: next });
+ };
- const slug = Array.isArray(router.query.slug)
- ? router.query.slug[0]
- : router.query.slug;
+ const handlePriceRangeClick = async (pf, pt, idx) => {
+ await setPriceFrom(pf);
+ await setPriceTo(pt);
+ setActiveRange(idx);
+ applyFilters({ priceFrom: pf, priceTo: pt });
+ };
- if (slug) {
- if (prefixUrl.includes('category') || prefixUrl.includes('lob')) {
- router.push(`${prefixUrl}?${params}`);
- } else {
- router.push(`${prefixUrl}/${slug}?${params}`);
- }
- } else {
- router.push(`${prefixUrl}?${params}`);
- }
+ const onPriceFromInput = (e) => {
+ setPriceFrom(e.target.value);
};
- /*const handleIndexAccordion = async () => {
- if (brandValues) {
- await setActiveIndeces([...activeIndeces, 0])
- }
- if (categoryValues) {
- await setActiveIndeces([...activeIndeces, !router.pathname.includes('brands') ? 1 : 0])
- }
- if (priceRange) {
- await setActiveIndeces([...activeIndeces, !router.pathname.includes('brands') ? 2 : 1])
- }
- if (stock) {
- await setActiveIndeces([...activeIndeces, !router.pathname.includes('brands') ? 3 : 2])
- }
- }*/
+ const onPriceToInput = (e) => {
+ setPriceTo(e.target.value);
+ };
useEffect(() => {
setActiveRange(indexRange);
- }, []);
+ }, []); // init active range
+
+ useEffect(() => {
+ setBrand(
+ router.query?.brand
+ ? String(router.query.brand).split(',').filter(Boolean)
+ : []
+ );
+ setCategory(
+ router.query?.category
+ ? String(router.query.category).split(',').filter(Boolean)
+ : []
+ );
+ setPriceFrom(router.query?.priceFrom ?? '');
+ setPriceTo(router.query?.priceTo ?? '');
+ setStock(router.query?.stock ?? null);
+ setOrder(router.query?.orderBy ?? '');
+ }, [router.query]);
return (
<>
@@ -159,23 +221,24 @@ const ProductFilterDesktop = ({
</Box>
<AccordionIcon />
</AccordionButton>
-
<AccordionPanel>
- <Stack gap={3} direction='column' maxH={'240px'} overflow='auto'>
- {brands && brands.length > 0 ? (
- brands.map((brand, index) => (
- <div className='flex items-center gap-2 ' key={index}>
+ <Stack gap={3} direction='column' maxH='240px' overflow='auto'>
+ {normBrands.length > 0 ? (
+ normBrands.map((b) => (
+ <div className='flex items-center gap-2' key={b.id}>
<Checkbox
- isChecked={brandValues.includes(brand.brand)}
+ isChecked={brandValues.includes(String(b.id))}
onChange={handleBrandsChange}
- value={brand.brand}
+ value={String(b.id)} // idealnya ID brand
size='md'
>
<div className='flex items-center gap-2'>
- <span>{brand.brand} </span>
- <span className='text-sm text-gray-600'>
- ({brand.qty})
- </span>
+ <span>{b.name}</span>
+ {b.qty !== undefined && (
+ <span className='text-sm text-gray-600'>
+ ({b.qty})
+ </span>
+ )}
</div>
</Checkbox>
</div>
@@ -197,23 +260,20 @@ const ProductFilterDesktop = ({
</Box>
<AccordionIcon />
</AccordionButton>
-
<AccordionPanel>
- <Stack gap={3} direction='column' maxH={'240px'} overflow='auto'>
- {categories && categories.length > 0 ? (
- categories.map((category, index) => (
- <div className='flex items-center gap-2' key={index}>
+ <Stack gap={3} direction='column' maxH='240px' overflow='auto'>
+ {(categories ?? []).length > 0 ? (
+ categories.map((c, i) => (
+ <div className='flex items-center gap-2' key={i}>
<Checkbox
- isChecked={categoryValues.includes(category.name)}
+ isChecked={categoryValues.includes(c.name)}
onChange={handleCategoriesChange}
- value={category.name}
+ value={c.name}
size='md'
>
<div className='flex items-center gap-2'>
- <span>{category.name} </span>
- <span className='text-sm text-gray-600'>
- ({category.qty})
- </span>
+ <span>{c.name}</span>
+ <span className='text-sm text-gray-600'>({c.qty})</span>
</div>
</Checkbox>
</div>
@@ -234,7 +294,6 @@ const ProductFilterDesktop = ({
</Box>
<AccordionIcon />
</AccordionButton>
-
<AccordionPanel paddingY={4}>
<VStack gap={4}>
<InputGroup>
@@ -243,32 +302,34 @@ const ProductFilterDesktop = ({
type='number'
placeholder='Harga minimum'
value={priceFrom}
- onChange={(e) => setPriceFrom(e.target.value)}
+ onChange={onPriceFromInput}
+ onKeyDown={handlePriceKeyDown} // âźµ apply saat Enter
/>
</InputGroup>
+
<InputGroup>
<InputLeftAddon>Rp</InputLeftAddon>
<Input
type='number'
- placeholder='Harga maximum'
+ placeholder='Harga maksimum'
value={priceTo}
- onChange={(e) => setPriceTo(e.target.value)}
+ onChange={onPriceToInput}
+ onKeyDown={handlePriceKeyDown} // âźµ apply saat Enter
/>
</InputGroup>
+
<div className='grid grid-cols-2 gap-x-3 gap-y-2'>
- {priceRange.map((price, i) => (
+ {priceRange.map((p, i) => (
<button
key={i}
onClick={() =>
- handlePriceFromChange(price.priceFrom, price.priceTo, i)
+ handlePriceRangeClick(p.priceFrom, p.priceTo, i)
}
className={`w-full border ${
i === activeRange ? 'border-red-600' : 'border-gray-400'
- }
- py-2 p-3 rounded-full text-sm whitespace-nowrap`}
+ } py-2 p-3 rounded-full text-sm whitespace-nowrap`}
>
- {formatCurrency(price.priceFrom)} -{' '}
- {formatCurrency(price.priceTo)}
+ {formatCurrency(p.priceFrom)} - {formatCurrency(p.priceTo)}
</button>
))}
</div>
@@ -279,27 +340,22 @@ const ProductFilterDesktop = ({
{/* <AccordionItem>
<AccordionButton padding={[2, 4]}>
<Box as='span' flex='1' textAlign='left' fontWeight='semibold'>
- Ketersedian Stok
+ Ketersediaan Stok
</Box>
<AccordionIcon />
</AccordionButton>
-
<AccordionPanel paddingY={4}>
<Checkbox
isChecked={stock === 'ready stock'}
onChange={handleReadyStockChange}
- value={'ready stock'}
+ value='ready stock'
size='md'
>
- Ketersedian Stock
+ Ready Stock
</Checkbox>
</AccordionPanel>
</AccordionItem> */}
</Accordion>
-
- <Button className='w-full mt-6' colorScheme='red' onClick={handleSubmit}>
- Terapkan
- </Button>
</>
);
};
diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx
index 2fb3138a..850d00cc 100644
--- a/src/lib/product/components/ProductSearch.jsx
+++ b/src/lib/product/components/ProductSearch.jsx
@@ -1,12 +1,12 @@
import NextImage from 'next/image';
import { useRouter } from 'next/router';
-import { useEffect, useMemo, useState } from 'react';
+import { useEffect, useMemo, useState, useRef } from 'react';
import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react';
import axios from 'axios';
import _ from 'lodash';
import { toQuery } from 'lodash-contrib';
-
+import { FunnelIcon, AdjustmentsHorizontalIcon } from '@heroicons/react/24/outline';
import odooApi from '@/core/api/odooApi';
import searchSpellApi from '@/core/api/searchSpellApi';
import Link from '@/core/components/elements/Link/Link';
@@ -50,7 +50,33 @@ const ProductSearch = ({
const categoryId = getIdFromSlug(prefixUrl);
const [data, setData] = useState([]);
const [dataLob, setDataLob] = useState([]);
+ const appliedDefaultBrandOrder = useRef(false);
+
if (defaultBrand) query.brand = defaultBrand.toLowerCase();
+ useEffect(() => {
+ if (!router.isReady) return;
+
+ const onBrandsPage = router.pathname.includes('brands');
+ const hasOrder = typeof router.query?.orderBy === 'string' && router.query.orderBy !== '';
+
+ if (onBrandsPage && !hasOrder && !appliedDefaultBrandOrder.current) {
+ let params = {
+ ...router.query,
+ orderBy: 'popular',
+ };
+ params = _.pickBy(params, _.identity);
+ const qs = toQuery(params);
+
+ // ganti URL tanpa nambah history & tanpa full reload
+ router.replace(`${prefixUrl}?${qs}`, undefined, { shallow: true });
+
+ // sinkronkan state lokal
+ setOrderBy('popular');
+
+ appliedDefaultBrandOrder.current = true;
+ }
+ }, [router.isReady, router.pathname, router.query?.orderBy, prefixUrl]);
+
const dataIdCategories = [];
useEffect(() => {
if (prefixUrl.includes('category')) {
@@ -84,7 +110,7 @@ const ProductSearch = ({
if (router.asPath.includes('penawaran')) {
query = {
...query,
- fq:`flashsale_id_i:${router.query.penawaran} AND flashsale_price_f:[1 TO *]`,
+ fq: `flashsale_id_i:${router.query.penawaran} AND flashsale_price_f:[1 TO *]`,
orderBy: 'flashsale-discount-desc',
};
setFinalQuery(query);
@@ -404,9 +430,7 @@ const ProductSearch = ({
<div className='p-4 pt-0'>
{isNotReadyStockPage && isBrand && isBrand.logo && (
<div className='mb-3'>
- <h1 className='mb-2 font-semibold text-h-sm'>
- Brand Pencarian {q}
- </h1>
+ <h1 className='mb-2 font-semibold text-h-sm'>Brand Pencarian {q}</h1>
<Link
href={createSlug('/shop/brands/', isBrand.name, isBrand.id)}
className='inline'
@@ -419,7 +443,9 @@ const ProductSearch = ({
</Link>
</div>
)}
+
<h1 className='mb-2 font-semibold text-h-sm'>Produk</h1>
+
<FilterChoicesComponent
brandValues={brandValues}
categoryValues={categoryValues}
@@ -428,6 +454,7 @@ const ProductSearch = ({
handleDeleteFilter={handleDeleteFilter}
/>
+ {/* info jumlah hasil */}
<div className='mb-2 leading-6 text-gray_r-11'>
{!spellings ? (
<>
@@ -435,8 +462,7 @@ const ProductSearch = ({
{pageCount > 1 ? (
<>
{productStart + 1}-
- {parseInt(productStart) + parseInt(productRows) >
- productFound
+ {parseInt(productStart) + parseInt(productRows) > productFound
? productFound
: parseInt(productStart) + parseInt(productRows)}
&nbsp;dari&nbsp;
@@ -448,8 +474,7 @@ const ProductSearch = ({
&nbsp;produk{' '}
{query.q && (
<>
- untuk pencarian{' '}
- <span className='font-semibold'>{query.q}</span>
+ untuk pencarian <span className='font-semibold'>{query.q}</span>
</>
)}
</>
@@ -457,37 +482,37 @@ const ProductSearch = ({
SpellingComponent
)}
</div>
- <LobSectionCategory categories={dataLob} />
- <CategorySection categories={dataCategories} />
{productFound > 0 && (
- <div className='flex items-center gap-x-2 mb-5 justify-between'>
+ <div className='flex items-center gap-x-2 mt-2 mb-3 justify-end'>
<div>
<button
- className='btn-light py-2 px-5 h-[40px]'
+ aria-label='Filter'
+ title='Filter'
onClick={popup.activate}
+ className='btn-light w-fit flex items-center justify-center rounded-md'
>
- Filter
+ <AdjustmentsHorizontalIcon className='w-5 h-5' />
</button>
</div>
- <div className=''>
+ <div>
<select
name='limit'
- className='form-input w-24'
+ className='form-input w-20'
value={router.query?.limit || ''}
onChange={(e) => handleLimit(e)}
>
{numRows.map((option, index) => (
<option key={index} value={option}>
- {' '}
- {option}{' '}
+ {option}
</option>
))}
</select>
</div>
</div>
)}
-
+ {!!dataLob?.length && <LobSectionCategory categories={dataLob} />}
+ {!!dataCategories?.length && <CategorySection categories={dataCategories} />}
<div className='grid grid-cols-2 gap-3'>
{products &&
products.map((product) => (
@@ -499,7 +524,6 @@ const ProductSearch = ({
pageCount={pageCount}
currentPage={parseInt(page)}
url={`${prefixUrl}?${toQuery(_.omit(query, ['page', 'fq']))}`}
- // url={prefixUrl.includes('category') || prefixUrl.includes('lob')? `${prefixUrl}?${toQuery(_.omit(finalQuery, ['page']))}` : `${prefixUrl}?${toQuery(_.omit(query, ['page']))}`}
className='mt-6 mb-2'
/>
@@ -597,7 +621,7 @@ const ProductSearch = ({
<>
{productStart + 1}-
{parseInt(productStart) + parseInt(productRows) >
- productFound
+ productFound
? productFound
: parseInt(productStart) + parseInt(productRows)}
&nbsp;dari&nbsp;
@@ -673,8 +697,8 @@ const ProductSearch = ({
href={
query?.q
? whatsappUrl('productSearch', {
- name: query.q,
- })
+ name: query.q,
+ })
: whatsappUrl()
}
className='text-danger-500'
@@ -759,9 +783,9 @@ const FilterChoicesComponent = ({
</Tag>
)}
{brandValues?.length > 0 ||
- categoryValues?.length > 0 ||
- priceFrom ||
- priceTo ? (
+ categoryValues?.length > 0 ||
+ priceFrom ||
+ priceTo ? (
<span>
<button
className='btn-transparent py-2 px-5 h-[40px] text-red-700'
diff --git a/src/lib/quotation/components/Quotation.jsx b/src/lib/quotation/components/Quotation.jsx
index f0791512..c7e5b16a 100644
--- a/src/lib/quotation/components/Quotation.jsx
+++ b/src/lib/quotation/components/Quotation.jsx
@@ -33,7 +33,7 @@ import ExpedisiList from '../../checkout/api/ExpedisiList';
import SectionQuotationExpedition from '@/lib/checkout/components/SectionQuotationExpedition';
import { useQuotation } from '@/lib/checkout/stores/stateQuotation';
-const { checkoutApi } = require('@/lib/checkout/api/checkoutApi');
+const { checkoutApi, checkoutQuotation } = require('@/lib/checkout/api/checkoutApi');
const { getProductsCheckout } = require('@/lib/checkout/api/checkoutApi');
const Quotation = () => {
@@ -243,8 +243,8 @@ const Quotation = () => {
note_website: note_websiteText,
};
- const isSuccess = await checkoutApi({ data });
-
+ const isSuccess = await checkoutQuotation(data);
+
if (isSuccess?.id) {
for (const product of products) {
deleteItemCart({ productId: product.id });
diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx
index 77e60dc1..96c89aec 100644
--- a/src/lib/transaction/components/Transaction.jsx
+++ b/src/lib/transaction/components/Transaction.jsx
@@ -43,13 +43,14 @@ import { gtagPurchase } from '@/core/utils/googleTag';
import { deleteItemCart } from '@/core/utils/cart';
import {
downloadInvoice,
- downloadTaxInvoice,
+ // downloadTaxInvoice, // (unused)
} from '@/lib/invoice/utils/invoices';
import { Download } from 'lucide-react';
import axios from 'axios';
import InformationSection from '../../treckingAwb/component/InformationSection';
-import { Button } from '@chakra-ui/react';
-import { div } from 'lodash-contrib';
+// import { Button } from '@chakra-ui/react'; // (unused)
+// import { div } from 'lodash-contrib'; // (unused)
+
const Transaction = ({ id }) => {
const PPN = process.env.NEXT_PUBLIC_PPN;
const router = useRouter();
@@ -73,6 +74,7 @@ const Transaction = ({ id }) => {
const [toOthers, setToOthers] = useState(null);
const [totalAmount, setTotalAmount] = useState(0);
const [totalDiscountAmount, setTotalDiscountAmount] = useState(0);
+ const [contLoading, setContLoading] = useState(false);
useEffect(() => {
if (transaction?.data?.products) {
@@ -88,6 +90,7 @@ const Transaction = ({ id }) => {
setTotalDiscountAmount(calculateTotalDiscountAmount);
}
}, [transaction.data, transaction.isLoading]);
+
const submitUploadPo = async () => {
const file = poFile.current.files[0];
const name = poNumber.current.value;
@@ -127,10 +130,6 @@ const Transaction = ({ id }) => {
}
}
};
- // const ContinueTransaction = () => {
- // setContinueNoPo(true);
- // checkoutNoPO();
- // };
const closeCancelTransaction = () => setCancelTransaction(false);
const closeContinueTransaction = () => setContinueTransaction(false);
@@ -138,6 +137,7 @@ const Transaction = ({ id }) => {
const openRejectTransaction = () => setRejectTransaction(true);
const closeRejectTransaction = () => setRejectTransaction(false);
+
const submitCancelTransaction = async () => {
const isCancelled = await cancelTransactionApi({
transaction: transaction.data,
@@ -148,6 +148,7 @@ const Transaction = ({ id }) => {
}
closeCancelTransaction();
};
+
const checkout = async () => {
if (!transaction.data?.purchaseOrderFile) {
toast.error('Mohon upload dokumen PO anda sebelum melanjutkan pesanan');
@@ -194,25 +195,6 @@ const Transaction = ({ id }) => {
}
toast.success('Berhasil melanjutkan pesanan');
transaction.refetch();
- // console.log(transaction);
-
- /* 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 handleApproval = async () => {
@@ -227,6 +209,73 @@ const Transaction = ({ id }) => {
transaction.refetch();
};
+ // ===== Bayar Sekarang (pakai link dari backend; fallback generate via Next API) =====
+ const handlePayNow = async () => {
+ try {
+ setContLoading(true);
+
+ const base = (process.env.NEXT_PUBLIC_ODOO_API_HOST || '').replace(
+ /\/$/,
+ ''
+ );
+ const token = auth?.token;
+ const partnerId = auth?.partnerId;
+
+ // 1) Minta Odoo ensure payment link
+ const { data: resp } = await axios.get(
+ `${base}/api/v1/partner/${partnerId}/sale_order/${transaction.data.id}`,
+ {
+ params: { ensure_payment_link: 1, ts: Date.now() },
+ headers: { Token: token },
+ }
+ );
+
+ // console.log('API Response:', resp); // Debug
+
+ // 2) Akses semua kemungkinan path
+ let url =
+ resp?.result?.payment_summary?.redirect_url ||
+ resp?.data?.result?.payment_summary?.redirect_url ||
+ resp?.payment_summary?.redirect_url ||
+ resp?.paymentSummary?.redirectUrl ||
+ '';
+
+ // console.log('Extracted URL:', url); // Debug
+
+ if (url) {
+ window.location.href = url;
+ return;
+ }
+
+ // 3) Fallback
+ await transaction.refetch();
+ // console.log('Transaction data:', transaction.data); // Debug
+
+ url =
+ transaction?.data?.result?.payment_summary?.redirect_url ||
+ transaction?.data?.paymentSummary?.redirectUrl ||
+ transaction?.data?.payment_summary?.redirect_url ||
+ '';
+
+ // console.log('Fallback URL:', url); // Debug
+
+ if (url) {
+ window.location.href = url;
+ return;
+ }
+
+ throw new Error('Link pembayaran belum tersedia.');
+ } catch (e) {
+ toast.error(
+ e?.response?.data?.description ||
+ e?.message ||
+ 'Gagal membuka pembayaran'
+ );
+ } finally {
+ setContLoading(false);
+ }
+ };
+
const memoizeVariantGroupCard = useMemo(
() => (
<div className='p-4 pt-0 flex flex-col gap-y-3'>
@@ -314,7 +363,7 @@ const Transaction = ({ id }) => {
navigator.clipboard.writeText(textToCopy);
setCopied(true);
toast.success('No Resi Berhasil di Copy');
- setTimeout(() => setCopied(false), 2000); // Reset copied state after 2 seconds
+ setTimeout(() => setCopied(false), 2000);
};
const formatDate = (dateString) => {
@@ -336,7 +385,7 @@ const Transaction = ({ id }) => {
const [day, month, year] = dateString.split('/');
return `${day} ${months[parseInt(month, 10) - 1]} ${year}`;
};
- // console.log(transaction);
+
return (
transaction.data?.name && (
<>
@@ -366,6 +415,7 @@ const Transaction = ({ id }) => {
</button>
</div>
</BottomPopup>
+
<BottomPopup
active={cancelTransaction}
close={closeCancelTransaction}
@@ -452,48 +502,53 @@ const Transaction = ({ id }) => {
active={toOthers}
close={() => setToOthers(null)}
>
- <div className='flex flex-col gap-y-4 mt-2'>
+ {transaction.data?.status === 'draft' && (
+ <>
<button
className='text-left disabled:opacity-60'
- disabled={!toOthers?.purchaseOrderFile}
+ disabled={toOthers?.status != 'draft'}
onClick={() => {
- downloadPurchaseOrder(toOthers);
+ downloadQuotation(toOthers);
setToOthers(null);
}}
>
- Download PO
+ Download Quotation
</button>
<button
className='text-left disabled:opacity-60'
- disabled={toOthers?.status != 'draft'}
+ disabled={toOthers?.status != 'waiting'}
onClick={() => {
- downloadQuotation(toOthers);
+ setToCancel(toOthers);
setToOthers(null);
}}
>
- Download Quotation
+ Batalkan Transaksi
</button>
+ </>
+ )}
+ <div className='flex flex-col gap-y-4 mt-2'>
<button
className='text-left disabled:opacity-60'
- disabled={toOthers?.status != 'waiting'}
+ disabled={!toOthers?.purchaseOrderFile}
onClick={() => {
- setToCancel(toOthers);
+ downloadPurchaseOrder(toOthers);
setToOthers(null);
}}
>
- Batalkan Transaksi
+ Download PO
</button>
</div>
</BottomPopup>
<Manifest idAWB={idAWB} closePopup={closePopup}></Manifest>
+ {/* ============ MOBILE ============ */}
<MobileView>
<div className='px-4'>
<div className='flex flex-row w-full justify-between items-center py-2 px-3 mb-4 text-sm border border-yellow-500 text-yellow-800 rounded-lg bg-yellow-50 gap-2'>
- <div class='flex items-center w-full ' role='alert'>
+ <div className='flex items-center w-full ' role='alert'>
<svg
- class='flex-shrink-0 inline w-4 h-4 mr-2'
+ className='flex-shrink-0 inline w-4 h-4 mr-2'
aria-hidden='true'
fill='currentColor'
viewBox='0 0 20 20'
@@ -514,6 +569,7 @@ const Transaction = ({ id }) => {
</span>
</div>
</div>
+
{auth?.feature?.soApproval && (
<div className='p-4'>
<StepApproval
@@ -559,7 +615,7 @@ const Transaction = ({ id }) => {
<Divider />
<div className='flex flex-col gap-y-4 p-4'>
- <h4 className="font-semibold">Detail Order</h4>
+ <h4 className='font-semibold'>Detail Order</h4>
<DescriptionRow label='No Transaksi'>
<p className='font-semibold'>{transaction.data?.name}</p>
</DescriptionRow>
@@ -579,9 +635,11 @@ const Transaction = ({ id }) => {
<Divider />
<div className='flex flex-col gap-y-4 p-4'>
- <h4 className="font-semibold">Alamat Pengiriman</h4>
+ <h4 className='font-semibold'>Alamat Pengiriman</h4>
<DescriptionRow label='Nama Penerima'>
- <p className='font-semibold'>{transaction?.data?.address?.customer?.name}</p>
+ <p className='font-semibold'>
+ {transaction?.data?.address?.customer?.name}
+ </p>
</DescriptionRow>
<DescriptionRow label='No. Telp'>
{transaction?.data?.address?.customer?.phone
@@ -602,9 +660,7 @@ const Transaction = ({ id }) => {
<div className='p-4'>
<div className='font-medium mb-4'>Info Pengiriman</div>
{transaction?.data?.pickings.length == 0 && (
- <div className='badge-red text-sm'>
- Belum ada pengiriman
- </div>
+ <div className='badge-red text-sm'>Belum ada pengiriman</div>
)}
{transaction?.data?.pickings?.map((airway) => (
<div
@@ -627,28 +683,7 @@ const Transaction = ({ id }) => {
</button>
</div>
</div>
- // <button
- // className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left h-20'
- // key={airway?.id}
- // onClick={() => setIdAWB(airway?.id)}
- // >
- // <div>
- // <span className='text-sm text-gray_r-11'>
- // No Resi : {airway?.trackingNumber || '-'}{' '}
- // </span>
- // <p className='mt-1 font-medium'>{airway?.name}</p>
- // </div>
- // <div className='flex gap-x-2'>
- // <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1'>
- // {airway?.delivered
- // ? 'Pesanan Tiba'
- // : 'Sedang Dikirim'}
- // </div>
- // <ChevronRightIcon className='w-5 stroke-2' />
- // </div>
- // </button>
))}
-
</div>
<Divider />
@@ -717,17 +752,20 @@ const Transaction = ({ id }) => {
<div className='font-medium p-4'>Detail Produk</div>
{transaction?.data?.products.length > 0 ? (
<div className='p-4 pt-0 flex flex-col gap-y-3'>
- <VariantGroupCard variants={transaction.data?.products}/>
+ <VariantGroupCard variants={transaction.data?.products} />
<div className='font-medium'>Rincian Pembayaran</div>
<div className='flex justify-between mt-1'>
<p className='text-gray_r-12/70'>Metode Pembayaran</p>
- <p>
- {transaction.data?.paymentTerm || '-'}
- </p>
+ <p>{transaction.data?.paymentTerm || '-'}</p>
</div>
<div className='flex justify-between mt-1'>
<p className='text-gray_r-12/70'>Berat Barang</p>
- <p>{(transaction.data?.products?.reduce((total, item) => total + (item.weight || 0), 0)) + ' Kg'}</p>
+ <p>
+ {transaction.data?.products?.reduce(
+ (total, item) => total + (item.weight || 0),
+ 0
+ ) + ' Kg'}
+ </p>
</div>
<hr className='mt-1 border border-gray-100' />
<div className='flex justify-between mt-1'>
@@ -774,12 +812,7 @@ const Transaction = ({ id }) => {
</div>
)}
- {/* <Divider /> */}
-
- {/* <SectionAddress address={transaction.data?.address} /> */}
-
- {/* <Divider /> */}
-
+ {/* Tombol aksi (Mobile) */}
{transaction.data?.status === 'draft' && (
<div className='p-4 pt-0'>
<button
@@ -796,18 +829,33 @@ const Transaction = ({ id }) => {
Batalkan Transaksi
</button>
{transaction.data?.status == 'draft' &&
- transaction?.data?.purchaseOrderFile && (
- <button
- className='btn-yellow w-full mt-4'
- onClick={openContinueTransaction}
- >
- Lanjutkan Transaksi
- </button>
- )}
+ transaction?.data?.purchaseOrderFile && (
+ <button
+ className='btn-yellow w-full mt-4'
+ onClick={openContinueTransaction}
+ >
+ Lanjutkan Transaksi
+ </button>
+ )}
+ </div>
+ )}
+
+ {/* Bayar Sekarang (Mobile) — tampil jika eligible */}
+ {transaction.data?.eligibleContinue && (
+ <div className='p-4 pt-0'>
+ <button
+ type='button'
+ disabled={contLoading}
+ onClick={handlePayNow}
+ className='w-full py-2 text-center rounded-md border border-red-500 text-red-500 bg-white disabled:opacity-60'
+ >
+ {contLoading ? 'Memproses…' : 'Bayar Sekarang'}
+ </button>
</div>
)}
</MobileView>
+ {/* ============ DESKTOP ============ */}
<DesktopView>
<div className='container mx-auto flex py-10'>
<div className='w-3/12 pr-4'>
@@ -827,44 +875,36 @@ const Transaction = ({ id }) => {
)}
</div>
- {/*new-release*/}
- {/*<div className='flex items-center justify-between mb-3'>*/}
- {/* <div className='flex items-center gap-x-2'>*/}
- {/* <span className='text-h-sm font-medium'>*/}
- {/* {transaction?.data?.name}*/}
- {/* </span>*/}
- {/* <TransactionStatusBadge status={transaction?.data?.status} />*/}
- {/* </div>*/}
- {/* <div className='text-h-sm'>*/}
- {/* Estimasi Barang Siap:{' '}*/}
- {/* <span className='text-red-500 font-semibold'>*/}
- {/* {transaction?.data?.expectedReadyToShip}*/}
- {/* </span>*/}
- {/* </div>*/}
- <div className='flex items-center gap-x-2 mb-3'>
- <span className='text-h-sm font-medium'>
- {transaction?.data?.name}
- </span>
- <TransactionStatusBadge status={transaction?.data?.status} />
- {transaction.data?.status === 'draft' && (
- <div className='flex items-center justify-between w-full'>
- <button
- type='button'
- className='btn-light px-3 py-2'
- onClick={() => downloadQuotation(transaction.data)}
- >
- <Download size={12} />
- </button>
+ {/* HEADER (Desktop) — sejajarkan kiri & kanan */}
+ <div className='flex items-center justify-between gap-3 mb-3'>
+ {/* Kiri: SO + badge */}
+ <div className='flex items-center gap-x-2 min-w-0'>
+ <span className='text-h-sm font-medium truncate'>
+ {transaction?.data?.name}
+ </span>
+ <TransactionStatusBadge status={transaction?.data?.status} />
+ </div>
+
+ {/* Kanan: aksi */}
+ <div className='flex items-center gap-3'>
+ {transaction.data?.status === 'draft' && (
+ <>
+ <button
+ type='button'
+ className='btn-light px-3 py-2'
+ onClick={() => downloadQuotation(transaction.data)}
+ >
+ <Download size={12} />
+ </button>
- <div className="flex gap-x-4">
<button
className='btn-solid-red'
onClick={openCancelTransaction}
>
Batalkan Transaksi
</button>
- {transaction.data?.status == 'draft' &&
- transaction?.data?.purchaseOrderFile && (
+
+ {transaction?.data?.purchaseOrderFile && (
<button
className='btn-yellow'
onClick={openContinueTransaction}
@@ -872,37 +912,20 @@ const Transaction = ({ id }) => {
Lanjutkan Transaksi
</button>
)}
- </div>
- </div>
- )}
- </div>
- {/* {transaction.data?.status === 'draft' && (
- <div className='flex gap-x-4'>
- <button
- type='button'
- className='btn-light px-3 py-2 mr-auto'
- onClick={() => downloadQuotation(transaction.data)}
- >
- <Download size={12} />
- </button>
- <button
- className='btn-solid-red'
- onClick={openCancelTransaction}
- >
- Batalkan Transaksi
- </button>
-
- {transaction.data?.status == 'draft' &&
- transaction?.data?.purchaseOrderFile && (
- <button
- className='btn-yellow'
- onClick={openContinueTransaction}
- >
- Lanjutkan Transaksi
- </button>
- )}
+ </>
+ )}
+
+ {transaction.data?.eligibleContinue && (
+ <button
+ className='px-4 py-2 rounded-md border border-red-500 text-red-500 bg-white disabled:opacity-60 mb-3'
+ disabled={contLoading}
+ onClick={handlePayNow}
+ >
+ {contLoading ? 'Memproses…' : 'Bayar Sekarang'}
+ </button>
+ )}
</div>
- )} */}
+ </div>
<div className='grid grid-cols-2 gap-x-6 mt-4'>
<div className='grid grid-cols-[35%_65%] gap-y-4'>
@@ -967,29 +990,13 @@ const Transaction = ({ id }) => {
key={index}
>
{invoice?.name}
- {/* <div className='shadow rounded-md p-4 text-gray_r-12 font-normal flex justify-between'>
- <div>
- <p className='mb-1'>{invoice?.name}</p>
- <div className='flex items-center gap-x-1'>
- {invoice.amountResidual > 0 ? (
- <div className='badge-red'>Belum Lunas</div>
- ) : (
- <div className='badge-green'>Lunas</div>
- )}
- <p className='text-caption-2 text-gray_r-11'>
- {currencyFormat(invoice.amountTotal)}
- </p>
- </div>
- </div>
- <ChevronRightIcon className='w-5 stroke-2' />
- </div> */}
</Link>
))}
</div>
</div>
</div>
<hr className='mt-4 mb-4 border border-gray-100' />
- {/* <div className='grid grid-cols-2 gap-x-6'> */}
+
<div className='flex flex-row justify-between items-start w-full h-fit '>
<div className='flex flex-col w-1/2 justify-start items-start'>
<span className='text-h-sm font-medium mb-2'>
@@ -1039,17 +1046,17 @@ const Transaction = ({ id }) => {
) : (
'-'
)}
- {transaction?.data?.carrierId !== 32 &&(
- <>
- <div>Jenis Service</div>
- <div>: </div>
- <div>
- {' '}
- {transaction?.data?.serviceType
- ? transaction?.data?.serviceType
- : '-'}
- </div>
- </>
+ {transaction?.data?.carrierId !== 32 && (
+ <>
+ <div>Jenis Service</div>
+ <div>: </div>
+ <div>
+ {' '}
+ {transaction?.data?.serviceType
+ ? transaction?.data?.serviceType
+ : '-'}
+ </div>
+ </>
)}
<div>Estimasi Tanggal Kirim</div>
@@ -1059,41 +1066,42 @@ const Transaction = ({ id }) => {
? transaction?.data?.expectedReadyToShip
: '-'}
</div>
- {transaction?.data?.carrierId !== 32 &&(
- <>
- <div>Estimasi Tiba</div>
- <div>: </div>
- <div className=''>
- {transaction?.data?.etaDateStart && transaction?.data?.etaDateEnd ? (
- `${transaction.data.etaDateStart} - ${transaction.data.etaDateEnd}`
- ) : (
- '-'
- )}
- </div>
- </>
+ {transaction?.data?.carrierId !== 32 && (
+ <>
+ <div>Estimasi Tiba</div>
+ <div>: </div>
+ <div className=''>
+ {transaction?.data?.etaDateStart &&
+ transaction?.data?.etaDateEnd
+ ? `${transaction.data.etaDateStart} - ${transaction.data.etaDateEnd}`
+ : '-'}
+ </div>
+ </>
)}
- {transaction?.data?.pickings[0] && transaction?.data?.carrierId !== 32 && (
- <div className='w-full bagian-informasi col-span-3'>
- <div
- class='flex items-center w-fit py-2 px-3 mb-4 text-sm border border-yellow-500 text-yellow-800 rounded-lg bg-yellow-50'
- role='alert'
- >
- <svg
- class='flex-shrink-0 inline w-4 h-4 mr-2'
- aria-hidden='true'
- fill='currentColor'
- viewBox='0 0 20 20'
+ {transaction?.data?.pickings[0] &&
+ transaction?.data?.carrierId !== 32 && (
+ <div className='w-full bagian-informasi col-span-3'>
+ <div
+ className='flex items-center w-fit py-2 px-3 mb-4 text-sm border border-yellow-500 text-yellow-800 rounded-lg bg-yellow-50'
+ role='alert'
>
- <path d='M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z' />
- </svg>
- <div className='text-justify flex flex-col gap-1'>
- <span className='text-black text-xs'>
- Pesanan anda mungkin mengalami keterlambatan tiba
- </span>
+ <svg
+ className='flex-shrink-0 inline w-4 h-4 mr-2'
+ aria-hidden='true'
+ fill='currentColor'
+ viewBox='0 0 20 20'
+ >
+ <path d='M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z' />
+ </svg>
+ <div className='text-justify flex flex-col gap-1'>
+ <span className='text-black text-xs'>
+ Pesanan anda mungkin mengalami keterlambatan
+ tiba
+ </span>
+ </div>
</div>
</div>
- </div>
- )}
+ )}
</div>
</div>
</div>
@@ -1103,9 +1111,7 @@ const Transaction = ({ id }) => {
</div>
<div className='grid grid-cols-1 md:grid-cols-2 gap-3'>
{transaction?.data?.pickings.length == 0 && (
- <div className='badge-red text-sm'>
- Belum ada pengiriman
- </div>
+ <div className='badge-red text-sm'>Belum ada pengiriman</div>
)}
{transaction?.data?.pickings?.map((airway) => (
<div
@@ -1129,39 +1135,9 @@ const Transaction = ({ id }) => {
</div>
</div>
))}
- {/* </div> */}
-
</div>
<div className='flex '>
- {/*New release*/}
- {/* <div className='grid grid-cols-1 gap-1 w-2/3'>*/}
- {/* {transaction?.data?.pickings?.map((airway) => (*/}
- {/* <button*/}
- {/* className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left h-20'*/}
- {/* key={airway?.id}*/}
- {/* onClick={() => setIdAWB(airway?.id)}*/}
- {/* >*/}
- {/* <div>*/}
- {/* <p className='text-sm text-gray_r-11'>*/}
- {/* {airway?.name}*/}
- {/* </p>*/}
- {/* <span className='text-md text-bold mt-1'>*/}
- {/* No Resi : {airway?.trackingNumber || '-'}{' '}*/}
- {/* </span>*/}
- {/* </div>*/}
- {/* <div className='flex gap-x-2'>*/}
- {/* <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1 text-center'>*/}
- {/* {airway?.delivered*/}
- {/* ? 'Pesanan Tiba'*/}
- {/* : 'Sedang Dikirim'}*/}
- {/* </div>*/}
- {/* <ChevronRightIcon className='w-5 stroke-2' />*/}
- {/* </div>*/}
- {/* </button>*/}
- {/* ))}*/}
- {/* </div>*/}
- {/*</div>*/}
<div className='invoice w-1/2 '>
<div className='text-h-sm font-semibold mt-10 mb-4 '>
Invoice
@@ -1202,7 +1178,6 @@ const Transaction = ({ id }) => {
<thead>
<tr>
<th>Nama Produk</th>
- {/* <th>Diskon</th> */}
<th>Jumlah</th>
<th>Harga</th>
<th>Subtotal</th>
@@ -1280,24 +1255,13 @@ const Transaction = ({ id }) => {
)}
</div>
</td>
- {/* <td>
- {product.price.discountPercentage > 0
- ? `${product.price.discountPercentage}%`
- : ''}
- </td> */}
<td>{product.quantity}</td>
<td>
- {/* {product.price.discountPercentage > 0 && (
- <div className='line-through mb-1 text-caption-1 text-gray_r-12/70'>
- {currencyFormat(product.price.price)}
- </div>
- )} */}
<div>
{currencyFormat(product.price.priceDiscount)}
</div>
</td>
<td>{currencyFormat(product.price.subtotal)}</td>
- {/* {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (transaction.data.isReaject == false) && ( */}
{auth?.feature.soApproval &&
(auth.webRole == 2 || auth.webRole == 3) &&
router.asPath.includes('/my/quotations/') &&
@@ -1354,34 +1318,6 @@ const Transaction = ({ id }) => {
)}
{transaction?.data?.products?.length > 0 && (
- // <div className='flex justify-end mt-4'>
- // <div className='w-1/4 grid grid-cols-2 gap-y-3 text-gray_r-12/80'>
- // <div className='text-right'>Subtotal</div>
- // <div className='text-right font-medium'>
- // {currencyFormat(transaction.data?.amountUntaxed)}
- // </div>
-
- // <div className='text-right'>
- // PPN {((PPN - 1) * 100).toFixed(0)}%
- // </div>
- // <div className='text-right font-medium'>
- // {currencyFormat(transaction.data?.amountTax)}
- // </div>
-
- // <div className='text-right whitespace-nowrap'>
- // Biaya Pengiriman
- // </div>
- // <div className='text-right font-medium'>
- // {currencyFormat(transaction.data?.deliveryAmount)}
- // </div>
-
- // <div className='text-right'>Grand Total</div>
- // <div className='text-right font-medium text-gray_r-12'>
- // {currencyFormat(transaction.data?.amountTotal)}
- // </div>
- // </div>
- // </div>
-
<div className='flex justify-end mt-4 flex-col items-end'>
<div className='w-1/4 grid grid-cols-2 gap-y-3 text-gray_r-12/80'>
<div className='text-right'>Total Belanja</div>
@@ -1437,7 +1373,6 @@ const Transaction = ({ id }) => {
<thead>
<tr>
<th>Nama Produk</th>
- {/* <th>Diskon</th> */}
<th>Jumlah</th>
<th>Harga</th>
<th>Subtotal</th>
@@ -1480,18 +1415,8 @@ const Transaction = ({ id }) => {
</div>
</div>
</td>
- {/* <td>
- {product.price.discountPercentage > 0
- ? `${product.price.discountPercentage}%`
- : ''}
- </td> */}
<td>{product.quantity}</td>
<td>
- {/* {product.price.discountPercentage > 0 && (
- <div className='line-through mb-1 text-caption-1 text-gray_r-12/70'>
- {currencyFormat(product.price.price)}
- </div>
- )} */}
<div>
{currencyFormat(product.price.priceDiscount)}
</div>
@@ -1512,58 +1437,6 @@ const Transaction = ({ id }) => {
</div>
</div>
</DesktopView>
-
- {/* {queryAirwayBill.data?.airways?.map((airway) => (
- <BottomPopup
- key={airway.waybillNumber}
- title='Detail Pengiriman'
- active={airwayBillPopup == airway.waybillNumber}
- close={() => setAirwayBillPopup(null)}
- >
- <div className='flex flex-col gap-y-4 my-4'>
- <div className='flex justify-between'>
- <div className='text-gray_r-11'>No Pengiriman</div>
- <div>{airway?.deliveryOrder?.name}</div>
- </div>
- <div className='flex justify-between'>
- <div className='text-gray_r-11'>Kurir</div>
- <div>{airway?.deliveryOrder?.carrier}</div>
- </div>
- <div className='flex justify-between'>
- <div className='text-gray_r-11'>No Resi</div>
- <div>{airway?.waybillNumber}</div>
- </div>
- </div>
-
- <div className='pt-4'>
- <div className='font-semibold text-body-1 mb-4'>Status Pengiriman</div>
- <ol class='relative border-l border-gray_r-7'>
- {airway?.manifests?.map((manifest, index) => (
- <li class='mb-6 ml-4' key={index}>
- <div
- class={`absolute w-3 h-3 rounded-full mt-1.5 -left-1.5 border ${
- index == 0 ? 'bg-red-600 border-red-600' : 'bg-gray_r-7 border-white'
- }`}
- />
- <time class='text-sm leading-none text-gray-400'>
- {manifest.datetime}
- </time>
- <p
- class={`leading-6 font-medium text-body-2 mt-2 ${
- index == 0 ? 'text-red-600' : 'text-gray_r-11'
- }`}
- >
- {manifest.description}
- </p>
- </li>
- ))}
- {(!airway?.manifests || airway?.manifests?.length == 0) && (
- <div className='badge-red text-sm'>Belum ada pengiriman</div>
- )}
- </ol>
- </div>
- </BottomPopup>
- ))} */}
</>
)
);
@@ -1589,24 +1462,7 @@ const SectionAddress = ({ address }) => {
{section.customer && <SectionContent address={address?.customer} />}
- {/* <Divider />
-
- <SectionButton
- label='Detail Pengiriman'
- active={section.shipping}
- toggle={() => toggleSection('shipping')}
- />
-
- {section.shipping && <SectionContent address={address?.shipping} />}
-
- <Divider />
-
- <SectionButton
- label='Detail Penagihan'
- active={section.invoice}
- toggle={() => toggleSection('invoice')}
- />
- {section.invoice && <SectionContent address={address?.invoice} />} */}
+ {/* Bagian shipping/invoice disembunyikan */}
</>
);
};
diff --git a/src/lib/transaction/components/TransactionStatusBadge.jsx b/src/lib/transaction/components/TransactionStatusBadge.jsx
index cb8cbcd9..d23b17cd 100644
--- a/src/lib/transaction/components/TransactionStatusBadge.jsx
+++ b/src/lib/transaction/components/TransactionStatusBadge.jsx
@@ -4,6 +4,10 @@ const TransactionStatusBadge = ({ status }) => {
text: ''
}
switch (status) {
+ case 'belum_bayar':
+ badgeProps.className.push('badge-solid-red')
+ badgeProps.text = 'Belum Bayar'
+ break
case 'cancel':
badgeProps.className.push('badge-solid-red')
badgeProps.text = 'Pesanan Batal'
diff --git a/src/lib/transaction/components/Transactions.jsx b/src/lib/transaction/components/Transactions.jsx
index 5e37be50..600518fa 100644
--- a/src/lib/transaction/components/Transactions.jsx
+++ b/src/lib/transaction/components/Transactions.jsx
@@ -1,11 +1,11 @@
+import axios from 'axios';
import { useRouter } from 'next/router';
-import { useEffect, useState, useRef } from 'react';
+import { useEffect, useState, useRef } from 'react';
import { toast } from 'react-hot-toast';
import {
EllipsisVerticalIcon,
MagnifyingGlassIcon,
ChevronDownIcon,
- ChevronUpIcon,
} from '@heroicons/react/24/outline';
import useAuth from '@/core/hooks/useAuth';
import {
@@ -38,17 +38,13 @@ import { Navigation } from 'swiper';
import 'swiper/css';
import 'swiper/css/navigation';
import { Calendar } from 'lucide-react';
-import DatePicker from 'react-datepicker';
-import 'react-datepicker/dist/react-datepicker.css';
import { DateRangePicker } from 'react-date-range';
-import { addDays } from 'date-fns';
-import 'react-date-range/dist/styles.css'; // main style file
-import 'react-date-range/dist/theme/default.css'; // theme css file
-import { Popover } from '@headlessui/react';
+import 'react-date-range/dist/styles.css';
+import 'react-date-range/dist/theme/default.css';
+
const Transactions = ({ context = '' }) => {
const auth = useAuth();
const router = useRouter();
- const swiperRef = useRef(null);
const {
q = '',
page = 1,
@@ -59,15 +55,11 @@ const Transactions = ({ context = '' }) => {
startDate = null,
endDate = new Date(),
} = router.query;
- const {
- productCart,
- setRefreshCart,
- setProductCart,
- refreshCart,
- isLoading,
- setIsloading,
- } = useProductCartContext();
+ const { setRefreshCart } = useProductCartContext();
+
const [inputQuery, setInputQuery] = useState(q);
+ const [cachedAllData, setCachedAllData] = useState(null); // Simpan data "All"
+ const [currentData, setCurrentData] = useState([]); // Data yang ditampilkan
const [toOthers, setToOthers] = useState(null);
const [toCancel, setToCancel] = useState(null);
const [listSites, setListSites] = useState([]);
@@ -75,17 +67,16 @@ const Transactions = ({ context = '' }) => {
const [siteFilter, setSiteFilter] = useState(site);
const [pageNew, setPageNew] = useState(page);
const [limitNew, setLimitNew] = useState(limit);
- // const [status, setStatus] = useState('idle');
const [statusNew, setStatusNew] = useState(status);
const [sortNew, setSortNew] = useState(sort);
const [contextNew, setcontextNew] = useState(router.query.context || context);
- const [dateRange, setDateRange] = useState([null, null]);
- // const [startDate, endDate] = dateRange;
const [isOpenCalender, setIsOpenCalender] = useState(false);
- const [cachedAllData, setCachedAllData] = useState(null); // Simpan data "All"
- const [currentData, setCurrentData] = useState([]); // Data yang ditampilkan
const calendarRef = useRef(null);
- const [isDateSelected, setIsDateSelected] = useState(false);
+ const isUnpaid = (s) =>
+ ['belum_bayar'].includes(String(s || '').toLowerCase());
+
+ // loading id utk tombol lanjutkan transaksi
+ const [contLoadingId, setContLoadingId] = useState(null);
const parseDate = (date) => {
if (!date || date === 'null') return null;
@@ -96,7 +87,7 @@ const Transactions = ({ context = '' }) => {
const [state, setState] = useState([
{
- startDate: startDate != null || 'null' ? parseDate(startDate) : null, // Gunakan `parseDate`
+ startDate: startDate != null || 'null' ? parseDate(startDate) : null,
endDate: startDate == null ? endDate : parseDate(endDate),
key: 'selection',
},
@@ -116,9 +107,11 @@ const Transactions = ({ context = '' }) => {
site:
siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null),
};
+
const statuses = [
{ id: 'all', label: 'Semua' },
{ id: 'quotation', label: 'Pending Quotation' },
+ { id: 'belum_bayar', label: 'Belum Bayar' },
{ id: 'diproses', label: 'Pesanan Diproses' },
{ id: 'dikemas', label: 'Pesanan Dikemas' },
{ id: 'partial', label: 'Dikirim Sebagian' },
@@ -135,6 +128,7 @@ const Transactions = ({ context = '' }) => {
shipping: 'Pesanan Dikirim',
done: 'Pesanan Selesai',
cancel: 'Pesanan Dibatalkan',
+ belum_bayar: 'Belum Bayar',
};
const sortes = [
@@ -142,16 +136,16 @@ const Transactions = ({ context = '' }) => {
{ id: 'asc', label: 'dari yang terkecil' },
{ id: 'desc', label: 'dari yang terbesar' },
];
+
const { transactions } = useTransactions({ query });
+
const fetchSite = async () => {
const site = await getSite();
setListSites(site.sites);
};
const submitCancelTransaction = async () => {
- const isCancelled = await cancelTransactionApi({
- transaction: toCancel,
- });
+ const isCancelled = await cancelTransactionApi({ transaction: toCancel });
if (isCancelled) {
toast.success('Berhasil batalkan transaksi');
transactions.refetch();
@@ -159,23 +153,16 @@ const Transactions = ({ context = '' }) => {
setToCancel(null);
};
- const pageCount = Math.ceil(transactions?.data?.saleOrderTotal / limitNew);
- let pageQuery = _.omit(query, ['limit', 'offset', 'context']);
- pageQuery = _.pickBy(
- pageQuery,
- (value, key) => value !== '' && !(key === 'page' && value === '1')
+ const pageCount = Math.ceil(
+ (transactions?.data?.saleOrderTotal || 0) / (limitNew || 1)
);
- pageQuery = toQuery(pageQuery);
const handleSubmit = (e) => {
e.preventDefault();
const queryParams = {};
if (inputQuery) queryParams.q = inputQuery;
if (siteFilter) queryParams.site = siteFilter;
- router.push({
- pathname: router.pathname,
- query: queryParams,
- });
+ router.push({ pathname: router.pathname, query: queryParams });
};
const handleSiteFilterChange = (e) => {
@@ -183,10 +170,7 @@ const Transactions = ({ context = '' }) => {
const queryParams = {};
if (inputQuery) queryParams.q = inputQuery;
if (e.target.value) queryParams.site = e.target.value;
- router.push({
- pathname: router.pathname,
- query: queryParams,
- });
+ router.push({ pathname: router.pathname, query: queryParams });
};
const exportToExcel = (data, siteFilter) => {
@@ -201,19 +185,17 @@ const Transactions = ({ context = '' }) => {
];
const rowsToExport = [];
- data.forEach((saleOrder) => {
+ (data || []).forEach((saleOrder) => {
const row = {
'No. Transaksi': saleOrder.name,
'No. PO': saleOrder.purchaseOrderName || '-',
Tanggal: saleOrder.dateOrder || '-',
- 'Created By': saleOrder.address.customer?.name || '-',
+ 'Created By': saleOrder.address?.customer?.name || '-',
Salesperson: saleOrder.sales,
Total: currencyFormat(saleOrder.amountTotal),
Status: contextLabelMap[saleOrder.status] || saleOrder.status,
};
- if (siteFilter) {
- row['Site'] = siteFilter;
- }
+ if (siteFilter) row['Site'] = siteFilter;
rowsToExport.push(row);
});
@@ -226,13 +208,30 @@ const Transactions = ({ context = '' }) => {
XLSX.writeFile(workbook, 'transactions.xlsx');
};
+ const getAllData = async () => {
+ const qobj = {
+ name: q,
+ offset: (pageNew - 1) * limitNew,
+ limit: limitNew,
+ context: contextNew[statusNew] || 'all',
+ sort: sortNew,
+ startDate: state[0]?.startDate
+ ? state[0].startDate.toLocaleDateString('id-ID')
+ : null,
+ endDate: state[0]?.endDate
+ ? state[0].endDate.toLocaleDateString('id-ID')
+ : null,
+ site:
+ siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null),
+ };
+ const queryString = toQuery(qobj);
+ const data = await transactionsApi({ query: queryString });
+ return data;
+ };
+
const handleExportCSV = async () => {
const dataToExport = await getAllData();
- exportToCSV(dataToExport?.saleOrders, siteFilter);
- };
-
- const exportToCSV = (data, siteFilter) => {
const fieldsToExport = [
'No. Transaksi',
'No. PO',
@@ -242,28 +241,23 @@ const Transactions = ({ context = '' }) => {
'Total',
'Status',
];
-
- if (siteFilter) {
- fieldsToExport.push('Site');
- }
-
- const rowsToExport = data.map((saleOrder) => {
- const row = [
- saleOrder.name,
- saleOrder.purchaseOrderName || '-',
- saleOrder.dateOrder || '-',
- saleOrder.address.customer?.name || '-',
- saleOrder.sales,
- currencyFormat(saleOrder.amountTotal),
- contextLabelMap[saleOrder.status] || saleOrder.status,
- ];
-
- if (siteFilter) {
- row.push(siteFilter);
- }
-
- return row.join(',');
- });
+ const rowsToExport =
+ dataToExport?.saleOrders?.map((saleOrder) => {
+ const row = [
+ saleOrder.name,
+ saleOrder.purchaseOrderName || '-',
+ saleOrder.dateOrder || '-',
+ saleOrder.address?.customer?.name || '-',
+ saleOrder.sales,
+ currencyFormat(saleOrder.amountTotal),
+ (contextLabelMap[saleOrder.status] || saleOrder.status || '').replace(
+ /,/g,
+ ' '
+ ),
+ ];
+ if (siteFilter) row.push((siteFilter || '').replace(/,/g, ' '));
+ return row.join(',');
+ }) || [];
const csvContent =
'data:text/csv;charset=utf-8,' +
@@ -278,66 +272,31 @@ const Transactions = ({ context = '' }) => {
document.body.removeChild(link);
};
- const getAllData = async () => {
- const query = {
- name: q,
- offset: (pageNew - 1) * limitNew,
- limit: limitNew,
- context: contextNew[statusNew] || 'all',
- sort: sortNew,
- startDate: state[0]?.startDate
- ? state[0].startDate.toLocaleDateString('id-ID')
- : null,
- endDate: state[0]?.endDate
- ? state[0].endDate.toLocaleDateString('id-ID')
- : null,
- site:
- siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null),
- };
- const queryString = toQuery(query);
- const data = await transactionsApi({ query: queryString });
- return data;
- };
-
const handleExportExcel = async () => {
const dataToExport = await getAllData();
-
exportToExcel(dataToExport?.saleOrders, siteFilter);
};
const handleDownload = (format) => {
- handleExport(format);
+ if (format === 'csv') handleExportCSV();
+ else if (format === 'xlsx') handleExportExcel();
setIsOpen(false);
};
- const handleExport = (format) => {
- if (format === 'csv') {
- handleExportCSV();
- } else if (format === 'xlsx') {
- handleExportExcel();
- }
- };
-
useEffect(() => {
const handleClickOutside = (event) => {
- if (
- calendarRef.current &&
- !calendarRef.current.contains(event.target)
- ) {
+ if (calendarRef.current && !calendarRef.current.contains(event.target)) {
setIsOpenCalender(false);
}
};
-
- document.addEventListener("mousedown", handleClickOutside);
- return () => {
- document.removeEventListener("mousedown", handleClickOutside);
- };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const startItem = 1 + (pageNew - 1) * limitNew;
const endItem = Math.min(
limitNew * pageNew,
- transactions?.data?.saleOrderTotal
+ transactions?.data?.saleOrderTotal || 0
);
useEffect(() => {
@@ -346,11 +305,8 @@ const Transactions = ({ context = '' }) => {
const handleBuyBack = async (products) => {
try {
- // setStatus('loading');
- console.log("Products to add:", products);
-
const results = await Promise.all(
- products.map((product) =>
+ (products || []).map((product) =>
upsertUserCart({
userId: auth.id,
type: 'product',
@@ -359,51 +315,42 @@ const Transactions = ({ context = '' }) => {
selected: true,
source: 'add_to_cart',
qtyAppend: true,
- }).catch(error => {
- return { error, product };
- })
+ }).catch((error) => ({ error, product }))
)
);
- const failedOperations = results.filter(result => result && result.error);
- // console.log(results);
-
- if (failedOperations.length > 0) {
- console.error('Some products failed to add to cart:', failedOperations);
- toast.error(`${failedOperations.length} produk gagal ditambahkan ke keranjang`);
-
- // You might want to proceed with the successful ones or handle differently
- if (failedOperations.length < products.length) {
- toast.success(`${products.length - failedOperations.length} produk berhasil ditambahkan ke keranjang`);
+ const failed = results.filter((r) => r && r.error);
+ if (failed.length > 0) {
+ toast.error(`${failed.length} produk gagal ditambahkan ke keranjang`);
+ if (failed.length < (products || []).length) {
+ toast.success(
+ `${
+ (products || []).length - failed.length
+ } produk berhasil ditambahkan`
+ );
setRefreshCart(true);
router.push('/shop/cart');
}
return;
}
-
- // All operations succeeded
setRefreshCart(true);
- toast.success('Semua produk berhasil ditambahkan ke keranjang belanja');
+ toast.success('Semua produk berhasil ditambahkan ke keranjang');
router.push('/shop/cart');
-
- } catch (error) {
- console.error('Gagal menambahkan produk ke keranjang:', error);
+ } catch {
toast.error('Terjadi kesalahan saat menambahkan produk ke keranjang');
- // setStatus('error');
}
};
-
const handleStatusChange = async (status) => {
setStatusNew(status);
setPageNew(1);
if (status === 'all' && cachedAllData) {
- setCurrentData(cachedAllData);
- return;
+ setCurrentData(cachedAllData);
+ return;
}
const data = await fetchSite(status, 1);
-
+
if (status === 'all') {
setCachedAllData(data);
}
@@ -411,19 +358,8 @@ const Transactions = ({ context = '' }) => {
setCurrentData(data);
};
- useEffect(() => {
- setCachedAllData([]);
- }, []);
-
-
const handleReset = () => {
- setState([
- {
- startDate: null,
- endDate: new Date(),
- key: 'selection',
- },
- ]);
+ setState([{ startDate: null, endDate: new Date(), key: 'selection' }]);
setIsOpenCalender(false);
router.push(`${router.pathname}`);
};
@@ -443,20 +379,95 @@ const Transactions = ({ context = '' }) => {
'November',
'Desember',
];
-
- const [day, month, year] = dateString.split('/');
+ const [day, month, year] = (dateString || '').split('/');
+ if (!day || !month || !year) return dateString || '-';
return `${day} ${months[parseInt(month, 10) - 1]} ${year}`;
};
+ // ==== Lanjutkan Transaksi (tanpa endpoint baru) ====
+ const handleContinuePayment = async (saleOrder) => {
+ try {
+ setContLoadingId(saleOrder.id);
+
+ const base = (process.env.NEXT_PUBLIC_ODOO_API_HOST || '').replace(
+ /\/$/,
+ ''
+ );
+ const token = auth?.token;
+ const partnerId = auth?.partnerId;
+
+ // 1. TRIGGER GENERATE + GET URL
+ const { data: response } = await axios.get(
+ `${base}/api/v1/partner/${partnerId}/sale_order/${saleOrder.id}`,
+ {
+ params: { ensure_payment_link: 1, ts: Date.now() },
+ headers: { Token: token, 'Cache-Control': 'no-cache' },
+ timeout: 10000,
+ }
+ );
+
+ // 2. EKSTRAK URL
+ let paymentUrl =
+ response?.result?.payment_summary?.redirect_url ||
+ response?.data?.result?.payment_summary?.redirect_url;
+
+ // 3. JIKA DAPAT URL, BUKA
+ if (paymentUrl) {
+ window.location.href = paymentUrl;
+ toast.success('Membuka halaman pembayaran…');
+ return;
+ }
+
+ // 4. FALLBACK: COBA TANPA ensure_payment_link
+ try {
+ const { data: fallbackResponse } = await axios.get(
+ `${base}/api/v1/partner/${partnerId}/sale_order/${saleOrder.id}`,
+ { headers: { Token: token }, timeout: 5000 }
+ );
+
+ const fallbackUrl =
+ fallbackResponse?.result?.payment_summary?.redirect_url ||
+ fallbackResponse?.data?.result?.payment_summary?.redirect_url;
+
+ if (fallbackUrl) {
+ window.location.href = fallbackUrl;
+ toast.success('Membuka halaman pembayaran…');
+ return;
+ }
+ } catch (fallbackError) {
+ // Continue to next fallback
+ }
+
+ // 5. ULTIMATE FALLBACK: PAKAI URL DARI DATA LAMA
+ const existingUrl =
+ saleOrder?.paymentSummary?.redirectUrl ||
+ saleOrder?.payment_summary?.redirect_url;
+
+ if (existingUrl) {
+ window.open(existingUrl, '_blank', 'noopener,noreferrer');
+ toast.success('Membuka halaman pembayaran…');
+ } else {
+ toast.error('Link pembayaran tidak ditemukan. Silakan coba lagi.');
+ }
+ } catch (error) {
+ toast.error(
+ error.response?.data?.description || 'Gagal memproses pembayaran'
+ );
+ } finally {
+ setContLoadingId(null);
+ }
+ };
+
return (
<>
+ {/* ===== MOBILE ===== */}
<MobileView>
- <div className='p-4 flex flex-col gap-y-4'>
- <div className='grid grid-cols-[30%_30%_40%] justify-between items-center gap-2 w-full '>
+ <div className=' flex flex-col gap-y-4'>
+ <div className='grid grid-cols-[40%_40%_15%] justify-between items-center gap-2 w-full '>
<select
value={statusNew}
onChange={(e) => handleStatusChange(e.target.value)}
- className='border border-gray-300 rounded-lg px-2 py-1 text-xs bg-white shadow-sm focus:outline-none focus:ring-2 focus:ring-red-500'
+ className='border border-gray-300 rounded-lg px-2 py-2 text-xs bg-white shadow-sm focus:outline-none focus:ring-2 focus:ring-red-500'
>
{statuses.map((status) => (
<option key={status.id} value={status.id}>
@@ -467,7 +478,7 @@ const Transactions = ({ context = '' }) => {
<select
value={sortNew}
onChange={(e) => setSortNew(e.target.value)}
- className='border border-gray-300 rounded-lg px-2 py-1 text-xs bg-white shadow-sm focus:outline-none focus:ring-2 focus:ring-red-500'
+ className='border border-gray-300 rounded-lg px-2 py-2 text-xs bg-white shadow-sm focus:outline-none focus:ring-2 focus:ring-red-500'
>
{sortes.map((status) => (
<option key={status.id} value={status.id}>
@@ -475,23 +486,22 @@ const Transactions = ({ context = '' }) => {
</option>
))}
</select>
- <div ref={calendarRef} className="relative inline-block">
- <button
- type='button'
- className='p-2 w-auto h-auto cursor-pointer hover:bg-gray-100 rounded transition duration-150 ease-in-out flex items-center justify-center'
- onClick={() => setIsOpenCalender((prev) => !prev)}
- >
- <span className='text-nowrap px-1 truncate flex items-center gap-1'>
- {state[0]?.startDate ? (
- `${state[0].startDate.toLocaleDateString()} - ${state[0].endDate.toLocaleDateString()}`
- ) : (
- <Calendar size={16} className="text-gray-500" />
- )}
- </span>
- </button>
- {isOpenCalender && (
+ <div ref={calendarRef} className='relative inline-block'>
+ <button
+ type='button'
+ className='p-1 w-full h-auto cursor-pointer border hover:bg-gray-100 rounded transition duration-150 ease-in-out flex items-center justify-center'
+ onClick={() => setIsOpenCalender((prev) => !prev)}
+ >
+ <span className='text-nowrap px-1 truncate flex items-center gap-1'>
+ {state[0]?.startDate ? (
+ `${state[0].startDate.toLocaleDateString()} - ${state[0].endDate.toLocaleDateString()}`
+ ) : (
+ <Calendar size={20} className='text-gray-500' />
+ )}
+ </span>
+ </button>
+ {isOpenCalender && (
<div className='absolute right-1 mt-2 bg-white p-4 rounded shadow-lg z-50'>
- {/* Tombol silang di sudut kanan atas */}
<button
onClick={() => setIsOpenCalender(false)}
className='absolute top-2 right-2 text-gray-600 hover:text-black text-xl font-bold'
@@ -508,37 +518,13 @@ const Transactions = ({ context = '' }) => {
className='w-full'
/>
<style>{`
- /* Atur container agar menjadi column */
- .rdrCalendarWrapper {
- display: flex;
- flex-direction: column;
- }
- .rdrDateRangePickerWrapper {
- display: flex;
- flex-direction: column;
- }
-
- /* Pindahkan rdrStaticRanges ke atas */
- .rdrDefinedRangesWrapper {
- order: -1;
- width: fit-content;
- }
- .rdrStaticRanges {
- flex-direction: row;
- margin-right: 2px;
- }
-
- /* Sembunyikan bagian input manual */
- .rdrInputRanges {
- display: none !important;
- }
-
- .rdrStaticRangeLabel {
- padding: 10px 10px;
- }
- .rdrMonth {
- width: -moz-available;
- }
+ .rdrCalendarWrapper{display:flex;flex-direction:column;}
+ .rdrDateRangePickerWrapper{display:flex;flex-direction:column;}
+ .rdrDefinedRangesWrapper{order:-1;width:fit-content;}
+ .rdrStaticRanges{flex-direction:row;margin-right:2px;}
+ .rdrInputRanges{display:none !important;}
+ .rdrStaticRangeLabel{padding:10px 10px;}
+ .rdrMonth{width:-moz-available;}
`}</style>
<div className='flex flex-row justify-end gap-3 mt-2'>
<button
@@ -549,68 +535,7 @@ const Transactions = ({ context = '' }) => {
</button>
</div>
</div>
- )}
- </div>
- {/* <div className='border border-gray-300 rounded-lg px-1 py-1 bg-white shadow-sm focus:outline-none focus:ring-2 focus:ring-red-500 text-xs'>
- <DatePicker
- closeOnScroll={(e) => e.target === document}
- selectsRange={true}
- startDate={startDate}
- endDate={endDate}
- dateFormat='dd/MM'
- className='w-full'
- maxDate={new Date()}
- placeholderText='Semua Tanggal'
- onChange={(update) => {
- setDateRange(update);
- }}
- withPortal
- isClearable={true}
- />
- </div> */}
- </div>
- <div className='flex flex-row justify-between items-center gap-2'>
- <form className='flex' onSubmit={handleSubmit}>
- <button
- className='btn-light border-r-0 rounded-r-none bg-transparent px-3'
- type='submit'
- >
- <MagnifyingGlassIcon className='w-6' />
- </button>
- <input
- type='text'
- className='form-input border-l-0 rounded-l-none text-xs'
- placeholder='Cari Transaksi...'
- value={inputQuery}
- onChange={(e) => setInputQuery(e.target.value)}
- />
- </form>
- <div className='flex flex-row gap-2 items-center justify-center text-nowrap'>
- <p className='text-xs'>
- Menampilkan {startItem}-
- {endItem
- ? endItem
- : transactions?.data?.saleOrderTotal
- ? transactions?.data?.saleOrderTotal
- : limitNew * pageNew}{' '}
- dari{' '}
- {transactions?.data?.saleOrderTotal
- ? transactions?.data?.saleOrderTotal
- : limitNew * pageNew}
- </p>
- <select
- id='limitSelect'
- value={limitNew}
- onChange={(e) => {
- setLimitNew(Number(e.target.value));
- setPageNew(1);
- }}
- className='border p-2 text-xs'
- >
- <option value={10}>10</option>
- <option value={15}>15</option>
- <option value={20}>20</option>
- </select>
+ )}
</div>
</div>
@@ -634,7 +559,9 @@ const Transactions = ({ context = '' }) => {
>
<div className='flex flex-row justify-between items-start'>
<Link href={`${router.pathname}/${saleOrder.id}`}>
- <h2 className='text-danger-500'>{saleOrder.name}</h2>
+ <h2 className='text-danger-500 text-base'>
+ {saleOrder.name}
+ </h2>
<span className='font-medium text-black opacity-75'>
{formatDate(saleOrder.dateOrder.split(' ')[0]) || '-'}
</span>
@@ -662,8 +589,8 @@ const Transactions = ({ context = '' }) => {
</div>
<div className='flex w-4/5 flex-col gap-2 justify-start'>
<p className='flex flex-row gap-2'>
- <span className=' text-black'>Nomor PO:</span>
- <span className=' text-red-500 font-semibold'>
+ <span className=' text-black text-sm'>Nomor PO:</span>
+ <span className=' text-red-500 font-semibold text-sm'>
{saleOrder.purchaseOrderName || '-'}
</span>
</p>
@@ -681,29 +608,30 @@ const Transactions = ({ context = '' }) => {
<div className='flex flex-row gap-1 justify-start items-center'>
{saleOrder.products
.slice(1, 4)
- .map((product, index) => (
+ .map((product, idx) => (
<Image
- key={index} // Tambahkan key untuk setiap elemen dalam map()
+ key={idx}
src={product?.parent?.image}
alt={product?.name}
className='object-contain object-center border border-gray_r-6 h-8 w-8 rounded-md'
/>
))}
{saleOrder.products.length > 4 ? (
- <Link
- href={`${router.pathname}/${saleOrder?.id}`}
- className='text-red-500 text-nowrap'
- >
- +{saleOrder.products.length - 4} lihat semua produk
- </Link>
- ) : (
- <Link
- href={`${router.pathname}/${saleOrder?.id}`}
- className='text-red-500 text-nowrap'
- >
- Lihat semua produk
- </Link>
- )}
+ <Link
+ href={`${router.pathname}/${saleOrder?.id}`}
+ className='text-red-500 text-nowrap'
+ >
+ +{saleOrder.products.length - 4} lihat semua
+ produk
+ </Link>
+ ) : (
+ <Link
+ href={`${router.pathname}/${saleOrder?.id}`}
+ className='text-red-500 text-nowrap'
+ >
+ Lihat semua produk
+ </Link>
+ )}
</div>
)}
</div>
@@ -716,58 +644,50 @@ const Transactions = ({ context = '' }) => {
</div>
</div>
<div className='col-span-2 h-[1px] w-full bg-gray-300'></div>
- <div className='flex flex-row gap-3 justify-between items-center text-sm'>
- <div className='flex flex-col text-black text-xs'>
+
+ <div className='flex flex-col gap-3 text-sm'>
+ <div className='flex flex-col text-black'>
<p className='font-extralight'>Total Harga</p>
- <p className='font-semibold'>
+ <p className='font-semibold text-lg'>
{currencyFormat(saleOrder.amountTotal)}
</p>
</div>
- <div>
- <button
- type='button'
- onClick={() => handleBuyBack(saleOrder.products)}
- className='flex-1 py-2 btn-solid-red text-nowrap'
- >
- Beli Lagi
- </button>
- </div>
- </div>
- {/* <div className='grid grid-cols-2 mt-3'>
- <div>
- <span className='text-caption-2 text-gray_r-11'>
- No. Purchase Order
- </span>
- <p className='mt-1 font-medium text-gray_r-12'>
- {saleOrder.purchaseOrderName || '-'}
- </p>
- </div>
- <div className='text-right'>
- <span className='text-caption-2 text-gray_r-11'>
- Total Invoice
- </span>
- <p className='mt-1 font-medium text-gray_r-12'>
- {saleOrder.invoiceCount} Invoice
- </p>
- </div>
- </div> */}
- {/* <div className='grid grid-cols-2 mt-3'>
- <div>
- <span className='text-caption-2 text-gray_r-11'>Sales</span>
- <p className='mt-1 font-medium text-gray_r-12'>
- {saleOrder.sales}
- </p>
- </div>
- <div className='text-right'>
- <span className='text-caption-2 text-gray_r-11'>
- Total Harga
- </span>
- <p className='mt-1 font-medium text-gray_r-12'>
- {currencyFormat(saleOrder.amountTotal)}
- </p>
+ <div className='flex flex-col gap-2 w-full'>
+ {/* Beli Lagi hanya muncul jika status bukan unpaid */}
+ {!isUnpaid(saleOrder.status) && (
+ <button
+ type='button'
+ onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ handleBuyBack(saleOrder.products);
+ }}
+ className='w-full py-2 btn-solid-red text-center rounded-md'
+ >
+ Beli Lagi
+ </button>
+ )}
+
+ {/* Bayar Sekarang hanya kalau eligible */}
+ {saleOrder?.eligibleContinue && (
+ <button
+ type='button'
+ disabled={contLoadingId === saleOrder.id}
+ onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ handleContinuePayment(saleOrder);
+ }}
+ className='w-full py-2 text-center rounded-md border border-red-300 text-red-500 bg-white disabled:opacity-60'
+ >
+ {contLoadingId === saleOrder.id
+ ? 'Memproses…'
+ : 'Bayar Sekarang'}
+ </button>
+ )}
</div>
- </div> */}
+ </div>
</Link>
</div>
))}
@@ -775,7 +695,6 @@ const Transactions = ({ context = '' }) => {
<Pagination
pageCount={pageCount}
currentPage={parseInt(pageNew)}
- // url={router.pathname + pageQuery}
url={`${router.pathname}?${toQuery(_.omit(query, ['page']))}`}
className='mt-2 mb-2'
/>
@@ -785,6 +704,30 @@ const Transactions = ({ context = '' }) => {
active={toOthers}
close={() => setToOthers(null)}
>
+ {transactions.data?.status === 'draft' && (
+ <>
+ <button
+ className='text-left disabled:opacity-60'
+ disabled={toOthers?.status != 'draft'}
+ onClick={() => {
+ downloadQuotation(toOthers);
+ setToOthers(null);
+ }}
+ >
+ Download Quotation
+ </button>
+ <button
+ className='text-left disabled:opacity-60'
+ disabled={toOthers?.status != 'waiting'}
+ onClick={() => {
+ setToCancel(toOthers);
+ setToOthers(null);
+ }}
+ >
+ Batalkan Transaksi
+ </button>
+ </>
+ )}
<div className='flex flex-col gap-y-4 mt-2'>
<button
className='text-left disabled:opacity-60'
@@ -796,26 +739,6 @@ const Transactions = ({ context = '' }) => {
>
Download PO
</button>
- <button
- className='text-left disabled:opacity-60'
- disabled={toOthers?.status != 'draft'}
- onClick={() => {
- downloadQuotation(toOthers);
- setToOthers(null);
- }}
- >
- Download Quotation
- </button>
- <button
- className='text-left disabled:opacity-60'
- disabled={toOthers?.status != 'waiting'}
- onClick={() => {
- setToCancel(toOthers);
- setToOthers(null);
- }}
- >
- Batalkan Transaksi
- </button>
</div>
</BottomPopup>
@@ -848,6 +771,7 @@ const Transactions = ({ context = '' }) => {
</div>
</MobileView>
+ {/* ===== DESKTOP ===== */}
<DesktopView>
<div className='container mx-auto flex py-10'>
<div className='w-3/12 pr-4'>
@@ -900,51 +824,36 @@ const Transactions = ({ context = '' }) => {
)}
</div>
</div>
- <div className=''>
- <div
- class='flex items-center p-4 mb-4 text-sm border border-yellow-500 text-yellow-800 rounded-lg bg-yellow-50'
- role='alert'
- >
- <svg
- class='flex-shrink-0 inline w-5 h-5 mr-2'
- aria-hidden='true'
- fill='currentColor'
- viewBox='0 0 20 20'
- >
- <path d='M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z' />
- </svg>
- <span class='sr-only'>Info</span>
- <div className='text-justify flex flex-col gap-1'>
- <p className='font-bold text-black'>Info Transaksi</p>
- <span className='text-black'>
- Gunakan filter status untuk mempermudah pencarian transaksi anda di Daftar Transaksi
- </span>
- </div>
- </div>
- </div>
+
<div className='flex flex-col gap-y-2 border rounded-lg mb-2 w-full'>
<div className='p-2'>
<div className='flex items-center space-x-3'>
<span className='text-base font-semibold text-gray-600'>
Status
</span>
- <div className="relative w-full overflow-hidden">
- {/* Container flex: tombol prev - swiper - tombol next */}
- <div className="flex items-center space-x-2">
-
- {/* Prev */}
- <button className="custom-prev w-8 h-8 flex-shrink-0 flex items-center justify-center bg-white border rounded-full shadow z-1">
- <svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
+ <div className='relative w-full overflow-hidden'>
+ <div className='flex items-center space-x-2'>
+ <button className='custom-prev w-8 h-8 flex-shrink-0 flex items-center justify-center bg-white border rounded-full shadow z-1'>
+ <svg
+ className='w-4 h-4 text-gray-500'
+ fill='none'
+ stroke='currentColor'
+ viewBox='0 0 24 24'
+ >
+ <path
+ strokeLinecap='round'
+ strokeLinejoin='round'
+ strokeWidth={2}
+ d='M15 19l-7-7 7-7'
+ />
</svg>
</button>
- {/* Swiper container scrollable */}
- <div className="w-full overflow-hidden">
+ <div className='w-full overflow-hidden'>
<Swiper
spaceBetween={10}
- slidesPerView="auto"
- className="status-swiper"
+ slidesPerView='auto'
+ className='status-swiper'
modules={[Navigation]}
navigation={{
nextEl: '.custom-next',
@@ -952,12 +861,13 @@ const Transactions = ({ context = '' }) => {
}}
>
{statuses.map((status) => (
- <SwiperSlide key={status.id} className="!w-auto">
+ <SwiperSlide key={status.id} className='!w-auto'>
<button
className={`px-4 py-1 text-sm font-medium border rounded-lg transition whitespace-nowrap
- ${statusNew === status.id
- ? 'border-red-500 text-red-500 bg-white'
- : 'border-gray-300 text-gray-400 bg-gray-100 hover:bg-gray-200'
+ ${
+ statusNew === status.id
+ ? 'border-red-500 text-red-500 bg-white'
+ : 'border-gray-300 text-gray-400 bg-gray-100 hover:bg-gray-200'
}`}
onClick={() => handleStatusChange(status.id)}
>
@@ -968,16 +878,26 @@ const Transactions = ({ context = '' }) => {
</Swiper>
</div>
- {/* Next */}
- <button className="custom-next w-8 h-8 flex-shrink-0 flex items-center justify-center bg-white border rounded-full shadow z-10">
- <svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
+ <button className='custom-next w-8 h-8 flex-shrink-0 flex items-center justify-center bg-white border rounded-full shadow z-10'>
+ <svg
+ className='w-4 h-4 text-gray-500'
+ fill='none'
+ stroke='currentColor'
+ viewBox='0 0 24 24'
+ >
+ <path
+ strokeLinecap='round'
+ strokeLinejoin='round'
+ strokeWidth={2}
+ d='M9 5l7 7-7 7'
+ />
</svg>
</button>
</div>
</div>
</div>
</div>
+
<div className='flex flex-row items-center justify-between mb-2 p-2'>
<div className='flex flex-col gap-2 pb-2'>
{listSites?.length > 0 ? (
@@ -1013,6 +933,7 @@ const Transactions = ({ context = '' }) => {
</button>
</form>
</div>
+
<div className='flex flex-row gap-4 items-center justify-center'>
<p>
Menampilkan {startItem}-
@@ -1038,87 +959,66 @@ const Transactions = ({ context = '' }) => {
<option value={10}>10</option>
<option value={15}>15</option>
<option value={20}>20</option>
- <option value={transactions?.data?.saleOrderTotal}>Semua</option>
+ <option value={transactions?.data?.saleOrderTotal}>
+ Semua
+ </option>
</select>
- <div ref={calendarRef} className="relative inline-block">
+
+ <div ref={calendarRef} className='relative inline-block'>
<button
type='button'
className='p-2 w-auto h-auto cursor-pointer border hover:bg-gray-100 rounded transition duration-150 ease-in-out flex items-center justify-center'
onClick={() => setIsOpenCalender((prev) => !prev)}
>
<span className='text-nowrap px-1 truncate flex items-center gap-1'>
- {state[0]?.startDate ? (
- `${state[0].startDate.toLocaleDateString()} - ${state[0].endDate.toLocaleDateString()}`
- ) : (
- <Calendar size={16} className="text-gray-500" />
- )}
+ {state[0]?.startDate ? (
+ `${state[0].startDate.toLocaleDateString()} - ${state[0].endDate.toLocaleDateString()}`
+ ) : (
+ <Calendar size={16} className='text-gray-500' />
+ )}
</span>
</button>
{isOpenCalender && (
- <div className='absolute right-10 mt-2 bg-white p-4 rounded shadow-lg z-50'>
- {/* Tombol silang di sudut kanan atas */}
- <button
- onClick={() => setIsOpenCalender(false)}
- className='absolute top-2 right-2 text-gray-600 hover:text-black text-xl font-bold'
- >
- &times;
- </button>
- <DateRangePicker
- onChange={(item) => setState([item.selection])}
- showSelectionPreview={false}
- maxDate={new Date()}
- moveRangeOnFirstSelection={false}
- months={1}
- ranges={state}
- className='w-full'
- />
- <style>{`
- /* Atur container agar menjadi column */
- .rdrCalendarWrapper {
- display: flex;
- flex-direction: column;
- }
- .rdrDateRangePickerWrapper {
- display: flex;
- flex-direction: column;
- }
-
- /* Pindahkan rdrStaticRanges ke atas */
- .rdrDefinedRangesWrapper {
- order: -1;
- width: fit-content;
- }
- .rdrStaticRanges {
- flex-direction: row;
- margin-right: 2px;
- }
-
- /* Sembunyikan bagian input manual */
- .rdrInputRanges {
- display: none !important;
- }
-
- .rdrStaticRangeLabel {
- padding: 10px 10px;
- }
- .rdrMonth {
- width: -moz-available;
- }
- `}</style>
- <div className='flex flex-row justify-end gap-3 mt-2'>
+ <div className='absolute right-10 mt-2 bg-white p-4 rounded shadow-lg z-50'>
<button
- className='px-4 py-1 bg-red-500 text-white rounded'
- onClick={handleReset}
+ onClick={() => setIsOpenCalender(false)}
+ className='absolute top-2 right-2 text-gray-600 hover:text-black text-xl font-bold'
>
- Reset
+ &times;
</button>
+ <DateRangePicker
+ onChange={(item) => setState([item.selection])}
+ showSelectionPreview={false}
+ maxDate={new Date()}
+ moveRangeOnFirstSelection={false}
+ months={1}
+ ranges={state}
+ className='w-full'
+ />
+ <style>{`
+ .rdrCalendarWrapper{display:flex;flex-direction:column;}
+ .rdrDateRangePickerWrapper{display:flex;flex-direction:column;}
+ .rdrDefinedRangesWrapper{order:-1;width:fit-content;}
+ .rdrStaticRanges{flex-direction:row;margin-right:2px;}
+ .rdrInputRanges{display:none !important;}
+ .rdrStaticRangeLabel{padding:10px 10px;}
+ .rdrMonth{width:-moz-available;}
+ `}</style>
+ <div className='flex flex-row justify-end gap-3 mt-2'>
+ <button
+ className='px-4 py-1 bg-red-500 text-white rounded'
+ onClick={handleReset}
+ >
+ Reset
+ </button>
+ </div>
</div>
- </div>
)}
</div>
</div>
</div>
</div>
+
<div className='flex justify-center items-center'>
{!transactions.isLoading &&
transactions?.data?.saleOrders?.length == 0 && (
@@ -1156,11 +1056,9 @@ const Transactions = ({ context = '' }) => {
<p className='text-red-500'>{saleOrder.name}</p>
<p className='text-black'>
Salesperson:{' '}
- {
- <span className='font-semibold'>
- {saleOrder.sales}
- </span>
- }
+ <span className='font-semibold'>
+ {saleOrder.sales}
+ </span>
</p>
</div>
<div className='text-black'>
@@ -1172,7 +1070,9 @@ const Transactions = ({ context = '' }) => {
</span>
</div>
</div>
+
<hr className='mt-3 mb-3 border border-gray-100' />
+
<div className='flex flex-row gap-2 justify-between items-center '>
<div className='flex justify-start w-3/4 flex-col gap-2'>
<div className='flex gap-2'>
@@ -1207,9 +1107,9 @@ const Transactions = ({ context = '' }) => {
<div className='flex flex-row gap-1 justify-start items-center'>
{saleOrder.products
.slice(1, 4)
- .map((product, index) => (
+ .map((product, idx) => (
<Image
- key={index} // Tambahkan key untuk setiap elemen dalam map()
+ key={idx}
src={product?.parent?.image}
alt={product?.name}
className='object-contain object-center border border-gray_r-6 h-16 w-16 rounded-md'
@@ -1220,7 +1120,8 @@ const Transactions = ({ context = '' }) => {
href={`${router.pathname}/${saleOrder?.id}`}
className='text-red-500 text-nowrap'
>
- +{saleOrder.products.length - 4} lihat semua produk
+ +{saleOrder.products.length - 4}{' '}
+ lihat semua produk
</Link>
) : (
<Link
@@ -1244,25 +1145,48 @@ const Transactions = ({ context = '' }) => {
</p>
</div>
</div>
+
<div className='w-[1px] h-24 bg-gray-300'></div>
- <div className='w-1/4 flex flex-row gap-3 justify-center items-center'>
+
+ <div className='w-1/4 flex flex-col gap-2 items-center justify-center text-center pl-5'>
<div className='flex flex-col text-black'>
- <p>Total Harga</p>
- <p className='font-bold'>
+ <p className='text-sm'>Total Harga</p>
+ <p className='font-bold text-lg'>
{currencyFormat(saleOrder.amountTotal)}
</p>
</div>
- <div>
+
+ {!isUnpaid(saleOrder.status) && (
<button
type='button'
- onClick={() =>
- handleBuyBack(saleOrder.products)
- }
- className='flex-1 py-2 btn-solid-red text-nowrap'
+ onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ handleBuyBack(saleOrder.products);
+ }}
+ className='w-full py-2 btn-solid-red text-nowrap rounded-md'
>
Beli Lagi
</button>
- </div>
+ )}
+
+ {/* Bayar Sekarang: hanya kalau eligible */}
+ {saleOrder?.eligibleContinue && (
+ <button
+ type='button'
+ disabled={contLoadingId === saleOrder.id}
+ onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ handleContinuePayment(saleOrder);
+ }}
+ className='w-full py-2 text-nowrap border border-red-500 text-red-500 rounded-md disabled:opacity-60'
+ >
+ {contLoadingId === saleOrder.id
+ ? 'Memproses…'
+ : 'Bayar Sekarang'}
+ </button>
+ )}
</div>
</div>
</Link>
@@ -1271,70 +1195,10 @@ const Transactions = ({ context = '' }) => {
</div>
)}
</div>
- {/* <table className='table-data'>
- <thead>
- <tr>
- <th>No. Transaksi</th>
- <th>No. PO</th>
- <th>Tanggal</th>
- <th>Created By</th>
- {auth?.feature?.soApproval && <th>Site</th>}
- <th className='!text-left'>Salesperson</th>
- <th className='!text-left'>Total</th>
- <th>Status</th>
- </tr>
- </thead>
- <tbody>
- {transactions.isLoading && (
- <tr>
- <td colSpan={7}>
- <div className='flex justify-center my-2'>
- <Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' />
- </div>
- </td>
- </tr>
- )}
- {!transactions.isLoading &&
- (!transactions?.data?.saleOrders ||
- transactions?.data?.saleOrders?.length == 0) && (
- <tr>
- <td colSpan={7}>Tidak ada transaksi</td>
- </tr>
- )}
- {transactions.data?.saleOrders?.map((saleOrder) => (
- <tr key={saleOrder.id}>
- <td>
- <Link
- className='whitespace-nowrap'
- href={`${router.pathname}/${saleOrder.id}`}
- >
- {saleOrder.name}
- </Link>
- </td>
- <td>{saleOrder.purchaseOrderName || '-'}</td>
- <td>{saleOrder.dateOrder || '-'}</td>
- <td>{saleOrder.address.customer?.name || '-'}</td>
- {auth?.feature?.soApproval && (
- <td>{saleOrder.sitePartner || '-'}</td>
- )}
- <td className='!text-left'>{saleOrder.sales}</td>
- <td className='!text-left'>
- {currencyFormat(saleOrder.amountTotal)}
- </td>
- <td>
- <div className='flex justify-center'>
- <TransactionStatusBadge status={saleOrder.status} />
- </div>
- </td>
- </tr>
- ))}
- </tbody>
- </table> */}
<Pagination
pageCount={pageCount}
currentPage={parseInt(pageNew)}
- // url={router.pathname + (pageQuery ? `?${pageQuery}` : '')}
url={`${router.pathname}?${toQuery(_.omit(query, ['page']))}`}
className='mt-2 mb-2'
/>
diff --git a/src/lib/treckingAwb/component/Manifest.jsx b/src/lib/treckingAwb/component/Manifest.jsx
index 6eb0b0ac..d2896567 100644
--- a/src/lib/treckingAwb/component/Manifest.jsx
+++ b/src/lib/treckingAwb/component/Manifest.jsx
@@ -9,17 +9,24 @@ import { list } from 'postcss';
import InformationSection from './InformationSection';
import Link from 'next/link';
-function capitalizeFirstLetter(str) {
- return str.charAt(0).toUpperCase() + str.slice(1);
-}
+// function capitalizeFirstLetter(str) {
+// return str.charAt(0).toUpperCase() + str.slice(1);
+// }
function capitalizeWords(str) {
+ if (!str || typeof str !== 'string') {
+ return '';
+}
return str
.split(' ')
.map((word) => capitalizeFirstLetter(word))
.join(' ');
}
+function capitalizeFirstLetter(word) {
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
+}
+
function mappingLiveTracking(kurir, resi){
let url = null
switch (kurir){