diff options
| author | it-fixcomart <it@fixcomart.co.id> | 2025-09-08 15:04:49 +0700 |
|---|---|---|
| committer | it-fixcomart <it@fixcomart.co.id> | 2025-09-08 15:04:49 +0700 |
| commit | 776d26572e69fa9d0b57b586262e4bd86b21dd92 (patch) | |
| tree | 60acf47a8422ca4d2affb686fe9cddb9b5fca8da | |
| parent | 58e75871483917ac842c7d95dfbf0bdd65ecaafd (diff) | |
<hafid> validation Address
| -rw-r--r-- | src/lib/address/components/CreateAddress.jsx | 82 | ||||
| -rw-r--r-- | src/lib/address/components/EditAddress.jsx | 223 | ||||
| -rw-r--r-- | src/lib/checkout/components/Checkout.jsx | 45 | ||||
| -rw-r--r-- | src/lib/maps/components/PinPointMap.jsx | 21 | ||||
| -rw-r--r-- | src/lib/maps/stores/useMaps.js | 16 |
5 files changed, 296 insertions, 91 deletions
diff --git a/src/lib/address/components/CreateAddress.jsx b/src/lib/address/components/CreateAddress.jsx index 963a19aa..fcfad056 100644 --- a/src/lib/address/components/CreateAddress.jsx +++ b/src/lib/address/components/CreateAddress.jsx @@ -48,19 +48,39 @@ const CreateAddress = () => { pinedMaps, setPinedMaps } = useMaps(); + + const resetPin = useMaps((state) => state.resetPin); + const [showValidationPopup, setShowValidationPopup] = useState(false); + const [popupMessage, setPopupMessage] = useState(""); + const [selectedCityName, setSelectedCityName] = useState(""); + + useEffect(() => { + resetPin(); + }, [resetPin]); + + useEffect(() => { - if (detailAddress) { - setValue('zip', detailAddress.postalCode); + if (defaultValues?.zip) { + setValue("zip", defaultValues.zip); + } + + // set state/province + if (defaultValues?.province) { const selectedState = states.find( (state) => - detailAddress?.province.includes(state.label) || - state.label.includes(detailAddress?.province) + defaultValues.province.includes(state.label) || + state.label.includes(defaultValues.province) ); - setValue('state', selectedState?.value); - setValue('street', detailAddress?.street); - + if (selectedState) { + setValue("state", selectedState.value); + } } - }, [detailAddress, setValue]); + + // set street + if (defaultValues?.street) { + setValue("street", defaultValues.street); + } + }, [states, setValue, defaultValues]); useEffect(() => { const loadState = async () => { @@ -91,19 +111,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) { @@ -188,7 +195,22 @@ const CreateAddress = () => { } }, [subDistricts, detailAddress, setValue]); + useEffect(() => { + const currentCity = cities.find((c) => c.value === watch("city"))?.label || ""; + setSelectedCityName(currentCity); + }, [watch("city"), cities]); + const onSubmitHandler = async (values) => { + if (detailAddress?.district) { + if ( + selectedCityName && + selectedCityName.toLowerCase() !== detailAddress?.district?.toLowerCase() + ) { + setPopupMessage("Titik Koordinat tidak sesuai dengan Kota yang dipilih"); + setShowValidationPopup(true); + return; + } + } const data = { ...values, state_id: values.state, @@ -219,6 +241,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 6599a764..d9908726 100644 --- a/src/lib/address/components/EditAddress.jsx +++ b/src/lib/address/components/EditAddress.jsx @@ -44,6 +44,13 @@ const EditAddress = ({ id, defaultValues }) => { const [districts, setDistricts] = useState([]); const [subDistricts, setSubDistricts] = useState([]); const [tempAddress, setTempAddress] = useState(getValues('addressMap')); + 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 { addressMaps, @@ -56,48 +63,65 @@ const EditAddress = ({ id, defaultValues }) => { // Helper: cek apakah benar2 sudah PIN (bukan default center & ada addressMaps) const isPinned = useMemo(() => { - if ( - !selectedPosition || - typeof selectedPosition.lat !== 'number' || - typeof selectedPosition.lng !== 'number' - ) - return false; + 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' + typeof getDefaultCenter === "function" ? getDefaultCenter() : { lat: -6.2, lng: 106.816666 }; + const nearDefault = - Math.abs(selectedPosition.lat - dc.lat) < 1e-4 && - Math.abs(selectedPosition.lng - dc.lng) < 1e-4; + 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 && isPinned) { - setTempAddress(addressMaps); - setValue('addressMap', addressMaps); - } - if (isPinned && selectedPosition) { - 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, isPinned, setValue]); + }, [setSelectedPosition, setAddressMaps, getValues, setValue]); - // Isi ZIP/Prov dari detailAddress (JANGAN isi street) useEffect(() => { - if (Object.keys(detailAddress || {}).length > 0 && isPinned) { - setValue('zip', detailAddress.postalCode); - const selectedState = states.find( - (state) => - detailAddress?.province?.includes(state.label) || - state.label?.includes(detailAddress?.province) - ); - setValue('state', selectedState?.value); - // jangan override street: - // setValue('street', detailAddress?.street); + 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) }); + } } - }, [detailAddress, states, isPinned, setValue]); + }, [getValues, resetPin, setAddressMaps, setSelectedPosition]); + + useEffect(() => { const loadProfile = async () => { @@ -109,13 +133,29 @@ 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, setValue]); + // Isi ZIP/Prov dari detailAddress (JANGAN isi street) + useEffect(() => { + const zip = getValues("zip"); + const province = getValues("state"); + if (!zip && defaultValues?.zip) { + setValue("zip", defaultValues.zip); + } + + if (!getValues("state") && province) { + const selectedState = states.find( + (state) => + province.includes(state.label) || state.label.includes(province) + ); + if (selectedState) { + setValue("state", selectedState.value); + } + } + }, [states, setValue, getValues, defaultValues]); + useEffect(() => { const loadStates = async () => { let dataStates = await stateApi({ tempo: false }); @@ -150,7 +190,11 @@ const EditAddress = ({ id, defaultValues }) => { }, [watchState, setValue, getValues]); useEffect(() => { - if (Object.keys(detailAddress || {}).length > 0 && isPinned) { + if (!isPinned) return; + + if (getValues("city")) return; + + if (Object.keys(detailAddress || {}).length > 0) { const selectedCities = cities.find( (city) => @@ -165,9 +209,12 @@ const EditAddress = ({ id, defaultValues }) => { .toLowerCase() .includes(detailAddress?.district?.toLowerCase()) ); - setValue('city', selectedCities?.value); + + if (selectedCities) { + setValue("city", selectedCities.value); + } } - }, [cities, detailAddress, isPinned, setValue]); + }, [cities, detailAddress, isPinned, getValues, setValue]); const watchCity = watch('city'); useEffect(() => { @@ -189,7 +236,12 @@ const EditAddress = ({ id, defaultValues }) => { }, [watchCity, setValue, getValues]); useEffect(() => { - if (Object.keys(detailAddress || {}).length > 0 && isPinned) { + if (!isPinned) return; // skip kalau belum pin + + // jangan override kalau form sudah punya nilai district + if (getValues("district")) return; + + if (Object.keys(detailAddress || {}).length > 0) { const selectedDistrict = districts.find( (district) => detailAddress?.subDistrict @@ -199,9 +251,13 @@ const EditAddress = ({ id, defaultValues }) => { .toLowerCase() .includes(detailAddress?.subDistrict?.toLowerCase()) ); - setValue('district', selectedDistrict?.value); + + if (selectedDistrict) { + setValue("district", selectedDistrict.value); + } } - }, [districts, detailAddress, isPinned, setValue]); + }, [districts, detailAddress, isPinned, getValues, setValue]); + const watchDistrict = watch('district'); useEffect(() => { @@ -227,7 +283,12 @@ const EditAddress = ({ id, defaultValues }) => { }, [watchDistrict, setValue, getValues]); useEffect(() => { - if (Object.keys(detailAddress || {}).length > 0 && isPinned) { + if (!isPinned) return; // skip kalau belum pin + + // jangan override kalau form sudah punya nilai subDistrict + if (getValues("subDistrict")) return; + + if (Object.keys(detailAddress || {}).length > 0) { const selectedSubDistrict = subDistricts.find( (district) => detailAddress?.village @@ -237,9 +298,12 @@ const EditAddress = ({ id, defaultValues }) => { .toLowerCase() .includes(detailAddress?.village?.toLowerCase()) ); - setValue('subDistrict', selectedSubDistrict?.value); + + if (selectedSubDistrict) { + setValue("subDistrict", selectedSubDistrict.value); + } } - }, [subDistricts, detailAddress, isPinned, setValue]); + }, [subDistricts, detailAddress, isPinned, getValues, setValue]); useEffect(() => { if (id) { @@ -247,7 +311,52 @@ const EditAddress = ({ id, defaultValues }) => { } }, [id, setValue]); + useEffect(() => { + const currentCity = cities.find((c) => c.value === watch("city"))?.label || ""; + + let normalized = currentCity.toLowerCase().trim(); + + const parts = normalized.split(" "); + + if (parts.length >= 3) { + // hapus prefix kabupaten/kota kalau ada + normalized = normalized + .replace(/^kabupaten\s+/i, "") + .replace(/^kota\s+/i, "") + .trim(); + } + + setSelectedCityName(normalized); + }, [watch("city"), cities]); + // console.log(defaultValues); + + // console.log(selectedCityName, '=', detailAddress?.district); 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){ + if (selectedCityName && selectedCityName !== detailAddress?.district?.toLowerCase()) { + 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, @@ -257,7 +366,7 @@ const EditAddress = ({ id, defaultValues }) => { sub_district_id: parseInt(values.subDistrict, 10), }; - // kirim koordinat + address_map + use_pin HANYA jika sudah PIN + if (isPinned) { data.longtitude = selectedPosition?.lng; data.latitude = selectedPosition?.lat; @@ -324,7 +433,7 @@ const EditAddress = ({ id, defaultValues }) => { const dataProfile = await addressApi({ id: auth.partnerId }); console.log('ini adalah', dataProfile); }; - + // console.log('ini adalah', detailAddress); return ( <> <BottomPopup @@ -335,12 +444,30 @@ const EditAddress = ({ id, defaultValues }) => { > <div className='flex mt-4'> <PinPointMap - initialLatitude={selectedPosition?.lat} - initialLongitude={selectedPosition?.lng} - initialAddress={tempAddress} + 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'> <div className='hidden md:block w-3/12 pr-4'> <Menu /> @@ -355,7 +482,7 @@ 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' @@ -366,7 +493,7 @@ const EditAddress = ({ id, defaultValues }) => { onClick={() => setPinedMaps(true)} /> </button> - <span> {tempAddress} </span> + <span> {addressMaps} </span> </div> ) : ( <Button diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index d8ede118..b36c09bf 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -1321,24 +1321,33 @@ const Checkout = () => { <PickupAddress label='Alamat Pickup' /> )} {selectedCarrierId != SELF_PICKUP_ID && ( - <Skeleton - isLoaded={ - !!selectedAddress.invoicing && !!selectedAddress.shipping - } - minHeight={290} - > - <SectionAddress - address={selectedAddress.shipping} - label='Alamat Pengiriman' - url='/my/address?select=shipping' - /> - <Divider /> - <SectionAddress - address={selectedAddress.invoicing} - label='Alamat Penagihan' - url='/my/address?select=invoice' - /> - </Skeleton> + <> + {(!selectedAddress?.shipping || !selectedAddress?.invoicing) ? ( + <div className="p-4 border rounded-xl text-center text-red-600 bg-red-50"> + <p>⚠️ Anda belum memiliki alamat yang terdaftar.</p> + <a + href="/my/address?select=shipping" + className="mt-2 inline-block px-4 py-2 bg-solid-red text-white rounded-lg" + > + Tambahkan Alamat + </a> + </div> + ) : ( + <Skeleton isLoaded minHeight={290}> + <SectionAddress + address={selectedAddress.shipping} + label="Alamat Pengiriman" + url="/my/address?select=shipping" + /> + <Divider /> + <SectionAddress + address={selectedAddress.invoicing} + label="Alamat Penagihan" + url="/my/address?select=invoice" + /> + </Skeleton> + )} + </> )} {products && <SectionExpedition products={products} />} <Divider /> diff --git a/src/lib/maps/components/PinPointMap.jsx b/src/lib/maps/components/PinPointMap.jsx index 75ab1d59..753f65c7 100644 --- a/src/lib/maps/components/PinPointMap.jsx +++ b/src/lib/maps/components/PinPointMap.jsx @@ -33,6 +33,8 @@ const PinpointLocation = ({ getDefaultCenter, // ✅ ambil default center dari store } = useMaps(); + + const [tempAddress, setTempAddress] = useState(initialAddress || ''); const [tempPosition, setTempPosition] = useState( initialLatitude && initialLongitude @@ -42,6 +44,23 @@ const PinpointLocation = ({ : getDefaultCenter() // ✅ fallback aman untuk view ); + 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); @@ -68,7 +87,7 @@ const PinpointLocation = ({ 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(); diff --git a/src/lib/maps/stores/useMaps.js b/src/lib/maps/stores/useMaps.js index f7636c24..b02c2ae3 100644 --- a/src/lib/maps/stores/useMaps.js +++ b/src/lib/maps/stores/useMaps.js @@ -29,10 +29,20 @@ export const useMaps = create((set, get) => ({ isPinned: () => { const p = get().selectedPosition; - if (!p || typeof p.lat !== 'number' || typeof p.lng !== 'number') return false; + 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(p.lat - DEFAULT_CENTER.lat) < 1e-6 && - Math.abs(p.lng - DEFAULT_CENTER.lng) < 1e-6; + Math.abs(lat - DEFAULT_CENTER.lat) < 1e-6 && + Math.abs(lng - DEFAULT_CENTER.lng) < 1e-6; + return !isDefault; }, |
