diff options
| author | Indoteknik . <andrifebriyadiputra@gmail.com> | 2025-08-16 14:46:48 +0700 |
|---|---|---|
| committer | Indoteknik . <andrifebriyadiputra@gmail.com> | 2025-08-16 14:46:48 +0700 |
| commit | c6b75363821e5c1153d8a9e2c1a4326568ab6026 (patch) | |
| tree | d5940768d254d3aa6862c32012002d5274467227 | |
| parent | ba157d5e0cd30ae2ed13edba051038c2c7bb1a1f (diff) | |
| parent | e3bf34095ac7571d04ebddba6f04815d7a71ed13 (diff) | |
fix merge
| -rw-r--r-- | src/lib/address/components/EditAddress.jsx | 222 | ||||
| -rw-r--r-- | src/lib/auth/api/checkParentStatusApi.js | 13 | ||||
| -rw-r--r-- | src/lib/auth/components/SwitchAccount.jsx | 10 | ||||
| -rw-r--r-- | src/lib/checkout/components/SectionExpedition.jsx | 4 | ||||
| -rw-r--r-- | src/lib/checkout/components/SectionQuotationExpedition.jsx | 369 | ||||
| -rw-r--r-- | src/lib/checkout/stores/stateQuotation.js | 30 | ||||
| -rw-r--r-- | src/lib/maps/components/PinPointMap.jsx | 71 | ||||
| -rw-r--r-- | src/lib/maps/stores/useMaps.js | 39 | ||||
| -rw-r--r-- | src/lib/pengajuan-tempo/component/PengajuanTempo.jsx | 251 | ||||
| -rw-r--r-- | src/lib/quotation/components/Quotation.jsx | 390 | ||||
| -rw-r--r-- | src/lib/transaction/components/Transaction.jsx | 452 | ||||
| -rw-r--r-- | src/lib/transaction/components/Transactions.jsx | 43 | ||||
| -rw-r--r-- | src/lib/treckingAwb/component/InformationSection.jsx | 4 | ||||
| -rw-r--r-- | src/lib/treckingAwb/component/Manifest.jsx | 25 | ||||
| -rw-r--r-- | src/lib/variant/components/VariantGroupCard.jsx | 7 | ||||
| -rw-r--r-- | src/pages/api/shop/search.js | 3 | ||||
| -rw-r--r-- | src/pages/my/profile.jsx | 11 |
17 files changed, 1200 insertions, 744 deletions
diff --git a/src/lib/address/components/EditAddress.jsx b/src/lib/address/components/EditAddress.jsx index deaa8a3e..6599a764 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,35 +44,61 @@ const EditAddress = ({ id, defaultValues }) => { const [districts, setDistricts] = useState([]); const [subDistricts, setSubDistricts] = useState([]); const [tempAddress, setTempAddress] = useState(getValues('addressMap')); - const { addressMaps, + + const { + addressMaps, selectedPosition, detailAddress, pinedMaps, - setPinedMaps } = useMaps(); - - + setPinedMaps, + getDefaultCenter, // penting untuk deteksi default center + } = useMaps(); + + // 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; + const dc = + 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; + return Boolean(addressMaps) && !nearDefault; + }, [selectedPosition, addressMaps, getDefaultCenter]); + + // Hanya isi addressMap & lat/lng di form kalau SUDAH PIN useEffect(() => { - if (addressMaps) { + if (addressMaps && isPinned) { setTempAddress(addressMaps); setValue('addressMap', addressMaps); + } + if (isPinned && selectedPosition) { setValue('longtitude', selectedPosition.lng); setValue('latitude', selectedPosition.lat); } - }, [addressMaps, selectedPosition, setValue]); - + }, [addressMaps, selectedPosition, isPinned, setValue]); + + // Isi ZIP/Prov dari detailAddress (JANGAN isi street) useEffect(() => { - if (Object.keys(detailAddress).length > 0) { + 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) + detailAddress?.province?.includes(state.label) || + state.label?.includes(detailAddress?.province) ); - setValue('state', selectedState?.value); - setValue('street', detailAddress?.street); + setValue('state', selectedState?.value); + // jangan override street: + // setValue('street', detailAddress?.street); } - }, [detailAddress, setValue]); - + }, [detailAddress, states, isPinned, setValue]); + useEffect(() => { const loadProfile = async () => { const dataProfile = await addressApi({ id: auth.partnerId }); @@ -88,8 +114,8 @@ const EditAddress = ({ id, defaultValues }) => { setValue('subDistrict', dataProfile.subDistrict?.id); }; if (auth) loadProfile(); - }, [auth?.parentId]); - + }, [auth?.parentId, setValue]); + useEffect(() => { const loadStates = async () => { let dataStates = await stateApi({ tempo: false }); @@ -101,7 +127,7 @@ const EditAddress = ({ id, defaultValues }) => { }; loadStates(); }, []); - + const watchState = watch('state'); useEffect(() => { setValue('city', ''); @@ -122,63 +148,64 @@ 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 (Object.keys(detailAddress || {}).length > 0 && isPinned) { + const selectedCities = + cities.find( (city) => - detailAddress?.district.toLowerCase().includes(city.label.toLowerCase()) || - city.label.toLowerCase().includes(detailAddress?.district.toLowerCase()) + city.label.toLowerCase() === detailAddress?.district?.toLowerCase() + ) || + cities.find( + (city) => + detailAddress?.district + ?.toLowerCase() + .includes(city.label.toLowerCase()) || + city.label + .toLowerCase() + .includes(detailAddress?.district?.toLowerCase()) ); - setValue('city', selectedCities?.value); - } - }, [cities, detailAddress, setValue]); - - const watchCity = watch('city'); - useEffect(() => { - if (watchCity) { - // setValue('district', ''); - const loadDistricts = async () => { - let dataDistricts = await districtApi({ cityId: watchCity }); - dataDistricts = dataDistricts.map((district) => ({ - value: district.id, - label: district.name, + setValue('city', selectedCities?.value); + } + }, [cities, detailAddress, isPinned, 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', ''); } }; loadDistricts(); } }, [watchCity, setValue, getValues]); - + useEffect(() => { - if (Object.keys(detailAddress).length > 0) { + if (Object.keys(detailAddress || {}).length > 0 && isPinned) { const selectedDistrict = districts.find( (district) => - detailAddress.subDistrict - .toLowerCase() + detailAddress?.subDistrict + ?.toLowerCase() .includes(district.label.toLowerCase()) || district.label .toLowerCase() - .includes(detailAddress.subDistrict.toLowerCase()) + .includes(detailAddress?.subDistrict?.toLowerCase()) ); setValue('district', selectedDistrict?.value); } - }, [districts, detailAddress, setValue]); - - - + }, [districts, detailAddress, isPinned, setValue]); + const watchDistrict = watch('district'); useEffect(() => { if (watchDistrict) { - // setValue('subDistrict', ''); const loadSubDistricts = async () => { let dataSubDistricts = await subDistrictApi({ districtId: watchDistrict, @@ -199,29 +226,27 @@ const EditAddress = ({ id, defaultValues }) => { } }, [watchDistrict, setValue, getValues]); - useEffect(() => { - if (Object.keys(detailAddress).length > 0) { + if (Object.keys(detailAddress || {}).length > 0 && isPinned) { const selectedSubDistrict = subDistricts.find( (district) => - detailAddress.village - .toLowerCase() + detailAddress?.village + ?.toLowerCase() .includes(district.label.toLowerCase()) || - district.label + district.label .toLowerCase() - .includes(detailAddress.village.toLowerCase()) - ); - - setValue('subDistrict', selectedSubDistrict?.value); + .includes(detailAddress?.village?.toLowerCase()) + ); + setValue('subDistrict', selectedSubDistrict?.value); } - }, [subDistricts, detailAddress, setValue]); + }, [subDistricts, detailAddress, isPinned, setValue]); useEffect(() => { if (id) { setValue('id', id); } }, [id, setValue]); - + const onSubmitHandler = async (values) => { const data = { ...values, @@ -230,20 +255,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, }; + + // kirim koordinat + address_map + use_pin HANYA jika sudah PIN + 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 +297,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,19 +321,8 @@ 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(); - // } }; return ( @@ -310,12 +334,11 @@ const EditAddress = ({ id, defaultValues }) => { close={() => setPinedMaps(false)} > <div className='flex mt-4'> - <PinPointMap - initialLatitude={selectedPosition?.lat} - initialLongitude={selectedPosition?.lng} - initialAddress={tempAddress} - /> - + <PinPointMap + initialLatitude={selectedPosition?.lat} + initialLongitude={selectedPosition?.lng} + initialAddress={tempAddress} + /> </div> </BottomPopup> <div className='max-w-none md:container mx-auto flex p-0 md:py-10'> @@ -334,15 +357,28 @@ const EditAddress = ({ id, defaultValues }) => { <label className='form-label mb-2'>Koordinat Alamat</label> {tempAddress ? ( <div className='flex gap-x-2 items-center'> - <button type='button' className="flex items-center justify-center me-3 p-2 badge-solid-red text-white rounded-full hover:bg-red-500 transition"> - <MapPinIcon class='h-6 w-6' onClick={() => setPinedMaps(true)} />{' '} - </button> + <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> {tempAddress} </span> </div> ) : ( - <Button variant='plain' style={{ padding: 0 }} onClick={() => setPinedMaps(true)}> - <button type='button' className="flex items-center justify-center me-3 p-2 badge-solid-red text-white rounded-full hover:bg-red-500 transition"> - <MapPinIcon className="h-6 w-6" /> + <Button + 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 +566,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/components/SwitchAccount.jsx b/src/lib/auth/components/SwitchAccount.jsx index fc2ac941..840758c9 100644 --- a/src/lib/auth/components/SwitchAccount.jsx +++ b/src/lib/auth/components/SwitchAccount.jsx @@ -16,7 +16,7 @@ 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); @@ -152,21 +152,23 @@ const SwitchAccount = ({ company_type }) => { try { const isUpdated = await switchAccountApi({ data }); - if (isUpdated?.switch === 'Pending') { + 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. Jika kendala masih berlanjut, silakan hubungi admin.'); + 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. Periksa koneksi internet Anda, dan jika kendala masih berlanjut, silakan hubungi admin.'); + toast.error('Terjadi kesalahan saat menghubungi server, silahkan cek internet Anda atau hubungi admin Indoteknik.'); setIsLoadingPopup(false); } }; diff --git a/src/lib/checkout/components/SectionExpedition.jsx b/src/lib/checkout/components/SectionExpedition.jsx index 7a02c6e9..2e92ffbc 100644 --- a/src/lib/checkout/components/SectionExpedition.jsx +++ b/src/lib/checkout/components/SectionExpedition.jsx @@ -341,7 +341,7 @@ export default function SectionExpedition({ products }) { <div className='w-[350px] max'> <div className='px-4 py-2'> <div className='flex justify-between items-center'> - <div className='w-[450px]'> + <div className='w-full'> <div className='relative'> {/* Custom Select Input Field */} <div @@ -407,7 +407,7 @@ export default function SectionExpedition({ products }) { </div> {checkoutValidation && ( <span className='text-sm text-red-500'> - *silahkan pilih expedisi + *Silahkan pilih expedisi </span> )} </div> diff --git a/src/lib/checkout/components/SectionQuotationExpedition.jsx b/src/lib/checkout/components/SectionQuotationExpedition.jsx new file mode 100644 index 00000000..b8ea04ef --- /dev/null +++ b/src/lib/checkout/components/SectionQuotationExpedition.jsx @@ -0,0 +1,369 @@ +'use client'; + +import { Skeleton } from '@chakra-ui/react'; +import axios from 'axios'; +import Image from 'next/image'; +import React, { useEffect, useState } from 'react'; +import { useQuery } from 'react-query'; +import toast from 'react-hot-toast'; +import { useAddress } from '../stores/useAdress'; +import { useQuotation } from '../stores/stateQuotation'; + +import currencyFormat from '@/core/utils/currencyFormat'; +import { formatShipmentRange } from '../utils/functionCheckouit'; +import odooApi from '@/core/api/odooApi'; + +function mappingItems(products) { + return products?.map((item) => ({ + name: item?.name, + description: `${item.code} - ${item.name}`, + value: item.price.priceDiscount, + weight: item.weight * 1000, + quantity: item.quantity, + })); +} + +function reverseMappingCourier(couriersOdoo, couriers, includeInstant = false) { + const courierMap = couriers.reduce((acc, item) => { + const { courier_name, courier_code, courier_service_code } = item; + const key = courier_code.toLowerCase(); + + if ( + !includeInstant && + (['hours'].includes(item.shipment_duration_unit.toLowerCase()) || + item.service_type === 'same_day') + ) { + return acc; + } + + if (!acc[key]) { + acc[key] = { + courier_name: item.courier_name, + courier_code: courier_code, + service_type: {}, + }; + } + + acc[key].service_type[courier_service_code] = { + service_name: item.courier_service_name, + duration: item.duration, + shipment_range: item.shipment_duration_range, + shipment_unit: item.shipment_duration_unit, + price: item.price, + service_type: courier_service_code, + description: item.description, + }; + + return acc; + }, {}); + + return couriersOdoo.map((courierOdoo) => { + const courierNameKey = courierOdoo.label.toLowerCase(); + const carrierId = courierOdoo.carrierId; + + const mappedCourier = courierMap[courierNameKey] || false; + + if (!mappedCourier) { + return { + ...courierOdoo, + courier: false, + }; + } + + return { + ...courierOdoo, + courier: { + ...mappedCourier, + courier_id_odoo: carrierId, + }, + }; + }); +} + +export default function SectionExpeditionQuotation({ products }) { + const { addressMaps, coordinate, postalCode } = useAddress(); + const [serviceOptions, setServiceOptions] = useState([]); + const [isOpen, setIsOpen] = useState(false); + const [onFocusSelectedCourier, setOnFocuseSelectedCourier] = useState(false); + const [couriers, setCouriers] = useState(null); + const [slaProducts, setSlaProducts] = useState(null); + const [savedServiceOptions, setSavedServiceOptions] = useState([]); + + const { + checkWeigth, + checkoutValidation, + setBiayaKirim, + setUnit, + setEtd, + selectedCourier, + setSelectedCourier, + selectedService, + setSelectedService, + listExpedisi, + productSla, + setProductSla, + setSelectedCourierId, + } = useQuotation(); + + let destination = {}; + let items = mappingItems(products); + + if (addressMaps) { + destination = { + origin_latitude: -6.3031123, + origin_longitude: 106.7794934999, + ...coordinate, + }; + } else if (postalCode) { + destination = { + origin_postal_code: 14440, + destination_postal_code: postalCode, + }; + } + + const fetchSlaProducts = async () => { + try { + let productsMapped = products.map((item) => ({ + id: item.id, + quantity: item.quantity, + })); + + let data = { + products: JSON.stringify(productsMapped), + }; + const res = await odooApi('POST', `/api/v1/product/variants/sla`, data); + setSlaProducts(res); + } catch (error) { + console.error('Failed to fetch SLA:', error); + } + }; + + useEffect(() => { + fetchSlaProducts(); + }, []); + + useEffect(() => { + if (slaProducts) { + let productSla = slaProducts?.slaTotal; + if (slaProducts.slaUnit === 'jam') { + productSla = 1; + } + setProductSla(productSla); + } + }, [slaProducts]); + + const fetchExpedition = async () => { + const body = { + ...destination, + couriers: + 'gojek,grab,deliveree,lalamove,jne,tiki,ninja,lion,rara,sicepat,jnt,pos,idexpress,rpx,wahana,jdl,pos,anteraja,sap,paxel,borzo', + items, + }; + const response = await axios.get(`/api/biteship-service`, { + params: { body: JSON.stringify(body) }, + }); + return response; + }; + + const { data, isLoading } = useQuery( + ['expedition', JSON.stringify(destination), JSON.stringify(items)], + fetchExpedition, + { + enabled: + Boolean(Object.keys(destination).length) && + items?.length > 0 && + !checkWeigth && + onFocusSelectedCourier, + staleTime: Infinity, + cacheTime: Infinity, + } + ); + + useEffect(() => { + const instant = slaProducts?.includeInstant || false; + if (data) { + const couriers = reverseMappingCourier( + listExpedisi, + data?.data?.pricing, + instant + ); + setCouriers(couriers); + } + }, [data, slaProducts]); + + const onCourierChange = (courier) => { + setIsOpen(false); + setOnFocuseSelectedCourier(false); + setSelectedService(null); + setBiayaKirim(0); + if (courier !== 0 && courier !== 32) { + if (courier.courier) { + setSelectedCourier(courier.courier.courier_code); + setSelectedCourierId(courier.carrierId); + setServiceOptions(Object.values(courier.courier.service_type)); + } else { + if ( + (courier.label === 'GRAB' || courier.label === 'GOJEK') && + !addressMaps + ) { + toast.error( + `Maaf, layanan kurir ${courier.label} tidak tersedia karena belum mengatur PinPoint.` + ); + } else { + toast.error('Maaf, layanan tidak tersedia. Mohon pilih ekspedisi lain.'); + } + setServiceOptions([]); + } + } else { + setSelectedCourier(courier === 32 ? 'SELF PICKUP' : null); + setSelectedCourierId(courier); + setServiceOptions([]); + } + }; + + const handleSelect = (service) => { + setSelectedService(service); + setBiayaKirim(service?.price); + setEtd(service?.shipment_range); + setUnit(service?.shipment_unit); + setIsOpen(false); + }; + + useEffect(() => { + if (serviceOptions.length > 0) { + setSavedServiceOptions(serviceOptions); + } + }, [serviceOptions]); + + return ( + <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='w-full p-2 border rounded-lg bg-white cursor-pointer' + onClick={() => setOnFocuseSelectedCourier(!onFocusSelectedCourier)} + > + {selectedCourier ? ( + <div className='flex justify-between'> + <span>{selectedCourier}</span> + </div> + ) : ( + <span className='text-gray-500'>Pilih Expedisi</span> + )} + </div> + {onFocusSelectedCourier && ( + <div className='absolute bg-white border rounded-lg mt-1 shadow-lg z-10 max-h-[200px] overflow-y-auto w-[350px]'> + {!isLoading ? ( + <> + <div + key={32} + onClick={() => onCourierChange(32)} + className='p-2 hover:bg-gray-100 cursor-pointer' + > + <p className='font-semibold'>SELF PICKUP</p> + </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' + > + <p className='font-semibold'>{courier?.label}</p> + <Image + src={courier?.logo} + alt={courier?.courier?.courier_name} + width={50} + height={50} + /> + </div> + ))} + </> + ) : ( + <> + <Skeleton height={40} /> + <Skeleton height={40} /> + </> + )} + </div> + )} + {checkoutValidation && ( + <span className='text-sm text-red-500'> + *Silahkan pilih ekspedisi + </span> + )} + </div> + </div> + + {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{' '} + <a + className='text-blue-600 underline' + href='https://api.whatsapp.com/send?phone=6281717181922' + target='_blank' + rel='noopener noreferrer' + > + tautan ini + </a> + </p> + )} + + {(serviceOptions.length > 0 || selectedService) && + selectedCourier && + selectedCourier !== 32 && + 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='p-2 border rounded-lg bg-white cursor-pointer' + onClick={() => setIsOpen(!isOpen)} + > + {selectedService ? ( + <div className='flex justify-between'> + <span>{selectedService.service_name}</span> + <span> + {currencyFormat( + Math.round((selectedService?.price * 1.1) / 1000) * 1000 + )} + </span> + </div> + ) : ( + <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'> + {serviceOptions.map((service) => ( + <div + key={service.service_type} + onClick={() => handleSelect(service)} + className='flex justify-between p-2 items-center hover:bg-gray-100 cursor-pointer' + > + <div> + <p className='font-semibold'>{service.service_name}</p> + <p className='text-sm text-gray-600'> + {formatShipmentRange( + service.shipment_range, + service.shipment_unit, + productSla + )} + </p> + </div> + <span> + {currencyFormat( + Math.round((service?.price * 1.1) / 1000) * 1000 + )} + </span> + </div> + ))} + </div> + )} + </div> + </div> + )} + </div> + ); +} diff --git a/src/lib/checkout/stores/stateQuotation.js b/src/lib/checkout/stores/stateQuotation.js new file mode 100644 index 00000000..da84997a --- /dev/null +++ b/src/lib/checkout/stores/stateQuotation.js @@ -0,0 +1,30 @@ +import { create } from "zustand"; + +export const useQuotation = create((set) => ({ + products : null, + checkWeigth : false, + hasFlashSale : false, + checkoutValidation : false, + biayaKirim : 0, + etd : null, + unit : null, + selectedCourier : null, + selectedCourierId : null, + selectedService : null, + listExpedisi : [], + productSla : null, + setCheckWeight : (checkWeigth) => set({ checkWeigth }), + setHasFlashSale : (hasFlashSale) => set({ hasFlashSale }), + setCheckoutValidation : (checkoutValidation) => set({ checkoutValidation }), + setBiayaKirim : (biayaKirim) => set({ biayaKirim }), + setProducts : (products) => set({ products }), + setEtd : (etd) => set({ etd }), + setUnit : (unit) => set({ unit }), + setSelectedCourier : (selectedCourier) => set({ selectedCourier }), + setSelectedService : (selectedService) => set({ selectedService }), + setSelectedCourierId : (selectedCourierId) => set({ selectedCourierId }), + setExpedisi : (listExpedisi) => set({ listExpedisi }), + setProductSla : (productSla) => set({ productSla }) + + +}))
\ No newline at end of file diff --git a/src/lib/maps/components/PinPointMap.jsx b/src/lib/maps/components/PinPointMap.jsx index c46d838a..75ab1d59 100644 --- a/src/lib/maps/components/PinPointMap.jsx +++ b/src/lib/maps/components/PinPointMap.jsx @@ -14,37 +14,35 @@ 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); - const autocompleteRef = useRef(null); useEffect(() => { @@ -55,7 +53,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,6 +64,7 @@ const PinpointLocation = ({ initialLatitude, initialLongitude, initialAddress }) return component ? component.long_name : ''; }; + // fill from pin point const getAddress = async (lat, lng) => { try { const response = await fetch( @@ -78,14 +77,26 @@ const PinpointLocation = ({ initialLatitude, initialLongitude, initialAddress }) const formattedAddress = data.results[0].formatted_address; const details = { - street: - getAddressComponent(addressComponents, 'route') + - ' ' + - getAddressComponent(addressComponents, 'street_number'), - province: getAddressComponent(addressComponents, 'administrative_area_level_1'), - district: getAddressComponent(addressComponents, 'administrative_area_level_2'), - subDistrict: getAddressComponent(addressComponents, 'administrative_area_level_3'), - village: getAddressComponent(addressComponents, 'administrative_area_level_4'), + // street: + // getAddressComponent(addressComponents, 'route') + + // ' ' + + // getAddressComponent(addressComponents, 'street_number'), + province: getAddressComponent( + addressComponents, + 'administrative_area_level_1' + ), + district: getAddressComponent( + addressComponents, + 'administrative_area_level_2' + ), + subDistrict: getAddressComponent( + addressComponents, + 'administrative_area_level_3' + ), + village: getAddressComponent( + addressComponents, + 'administrative_area_level_4' + ), postalCode: getAddressComponent(addressComponents, 'postal_code'), }; @@ -136,8 +147,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 +191,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 +217,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..f7636c24 100644 --- a/src/lib/maps/stores/useMaps.js +++ b/src/lib/maps/stores/useMaps.js @@ -1,32 +1,47 @@ 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 || typeof p.lat !== 'number' || typeof p.lng !== 'number') return false; + const isDefault = + Math.abs(p.lat - DEFAULT_CENTER.lat) < 1e-6 && + Math.abs(p.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/component/PengajuanTempo.jsx b/src/lib/pengajuan-tempo/component/PengajuanTempo.jsx index d59bfd75..096fe1ed 100644 --- a/src/lib/pengajuan-tempo/component/PengajuanTempo.jsx +++ b/src/lib/pengajuan-tempo/component/PengajuanTempo.jsx @@ -38,8 +38,9 @@ const PengajuanTempo = () => { const [bigData, setBigData] = useState(); const [idTempo, setIdTempo] = useState(0); const { form, errors, validate, updateForm } = usePengajuanTempoStore(); - const { control, watch, setValue } = useForm(); + 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 } = @@ -426,111 +427,185 @@ const PengajuanTempo = () => { } }; - const handleDaftarTempo = async () => { - for (const error of stepDivsError) { - if (error.length > 0) { - return; +const handleDaftarTempo = async () => { +const phones = [ + { key: 'mobile', label: 'No. HP Perusahaan', value: form.mobile?.trim() }, + { + key: 'direkturMobile', + label: 'No. HP Direktur', + value: formKontakPerson.direkturMobile?.trim(), + }, + { + key: 'purchasingMobile', + label: 'No. HP Purchasing', + value: formKontakPerson.purchasingMobile?.trim(), + }, + { + key: 'financeMobile', + label: 'No. HP Finance', + value: formKontakPerson.financeMobile?.trim(), + }, + { + key: 'PICBarangMobile', + label: 'No. HP PIC Barang', + value: formPengiriman.PICBarangMobile?.trim(), + }, + { + key: 'invoicePicMobile', + label: 'No. HP PIC Invoice', + value: formPengiriman.invoicePicMobile?.trim(), + }, +].filter((p) => p.value); + + + const seen = new Map(); + let firstErrorField = null; + + // Reset error manual + phones.forEach((p) => setError(p.key, { type: 'manual', message: '' })); + + for (const phone of phones) { + if (!seen.has(phone.value)) { + seen.set(phone.value, phone); + } else { + const first = seen.get(phone.value); + + // Tampilkan toast + toast.error(`${phone.label} tidak boleh sama dengan ${first.label}`); + + // Error merah di bawah input + setError(phone.key, { + type: 'manual', + message: `${phone.label} tidak boleh sama dengan ${first.label}`, + }); + + // Pasangan pertama yang duplikat + setError(first.key, { + type: 'manual', + message: `${first.label} tidak boleh sama dengan ${phone.label}`, + }); + + if (!firstErrorField) { + firstErrorField = phone.key; } } + } + + if (firstErrorField) { + setTimeout(() => { + const el = document.querySelector(`[name="${firstErrorField}"]`); + if (el) { + el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + el.focus(); + } + }, 100); + return; + } - // Filter hanya dokumen dengan `format` yang tidak undefined - const formattedDokumen = Object.entries(formDokumen) - .filter(([_, doc]) => doc.format !== undefined) // Hanya dokumen dengan `format` tidak undefined - .map(([key, doc]) => ({ - documentName: key, - details: { - name: doc.name, - format: doc.format, - base64: doc.base64, - }, - })); + for (const error of stepDivsError) { + if (error.length > 0) { + return; + } + } + + const formattedDokumen = Object.entries(formDokumen) + .filter(([_, doc]) => doc.format !== undefined) + .map(([key, doc]) => ({ + documentName: key, + details: { + name: doc.name, + format: doc.format, + base64: doc.base64, + }, + })); - const toastId = toast.loading('Mengirimkan formulir pengajuan tempo...'); - setIsLoading(true); - try { - let address4; - let address3; - const address = await createPengajuanTempoApi({ - id: 0, + const toastId = toast.loading('Mengirimkan formulir pengajuan tempo...'); + setIsLoading(true); + + try { + let address4; + let address3; + const address = await createPengajuanTempoApi({ + id: 0, + partner_id: auth.partnerId, + user_id: auth.parentId ? auth.parentId : auth.partnerId, + tempo_request: false, + ...form, + }); + + if (address.id) { + const address2 = await createPengajuanTempoApi({ + id: address.id, partner_id: auth.partnerId, - user_id: auth.parentId ? auth.parentId : auth.partnerId, + user_id: address.userId, tempo_request: false, - ...form, + ...formKontakPerson, }); - if (address.id) { - const address2 = await createPengajuanTempoApi({ - id: address.id, + + if (address2.id) { + address3 = await createPengajuanTempoApi({ + id: address2.id, partner_id: auth.partnerId, - user_id: address.userId, + user_id: address2.userId, tempo_request: false, - ...formKontakPerson, - }); - if (address2.id) { - address3 = await createPengajuanTempoApi({ - id: address2.id, - partner_id: auth.partnerId, - user_id: address2.userId, - tempo_request: false, - ...formPengiriman, - formDokumenProsedur: formPengiriman.dokumenProsedur + ...formPengiriman, + formDokumenProsedur: formPengiriman.dokumenProsedur ? JSON.stringify(formPengiriman.dokumenProsedur) : false, + }); + + if (address3.id && formattedDokumen.length > 0) { + address4 = await createPengajuanTempoApi({ + id: address3.id, + partner_id: auth.partnerId, + user_id: address3.userId, + tempo_request: true, + formDocs: JSON.stringify(formattedDokumen), + }); + } else { + address4 = await createPengajuanTempoApi({ + id: address3.id, + partner_id: auth.partnerId, + user_id: address3.userId, + tempo_request: true, }); - if (address3.id && formattedDokumen.length > 0) { - // Kirim dokumen yang sudah difilter - address4 = await createPengajuanTempoApi({ - id: address3.id, - partner_id: auth.partnerId, - user_id: address3.userId, - tempo_request: true, - formDocs: JSON.stringify(formattedDokumen), - }); - } else { - address4 = await createPengajuanTempoApi({ - id: address3.id, - partner_id: auth.partnerId, - user_id: address3.userId, - tempo_request: true, - }); - } } } + } - if (address4?.id) { - toast.success('Pengajuan tempo berhasil dilakukan'); - const toastId = toast.loading('Mengubah status akun...'); - const isUpdated = await editAuthTempo(); - if (isUpdated?.user) { - const update = await setAuth(isUpdated.user); - if (update) { - toast.dismiss(toastId); - setIsLoading(false); - toast.success('Berhasil mengubah status akun', { duration: 1000 }); - router.push('/pengajuan-tempo/finish'); - } else { - toast.dismiss(toastId); - setIsLoading(false); - toast.success('Pengajuan tempo berhasil dilakukan'); - toast.error('Gagal mengubah status akun', { duration: 1000 }); - router.push('/pengajuan-tempo'); - } - removeFromLocalStorage(); - return; + if (address4?.id) { + toast.success('Pengajuan tempo berhasil dilakukan'); + const toastId2 = toast.loading('Mengubah status akun...'); + const isUpdated = await editAuthTempo(); + if (isUpdated?.user) { + const update = await setAuth(isUpdated.user); + if (update) { + toast.dismiss(toastId2); + setIsLoading(false); + toast.success('Berhasil mengubah status akun', { duration: 1000 }); + router.push('/pengajuan-tempo/finish'); + } else { + toast.dismiss(toastId2); + setIsLoading(false); + toast.success('Pengajuan tempo berhasil dilakukan'); + toast.error('Gagal mengubah status akun', { duration: 1000 }); + router.push('/pengajuan-tempo'); } - } else { - toast.dismiss(toastId); - setIsLoading(false); - - toast.error('Terjadi kesalahan dalam pengiriman formulir'); + removeFromLocalStorage(); + return; } - } catch (error) { + } else { toast.dismiss(toastId); setIsLoading(false); - toast.error('Terjadi kesalahan dalam pengiriman formulir'); - console.error(error); } - }; + } catch (error) { + toast.dismiss(toastId); + setIsLoading(false); + toast.error('Terjadi kesalahan dalam pengiriman formulir'); + console.error(error); + } +}; const removeFromLocalStorage = () => { for (const key of stepLabels) { @@ -660,10 +735,8 @@ const PengajuanTempo = () => { isDisabled={!isCheckedTNC || isLoading} onClick={handleDaftarTempo} > - {isLoading - ? 'Loading...' - : 'Daftar Tempo'} - {!isLoading && <ChevronRightIcon className='w-5' />} + {isLoading ? 'Loading...' : 'Daftar Tempo'} + {!isLoading && <ChevronRightIcon className='w-5' />} </Button> </div> )} diff --git a/src/lib/quotation/components/Quotation.jsx b/src/lib/quotation/components/Quotation.jsx index 2f4d6c46..f0791512 100644 --- a/src/lib/quotation/components/Quotation.jsx +++ b/src/lib/quotation/components/Quotation.jsx @@ -10,7 +10,6 @@ import { deleteItemCart, getCart, getItemCart } from '@/core/utils/cart'; import currencyFormat from '@/core/utils/currencyFormat'; import { toast } from 'react-hot-toast'; import { useProductCartContext } from '@/contexts/ProductCartContext'; -// import checkoutApi from '@/lib/checkout/api/checkoutApi' import { useRouter } from 'next/router'; import VariantGroupCard from '@/lib/variant/components/VariantGroupCard'; import MobileView from '@/core/components/views/MobileView'; @@ -19,11 +18,11 @@ import Image from '@/core/components/elements/Image/Image'; import { useQuery } from 'react-query'; import CardProdcuctsList from '@/core/components/elements/Product/cartProductsList'; import { Skeleton } from '@chakra-ui/react'; +import { useAddress } from '@/lib/checkout/stores/useAdress'; +import { useCheckout } from '@/lib/checkout/stores/stateCheckout'; import { PickupAddress, SectionAddress, - SectionExpedisi, - SectionListService, SectionValidation, calculateEstimatedArrival, splitDuration, @@ -31,7 +30,8 @@ import { import addressesApi from '@/lib/address/api/addressesApi'; import { getItemAddress } from '@/core/utils/address'; import ExpedisiList from '../../checkout/api/ExpedisiList'; -import axios from 'axios'; +import SectionQuotationExpedition from '@/lib/checkout/components/SectionQuotationExpedition'; +import { useQuotation } from '@/lib/checkout/stores/stateQuotation'; const { checkoutApi } = require('@/lib/checkout/api/checkoutApi'); const { getProductsCheckout } = require('@/lib/checkout/api/checkoutApi'); @@ -51,41 +51,48 @@ const Quotation = () => { const { setRefreshCart } = useProductCartContext(); const SELF_PICKUP_ID = 32; - const [products, setProducts] = useState(null); - const [totalAmount, setTotalAmount] = useState(0); + const [totalAmount, setTotalAmount] = useState(0); const [totalDiscountAmount, setTotalDiscountAmount] = useState(0); - - //start set up address and carrier - const [selectedCarrierId, setselectedCarrierId] = useState(0); - const [listExpedisi, setExpedisi] = useState([]); - const [selectedExpedisi, setSelectedExpedisi] = useState(0); - const [checkWeigth, setCheckWeight] = useState(false); - const [checkoutValidation, setCheckoutValidation] = useState(false); - const [loadingRajaOngkir, setLoadingRajaOngkir] = useState(false); - - const [listserviceExpedisi, setListServiceExpedisi] = useState([]); - const [selectedServiceType, setSelectedServiceType] = useState(null); - - const [selectedCarrier, setselectedCarrier] = useState(0); - const [totalWeight, setTotalWeight] = useState(0); - - const [biayaKirim, setBiayaKirim] = useState(0); - const [selectedExpedisiService, setselectedExpedisiService] = useState(null); - const [etd, setEtd] = useState(null); - const [etdFix, setEtdFix] = useState(null); - const [isApproval, setIsApproval] = useState(false); - - const expedisiValidation = useRef(null); - - const [selectedAddress, setSelectedAddress] = useState({ - shipping: null, - invoicing: null, - }); - - const [addresses, setAddresses] = useState(null); - const [note_websiteText, setselectedNote_websiteText] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [etdFix, setEtdFix] = useState(null); + + const { + selectedAddress, + setSelectedAddress, + addresses, + setAddresses, + setAddressMaps, + setCoordinate, + setPostalCode, + } = useAddress(); + + const { + products, + setProducts, + checkWeigth, + setCheckWeight, + checkoutValidation, + setCheckoutValidation, + biayaKirim, + etd, + unit, + selectedCourier, + selectedCourierId, + selectedService, + listExpedisi, + setExpedisi, + productSla, + setProductSla, + setBiayaKirim, + setUnit, + setEtd, + setSelectedCourier, + setSelectedService, + setSelectedCourierId + } = useQuotation(); + useEffect(() => { if (!auth) return; @@ -116,198 +123,145 @@ const Quotation = () => { return addresses[0]; }; + let ship = matchAddress('shipping'); + setSelectedAddress({ shipping: matchAddress('shipping'), invoicing: matchAddress('invoicing'), }); - }, [addresses]); - - const loadExpedisi = async () => { - let dataExpedisi = await ExpedisiList(); - dataExpedisi = dataExpedisi.map((expedisi) => ({ - value: expedisi.id, - label: expedisi.name, - carrierId: expedisi.deliveryCarrierId, - })); - setExpedisi(dataExpedisi); - }; - - const loadServiceRajaOngkir = async () => { - setLoadingRajaOngkir(true); - const body = { - origin: 2127, - destination: selectedAddress.shipping.rajaongkirCityId, - weight: totalWeight, - courier: selectedCarrier, - originType: 'subdistrict', - destinationType: 'subdistrict', - }; - setBiayaKirim(0); - const dataService = await axios( - '/api/rajaongkir-service?body=' + JSON.stringify(body) - ); - setLoadingRajaOngkir(false); - setListServiceExpedisi(dataService.data[0].costs); - if (dataService.data[0].costs[0]) { - setBiayaKirim(dataService.data[0].costs[0]?.cost[0].value); - setselectedExpedisiService( - dataService.data[0].costs[0]?.description + - '-' + - dataService.data[0].costs[0]?.service - ); - setEtd(dataService.data[0].costs[0]?.cost[0].etd); - toast.success('Harap pilih tipe layanan pengiriman'); - } else { - toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'); - } - }; - - useEffect(() => { - setCheckoutValidation(false); - - if (selectedCarrier != 0 && selectedCarrier != 1 && totalWeight > 0) { - loadServiceRajaOngkir(); - } else { - setListServiceExpedisi(); - setBiayaKirim(0); - setselectedExpedisiService(); - setEtd(); + setPostalCode(ship?.zip); + if (ship?.addressMap) { + setAddressMaps(ship?.addressMap); + setCoordinate({ + destination_latitude: ship?.latitude, + destination_longitude: ship?.longtitude, + }); } - }, [selectedCarrier, selectedAddress, totalWeight]); + }, [addresses]); useEffect(() => { - if (selectedExpedisi) { - let serviceType = selectedExpedisi.split(','); - if (serviceType[0] === 0) return; + const loadExpedisi = async () => { + let dataExpedisi = await ExpedisiList(); + dataExpedisi = dataExpedisi.map((expedisi) => ({ + value: expedisi.id, + label: expedisi.name, + carrierId: expedisi.deliveryCarrierId, + logo: expedisi.image, + })); + setExpedisi(dataExpedisi); + }; + + loadExpedisi(); - setselectedCarrier(serviceType[0]); - setselectedCarrierId(serviceType[1]); - setListServiceExpedisi([]); - } - }, [selectedExpedisi]); + const handlePopState = () => { + router.push('/shop/cart'); + }; - useEffect(() => { - if (selectedServiceType) { - let serviceType = selectedServiceType.split(','); - setBiayaKirim(serviceType[0]); - setselectedExpedisiService(serviceType[1]); - setEtd(serviceType[2]); - } - }, [selectedServiceType]); + window.onpopstate = handlePopState; - useEffect(() => { - if (etd) setEtdFix(calculateEstimatedArrival(etd)); - }, [etd]); + return () => { + window.onpopstate = null; + }; + }, []); useEffect(() => { if (isApproval) { - setselectedCarrierId(1); - setselectedExpedisiService('indoteknik'); + setSelectedCourierId(1); + setSelectedCourier('indoteknik'); } }, [isApproval]); - // end set up address and carrier - - useEffect(() => { - const loadProducts = async () => { - const cart = getCart(); - const variantIds = _.filter(cart, (o) => o.selected == true) - .map((o) => o.productId) - .join(','); - const dataProducts = await CartApi({ variantIds }); - const productsWithQuantity = dataProducts?.map((product) => { - return { - ...product, - quantity: getItemCart({ productId: product.id }).quantity, - }; - }); - if (productsWithQuantity) { - Promise.all(productsWithQuantity).then((resolvedProducts) => { - setProducts(resolvedProducts); - }); - } - }; - loadExpedisi(); - // loadProducts() - }, []); - useEffect(() => { - setProducts(cartCheckout?.products); - setCheckWeight(cartCheckout?.hasProductWithoutWeight); - setTotalWeight(cartCheckout?.totalWeight.g); + if (cartCheckout) { + setProducts(cartCheckout?.products); + setCheckWeight(cartCheckout?.hasProductWithoutWeight); + } }, [cartCheckout]); useEffect(() => { if (products) { - let calculateTotalAmount = 0; - let calculateTotalDiscountAmount = 0; - products.forEach((product) => { - calculateTotalAmount += product.price.price * product.quantity; - calculateTotalDiscountAmount += - (product.price.price - product.price.priceDiscount) * - product.quantity; - }); - setTotalAmount(calculateTotalAmount); - setTotalDiscountAmount(calculateTotalDiscountAmount); + const calculateTotals = () => { + let calculateTotalAmount = 0; + let calculateTotalDiscountAmount = 0; + + products.forEach((product) => { + calculateTotalAmount += product.price.price * product.quantity; + calculateTotalDiscountAmount += + (product.price.price - product.price.priceDiscount) * product.quantity; + }); + + setTotalAmount(calculateTotalAmount); + setTotalDiscountAmount(calculateTotalDiscountAmount); + }; + + calculateTotals(); } }, [products]); - const [isLoading, setIsLoading] = useState(false); + useEffect(() => { + if (etd) { + setEtdFix(calculateEstimatedArrival(etd)); + } + }, [etd]); const checkout = async () => { - // validation checkout - if (selectedExpedisi === 0 && !isApproval) { + // Validation + if (!selectedCourierId && !isApproval) { setCheckoutValidation(true); - if (expedisiValidation.current) { - const position = expedisiValidation.current.getBoundingClientRect(); - window.scrollTo({ - top: position.top - 300 + window.pageYOffset, - behavior: 'smooth', - }); - } + toast.error('Silahkan pilih ekspedisi'); return; } - if (selectedCarrier != 1 && biayaKirim == 0 && !isApproval) { + + if (selectedCourierId !== SELF_PICKUP_ID && biayaKirim === 0 && !isApproval) { toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'); return; } - if (!products || products.length == 0) return; + if (!products || products.length === 0) return; - if (isApproval && note_websiteText == '') { + if (isApproval && note_websiteText === '') { toast.error('Maaf, Note wajib dimasukkan.'); return; } setIsLoading(true); - const productOrder = products.map((product) => ({ - product_id: product.id, - quantity: product.quantity, - })); - let data = { - partner_shipping_id: selectedAddress.shipping.id, - partner_invoice_id: selectedAddress.invoicing.id, - user_id: auth.id, - order_line: JSON.stringify(productOrder), - delivery_amount: biayaKirim, - carrier_id: selectedCarrierId, - estimated_arrival_days: splitDuration(etd), - delivery_service_type: selectedExpedisiService, - note_website: note_websiteText, - }; - - const isSuccess = await checkoutApi({ data }); - setIsLoading(false); - if (isSuccess?.id) { - for (const product of products) deleteItemCart({ productId: product.id }); - router.push(`/shop/quotation/finish?id=${isSuccess.id}`); - setRefreshCart(true); - return; + try { + const productOrder = products.map((product) => ({ + product_id: product.id, + quantity: product.quantity, + })); + + const data = { + partner_shipping_id: selectedAddress.shipping.id, + partner_invoice_id: selectedAddress.invoicing.id, + user_id: auth.id, + order_line: JSON.stringify(productOrder), + delivery_amount: biayaKirim, + carrier_id: selectedCourierId, + estimated_arrival_days: splitDuration(etd), + delivery_service_type: selectedService?.service_type || selectedCourier, + note_website: note_websiteText, + }; + + const isSuccess = await checkoutApi({ data }); + + if (isSuccess?.id) { + for (const product of products) { + deleteItemCart({ productId: product.id }); + } + router.push(`/shop/quotation/finish?id=${isSuccess.id}`); + setRefreshCart(true); + } else { + toast.error('Gagal melakukan transaksi, terjadi kesalahan internal'); + } + } catch (error) { + toast.error('Terjadi kesalahan saat memproses quotation'); + } finally { + setIsLoading(false); } - - toast.error('Gagal melakukan transaksi, terjadi kesalahan internal'); }; + const taxTotal = (totalAmount - totalDiscountAmount) * (PPN - 1); return ( @@ -327,21 +281,21 @@ const Quotation = () => { <Divider /> - {selectedCarrierId == SELF_PICKUP_ID && ( + {selectedCourierId == SELF_PICKUP_ID && ( <div className='p-4'> <div - class='flex items-center p-4 mb-4 text-sm border border-yellow-500 text-yellow-800 rounded-lg bg-yellow-50' + className='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-4 h-4 mr-3' + className='flex-shrink-0 inline w-4 h-4 mr-3' aria-hidden='true' fill='currentColor' viewBox='0 0 20 20' > <path d='M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z' /> </svg> - <span class='sr-only'>Info</span> + <span className='sr-only'>Info</span> <div className='text-justify'> Fitur Self Pickup, hanya berlaku untuk customer di area jakarta. Apa bila memilih fitur ini, anda akan dihubungi setelah barang @@ -351,10 +305,10 @@ const Quotation = () => { </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} @@ -374,32 +328,14 @@ const Quotation = () => { )} <Divider /> <SectionValidation address={selectedAddress.invoicing} /> - {!isApproval && ( - <> - <SectionExpedisi - address={selectedAddress.shipping} - listExpedisi={listExpedisi} - setSelectedExpedisi={setSelectedExpedisi} - checkWeigth={checkWeigth} - checkoutValidation={checkoutValidation} - expedisiValidation={expedisiValidation} - loadingRajaOngkir={loadingRajaOngkir} - /> - <Divider /> - </> - )} - - <SectionListService - listserviceExpedisi={listserviceExpedisi} - setSelectedServiceType={setSelectedServiceType} - /> - + <div className='p-4 flex flex-col gap-y-4'> {products && ( <VariantGroupCard openOnClick={false} variants={products} /> )} </div> + <SectionQuotationExpedition products={products} /> <Divider /> <div className='p-4'> @@ -497,10 +433,10 @@ const Quotation = () => { <DesktopView> <div className='container mx-auto py-10 flex'> <div className='w-3/4 border border-gray_r-6 rounded bg-white p-4'> - {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 @@ -522,31 +458,16 @@ const Quotation = () => { )} <Divider /> <SectionValidation address={selectedAddress.invoicing} /> - {!isApproval && ( - <SectionExpedisi - address={selectedAddress.shipping} - listExpedisi={listExpedisi} - setSelectedExpedisi={setSelectedExpedisi} - checkWeigth={checkWeigth} - checkoutValidation={checkoutValidation} - expedisiValidation={expedisiValidation} - loadingRajaOngkir={loadingRajaOngkir} - /> - )} - + + <SectionQuotationExpedition products={products} /> <Divider /> - <SectionListService - listserviceExpedisi={listserviceExpedisi} - setSelectedServiceType={setSelectedServiceType} - /> - {/* <div className='p-4'> */} + <div className='font-medium mb-6'>Detail Barang</div> <CardProdcuctsList isLoading={isLoading} products={products} source='checkout' /> - {/* </div> */} </div> <div className='w-1/4 pl-4'> @@ -601,9 +522,6 @@ const Quotation = () => { )} </div> </div> - {/* <p className='text-caption-2 text-gray_r-11 mb-2'> - *) Belum termasuk biaya pengiriman - </p> */} <p className='text-caption-2 text-gray_r-11 leading-5'> Dengan melakukan pembelian melalui website Indoteknik, saya menyetujui{' '} @@ -655,4 +573,4 @@ const Quotation = () => { ); }; -export default Quotation; +export default Quotation;
\ No newline at end of file diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx index 842567f8..77e60dc1 100644 --- a/src/lib/transaction/components/Transaction.jsx +++ b/src/lib/transaction/components/Transaction.jsx @@ -45,9 +45,11 @@ import { downloadInvoice, downloadTaxInvoice, } 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'; const Transaction = ({ id }) => { const PPN = process.env.NEXT_PUBLIC_PPN; const router = useRouter(); @@ -56,7 +58,6 @@ const Transaction = ({ id }) => { const [reason, setReason] = useState(''); const auth = useAuth(); const { transaction } = useTransaction({ id }); - console.log('transaction', transaction); const statusApprovalWeb = transaction.data?.approvalStep; const [isLoading, setIsLoading] = useState(false); const { queryAirwayBill } = useAirwayBill({ orderId: id }); @@ -87,8 +88,6 @@ const Transaction = ({ id }) => { setTotalDiscountAmount(calculateTotalDiscountAmount); } }, [transaction.data, transaction.isLoading]); - console.log('totalAmount', totalAmount); - console.log('totalDiscountAmount', totalDiscountAmount); const submitUploadPo = async () => { const file = poFile.current.files[0]; const name = poNumber.current.value; @@ -195,6 +194,7 @@ 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 }); @@ -336,7 +336,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 && ( <> @@ -526,7 +526,7 @@ const Transaction = ({ id }) => { <div className='flex flex-row justify-between items-center gap-2 px-4'> <div className='flex flex-col justify-start items-start gap-2'> - <div className='font-medium'>Status Transaksi</div> + <div className='font-semibold'>{transaction.data?.name}</div> <TransactionStatusBadge status={transaction.data?.status} /> </div> <div> @@ -559,16 +559,7 @@ const Transaction = ({ id }) => { <Divider /> <div className='flex flex-col gap-y-4 p-4'> - <DescriptionRow label='Status Transaksi'> - <div className='flex justify-end'> - <TransactionStatusBadge status={transaction.data?.status} /> - </div> - </DescriptionRow> - <DescriptionRow label='Status Transaksi'> - <div className='flex justify-end font-semibold text-red-500'> - {transaction.data?.expectedReadyToShip} - </div> - </DescriptionRow> + <h4 className="font-semibold">Detail Order</h4> <DescriptionRow label='No Transaksi'> <p className='font-semibold'>{transaction.data?.name}</p> </DescriptionRow> @@ -580,9 +571,6 @@ const Transaction = ({ id }) => { <DescriptionRow label='Purchase Order'> {transaction.data?.purchaseOrderName || '-'} </DescriptionRow> - <DescriptionRow label='Ketentuan Pembayaran'> - {transaction.data?.paymentTerm || '-'} - </DescriptionRow> <DescriptionRow label='Nama Sales'> {transaction.data?.sales} </DescriptionRow> @@ -590,103 +578,80 @@ const Transaction = ({ id }) => { <Divider /> + <div className='flex flex-col gap-y-4 p-4'> + <h4 className="font-semibold">Alamat Pengiriman</h4> + <DescriptionRow label='Nama Penerima'> + <p className='font-semibold'>{transaction?.data?.address?.customer?.name}</p> + </DescriptionRow> + <DescriptionRow label='No. Telp'> + {transaction?.data?.address?.customer?.phone + ? transaction?.data?.address?.customer?.phone + : '-'} + </DescriptionRow> + <DescriptionRow label='Email'> + {transaction?.data?.address?.customer?.email + ? transaction?.data?.address?.customer?.email + : '-'} + </DescriptionRow> + <DescriptionRow label='Alamat Pengiriman'> + {transaction?.data?.address?.customer?.alamatBisnis} + </DescriptionRow> + </div> + + <Divider /> <div className='p-4'> - <div className='flex flex-row justify-between items-center'> - <div className='font-medium'>Info Pengiriman</div> - <span - className='text-red-500' - onClick={() => setIdAWB(transaction?.data?.pickings[0]?.id)} + <div className='font-medium mb-4'>Info Pengiriman</div> + {transaction?.data?.pickings.length == 0 && ( + <div className='badge-red text-sm'> + Belum ada pengiriman + </div> + )} + {transaction?.data?.pickings?.map((airway) => ( + <div + key={airway?.id} + className='border border-gray_r-6 rounded mb-3' > - Lihat Detail - </span> - </div> - <hr className='mt-4 mb-4 border border-gray-100' /> - <div className='flex flex-col gap-y-4'> - <DescriptionRow label='Dokumen Pengiriman'> - <p className='text-red-500 font-semibold text-start'> - {transaction.data?.pickings?.length == 0 - ? 'Belum ada pengiriman' - : transaction?.data?.pickings[0].name} - </p> - </DescriptionRow> - <DescriptionRow label='Kurir'> - <p className='text-start'> - {transaction?.data?.pickings[0]?.carrierName ? ( - <p className=' text-nowrap'> - {transaction?.data?.pickings[0]?.carrierName} - </p> - ) : ( - '-' - )} - </p> - </DescriptionRow> - <DescriptionRow label='Jenis Service'> - <p className='text-start'> - {transaction?.data?.pickings[0]?.serviceType && - transaction?.data?.pickings[0]?.carrierName - ? transaction?.data?.pickings[0]?.serviceType - : '-'} - </p> - </DescriptionRow> - <DescriptionRow label='Nomor Resi'> - <div className='flex flex-row gap-1 text-start'> - {transaction?.data?.pickings[0]?.trackingNumber || '-'} - {transaction?.data?.pickings[0]?.trackingNumber && ( - <button - className={`${ - copied ? 'text-gray-400' : 'text-red-600 ' - }`} - onClick={() => - handleCopyClick( - transaction?.data?.pickings[0]?.trackingNumber - ) + <InformationSection manifests={airway} /> + <div className='p-4'> + <button + className='bg-transparent text-red-600 hover:underline p-0 font-semibold' + onClick={() => { + if (airway?.waybillNumber == '-') { + toast.error('Nomor Resi belum tersedia'); + return; } - > - <svg - aria-hidden='true' - fill='none' - stroke='currentColor' - stroke-width='1.5' - viewBox='0 0 24 24' - className='w-5 h-6' - > - <path - d='M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75' - stroke-linecap='round' - stroke-linejoin='round' - ></path> - </svg> - </button> - )} - </div> - </DescriptionRow> - <DescriptionRow label='Estimasi Tiba'> - <p className='text-start'> - {transaction?.data?.pickings[0]?.eta - ? transaction?.data?.pickings[0]?.eta - : '-'} - </p> - </DescriptionRow> - <DescriptionRow label='Alamat Pengiriman'> - <div className='flex flex-col justify-start items-start'> - <div className='text-start text-nowrap truncate w-full'> - {transaction?.data?.address?.customer?.name} - </div> - <div className='text-start'> - {transaction?.data?.address?.customer?.phone - ? transaction?.data?.address?.customer?.phone - : '-'} - </div> - <div className='text-start'> - {transaction?.data?.address?.customer?.alamatBisnis} - </div> + setIdAWB(airway.id); + }} + > + Lacak Pengiriman + </button> </div> - </DescriptionRow> - </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 /> + <Divider /> <div className='p-4'> <p className='font-medium'>Invoice</p> @@ -715,17 +680,17 @@ const Transaction = ({ id }) => { <div className='badge-red text-sm px-2'>Belum ada invoice</div> )} </div> - </div> */} + </div> <Divider /> - {/* {!auth?.feature.soApproval && ( + {!auth?.feature.soApproval && ( <div className='p-4 flex flex-col gap-y-4'> <DescriptionRow label='Purchase Order'> {transaction.data?.purchaseOrderName || '-'} </DescriptionRow> - <div className='flex items-center'> - <p className='text-gray_r-11 leading-none'>Dokumen PO</p> + <div className='flex items-center justify-between'> + <p className='text-gray_r-11 leading-none'>Dokumen PO : </p> <button type='button' className='inline-block text-danger-500' @@ -747,26 +712,22 @@ const Transaction = ({ id }) => { </div> )} - <Divider /> */} + <Divider /> <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} buyMore /> + <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?.paymentType - ? transaction.data?.paymentType - ?.replace(/_/g, ' ') - .replace(/\b\w/g, (char) => char.toUpperCase()) - : '-'} + {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?.pickings[0]?.weightTotal + ' 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'> @@ -822,7 +783,7 @@ const Transaction = ({ id }) => { {transaction.data?.status === 'draft' && ( <div className='p-4 pt-0'> <button - className='btn-yellow w-full mt-4' + className='btn-light w-full mt-4' disabled={transaction.data?.status != 'draft'} onClick={() => downloadQuotation(transaction.data)} > @@ -834,6 +795,15 @@ 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> + )} </div> )} </MobileView> @@ -876,15 +846,44 @@ const Transaction = ({ id }) => { {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> + + <div className="flex gap-x-4"> + <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> + )} + </div> + </div> + )} </div> - {transaction.data?.status === 'draft' && ( + {/* {transaction.data?.status === 'draft' && ( <div className='flex gap-x-4'> <button type='button' - className='btn-yellow px-3 py-2 mr-auto' + className='btn-light px-3 py-2 mr-auto' onClick={() => downloadQuotation(transaction.data)} > - Download + <Download size={12} /> </button> <button className='btn-solid-red' @@ -892,8 +891,18 @@ const Transaction = ({ id }) => { > Batalkan Transaksi </button> + + {transaction.data?.status == 'draft' && + transaction?.data?.purchaseOrderFile && ( + <button + className='btn-yellow' + onClick={openContinueTransaction} + > + Lanjutkan Transaksi + </button> + )} </div> - )} + )} */} <div className='grid grid-cols-2 gap-x-6 mt-4'> <div className='grid grid-cols-[35%_65%] gap-y-4'> @@ -1016,88 +1025,54 @@ const Transaction = ({ id }) => { </div> <div className='flex flex-col w-1/2 justify-start items-start'> <span className='text-h-sm font-medium mb-2'> - Info Pengiriman + Info Ekspedisi </span> <div className='grid grid-cols-[34%_2%_64%] gap-y-4 w-full'> - <div>Nomor Resi</div> - <div>: </div> - <div className='flex flex-row gap-1 '> - {transaction?.data?.pickings[0]?.trackingNumber || '-'} - {transaction?.data?.pickings[0]?.trackingNumber && ( - <button - className={`${ - copied ? 'text-gray-400' : 'text-red-600 ' - }`} - onClick={() => - handleCopyClick( - transaction?.data?.pickings[0]?.trackingNumber - ) - } - > - <svg - aria-hidden='true' - fill='none' - stroke='currentColor' - stroke-width='1.5' - viewBox='0 0 24 24' - className='w-5 h-6' - > - <path - d='M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75' - stroke-linecap='round' - stroke-linejoin='round' - ></path> - </svg> - </button> - )} - </div> - <div>Kurir</div> <div>: </div> - {transaction?.data?.pickings[0]?.carrierName ? ( + {transaction?.data?.carrierName ? ( <div className='flex flex-row w-full gap-1 items-center justify-start '> <p className=' text-nowrap'> - {transaction?.data?.pickings[0]?.carrierName} + {transaction?.data?.carrierName} </p> - <span - className='text-red-500 text-sm font-semibold hover:cursor-pointer' - onClick={() => - setIdAWB(transaction?.data?.pickings[0]?.id) - } - > - Lacak Pengiriman - </span> </div> ) : ( '-' )} + {transaction?.data?.carrierId !== 32 &&( + <> + <div>Jenis Service</div> + <div>: </div> + <div> + {' '} + {transaction?.data?.serviceType + ? transaction?.data?.serviceType + : '-'} + </div> + </> + )} - <div>Jenis Service</div> - <div>: </div> - <div> - {' '} - {transaction?.data?.pickings[0]?.serviceType && - transaction?.data?.pickings[0]?.carrierName - ? transaction?.data?.pickings[0]?.serviceType - : '-'} - </div> - - <div>Tanggal Kirim</div> + <div>Estimasi Tanggal Kirim</div> <div>: </div> <div> - {transaction?.data?.pickings[0]?.date - ? formatDate(transaction?.data?.pickings[0]?.date) - : '-'} - </div> - - <div>Estimasi Tiba</div> - <div>: </div> - <div className='text-red-500'> - {transaction?.data?.pickings[0]?.eta - ? transaction?.data?.pickings[0]?.eta + {transaction?.data?.expectedReadyToShip + ? transaction?.data?.expectedReadyToShip : '-'} </div> - {transaction?.data?.pickings[0] && ( + {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' @@ -1123,72 +1098,39 @@ const Transaction = ({ id }) => { </div> </div> - <div className='flex gap-x-3'> - <div className='w-1/2'> - <div className='text-h-sm font-semibold mt-10 mb-4'> - Informasi Pelanggan - </div> - <div className='border border-gray_r-6 rounded p-3'> - <div className='font-medium mb-4'>Detail Pelanggan</div> - <SectionContent - address={transaction?.data?.address?.customer} - /> - </div> - </div> - <div className='w-1/2'> - <div className='text-h-sm font-semibold mt-10 mb-4'> - Informasi Pengiriman + <div className='text-h-sm font-semibold mt-4 mb-4'> + Informasi Pengiriman + </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> - {transaction?.data?.pickings.length == 0 && ( - <div className='badge-red text-sm'> - Belum ada pengiriman - </div> - )} - {/* <div className='grid grid-cols-1 gap-1 w-1/2'> */} - {transaction?.data?.pickings?.map((airway) => ( - <div - key={airway?.id} - className='border border-gray_r-6 rounded p-3' - > - <InformationSection manifests={airway} /> - <div className='p-4'> - <button - className='bg-transparent text-red-600 hover:underline p-0 font-semibold' - onClick={() => { - if (airway?.waybillNumber == '-') { - toast.error('Nomor Resi belum tersedia'); - return; - } - setIdAWB(airway.id); - }} - > - Lacak Pengiriman - </button> - </div> + )} + {transaction?.data?.pickings?.map((airway) => ( + <div + key={airway?.id} + className='border border-gray_r-6 rounded p-3' + > + <InformationSection manifests={airway} /> + <div className='p-4'> + <button + className='bg-transparent text-red-600 hover:underline p-0 font-semibold' + onClick={() => { + if (airway?.waybillNumber == '-') { + toast.error('Nomor Resi belum tersedia'); + return; + } + setIdAWB(airway.id); + }} + > + Lacak Pengiriman + </button> </div> - // <button - // className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left h-20' - // key={airway?.id} - // onClick={() => setIdAWB(airway?.id)} - // > - // <div> - // <span className='text-sm text-gray_r-11'> - // No Resi : {airway?.trackingNumber || '-'}{' '} - // </span> - // <p className='mt-1 font-medium'>{airway?.name}</p> - // </div> - // <div className='flex gap-x-2'> - // <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1'> - // {airway?.delivered - // ? 'Pesanan Tiba' - // : 'Sedang Dikirim'} - // </div> - // <ChevronRightIcon className='w-5 stroke-2' /> - // </div> - // </button> - ))} - {/* </div> */} - </div> + </div> + ))} + {/* </div> */} + </div> <div className='flex '> diff --git a/src/lib/transaction/components/Transactions.jsx b/src/lib/transaction/components/Transactions.jsx index de93d742..5e37be50 100644 --- a/src/lib/transaction/components/Transactions.jsx +++ b/src/lib/transaction/components/Transactions.jsx @@ -345,10 +345,9 @@ const Transactions = ({ context = '' }) => { }, []); const handleBuyBack = async (products) => { - // if (status === 'success') return; - try { // setStatus('loading'); + console.log("Products to add:", products); const results = await Promise.all( products.map((product) => @@ -358,32 +357,38 @@ const Transactions = ({ context = '' }) => { id: product.id, qty: product.quantity, selected: true, - source: 'buy', // Tetap gunakan 'buy' agar bisa masuk ke halaman pembelian - qtyAppend: false, + source: 'add_to_cart', + qtyAppend: true, + }).catch(error => { + return { error, product }; }) ) ); - // ✅ Panggil setRefreshCart(true) setiap kali satu produk berhasil ditambahkan + 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`); + setRefreshCart(true); + router.push('/shop/cart'); + } + return; + } + // All operations succeeded setRefreshCart(true); - - // setStatus('idle'); toast.success('Semua produk berhasil ditambahkan ke keranjang belanja'); - // Tampilkan notifikasi - // toast({ - // title: 'Tambah ke keranjang', - // description: 'Semua produk berhasil ditambahkan ke keranjang belanja', - // status: 'success', - // duration: 3000, - // isClosable: true, - // position: 'top', - // }); - - // Redirect ke halaman checkout - router.push('/shop/checkout?source=buy'); + router.push('/shop/cart'); + } catch (error) { console.error('Gagal menambahkan produk ke keranjang:', error); + toast.error('Terjadi kesalahan saat menambahkan produk ke keranjang'); // setStatus('error'); } }; diff --git a/src/lib/treckingAwb/component/InformationSection.jsx b/src/lib/treckingAwb/component/InformationSection.jsx index a2297af3..4b3bd5fb 100644 --- a/src/lib/treckingAwb/component/InformationSection.jsx +++ b/src/lib/treckingAwb/component/InformationSection.jsx @@ -69,6 +69,10 @@ const InformationSection = ({ manifests }) => { <span className='text-red-600 font-semibold'>{manifests?.eta}</span> </span> </div> + <div className='grid grid-cols-[150px_auto]'> + <span>Total Product</span> + <span className='font-semibold'> : {Array.isArray(manifests?.products) ? manifests.products.length : 0} Product</span> + </div> </div> </div> ); diff --git a/src/lib/treckingAwb/component/Manifest.jsx b/src/lib/treckingAwb/component/Manifest.jsx index acb86f57..6eb0b0ac 100644 --- a/src/lib/treckingAwb/component/Manifest.jsx +++ b/src/lib/treckingAwb/component/Manifest.jsx @@ -223,6 +223,31 @@ const Manifest = ({ idAWB, closePopup }) => { ) } </div> + + {/* Barang */} + <div className='mt-1'> + {Array.isArray(manifests?.products) && manifests.products.length > 0 ? ( + <div className='flex flex-col gap-4'> + {manifests.products.map((product, idx) => ( + <div key={idx} className='flex gap-4 border-b pb-4'> + {/* Gambar Produk */} + <img + src={product.image} + alt={product.name} + className='w-16 h-16 object-contain border' + /> + {/* Info Produk */} + <div className='flex flex-col flex-1'> + <span className='font-semibold'>{product.name}</span> + <span className='text-sm text-gray-500'>{product.code}</span> + </div> + </div> + ))} + </div> + ) : ( + <span></span> + )} + </div> </BottomPopup> )} </> diff --git a/src/lib/variant/components/VariantGroupCard.jsx b/src/lib/variant/components/VariantGroupCard.jsx index 1e921546..7db9703b 100644 --- a/src/lib/variant/components/VariantGroupCard.jsx +++ b/src/lib/variant/components/VariantGroupCard.jsx @@ -10,7 +10,10 @@ const VariantGroupCard = ({ variants, ...props }) => { return ( <> {variantsToShow?.map((variant, index) => ( - <> + <div + key={index} + className='shadow border border-gray rounded-md p-4 mb-1 shadow-sm bg-white' + > <VariantCard key={index} product={variant} {...props} /> {variant.program && variant.program.items && @@ -48,7 +51,7 @@ const VariantGroupCard = ({ variants, ...props }) => { </div> </div> ))} - </> + </div> ))} {variants.length > 2 && ( <button diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index e14b0ca2..3d258a97 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -90,7 +90,8 @@ export default async function handler(req, res) { ]; if (orderBy === 'stock') { - filterQueries.push('stock_total_f:{0 TO *}'); + filterQueries.push('stock_total_f:{1 TO *}&sort=stock_total_f desc'); + // filterQueries.push(`stock_total_f DESC`) } if (fq && source != 'similar' && typeof fq != 'string') { diff --git a/src/pages/my/profile.jsx b/src/pages/my/profile.jsx index 887489e0..98e95b4f 100644 --- a/src/pages/my/profile.jsx +++ b/src/pages/my/profile.jsx @@ -18,6 +18,7 @@ import { useRouter } from 'next/router'; export default function Profile() { const auth = useAuth(); + console.log('auth', auth); const [isChecked, setIsChecked] = useState(false); const [ubahAkun, setUbahAkun] = useState(false); const [isAprove, setIsAprove] = useState(); @@ -73,7 +74,7 @@ export default function Profile() { <> <BottomPopup active={changeConfirmation} - close={() => setChangeConfirmation(false)} // Menutup popup + close={() => setChangeConfirmation(false)} //Menutup popup title="Ubah type akun" > <div className="leading-7 text-gray_r-12/80"> @@ -111,9 +112,9 @@ export default function Profile() { <p className="ml-2">Ubah ke akun bisnis</p> </div> ))} - {isChecked && ( + {isChecked && ubahAkun !== 'pending' && ( <div> - <SwitchAccount company_type="nonpkp" /> + <SwitchAccount company_type="nonpkp" setIsAprove={setIsAprove} setUbahAkun={setUbahAkun}/> <Divider /> </div> )} @@ -148,9 +149,9 @@ export default function Profile() { <p className="ml-2">Ubah ke akun bisnis</p> </div> ))} - {isChecked && ( + {isChecked && ubahAkun !== 'pending' && ( <div> - <SwitchAccount company_type="nonpkp" /> + <SwitchAccount company_type="nonpkp" setIsAprove={setIsAprove} setUbahAkun={setUbahAkun}/> <Divider /> </div> )} |
