summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/lib/address/components/CreateAddress.jsx82
-rw-r--r--src/lib/address/components/EditAddress.jsx223
-rw-r--r--src/lib/checkout/components/Checkout.jsx50
-rw-r--r--src/lib/maps/components/PinPointMap.jsx21
-rw-r--r--src/lib/maps/stores/useMaps.js16
-rw-r--r--src/lib/transaction/components/Transaction.jsx576
-rw-r--r--src/lib/transaction/components/TransactionStatusBadge.jsx4
-rw-r--r--src/lib/transaction/components/Transactions.jsx922
8 files changed, 912 insertions, 982 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 c789edc1..95916b22 100644
--- a/src/lib/checkout/components/Checkout.jsx
+++ b/src/lib/checkout/components/Checkout.jsx
@@ -1320,25 +1320,34 @@ const Checkout = () => {
{selectedCourierId == SELF_PICKUP_ID && (
<PickupAddress label='Alamat Pickup' />
)}
- {selectedCourierId != 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>
+ {selectedCarrierId != SELF_PICKUP_ID && (
+ <>
+ {(!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 />
@@ -1577,7 +1586,8 @@ const Checkout = () => {
!products ||
products?.length == 0 ||
priceCheck ||
- hasNoPrice
+ hasNoPrice ||
+ isLoading
}
>
{isLoading ? 'Loading...' : 'Lanjut Pembayaran'}
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;
},
diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx
index 77e60dc1..96c89aec 100644
--- a/src/lib/transaction/components/Transaction.jsx
+++ b/src/lib/transaction/components/Transaction.jsx
@@ -43,13 +43,14 @@ import { gtagPurchase } from '@/core/utils/googleTag';
import { deleteItemCart } from '@/core/utils/cart';
import {
downloadInvoice,
- downloadTaxInvoice,
+ // downloadTaxInvoice, // (unused)
} from '@/lib/invoice/utils/invoices';
import { Download } from 'lucide-react';
import axios from 'axios';
import InformationSection from '../../treckingAwb/component/InformationSection';
-import { Button } from '@chakra-ui/react';
-import { div } from 'lodash-contrib';
+// import { Button } from '@chakra-ui/react'; // (unused)
+// import { div } from 'lodash-contrib'; // (unused)
+
const Transaction = ({ id }) => {
const PPN = process.env.NEXT_PUBLIC_PPN;
const router = useRouter();
@@ -73,6 +74,7 @@ const Transaction = ({ id }) => {
const [toOthers, setToOthers] = useState(null);
const [totalAmount, setTotalAmount] = useState(0);
const [totalDiscountAmount, setTotalDiscountAmount] = useState(0);
+ const [contLoading, setContLoading] = useState(false);
useEffect(() => {
if (transaction?.data?.products) {
@@ -88,6 +90,7 @@ const Transaction = ({ id }) => {
setTotalDiscountAmount(calculateTotalDiscountAmount);
}
}, [transaction.data, transaction.isLoading]);
+
const submitUploadPo = async () => {
const file = poFile.current.files[0];
const name = poNumber.current.value;
@@ -127,10 +130,6 @@ const Transaction = ({ id }) => {
}
}
};
- // const ContinueTransaction = () => {
- // setContinueNoPo(true);
- // checkoutNoPO();
- // };
const closeCancelTransaction = () => setCancelTransaction(false);
const closeContinueTransaction = () => setContinueTransaction(false);
@@ -138,6 +137,7 @@ const Transaction = ({ id }) => {
const openRejectTransaction = () => setRejectTransaction(true);
const closeRejectTransaction = () => setRejectTransaction(false);
+
const submitCancelTransaction = async () => {
const isCancelled = await cancelTransactionApi({
transaction: transaction.data,
@@ -148,6 +148,7 @@ const Transaction = ({ id }) => {
}
closeCancelTransaction();
};
+
const checkout = async () => {
if (!transaction.data?.purchaseOrderFile) {
toast.error('Mohon upload dokumen PO anda sebelum melanjutkan pesanan');
@@ -194,25 +195,6 @@ const Transaction = ({ id }) => {
}
toast.success('Berhasil melanjutkan pesanan');
transaction.refetch();
- // console.log(transaction);
-
- /* const midtrans = async () => {
- for (const product of products) deleteItemCart({ productId: product.id });
- if (grandTotal > 0) {
- const payment = await axios.post(
- `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/midtrans-payment?transactionId=${isCheckouted.id}`
- );
- setIsLoading(false);
- window.location.href = payment.data.redirectUrl;
- } else {
- window.location.href = `${
- process.env.NEXT_PUBLIC_SELF_HOST
- }/shop/checkout/success?order_id=${isCheckouted.name.replace(
- /\//g,
- '-'
- )}`;
- }
- };*/
};
const handleApproval = async () => {
@@ -227,6 +209,73 @@ const Transaction = ({ id }) => {
transaction.refetch();
};
+ // ===== Bayar Sekarang (pakai link dari backend; fallback generate via Next API) =====
+ const handlePayNow = async () => {
+ try {
+ setContLoading(true);
+
+ const base = (process.env.NEXT_PUBLIC_ODOO_API_HOST || '').replace(
+ /\/$/,
+ ''
+ );
+ const token = auth?.token;
+ const partnerId = auth?.partnerId;
+
+ // 1) Minta Odoo ensure payment link
+ const { data: resp } = await axios.get(
+ `${base}/api/v1/partner/${partnerId}/sale_order/${transaction.data.id}`,
+ {
+ params: { ensure_payment_link: 1, ts: Date.now() },
+ headers: { Token: token },
+ }
+ );
+
+ // console.log('API Response:', resp); // Debug
+
+ // 2) Akses semua kemungkinan path
+ let url =
+ resp?.result?.payment_summary?.redirect_url ||
+ resp?.data?.result?.payment_summary?.redirect_url ||
+ resp?.payment_summary?.redirect_url ||
+ resp?.paymentSummary?.redirectUrl ||
+ '';
+
+ // console.log('Extracted URL:', url); // Debug
+
+ if (url) {
+ window.location.href = url;
+ return;
+ }
+
+ // 3) Fallback
+ await transaction.refetch();
+ // console.log('Transaction data:', transaction.data); // Debug
+
+ url =
+ transaction?.data?.result?.payment_summary?.redirect_url ||
+ transaction?.data?.paymentSummary?.redirectUrl ||
+ transaction?.data?.payment_summary?.redirect_url ||
+ '';
+
+ // console.log('Fallback URL:', url); // Debug
+
+ if (url) {
+ window.location.href = url;
+ return;
+ }
+
+ throw new Error('Link pembayaran belum tersedia.');
+ } catch (e) {
+ toast.error(
+ e?.response?.data?.description ||
+ e?.message ||
+ 'Gagal membuka pembayaran'
+ );
+ } finally {
+ setContLoading(false);
+ }
+ };
+
const memoizeVariantGroupCard = useMemo(
() => (
<div className='p-4 pt-0 flex flex-col gap-y-3'>
@@ -314,7 +363,7 @@ const Transaction = ({ id }) => {
navigator.clipboard.writeText(textToCopy);
setCopied(true);
toast.success('No Resi Berhasil di Copy');
- setTimeout(() => setCopied(false), 2000); // Reset copied state after 2 seconds
+ setTimeout(() => setCopied(false), 2000);
};
const formatDate = (dateString) => {
@@ -336,7 +385,7 @@ const Transaction = ({ id }) => {
const [day, month, year] = dateString.split('/');
return `${day} ${months[parseInt(month, 10) - 1]} ${year}`;
};
- // console.log(transaction);
+
return (
transaction.data?.name && (
<>
@@ -366,6 +415,7 @@ const Transaction = ({ id }) => {
</button>
</div>
</BottomPopup>
+
<BottomPopup
active={cancelTransaction}
close={closeCancelTransaction}
@@ -452,48 +502,53 @@ const Transaction = ({ id }) => {
active={toOthers}
close={() => setToOthers(null)}
>
- <div className='flex flex-col gap-y-4 mt-2'>
+ {transaction.data?.status === 'draft' && (
+ <>
<button
className='text-left disabled:opacity-60'
- disabled={!toOthers?.purchaseOrderFile}
+ disabled={toOthers?.status != 'draft'}
onClick={() => {
- downloadPurchaseOrder(toOthers);
+ downloadQuotation(toOthers);
setToOthers(null);
}}
>
- Download PO
+ Download Quotation
</button>
<button
className='text-left disabled:opacity-60'
- disabled={toOthers?.status != 'draft'}
+ disabled={toOthers?.status != 'waiting'}
onClick={() => {
- downloadQuotation(toOthers);
+ setToCancel(toOthers);
setToOthers(null);
}}
>
- Download Quotation
+ Batalkan Transaksi
</button>
+ </>
+ )}
+ <div className='flex flex-col gap-y-4 mt-2'>
<button
className='text-left disabled:opacity-60'
- disabled={toOthers?.status != 'waiting'}
+ disabled={!toOthers?.purchaseOrderFile}
onClick={() => {
- setToCancel(toOthers);
+ downloadPurchaseOrder(toOthers);
setToOthers(null);
}}
>
- Batalkan Transaksi
+ Download PO
</button>
</div>
</BottomPopup>
<Manifest idAWB={idAWB} closePopup={closePopup}></Manifest>
+ {/* ============ MOBILE ============ */}
<MobileView>
<div className='px-4'>
<div className='flex flex-row w-full justify-between items-center py-2 px-3 mb-4 text-sm border border-yellow-500 text-yellow-800 rounded-lg bg-yellow-50 gap-2'>
- <div class='flex items-center w-full ' role='alert'>
+ <div className='flex items-center w-full ' role='alert'>
<svg
- class='flex-shrink-0 inline w-4 h-4 mr-2'
+ className='flex-shrink-0 inline w-4 h-4 mr-2'
aria-hidden='true'
fill='currentColor'
viewBox='0 0 20 20'
@@ -514,6 +569,7 @@ const Transaction = ({ id }) => {
</span>
</div>
</div>
+
{auth?.feature?.soApproval && (
<div className='p-4'>
<StepApproval
@@ -559,7 +615,7 @@ const Transaction = ({ id }) => {
<Divider />
<div className='flex flex-col gap-y-4 p-4'>
- <h4 className="font-semibold">Detail Order</h4>
+ <h4 className='font-semibold'>Detail Order</h4>
<DescriptionRow label='No Transaksi'>
<p className='font-semibold'>{transaction.data?.name}</p>
</DescriptionRow>
@@ -579,9 +635,11 @@ const Transaction = ({ id }) => {
<Divider />
<div className='flex flex-col gap-y-4 p-4'>
- <h4 className="font-semibold">Alamat Pengiriman</h4>
+ <h4 className='font-semibold'>Alamat Pengiriman</h4>
<DescriptionRow label='Nama Penerima'>
- <p className='font-semibold'>{transaction?.data?.address?.customer?.name}</p>
+ <p className='font-semibold'>
+ {transaction?.data?.address?.customer?.name}
+ </p>
</DescriptionRow>
<DescriptionRow label='No. Telp'>
{transaction?.data?.address?.customer?.phone
@@ -602,9 +660,7 @@ const Transaction = ({ id }) => {
<div className='p-4'>
<div className='font-medium mb-4'>Info Pengiriman</div>
{transaction?.data?.pickings.length == 0 && (
- <div className='badge-red text-sm'>
- Belum ada pengiriman
- </div>
+ <div className='badge-red text-sm'>Belum ada pengiriman</div>
)}
{transaction?.data?.pickings?.map((airway) => (
<div
@@ -627,28 +683,7 @@ const Transaction = ({ id }) => {
</button>
</div>
</div>
- // <button
- // className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left h-20'
- // key={airway?.id}
- // onClick={() => setIdAWB(airway?.id)}
- // >
- // <div>
- // <span className='text-sm text-gray_r-11'>
- // No Resi : {airway?.trackingNumber || '-'}{' '}
- // </span>
- // <p className='mt-1 font-medium'>{airway?.name}</p>
- // </div>
- // <div className='flex gap-x-2'>
- // <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1'>
- // {airway?.delivered
- // ? 'Pesanan Tiba'
- // : 'Sedang Dikirim'}
- // </div>
- // <ChevronRightIcon className='w-5 stroke-2' />
- // </div>
- // </button>
))}
-
</div>
<Divider />
@@ -717,17 +752,20 @@ const Transaction = ({ id }) => {
<div className='font-medium p-4'>Detail Produk</div>
{transaction?.data?.products.length > 0 ? (
<div className='p-4 pt-0 flex flex-col gap-y-3'>
- <VariantGroupCard variants={transaction.data?.products}/>
+ <VariantGroupCard variants={transaction.data?.products} />
<div className='font-medium'>Rincian Pembayaran</div>
<div className='flex justify-between mt-1'>
<p className='text-gray_r-12/70'>Metode Pembayaran</p>
- <p>
- {transaction.data?.paymentTerm || '-'}
- </p>
+ <p>{transaction.data?.paymentTerm || '-'}</p>
</div>
<div className='flex justify-between mt-1'>
<p className='text-gray_r-12/70'>Berat Barang</p>
- <p>{(transaction.data?.products?.reduce((total, item) => total + (item.weight || 0), 0)) + ' Kg'}</p>
+ <p>
+ {transaction.data?.products?.reduce(
+ (total, item) => total + (item.weight || 0),
+ 0
+ ) + ' Kg'}
+ </p>
</div>
<hr className='mt-1 border border-gray-100' />
<div className='flex justify-between mt-1'>
@@ -774,12 +812,7 @@ const Transaction = ({ id }) => {
</div>
)}
- {/* <Divider /> */}
-
- {/* <SectionAddress address={transaction.data?.address} /> */}
-
- {/* <Divider /> */}
-
+ {/* Tombol aksi (Mobile) */}
{transaction.data?.status === 'draft' && (
<div className='p-4 pt-0'>
<button
@@ -796,18 +829,33 @@ const Transaction = ({ id }) => {
Batalkan Transaksi
</button>
{transaction.data?.status == 'draft' &&
- transaction?.data?.purchaseOrderFile && (
- <button
- className='btn-yellow w-full mt-4'
- onClick={openContinueTransaction}
- >
- Lanjutkan Transaksi
- </button>
- )}
+ transaction?.data?.purchaseOrderFile && (
+ <button
+ className='btn-yellow w-full mt-4'
+ onClick={openContinueTransaction}
+ >
+ Lanjutkan Transaksi
+ </button>
+ )}
+ </div>
+ )}
+
+ {/* Bayar Sekarang (Mobile) — tampil jika eligible */}
+ {transaction.data?.eligibleContinue && (
+ <div className='p-4 pt-0'>
+ <button
+ type='button'
+ disabled={contLoading}
+ onClick={handlePayNow}
+ className='w-full py-2 text-center rounded-md border border-red-500 text-red-500 bg-white disabled:opacity-60'
+ >
+ {contLoading ? 'Memproses…' : 'Bayar Sekarang'}
+ </button>
</div>
)}
</MobileView>
+ {/* ============ DESKTOP ============ */}
<DesktopView>
<div className='container mx-auto flex py-10'>
<div className='w-3/12 pr-4'>
@@ -827,44 +875,36 @@ const Transaction = ({ id }) => {
)}
</div>
- {/*new-release*/}
- {/*<div className='flex items-center justify-between mb-3'>*/}
- {/* <div className='flex items-center gap-x-2'>*/}
- {/* <span className='text-h-sm font-medium'>*/}
- {/* {transaction?.data?.name}*/}
- {/* </span>*/}
- {/* <TransactionStatusBadge status={transaction?.data?.status} />*/}
- {/* </div>*/}
- {/* <div className='text-h-sm'>*/}
- {/* Estimasi Barang Siap:{' '}*/}
- {/* <span className='text-red-500 font-semibold'>*/}
- {/* {transaction?.data?.expectedReadyToShip}*/}
- {/* </span>*/}
- {/* </div>*/}
- <div className='flex items-center gap-x-2 mb-3'>
- <span className='text-h-sm font-medium'>
- {transaction?.data?.name}
- </span>
- <TransactionStatusBadge status={transaction?.data?.status} />
- {transaction.data?.status === 'draft' && (
- <div className='flex items-center justify-between w-full'>
- <button
- type='button'
- className='btn-light px-3 py-2'
- onClick={() => downloadQuotation(transaction.data)}
- >
- <Download size={12} />
- </button>
+ {/* HEADER (Desktop) — sejajarkan kiri & kanan */}
+ <div className='flex items-center justify-between gap-3 mb-3'>
+ {/* Kiri: SO + badge */}
+ <div className='flex items-center gap-x-2 min-w-0'>
+ <span className='text-h-sm font-medium truncate'>
+ {transaction?.data?.name}
+ </span>
+ <TransactionStatusBadge status={transaction?.data?.status} />
+ </div>
+
+ {/* Kanan: aksi */}
+ <div className='flex items-center gap-3'>
+ {transaction.data?.status === 'draft' && (
+ <>
+ <button
+ type='button'
+ className='btn-light px-3 py-2'
+ onClick={() => downloadQuotation(transaction.data)}
+ >
+ <Download size={12} />
+ </button>
- <div className="flex gap-x-4">
<button
className='btn-solid-red'
onClick={openCancelTransaction}
>
Batalkan Transaksi
</button>
- {transaction.data?.status == 'draft' &&
- transaction?.data?.purchaseOrderFile && (
+
+ {transaction?.data?.purchaseOrderFile && (
<button
className='btn-yellow'
onClick={openContinueTransaction}
@@ -872,37 +912,20 @@ const Transaction = ({ id }) => {
Lanjutkan Transaksi
</button>
)}
- </div>
- </div>
- )}
- </div>
- {/* {transaction.data?.status === 'draft' && (
- <div className='flex gap-x-4'>
- <button
- type='button'
- className='btn-light px-3 py-2 mr-auto'
- onClick={() => downloadQuotation(transaction.data)}
- >
- <Download size={12} />
- </button>
- <button
- className='btn-solid-red'
- onClick={openCancelTransaction}
- >
- Batalkan Transaksi
- </button>
-
- {transaction.data?.status == 'draft' &&
- transaction?.data?.purchaseOrderFile && (
- <button
- className='btn-yellow'
- onClick={openContinueTransaction}
- >
- Lanjutkan Transaksi
- </button>
- )}
+ </>
+ )}
+
+ {transaction.data?.eligibleContinue && (
+ <button
+ className='px-4 py-2 rounded-md border border-red-500 text-red-500 bg-white disabled:opacity-60 mb-3'
+ disabled={contLoading}
+ onClick={handlePayNow}
+ >
+ {contLoading ? 'Memproses…' : 'Bayar Sekarang'}
+ </button>
+ )}
</div>
- )} */}
+ </div>
<div className='grid grid-cols-2 gap-x-6 mt-4'>
<div className='grid grid-cols-[35%_65%] gap-y-4'>
@@ -967,29 +990,13 @@ const Transaction = ({ id }) => {
key={index}
>
{invoice?.name}
- {/* <div className='shadow rounded-md p-4 text-gray_r-12 font-normal flex justify-between'>
- <div>
- <p className='mb-1'>{invoice?.name}</p>
- <div className='flex items-center gap-x-1'>
- {invoice.amountResidual > 0 ? (
- <div className='badge-red'>Belum Lunas</div>
- ) : (
- <div className='badge-green'>Lunas</div>
- )}
- <p className='text-caption-2 text-gray_r-11'>
- {currencyFormat(invoice.amountTotal)}
- </p>
- </div>
- </div>
- <ChevronRightIcon className='w-5 stroke-2' />
- </div> */}
</Link>
))}
</div>
</div>
</div>
<hr className='mt-4 mb-4 border border-gray-100' />
- {/* <div className='grid grid-cols-2 gap-x-6'> */}
+
<div className='flex flex-row justify-between items-start w-full h-fit '>
<div className='flex flex-col w-1/2 justify-start items-start'>
<span className='text-h-sm font-medium mb-2'>
@@ -1039,17 +1046,17 @@ const Transaction = ({ id }) => {
) : (
'-'
)}
- {transaction?.data?.carrierId !== 32 &&(
- <>
- <div>Jenis Service</div>
- <div>: </div>
- <div>
- {' '}
- {transaction?.data?.serviceType
- ? transaction?.data?.serviceType
- : '-'}
- </div>
- </>
+ {transaction?.data?.carrierId !== 32 && (
+ <>
+ <div>Jenis Service</div>
+ <div>: </div>
+ <div>
+ {' '}
+ {transaction?.data?.serviceType
+ ? transaction?.data?.serviceType
+ : '-'}
+ </div>
+ </>
)}
<div>Estimasi Tanggal Kirim</div>
@@ -1059,41 +1066,42 @@ const Transaction = ({ id }) => {
? transaction?.data?.expectedReadyToShip
: '-'}
</div>
- {transaction?.data?.carrierId !== 32 &&(
- <>
- <div>Estimasi Tiba</div>
- <div>: </div>
- <div className=''>
- {transaction?.data?.etaDateStart && transaction?.data?.etaDateEnd ? (
- `${transaction.data.etaDateStart} - ${transaction.data.etaDateEnd}`
- ) : (
- '-'
- )}
- </div>
- </>
+ {transaction?.data?.carrierId !== 32 && (
+ <>
+ <div>Estimasi Tiba</div>
+ <div>: </div>
+ <div className=''>
+ {transaction?.data?.etaDateStart &&
+ transaction?.data?.etaDateEnd
+ ? `${transaction.data.etaDateStart} - ${transaction.data.etaDateEnd}`
+ : '-'}
+ </div>
+ </>
)}
- {transaction?.data?.pickings[0] && transaction?.data?.carrierId !== 32 && (
- <div className='w-full bagian-informasi col-span-3'>
- <div
- class='flex items-center w-fit py-2 px-3 mb-4 text-sm border border-yellow-500 text-yellow-800 rounded-lg bg-yellow-50'
- role='alert'
- >
- <svg
- class='flex-shrink-0 inline w-4 h-4 mr-2'
- aria-hidden='true'
- fill='currentColor'
- viewBox='0 0 20 20'
+ {transaction?.data?.pickings[0] &&
+ transaction?.data?.carrierId !== 32 && (
+ <div className='w-full bagian-informasi col-span-3'>
+ <div
+ className='flex items-center w-fit py-2 px-3 mb-4 text-sm border border-yellow-500 text-yellow-800 rounded-lg bg-yellow-50'
+ role='alert'
>
- <path d='M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z' />
- </svg>
- <div className='text-justify flex flex-col gap-1'>
- <span className='text-black text-xs'>
- Pesanan anda mungkin mengalami keterlambatan tiba
- </span>
+ <svg
+ className='flex-shrink-0 inline w-4 h-4 mr-2'
+ aria-hidden='true'
+ fill='currentColor'
+ viewBox='0 0 20 20'
+ >
+ <path d='M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z' />
+ </svg>
+ <div className='text-justify flex flex-col gap-1'>
+ <span className='text-black text-xs'>
+ Pesanan anda mungkin mengalami keterlambatan
+ tiba
+ </span>
+ </div>
</div>
</div>
- </div>
- )}
+ )}
</div>
</div>
</div>
@@ -1103,9 +1111,7 @@ const Transaction = ({ id }) => {
</div>
<div className='grid grid-cols-1 md:grid-cols-2 gap-3'>
{transaction?.data?.pickings.length == 0 && (
- <div className='badge-red text-sm'>
- Belum ada pengiriman
- </div>
+ <div className='badge-red text-sm'>Belum ada pengiriman</div>
)}
{transaction?.data?.pickings?.map((airway) => (
<div
@@ -1129,39 +1135,9 @@ const Transaction = ({ id }) => {
</div>
</div>
))}
- {/* </div> */}
-
</div>
<div className='flex '>
- {/*New release*/}
- {/* <div className='grid grid-cols-1 gap-1 w-2/3'>*/}
- {/* {transaction?.data?.pickings?.map((airway) => (*/}
- {/* <button*/}
- {/* className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left h-20'*/}
- {/* key={airway?.id}*/}
- {/* onClick={() => setIdAWB(airway?.id)}*/}
- {/* >*/}
- {/* <div>*/}
- {/* <p className='text-sm text-gray_r-11'>*/}
- {/* {airway?.name}*/}
- {/* </p>*/}
- {/* <span className='text-md text-bold mt-1'>*/}
- {/* No Resi : {airway?.trackingNumber || '-'}{' '}*/}
- {/* </span>*/}
- {/* </div>*/}
- {/* <div className='flex gap-x-2'>*/}
- {/* <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1 text-center'>*/}
- {/* {airway?.delivered*/}
- {/* ? 'Pesanan Tiba'*/}
- {/* : 'Sedang Dikirim'}*/}
- {/* </div>*/}
- {/* <ChevronRightIcon className='w-5 stroke-2' />*/}
- {/* </div>*/}
- {/* </button>*/}
- {/* ))}*/}
- {/* </div>*/}
- {/*</div>*/}
<div className='invoice w-1/2 '>
<div className='text-h-sm font-semibold mt-10 mb-4 '>
Invoice
@@ -1202,7 +1178,6 @@ const Transaction = ({ id }) => {
<thead>
<tr>
<th>Nama Produk</th>
- {/* <th>Diskon</th> */}
<th>Jumlah</th>
<th>Harga</th>
<th>Subtotal</th>
@@ -1280,24 +1255,13 @@ const Transaction = ({ id }) => {
)}
</div>
</td>
- {/* <td>
- {product.price.discountPercentage > 0
- ? `${product.price.discountPercentage}%`
- : ''}
- </td> */}
<td>{product.quantity}</td>
<td>
- {/* {product.price.discountPercentage > 0 && (
- <div className='line-through mb-1 text-caption-1 text-gray_r-12/70'>
- {currencyFormat(product.price.price)}
- </div>
- )} */}
<div>
{currencyFormat(product.price.priceDiscount)}
</div>
</td>
<td>{currencyFormat(product.price.subtotal)}</td>
- {/* {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (transaction.data.isReaject == false) && ( */}
{auth?.feature.soApproval &&
(auth.webRole == 2 || auth.webRole == 3) &&
router.asPath.includes('/my/quotations/') &&
@@ -1354,34 +1318,6 @@ const Transaction = ({ id }) => {
)}
{transaction?.data?.products?.length > 0 && (
- // <div className='flex justify-end mt-4'>
- // <div className='w-1/4 grid grid-cols-2 gap-y-3 text-gray_r-12/80'>
- // <div className='text-right'>Subtotal</div>
- // <div className='text-right font-medium'>
- // {currencyFormat(transaction.data?.amountUntaxed)}
- // </div>
-
- // <div className='text-right'>
- // PPN {((PPN - 1) * 100).toFixed(0)}%
- // </div>
- // <div className='text-right font-medium'>
- // {currencyFormat(transaction.data?.amountTax)}
- // </div>
-
- // <div className='text-right whitespace-nowrap'>
- // Biaya Pengiriman
- // </div>
- // <div className='text-right font-medium'>
- // {currencyFormat(transaction.data?.deliveryAmount)}
- // </div>
-
- // <div className='text-right'>Grand Total</div>
- // <div className='text-right font-medium text-gray_r-12'>
- // {currencyFormat(transaction.data?.amountTotal)}
- // </div>
- // </div>
- // </div>
-
<div className='flex justify-end mt-4 flex-col items-end'>
<div className='w-1/4 grid grid-cols-2 gap-y-3 text-gray_r-12/80'>
<div className='text-right'>Total Belanja</div>
@@ -1437,7 +1373,6 @@ const Transaction = ({ id }) => {
<thead>
<tr>
<th>Nama Produk</th>
- {/* <th>Diskon</th> */}
<th>Jumlah</th>
<th>Harga</th>
<th>Subtotal</th>
@@ -1480,18 +1415,8 @@ const Transaction = ({ id }) => {
</div>
</div>
</td>
- {/* <td>
- {product.price.discountPercentage > 0
- ? `${product.price.discountPercentage}%`
- : ''}
- </td> */}
<td>{product.quantity}</td>
<td>
- {/* {product.price.discountPercentage > 0 && (
- <div className='line-through mb-1 text-caption-1 text-gray_r-12/70'>
- {currencyFormat(product.price.price)}
- </div>
- )} */}
<div>
{currencyFormat(product.price.priceDiscount)}
</div>
@@ -1512,58 +1437,6 @@ const Transaction = ({ id }) => {
</div>
</div>
</DesktopView>
-
- {/* {queryAirwayBill.data?.airways?.map((airway) => (
- <BottomPopup
- key={airway.waybillNumber}
- title='Detail Pengiriman'
- active={airwayBillPopup == airway.waybillNumber}
- close={() => setAirwayBillPopup(null)}
- >
- <div className='flex flex-col gap-y-4 my-4'>
- <div className='flex justify-between'>
- <div className='text-gray_r-11'>No Pengiriman</div>
- <div>{airway?.deliveryOrder?.name}</div>
- </div>
- <div className='flex justify-between'>
- <div className='text-gray_r-11'>Kurir</div>
- <div>{airway?.deliveryOrder?.carrier}</div>
- </div>
- <div className='flex justify-between'>
- <div className='text-gray_r-11'>No Resi</div>
- <div>{airway?.waybillNumber}</div>
- </div>
- </div>
-
- <div className='pt-4'>
- <div className='font-semibold text-body-1 mb-4'>Status Pengiriman</div>
- <ol class='relative border-l border-gray_r-7'>
- {airway?.manifests?.map((manifest, index) => (
- <li class='mb-6 ml-4' key={index}>
- <div
- class={`absolute w-3 h-3 rounded-full mt-1.5 -left-1.5 border ${
- index == 0 ? 'bg-red-600 border-red-600' : 'bg-gray_r-7 border-white'
- }`}
- />
- <time class='text-sm leading-none text-gray-400'>
- {manifest.datetime}
- </time>
- <p
- class={`leading-6 font-medium text-body-2 mt-2 ${
- index == 0 ? 'text-red-600' : 'text-gray_r-11'
- }`}
- >
- {manifest.description}
- </p>
- </li>
- ))}
- {(!airway?.manifests || airway?.manifests?.length == 0) && (
- <div className='badge-red text-sm'>Belum ada pengiriman</div>
- )}
- </ol>
- </div>
- </BottomPopup>
- ))} */}
</>
)
);
@@ -1589,24 +1462,7 @@ const SectionAddress = ({ address }) => {
{section.customer && <SectionContent address={address?.customer} />}
- {/* <Divider />
-
- <SectionButton
- label='Detail Pengiriman'
- active={section.shipping}
- toggle={() => toggleSection('shipping')}
- />
-
- {section.shipping && <SectionContent address={address?.shipping} />}
-
- <Divider />
-
- <SectionButton
- label='Detail Penagihan'
- active={section.invoice}
- toggle={() => toggleSection('invoice')}
- />
- {section.invoice && <SectionContent address={address?.invoice} />} */}
+ {/* Bagian shipping/invoice disembunyikan */}
</>
);
};
diff --git a/src/lib/transaction/components/TransactionStatusBadge.jsx b/src/lib/transaction/components/TransactionStatusBadge.jsx
index cb8cbcd9..d23b17cd 100644
--- a/src/lib/transaction/components/TransactionStatusBadge.jsx
+++ b/src/lib/transaction/components/TransactionStatusBadge.jsx
@@ -4,6 +4,10 @@ const TransactionStatusBadge = ({ status }) => {
text: ''
}
switch (status) {
+ case 'belum_bayar':
+ badgeProps.className.push('badge-solid-red')
+ badgeProps.text = 'Belum Bayar'
+ break
case 'cancel':
badgeProps.className.push('badge-solid-red')
badgeProps.text = 'Pesanan Batal'
diff --git a/src/lib/transaction/components/Transactions.jsx b/src/lib/transaction/components/Transactions.jsx
index 7efa773a..600518fa 100644
--- a/src/lib/transaction/components/Transactions.jsx
+++ b/src/lib/transaction/components/Transactions.jsx
@@ -1,11 +1,11 @@
+import axios from 'axios';
import { useRouter } from 'next/router';
-import { useEffect, useState, useRef } from 'react';
+import { useEffect, useState, useRef } from 'react';
import { toast } from 'react-hot-toast';
import {
EllipsisVerticalIcon,
MagnifyingGlassIcon,
ChevronDownIcon,
- ChevronUpIcon,
} from '@heroicons/react/24/outline';
import useAuth from '@/core/hooks/useAuth';
import {
@@ -38,17 +38,13 @@ import { Navigation } from 'swiper';
import 'swiper/css';
import 'swiper/css/navigation';
import { Calendar } from 'lucide-react';
-import DatePicker from 'react-datepicker';
-import 'react-datepicker/dist/react-datepicker.css';
import { DateRangePicker } from 'react-date-range';
-import { addDays } from 'date-fns';
-import 'react-date-range/dist/styles.css'; // main style file
-import 'react-date-range/dist/theme/default.css'; // theme css file
-import { Popover } from '@headlessui/react';
+import 'react-date-range/dist/styles.css';
+import 'react-date-range/dist/theme/default.css';
+
const Transactions = ({ context = '' }) => {
const auth = useAuth();
const router = useRouter();
- const swiperRef = useRef(null);
const {
q = '',
page = 1,
@@ -59,15 +55,11 @@ const Transactions = ({ context = '' }) => {
startDate = null,
endDate = new Date(),
} = router.query;
- const {
- productCart,
- setRefreshCart,
- setProductCart,
- refreshCart,
- isLoading,
- setIsloading,
- } = useProductCartContext();
+ const { setRefreshCart } = useProductCartContext();
+
const [inputQuery, setInputQuery] = useState(q);
+ const [cachedAllData, setCachedAllData] = useState(null); // Simpan data "All"
+ const [currentData, setCurrentData] = useState([]); // Data yang ditampilkan
const [toOthers, setToOthers] = useState(null);
const [toCancel, setToCancel] = useState(null);
const [listSites, setListSites] = useState([]);
@@ -75,17 +67,16 @@ const Transactions = ({ context = '' }) => {
const [siteFilter, setSiteFilter] = useState(site);
const [pageNew, setPageNew] = useState(page);
const [limitNew, setLimitNew] = useState(limit);
- // const [status, setStatus] = useState('idle');
const [statusNew, setStatusNew] = useState(status);
const [sortNew, setSortNew] = useState(sort);
const [contextNew, setcontextNew] = useState(router.query.context || context);
- const [dateRange, setDateRange] = useState([null, null]);
- // const [startDate, endDate] = dateRange;
const [isOpenCalender, setIsOpenCalender] = useState(false);
- const [cachedAllData, setCachedAllData] = useState(null); // Simpan data "All"
- const [currentData, setCurrentData] = useState([]); // Data yang ditampilkan
const calendarRef = useRef(null);
- const [isDateSelected, setIsDateSelected] = useState(false);
+ const isUnpaid = (s) =>
+ ['belum_bayar'].includes(String(s || '').toLowerCase());
+
+ // loading id utk tombol lanjutkan transaksi
+ const [contLoadingId, setContLoadingId] = useState(null);
const parseDate = (date) => {
if (!date || date === 'null') return null;
@@ -96,7 +87,7 @@ const Transactions = ({ context = '' }) => {
const [state, setState] = useState([
{
- startDate: startDate != null || 'null' ? parseDate(startDate) : null, // Gunakan `parseDate`
+ startDate: startDate != null || 'null' ? parseDate(startDate) : null,
endDate: startDate == null ? endDate : parseDate(endDate),
key: 'selection',
},
@@ -116,9 +107,11 @@ const Transactions = ({ context = '' }) => {
site:
siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null),
};
+
const statuses = [
{ id: 'all', label: 'Semua' },
{ id: 'quotation', label: 'Pending Quotation' },
+ { id: 'belum_bayar', label: 'Belum Bayar' },
{ id: 'diproses', label: 'Pesanan Diproses' },
{ id: 'dikemas', label: 'Pesanan Dikemas' },
{ id: 'partial', label: 'Dikirim Sebagian' },
@@ -135,6 +128,7 @@ const Transactions = ({ context = '' }) => {
shipping: 'Pesanan Dikirim',
done: 'Pesanan Selesai',
cancel: 'Pesanan Dibatalkan',
+ belum_bayar: 'Belum Bayar',
};
const sortes = [
@@ -142,16 +136,16 @@ const Transactions = ({ context = '' }) => {
{ id: 'asc', label: 'dari yang terkecil' },
{ id: 'desc', label: 'dari yang terbesar' },
];
+
const { transactions } = useTransactions({ query });
+
const fetchSite = async () => {
const site = await getSite();
setListSites(site.sites);
};
const submitCancelTransaction = async () => {
- const isCancelled = await cancelTransactionApi({
- transaction: toCancel,
- });
+ const isCancelled = await cancelTransactionApi({ transaction: toCancel });
if (isCancelled) {
toast.success('Berhasil batalkan transaksi');
transactions.refetch();
@@ -159,23 +153,16 @@ const Transactions = ({ context = '' }) => {
setToCancel(null);
};
- const pageCount = Math.ceil(transactions?.data?.saleOrderTotal / limitNew);
- let pageQuery = _.omit(query, ['limit', 'offset', 'context']);
- pageQuery = _.pickBy(
- pageQuery,
- (value, key) => value !== '' && !(key === 'page' && value === '1')
+ const pageCount = Math.ceil(
+ (transactions?.data?.saleOrderTotal || 0) / (limitNew || 1)
);
- pageQuery = toQuery(pageQuery);
const handleSubmit = (e) => {
e.preventDefault();
const queryParams = {};
if (inputQuery) queryParams.q = inputQuery;
if (siteFilter) queryParams.site = siteFilter;
- router.push({
- pathname: router.pathname,
- query: queryParams,
- });
+ router.push({ pathname: router.pathname, query: queryParams });
};
const handleSiteFilterChange = (e) => {
@@ -183,10 +170,7 @@ const Transactions = ({ context = '' }) => {
const queryParams = {};
if (inputQuery) queryParams.q = inputQuery;
if (e.target.value) queryParams.site = e.target.value;
- router.push({
- pathname: router.pathname,
- query: queryParams,
- });
+ router.push({ pathname: router.pathname, query: queryParams });
};
const exportToExcel = (data, siteFilter) => {
@@ -201,19 +185,17 @@ const Transactions = ({ context = '' }) => {
];
const rowsToExport = [];
- data.forEach((saleOrder) => {
+ (data || []).forEach((saleOrder) => {
const row = {
'No. Transaksi': saleOrder.name,
'No. PO': saleOrder.purchaseOrderName || '-',
Tanggal: saleOrder.dateOrder || '-',
- 'Created By': saleOrder.address.customer?.name || '-',
+ 'Created By': saleOrder.address?.customer?.name || '-',
Salesperson: saleOrder.sales,
Total: currencyFormat(saleOrder.amountTotal),
Status: contextLabelMap[saleOrder.status] || saleOrder.status,
};
- if (siteFilter) {
- row['Site'] = siteFilter;
- }
+ if (siteFilter) row['Site'] = siteFilter;
rowsToExport.push(row);
});
@@ -226,13 +208,30 @@ const Transactions = ({ context = '' }) => {
XLSX.writeFile(workbook, 'transactions.xlsx');
};
+ const getAllData = async () => {
+ const qobj = {
+ name: q,
+ offset: (pageNew - 1) * limitNew,
+ limit: limitNew,
+ context: contextNew[statusNew] || 'all',
+ sort: sortNew,
+ startDate: state[0]?.startDate
+ ? state[0].startDate.toLocaleDateString('id-ID')
+ : null,
+ endDate: state[0]?.endDate
+ ? state[0].endDate.toLocaleDateString('id-ID')
+ : null,
+ site:
+ siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null),
+ };
+ const queryString = toQuery(qobj);
+ const data = await transactionsApi({ query: queryString });
+ return data;
+ };
+
const handleExportCSV = async () => {
const dataToExport = await getAllData();
- exportToCSV(dataToExport?.saleOrders, siteFilter);
- };
-
- const exportToCSV = (data, siteFilter) => {
const fieldsToExport = [
'No. Transaksi',
'No. PO',
@@ -242,28 +241,23 @@ const Transactions = ({ context = '' }) => {
'Total',
'Status',
];
-
- if (siteFilter) {
- fieldsToExport.push('Site');
- }
-
- const rowsToExport = data.map((saleOrder) => {
- const row = [
- saleOrder.name,
- saleOrder.purchaseOrderName || '-',
- saleOrder.dateOrder || '-',
- saleOrder.address.customer?.name || '-',
- saleOrder.sales,
- currencyFormat(saleOrder.amountTotal),
- contextLabelMap[saleOrder.status] || saleOrder.status,
- ];
-
- if (siteFilter) {
- row.push(siteFilter);
- }
-
- return row.join(',');
- });
+ const rowsToExport =
+ dataToExport?.saleOrders?.map((saleOrder) => {
+ const row = [
+ saleOrder.name,
+ saleOrder.purchaseOrderName || '-',
+ saleOrder.dateOrder || '-',
+ saleOrder.address?.customer?.name || '-',
+ saleOrder.sales,
+ currencyFormat(saleOrder.amountTotal),
+ (contextLabelMap[saleOrder.status] || saleOrder.status || '').replace(
+ /,/g,
+ ' '
+ ),
+ ];
+ if (siteFilter) row.push((siteFilter || '').replace(/,/g, ' '));
+ return row.join(',');
+ }) || [];
const csvContent =
'data:text/csv;charset=utf-8,' +
@@ -278,66 +272,31 @@ const Transactions = ({ context = '' }) => {
document.body.removeChild(link);
};
- const getAllData = async () => {
- const query = {
- name: q,
- offset: (pageNew - 1) * limitNew,
- limit: limitNew,
- context: contextNew[statusNew] || 'all',
- sort: sortNew,
- startDate: state[0]?.startDate
- ? state[0].startDate.toLocaleDateString('id-ID')
- : null,
- endDate: state[0]?.endDate
- ? state[0].endDate.toLocaleDateString('id-ID')
- : null,
- site:
- siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null),
- };
- const queryString = toQuery(query);
- const data = await transactionsApi({ query: queryString });
- return data;
- };
-
const handleExportExcel = async () => {
const dataToExport = await getAllData();
-
exportToExcel(dataToExport?.saleOrders, siteFilter);
};
const handleDownload = (format) => {
- handleExport(format);
+ if (format === 'csv') handleExportCSV();
+ else if (format === 'xlsx') handleExportExcel();
setIsOpen(false);
};
- const handleExport = (format) => {
- if (format === 'csv') {
- handleExportCSV();
- } else if (format === 'xlsx') {
- handleExportExcel();
- }
- };
-
useEffect(() => {
const handleClickOutside = (event) => {
- if (
- calendarRef.current &&
- !calendarRef.current.contains(event.target)
- ) {
+ if (calendarRef.current && !calendarRef.current.contains(event.target)) {
setIsOpenCalender(false);
}
};
-
- document.addEventListener("mousedown", handleClickOutside);
- return () => {
- document.removeEventListener("mousedown", handleClickOutside);
- };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const startItem = 1 + (pageNew - 1) * limitNew;
const endItem = Math.min(
limitNew * pageNew,
- transactions?.data?.saleOrderTotal
+ transactions?.data?.saleOrderTotal || 0
);
useEffect(() => {
@@ -346,11 +305,8 @@ const Transactions = ({ context = '' }) => {
const handleBuyBack = async (products) => {
try {
- // setStatus('loading');
- console.log("Products to add:", products);
-
const results = await Promise.all(
- products.map((product) =>
+ (products || []).map((product) =>
upsertUserCart({
userId: auth.id,
type: 'product',
@@ -359,51 +315,42 @@ const Transactions = ({ context = '' }) => {
selected: true,
source: 'add_to_cart',
qtyAppend: true,
- }).catch(error => {
- return { error, product };
- })
+ }).catch((error) => ({ error, product }))
)
);
- const failedOperations = results.filter(result => result && result.error);
- // console.log(results);
-
- if (failedOperations.length > 0) {
- console.error('Some products failed to add to cart:', failedOperations);
- toast.error(`${failedOperations.length} produk gagal ditambahkan ke keranjang`);
-
- // You might want to proceed with the successful ones or handle differently
- if (failedOperations.length < products.length) {
- toast.success(`${products.length - failedOperations.length} produk berhasil ditambahkan ke keranjang`);
+ const failed = results.filter((r) => r && r.error);
+ if (failed.length > 0) {
+ toast.error(`${failed.length} produk gagal ditambahkan ke keranjang`);
+ if (failed.length < (products || []).length) {
+ toast.success(
+ `${
+ (products || []).length - failed.length
+ } produk berhasil ditambahkan`
+ );
setRefreshCart(true);
router.push('/shop/cart');
}
return;
}
-
- // All operations succeeded
setRefreshCart(true);
- toast.success('Semua produk berhasil ditambahkan ke keranjang belanja');
+ toast.success('Semua produk berhasil ditambahkan ke keranjang');
router.push('/shop/cart');
-
- } catch (error) {
- console.error('Gagal menambahkan produk ke keranjang:', error);
+ } catch {
toast.error('Terjadi kesalahan saat menambahkan produk ke keranjang');
- // setStatus('error');
}
};
-
const handleStatusChange = async (status) => {
setStatusNew(status);
setPageNew(1);
if (status === 'all' && cachedAllData) {
- setCurrentData(cachedAllData);
- return;
+ setCurrentData(cachedAllData);
+ return;
}
const data = await fetchSite(status, 1);
-
+
if (status === 'all') {
setCachedAllData(data);
}
@@ -411,19 +358,8 @@ const Transactions = ({ context = '' }) => {
setCurrentData(data);
};
- useEffect(() => {
- setCachedAllData([]);
- }, []);
-
-
const handleReset = () => {
- setState([
- {
- startDate: null,
- endDate: new Date(),
- key: 'selection',
- },
- ]);
+ setState([{ startDate: null, endDate: new Date(), key: 'selection' }]);
setIsOpenCalender(false);
router.push(`${router.pathname}`);
};
@@ -443,13 +379,88 @@ const Transactions = ({ context = '' }) => {
'November',
'Desember',
];
-
- const [day, month, year] = dateString.split('/');
+ const [day, month, year] = (dateString || '').split('/');
+ if (!day || !month || !year) return dateString || '-';
return `${day} ${months[parseInt(month, 10) - 1]} ${year}`;
};
+ // ==== Lanjutkan Transaksi (tanpa endpoint baru) ====
+ const handleContinuePayment = async (saleOrder) => {
+ try {
+ setContLoadingId(saleOrder.id);
+
+ const base = (process.env.NEXT_PUBLIC_ODOO_API_HOST || '').replace(
+ /\/$/,
+ ''
+ );
+ const token = auth?.token;
+ const partnerId = auth?.partnerId;
+
+ // 1. TRIGGER GENERATE + GET URL
+ const { data: response } = await axios.get(
+ `${base}/api/v1/partner/${partnerId}/sale_order/${saleOrder.id}`,
+ {
+ params: { ensure_payment_link: 1, ts: Date.now() },
+ headers: { Token: token, 'Cache-Control': 'no-cache' },
+ timeout: 10000,
+ }
+ );
+
+ // 2. EKSTRAK URL
+ let paymentUrl =
+ response?.result?.payment_summary?.redirect_url ||
+ response?.data?.result?.payment_summary?.redirect_url;
+
+ // 3. JIKA DAPAT URL, BUKA
+ if (paymentUrl) {
+ window.location.href = paymentUrl;
+ toast.success('Membuka halaman pembayaran…');
+ return;
+ }
+
+ // 4. FALLBACK: COBA TANPA ensure_payment_link
+ try {
+ const { data: fallbackResponse } = await axios.get(
+ `${base}/api/v1/partner/${partnerId}/sale_order/${saleOrder.id}`,
+ { headers: { Token: token }, timeout: 5000 }
+ );
+
+ const fallbackUrl =
+ fallbackResponse?.result?.payment_summary?.redirect_url ||
+ fallbackResponse?.data?.result?.payment_summary?.redirect_url;
+
+ if (fallbackUrl) {
+ window.location.href = fallbackUrl;
+ toast.success('Membuka halaman pembayaran…');
+ return;
+ }
+ } catch (fallbackError) {
+ // Continue to next fallback
+ }
+
+ // 5. ULTIMATE FALLBACK: PAKAI URL DARI DATA LAMA
+ const existingUrl =
+ saleOrder?.paymentSummary?.redirectUrl ||
+ saleOrder?.payment_summary?.redirect_url;
+
+ if (existingUrl) {
+ window.open(existingUrl, '_blank', 'noopener,noreferrer');
+ toast.success('Membuka halaman pembayaran…');
+ } else {
+ toast.error('Link pembayaran tidak ditemukan. Silakan coba lagi.');
+ }
+ } catch (error) {
+ toast.error(
+ error.response?.data?.description || 'Gagal memproses pembayaran'
+ );
+ } finally {
+ setContLoadingId(null);
+ }
+ };
+
return (
<>
+ {/* ===== MOBILE ===== */}
<MobileView>
<div className=' flex flex-col gap-y-4'>
<div className='grid grid-cols-[40%_40%_15%] justify-between items-center gap-2 w-full '>
@@ -475,23 +486,22 @@ const Transactions = ({ context = '' }) => {
</option>
))}
</select>
- <div ref={calendarRef} className="relative inline-block">
- <button
- type='button'
- className='p-1 w-full h-auto cursor-pointer border hover:bg-gray-100 rounded transition duration-150 ease-in-out flex items-center justify-center'
- onClick={() => setIsOpenCalender((prev) => !prev)}
- >
- <span className='text-nowrap px-1 truncate flex items-center gap-1'>
- {state[0]?.startDate ? (
- `${state[0].startDate.toLocaleDateString()} - ${state[0].endDate.toLocaleDateString()}`
- ) : (
- <Calendar size={20} className="text-gray-500" />
- )}
- </span>
- </button>
- {isOpenCalender && (
+ <div ref={calendarRef} className='relative inline-block'>
+ <button
+ type='button'
+ className='p-1 w-full h-auto cursor-pointer border hover:bg-gray-100 rounded transition duration-150 ease-in-out flex items-center justify-center'
+ onClick={() => setIsOpenCalender((prev) => !prev)}
+ >
+ <span className='text-nowrap px-1 truncate flex items-center gap-1'>
+ {state[0]?.startDate ? (
+ `${state[0].startDate.toLocaleDateString()} - ${state[0].endDate.toLocaleDateString()}`
+ ) : (
+ <Calendar size={20} className='text-gray-500' />
+ )}
+ </span>
+ </button>
+ {isOpenCalender && (
<div className='absolute right-1 mt-2 bg-white p-4 rounded shadow-lg z-50'>
- {/* Tombol silang di sudut kanan atas */}
<button
onClick={() => setIsOpenCalender(false)}
className='absolute top-2 right-2 text-gray-600 hover:text-black text-xl font-bold'
@@ -508,37 +518,13 @@ const Transactions = ({ context = '' }) => {
className='w-full'
/>
<style>{`
- /* Atur container agar menjadi column */
- .rdrCalendarWrapper {
- display: flex;
- flex-direction: column;
- }
- .rdrDateRangePickerWrapper {
- display: flex;
- flex-direction: column;
- }
-
- /* Pindahkan rdrStaticRanges ke atas */
- .rdrDefinedRangesWrapper {
- order: -1;
- width: fit-content;
- }
- .rdrStaticRanges {
- flex-direction: row;
- margin-right: 2px;
- }
-
- /* Sembunyikan bagian input manual */
- .rdrInputRanges {
- display: none !important;
- }
-
- .rdrStaticRangeLabel {
- padding: 10px 10px;
- }
- .rdrMonth {
- width: -moz-available;
- }
+ .rdrCalendarWrapper{display:flex;flex-direction:column;}
+ .rdrDateRangePickerWrapper{display:flex;flex-direction:column;}
+ .rdrDefinedRangesWrapper{order:-1;width:fit-content;}
+ .rdrStaticRanges{flex-direction:row;margin-right:2px;}
+ .rdrInputRanges{display:none !important;}
+ .rdrStaticRangeLabel{padding:10px 10px;}
+ .rdrMonth{width:-moz-available;}
`}</style>
<div className='flex flex-row justify-end gap-3 mt-2'>
<button
@@ -549,68 +535,7 @@ const Transactions = ({ context = '' }) => {
</button>
</div>
</div>
- )}
- </div>
- {/* <div className='border border-gray-300 rounded-lg px-1 py-1 bg-white shadow-sm focus:outline-none focus:ring-2 focus:ring-red-500 text-xs'>
- <DatePicker
- closeOnScroll={(e) => e.target === document}
- selectsRange={true}
- startDate={startDate}
- endDate={endDate}
- dateFormat='dd/MM'
- className='w-full'
- maxDate={new Date()}
- placeholderText='Semua Tanggal'
- onChange={(update) => {
- setDateRange(update);
- }}
- withPortal
- isClearable={true}
- />
- </div> */}
- </div>
- <div className='flex flex-row justify-between items-center gap-2'>
- <form className='flex' onSubmit={handleSubmit}>
- <button
- className='btn-light border-r-0 rounded-r-none bg-transparent px-3'
- type='submit'
- >
- <MagnifyingGlassIcon className='w-6' />
- </button>
- <input
- type='text'
- className='form-input border-l-0 rounded-l-none text-xs'
- placeholder='Cari Transaksi...'
- value={inputQuery}
- onChange={(e) => setInputQuery(e.target.value)}
- />
- </form>
- <div className='flex flex-row gap-2 items-center justify-center text-nowrap'>
- <p className='text-xs'>
- Menampilkan {startItem}-
- {endItem
- ? endItem
- : transactions?.data?.saleOrderTotal
- ? transactions?.data?.saleOrderTotal
- : limitNew * pageNew}{' '}
- dari{' '}
- {transactions?.data?.saleOrderTotal
- ? transactions?.data?.saleOrderTotal
- : limitNew * pageNew}
- </p>
- <select
- id='limitSelect'
- value={limitNew}
- onChange={(e) => {
- setLimitNew(Number(e.target.value));
- setPageNew(1);
- }}
- className='border p-2 text-xs'
- >
- <option value={10}>10</option>
- <option value={15}>15</option>
- <option value={20}>20</option>
- </select>
+ )}
</div>
</div>
@@ -634,7 +559,9 @@ const Transactions = ({ context = '' }) => {
>
<div className='flex flex-row justify-between items-start'>
<Link href={`${router.pathname}/${saleOrder.id}`}>
- <h2 className='text-danger-500 text-base'>{saleOrder.name}</h2>
+ <h2 className='text-danger-500 text-base'>
+ {saleOrder.name}
+ </h2>
<span className='font-medium text-black opacity-75'>
{formatDate(saleOrder.dateOrder.split(' ')[0]) || '-'}
</span>
@@ -681,29 +608,30 @@ const Transactions = ({ context = '' }) => {
<div className='flex flex-row gap-1 justify-start items-center'>
{saleOrder.products
.slice(1, 4)
- .map((product, index) => (
+ .map((product, idx) => (
<Image
- key={index} // Tambahkan key untuk setiap elemen dalam map()
+ key={idx}
src={product?.parent?.image}
alt={product?.name}
className='object-contain object-center border border-gray_r-6 h-8 w-8 rounded-md'
/>
))}
{saleOrder.products.length > 4 ? (
- <Link
- href={`${router.pathname}/${saleOrder?.id}`}
- className='text-red-500 text-nowrap'
- >
- +{saleOrder.products.length - 4} lihat semua produk
- </Link>
- ) : (
- <Link
- href={`${router.pathname}/${saleOrder?.id}`}
- className='text-red-500 text-nowrap'
- >
- Lihat semua produk
- </Link>
- )}
+ <Link
+ href={`${router.pathname}/${saleOrder?.id}`}
+ className='text-red-500 text-nowrap'
+ >
+ +{saleOrder.products.length - 4} lihat semua
+ produk
+ </Link>
+ ) : (
+ <Link
+ href={`${router.pathname}/${saleOrder?.id}`}
+ className='text-red-500 text-nowrap'
+ >
+ Lihat semua produk
+ </Link>
+ )}
</div>
)}
</div>
@@ -716,58 +644,50 @@ const Transactions = ({ context = '' }) => {
</div>
</div>
<div className='col-span-2 h-[1px] w-full bg-gray-300'></div>
- <div className='flex flex-row gap-3 justify-between items-center text-sm'>
- <div className='flex flex-col text-black text-xs'>
- <p className='font-extralight text-sm'>Total Harga</p>
+
+ <div className='flex flex-col gap-3 text-sm'>
+ <div className='flex flex-col text-black'>
+ <p className='font-extralight'>Total Harga</p>
<p className='font-semibold text-lg'>
{currencyFormat(saleOrder.amountTotal)}
</p>
</div>
- <div>
- <button
- type='button'
- onClick={() => handleBuyBack(saleOrder.products)}
- className='flex-1 py-2 btn-solid-red text-nowrap'
- >
- Beli Lagi
- </button>
- </div>
- </div>
- {/* <div className='grid grid-cols-2 mt-3'>
- <div>
- <span className='text-caption-2 text-gray_r-11'>
- No. Purchase Order
- </span>
- <p className='mt-1 font-medium text-gray_r-12'>
- {saleOrder.purchaseOrderName || '-'}
- </p>
- </div>
- <div className='text-right'>
- <span className='text-caption-2 text-gray_r-11'>
- Total Invoice
- </span>
- <p className='mt-1 font-medium text-gray_r-12'>
- {saleOrder.invoiceCount} Invoice
- </p>
- </div>
- </div> */}
- {/* <div className='grid grid-cols-2 mt-3'>
- <div>
- <span className='text-caption-2 text-gray_r-11'>Sales</span>
- <p className='mt-1 font-medium text-gray_r-12'>
- {saleOrder.sales}
- </p>
- </div>
- <div className='text-right'>
- <span className='text-caption-2 text-gray_r-11'>
- Total Harga
- </span>
- <p className='mt-1 font-medium text-gray_r-12'>
- {currencyFormat(saleOrder.amountTotal)}
- </p>
+ <div className='flex flex-col gap-2 w-full'>
+ {/* Beli Lagi hanya muncul jika status bukan unpaid */}
+ {!isUnpaid(saleOrder.status) && (
+ <button
+ type='button'
+ onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ handleBuyBack(saleOrder.products);
+ }}
+ className='w-full py-2 btn-solid-red text-center rounded-md'
+ >
+ Beli Lagi
+ </button>
+ )}
+
+ {/* Bayar Sekarang hanya kalau eligible */}
+ {saleOrder?.eligibleContinue && (
+ <button
+ type='button'
+ disabled={contLoadingId === saleOrder.id}
+ onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ handleContinuePayment(saleOrder);
+ }}
+ className='w-full py-2 text-center rounded-md border border-red-300 text-red-500 bg-white disabled:opacity-60'
+ >
+ {contLoadingId === saleOrder.id
+ ? 'Memproses…'
+ : 'Bayar Sekarang'}
+ </button>
+ )}
</div>
- </div> */}
+ </div>
</Link>
</div>
))}
@@ -775,7 +695,6 @@ const Transactions = ({ context = '' }) => {
<Pagination
pageCount={pageCount}
currentPage={parseInt(pageNew)}
- // url={router.pathname + pageQuery}
url={`${router.pathname}?${toQuery(_.omit(query, ['page']))}`}
className='mt-2 mb-2'
/>
@@ -785,6 +704,30 @@ const Transactions = ({ context = '' }) => {
active={toOthers}
close={() => setToOthers(null)}
>
+ {transactions.data?.status === 'draft' && (
+ <>
+ <button
+ className='text-left disabled:opacity-60'
+ disabled={toOthers?.status != 'draft'}
+ onClick={() => {
+ downloadQuotation(toOthers);
+ setToOthers(null);
+ }}
+ >
+ Download Quotation
+ </button>
+ <button
+ className='text-left disabled:opacity-60'
+ disabled={toOthers?.status != 'waiting'}
+ onClick={() => {
+ setToCancel(toOthers);
+ setToOthers(null);
+ }}
+ >
+ Batalkan Transaksi
+ </button>
+ </>
+ )}
<div className='flex flex-col gap-y-4 mt-2'>
<button
className='text-left disabled:opacity-60'
@@ -796,26 +739,6 @@ const Transactions = ({ context = '' }) => {
>
Download PO
</button>
- <button
- className='text-left disabled:opacity-60'
- disabled={toOthers?.status != 'draft'}
- onClick={() => {
- downloadQuotation(toOthers);
- setToOthers(null);
- }}
- >
- Download Quotation
- </button>
- <button
- className='text-left disabled:opacity-60'
- disabled={toOthers?.status != 'waiting'}
- onClick={() => {
- setToCancel(toOthers);
- setToOthers(null);
- }}
- >
- Batalkan Transaksi
- </button>
</div>
</BottomPopup>
@@ -848,6 +771,7 @@ const Transactions = ({ context = '' }) => {
</div>
</MobileView>
+ {/* ===== DESKTOP ===== */}
<DesktopView>
<div className='container mx-auto flex py-10'>
<div className='w-3/12 pr-4'>
@@ -900,51 +824,36 @@ const Transactions = ({ context = '' }) => {
)}
</div>
</div>
- <div className=''>
- <div
- class='flex items-center p-4 mb-4 text-sm border border-yellow-500 text-yellow-800 rounded-lg bg-yellow-50'
- role='alert'
- >
- <svg
- class='flex-shrink-0 inline w-5 h-5 mr-2'
- aria-hidden='true'
- fill='currentColor'
- viewBox='0 0 20 20'
- >
- <path d='M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z' />
- </svg>
- <span class='sr-only'>Info</span>
- <div className='text-justify flex flex-col gap-1'>
- <p className='font-bold text-black'>Info Transaksi</p>
- <span className='text-black'>
- Gunakan filter status untuk mempermudah pencarian transaksi anda di Daftar Transaksi
- </span>
- </div>
- </div>
- </div>
+
<div className='flex flex-col gap-y-2 border rounded-lg mb-2 w-full'>
<div className='p-2'>
<div className='flex items-center space-x-3'>
<span className='text-base font-semibold text-gray-600'>
Status
</span>
- <div className="relative w-full overflow-hidden">
- {/* Container flex: tombol prev - swiper - tombol next */}
- <div className="flex items-center space-x-2">
-
- {/* Prev */}
- <button className="custom-prev w-8 h-8 flex-shrink-0 flex items-center justify-center bg-white border rounded-full shadow z-1">
- <svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
+ <div className='relative w-full overflow-hidden'>
+ <div className='flex items-center space-x-2'>
+ <button className='custom-prev w-8 h-8 flex-shrink-0 flex items-center justify-center bg-white border rounded-full shadow z-1'>
+ <svg
+ className='w-4 h-4 text-gray-500'
+ fill='none'
+ stroke='currentColor'
+ viewBox='0 0 24 24'
+ >
+ <path
+ strokeLinecap='round'
+ strokeLinejoin='round'
+ strokeWidth={2}
+ d='M15 19l-7-7 7-7'
+ />
</svg>
</button>
- {/* Swiper container scrollable */}
- <div className="w-full overflow-hidden">
+ <div className='w-full overflow-hidden'>
<Swiper
spaceBetween={10}
- slidesPerView="auto"
- className="status-swiper"
+ slidesPerView='auto'
+ className='status-swiper'
modules={[Navigation]}
navigation={{
nextEl: '.custom-next',
@@ -952,12 +861,13 @@ const Transactions = ({ context = '' }) => {
}}
>
{statuses.map((status) => (
- <SwiperSlide key={status.id} className="!w-auto">
+ <SwiperSlide key={status.id} className='!w-auto'>
<button
className={`px-4 py-1 text-sm font-medium border rounded-lg transition whitespace-nowrap
- ${statusNew === status.id
- ? 'border-red-500 text-red-500 bg-white'
- : 'border-gray-300 text-gray-400 bg-gray-100 hover:bg-gray-200'
+ ${
+ statusNew === status.id
+ ? 'border-red-500 text-red-500 bg-white'
+ : 'border-gray-300 text-gray-400 bg-gray-100 hover:bg-gray-200'
}`}
onClick={() => handleStatusChange(status.id)}
>
@@ -968,16 +878,26 @@ const Transactions = ({ context = '' }) => {
</Swiper>
</div>
- {/* Next */}
- <button className="custom-next w-8 h-8 flex-shrink-0 flex items-center justify-center bg-white border rounded-full shadow z-10">
- <svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
+ <button className='custom-next w-8 h-8 flex-shrink-0 flex items-center justify-center bg-white border rounded-full shadow z-10'>
+ <svg
+ className='w-4 h-4 text-gray-500'
+ fill='none'
+ stroke='currentColor'
+ viewBox='0 0 24 24'
+ >
+ <path
+ strokeLinecap='round'
+ strokeLinejoin='round'
+ strokeWidth={2}
+ d='M9 5l7 7-7 7'
+ />
</svg>
</button>
</div>
</div>
</div>
</div>
+
<div className='flex flex-row items-center justify-between mb-2 p-2'>
<div className='flex flex-col gap-2 pb-2'>
{listSites?.length > 0 ? (
@@ -1013,6 +933,7 @@ const Transactions = ({ context = '' }) => {
</button>
</form>
</div>
+
<div className='flex flex-row gap-4 items-center justify-center'>
<p>
Menampilkan {startItem}-
@@ -1038,87 +959,66 @@ const Transactions = ({ context = '' }) => {
<option value={10}>10</option>
<option value={15}>15</option>
<option value={20}>20</option>
- <option value={transactions?.data?.saleOrderTotal}>Semua</option>
+ <option value={transactions?.data?.saleOrderTotal}>
+ Semua
+ </option>
</select>
- <div ref={calendarRef} className="relative inline-block">
+
+ <div ref={calendarRef} className='relative inline-block'>
<button
type='button'
className='p-2 w-auto h-auto cursor-pointer border hover:bg-gray-100 rounded transition duration-150 ease-in-out flex items-center justify-center'
onClick={() => setIsOpenCalender((prev) => !prev)}
>
<span className='text-nowrap px-1 truncate flex items-center gap-1'>
- {state[0]?.startDate ? (
- `${state[0].startDate.toLocaleDateString()} - ${state[0].endDate.toLocaleDateString()}`
- ) : (
- <Calendar size={16} className="text-gray-500" />
- )}
+ {state[0]?.startDate ? (
+ `${state[0].startDate.toLocaleDateString()} - ${state[0].endDate.toLocaleDateString()}`
+ ) : (
+ <Calendar size={16} className='text-gray-500' />
+ )}
</span>
</button>
{isOpenCalender && (
- <div className='absolute right-10 mt-2 bg-white p-4 rounded shadow-lg z-50'>
- {/* Tombol silang di sudut kanan atas */}
- <button
- onClick={() => setIsOpenCalender(false)}
- className='absolute top-2 right-2 text-gray-600 hover:text-black text-xl font-bold'
- >
- &times;
- </button>
- <DateRangePicker
- onChange={(item) => setState([item.selection])}
- showSelectionPreview={false}
- maxDate={new Date()}
- moveRangeOnFirstSelection={false}
- months={1}
- ranges={state}
- className='w-full'
- />
- <style>{`
- /* Atur container agar menjadi column */
- .rdrCalendarWrapper {
- display: flex;
- flex-direction: column;
- }
- .rdrDateRangePickerWrapper {
- display: flex;
- flex-direction: column;
- }
-
- /* Pindahkan rdrStaticRanges ke atas */
- .rdrDefinedRangesWrapper {
- order: -1;
- width: fit-content;
- }
- .rdrStaticRanges {
- flex-direction: row;
- margin-right: 2px;
- }
-
- /* Sembunyikan bagian input manual */
- .rdrInputRanges {
- display: none !important;
- }
-
- .rdrStaticRangeLabel {
- padding: 10px 10px;
- }
- .rdrMonth {
- width: -moz-available;
- }
- `}</style>
- <div className='flex flex-row justify-end gap-3 mt-2'>
+ <div className='absolute right-10 mt-2 bg-white p-4 rounded shadow-lg z-50'>
<button
- className='px-4 py-1 bg-red-500 text-white rounded'
- onClick={handleReset}
+ onClick={() => setIsOpenCalender(false)}
+ className='absolute top-2 right-2 text-gray-600 hover:text-black text-xl font-bold'
>
- Reset
+ &times;
</button>
+ <DateRangePicker
+ onChange={(item) => setState([item.selection])}
+ showSelectionPreview={false}
+ maxDate={new Date()}
+ moveRangeOnFirstSelection={false}
+ months={1}
+ ranges={state}
+ className='w-full'
+ />
+ <style>{`
+ .rdrCalendarWrapper{display:flex;flex-direction:column;}
+ .rdrDateRangePickerWrapper{display:flex;flex-direction:column;}
+ .rdrDefinedRangesWrapper{order:-1;width:fit-content;}
+ .rdrStaticRanges{flex-direction:row;margin-right:2px;}
+ .rdrInputRanges{display:none !important;}
+ .rdrStaticRangeLabel{padding:10px 10px;}
+ .rdrMonth{width:-moz-available;}
+ `}</style>
+ <div className='flex flex-row justify-end gap-3 mt-2'>
+ <button
+ className='px-4 py-1 bg-red-500 text-white rounded'
+ onClick={handleReset}
+ >
+ Reset
+ </button>
+ </div>
</div>
- </div>
)}
</div>
</div>
</div>
</div>
+
<div className='flex justify-center items-center'>
{!transactions.isLoading &&
transactions?.data?.saleOrders?.length == 0 && (
@@ -1156,11 +1056,9 @@ const Transactions = ({ context = '' }) => {
<p className='text-red-500'>{saleOrder.name}</p>
<p className='text-black'>
Salesperson:{' '}
- {
- <span className='font-semibold'>
- {saleOrder.sales}
- </span>
- }
+ <span className='font-semibold'>
+ {saleOrder.sales}
+ </span>
</p>
</div>
<div className='text-black'>
@@ -1172,7 +1070,9 @@ const Transactions = ({ context = '' }) => {
</span>
</div>
</div>
+
<hr className='mt-3 mb-3 border border-gray-100' />
+
<div className='flex flex-row gap-2 justify-between items-center '>
<div className='flex justify-start w-3/4 flex-col gap-2'>
<div className='flex gap-2'>
@@ -1207,9 +1107,9 @@ const Transactions = ({ context = '' }) => {
<div className='flex flex-row gap-1 justify-start items-center'>
{saleOrder.products
.slice(1, 4)
- .map((product, index) => (
+ .map((product, idx) => (
<Image
- key={index} // Tambahkan key untuk setiap elemen dalam map()
+ key={idx}
src={product?.parent?.image}
alt={product?.name}
className='object-contain object-center border border-gray_r-6 h-16 w-16 rounded-md'
@@ -1220,7 +1120,8 @@ const Transactions = ({ context = '' }) => {
href={`${router.pathname}/${saleOrder?.id}`}
className='text-red-500 text-nowrap'
>
- +{saleOrder.products.length - 4} lihat semua produk
+ +{saleOrder.products.length - 4}{' '}
+ lihat semua produk
</Link>
) : (
<Link
@@ -1244,25 +1145,48 @@ const Transactions = ({ context = '' }) => {
</p>
</div>
</div>
+
<div className='w-[1px] h-24 bg-gray-300'></div>
- <div className='w-1/4 flex flex-row gap-3 justify-center items-center'>
+
+ <div className='w-1/4 flex flex-col gap-2 items-center justify-center text-center pl-5'>
<div className='flex flex-col text-black'>
- <p>Total Harga</p>
- <p className='font-bold'>
+ <p className='text-sm'>Total Harga</p>
+ <p className='font-bold text-lg'>
{currencyFormat(saleOrder.amountTotal)}
</p>
</div>
- <div>
+
+ {!isUnpaid(saleOrder.status) && (
<button
type='button'
- onClick={() =>
- handleBuyBack(saleOrder.products)
- }
- className='flex-1 py-2 btn-solid-red text-nowrap'
+ onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ handleBuyBack(saleOrder.products);
+ }}
+ className='w-full py-2 btn-solid-red text-nowrap rounded-md'
>
Beli Lagi
</button>
- </div>
+ )}
+
+ {/* Bayar Sekarang: hanya kalau eligible */}
+ {saleOrder?.eligibleContinue && (
+ <button
+ type='button'
+ disabled={contLoadingId === saleOrder.id}
+ onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ handleContinuePayment(saleOrder);
+ }}
+ className='w-full py-2 text-nowrap border border-red-500 text-red-500 rounded-md disabled:opacity-60'
+ >
+ {contLoadingId === saleOrder.id
+ ? 'Memproses…'
+ : 'Bayar Sekarang'}
+ </button>
+ )}
</div>
</div>
</Link>
@@ -1271,70 +1195,10 @@ const Transactions = ({ context = '' }) => {
</div>
)}
</div>
- {/* <table className='table-data'>
- <thead>
- <tr>
- <th>No. Transaksi</th>
- <th>No. PO</th>
- <th>Tanggal</th>
- <th>Created By</th>
- {auth?.feature?.soApproval && <th>Site</th>}
- <th className='!text-left'>Salesperson</th>
- <th className='!text-left'>Total</th>
- <th>Status</th>
- </tr>
- </thead>
- <tbody>
- {transactions.isLoading && (
- <tr>
- <td colSpan={7}>
- <div className='flex justify-center my-2'>
- <Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' />
- </div>
- </td>
- </tr>
- )}
- {!transactions.isLoading &&
- (!transactions?.data?.saleOrders ||
- transactions?.data?.saleOrders?.length == 0) && (
- <tr>
- <td colSpan={7}>Tidak ada transaksi</td>
- </tr>
- )}
- {transactions.data?.saleOrders?.map((saleOrder) => (
- <tr key={saleOrder.id}>
- <td>
- <Link
- className='whitespace-nowrap'
- href={`${router.pathname}/${saleOrder.id}`}
- >
- {saleOrder.name}
- </Link>
- </td>
- <td>{saleOrder.purchaseOrderName || '-'}</td>
- <td>{saleOrder.dateOrder || '-'}</td>
- <td>{saleOrder.address.customer?.name || '-'}</td>
- {auth?.feature?.soApproval && (
- <td>{saleOrder.sitePartner || '-'}</td>
- )}
- <td className='!text-left'>{saleOrder.sales}</td>
- <td className='!text-left'>
- {currencyFormat(saleOrder.amountTotal)}
- </td>
- <td>
- <div className='flex justify-center'>
- <TransactionStatusBadge status={saleOrder.status} />
- </div>
- </td>
- </tr>
- ))}
- </tbody>
- </table> */}
<Pagination
pageCount={pageCount}
currentPage={parseInt(pageNew)}
- // url={router.pathname + (pageQuery ? `?${pageQuery}` : '')}
url={`${router.pathname}?${toQuery(_.omit(query, ['page']))}`}
className='mt-2 mb-2'
/>