diff options
| author | IT Fixcomart <it@fixcomart.co.id> | 2025-09-08 10:21:55 +0000 |
|---|---|---|
| committer | IT Fixcomart <it@fixcomart.co.id> | 2025-09-08 10:21:55 +0000 |
| commit | b2032b16b36410b8f8185b14a43d234966554da0 (patch) | |
| tree | c2adf164941a6099bc24619a3734f1bffbe2625f /src/lib | |
| parent | 0d57c2629d9658afe888fbad8f09a29f4353f115 (diff) | |
| parent | 57ace93dc3090a54d65d3b84ae50e8a9249eb314 (diff) | |
Merged in regenerate-midtrans (pull request #453)
<Miqdad> Regenerate midtrans
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/transaction/components/Transaction.jsx | 576 | ||||
| -rw-r--r-- | src/lib/transaction/components/TransactionStatusBadge.jsx | 4 | ||||
| -rw-r--r-- | src/lib/transaction/components/Transactions.jsx | 922 |
3 files changed, 613 insertions, 889 deletions
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' - > - × - </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 + × </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' /> |
