diff options
| author | IT Fixcomart <it@fixcomart.co.id> | 2025-07-31 11:02:58 +0000 |
|---|---|---|
| committer | IT Fixcomart <it@fixcomart.co.id> | 2025-07-31 11:02:58 +0000 |
| commit | 4cf94e241af5f95f567aac952dd990852026d23f (patch) | |
| tree | 5c519ac632391b34e1cc2bb31d011be37bb2d779 /src | |
| parent | dc31efb2fec4c7b79917324d922ae820c4b5bb50 (diff) | |
| parent | 04961a55929017f77ee6801d2b7ada4c05689821 (diff) | |
Merged in cr/repeat-order (pull request #430)
Cr/repeat order
Diffstat (limited to 'src')
| -rw-r--r-- | src/core/components/elements/Navbar/NavbarDesktop.jsx | 2 | ||||
| -rw-r--r-- | src/core/components/elements/Navbar/NavbarUserDropdown.jsx | 1 | ||||
| -rw-r--r-- | src/core/components/layouts/BasicLayout.jsx | 2 | ||||
| -rw-r--r-- | src/lib/auth/components/Menu.jsx | 14 | ||||
| -rw-r--r-- | src/lib/quotation/components/Quotationheader.jsx | 23 | ||||
| -rw-r--r-- | src/lib/transaction/components/Transaction.jsx | 615 | ||||
| -rw-r--r-- | src/lib/transaction/components/TransactionStatusBadge.jsx | 4 | ||||
| -rw-r--r-- | src/lib/transaction/components/Transactions.jsx | 1045 | ||||
| -rw-r--r-- | src/pages/my/transactions/index.jsx | 4 | ||||
| -rw-r--r-- | src/styles/globals.css | 12 |
10 files changed, 1548 insertions, 174 deletions
diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx index 03d7fa0c..db4fcbb8 100644 --- a/src/core/components/elements/Navbar/NavbarDesktop.jsx +++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx @@ -51,7 +51,7 @@ const NavbarDesktop = () => { const { transactions } = useTransactions({ query }); const data = transactions?.data?.saleOrders.filter( - (transaction) => transaction.status === 'draft' + (transaction) => transaction.status === 'waiting' ); const [showPopup, setShowPopup] = useState(false); diff --git a/src/core/components/elements/Navbar/NavbarUserDropdown.jsx b/src/core/components/elements/Navbar/NavbarUserDropdown.jsx index b2ad6309..5fe0cfa7 100644 --- a/src/core/components/elements/Navbar/NavbarUserDropdown.jsx +++ b/src/core/components/elements/Navbar/NavbarUserDropdown.jsx @@ -16,7 +16,6 @@ const NavbarUserDropdown = () => { <div className='navbar-user-dropdown-wrapper'> <div className='navbar-user-dropdown'> <Link href='/my/profile'>Profil Saya</Link> - <Link href='/my/quotations'>Daftar Quotation</Link> <Link href='/my/transactions'>Daftar Transaksi</Link> <Link href='/my/shipments'>Daftar Pengiriman</Link> <Link href='/my/invoices'>Invoice & Faktur Pajak</Link> diff --git a/src/core/components/layouts/BasicLayout.jsx b/src/core/components/layouts/BasicLayout.jsx index 2998fa63..81f8b41f 100644 --- a/src/core/components/layouts/BasicLayout.jsx +++ b/src/core/components/layouts/BasicLayout.jsx @@ -54,6 +54,8 @@ const BasicLayout = ({ children }) => { useEffect(() => { const handleMouseOut = (event) => { + + if (!buttonRef.current) return; const rect = buttonRef.current.getBoundingClientRect(); if (event.clientY <= 0) { setButtonPosition(rect); diff --git a/src/lib/auth/components/Menu.jsx b/src/lib/auth/components/Menu.jsx index df33314c..20bc54fd 100644 --- a/src/lib/auth/components/Menu.jsx +++ b/src/lib/auth/components/Menu.jsx @@ -85,20 +85,6 @@ const Menu = () => { <div className='mt-2 mb-1 font-medium'>Menu</div> <div className='flex flex-col gap-y-2'> <LinkItem - href='/my/quotations' - active={routeStartWith('/my/quotations')} - className='' - > - <div className='flex gap-x-3 items-center'> - <ImageNext - src='/images/icon/icon_daftar_quotation.svg' - width={18} - height={20} - /> - <p>Daftar Quotation</p> - </div> - </LinkItem> - <LinkItem href='/my/transactions' active={routeStartWith('/my/transactions')} > diff --git a/src/lib/quotation/components/Quotationheader.jsx b/src/lib/quotation/components/Quotationheader.jsx index d94a55de..a035edc7 100644 --- a/src/lib/quotation/components/Quotationheader.jsx +++ b/src/lib/quotation/components/Quotationheader.jsx @@ -53,7 +53,7 @@ const Quotationheader = (quotationCount) => { }; const getCart = () => { - if (!productQuotation && auth) { + if ((productQuotation?.length === 0) && auth) { refreshCartf(); } }; @@ -62,7 +62,7 @@ const Quotationheader = (quotationCount) => { const refreshCartf = useCallback(async () => { setIsloading(true); let pendingTransactions = transactions?.data?.saleOrders.filter( - (transaction) => transaction.status === 'draft' + (transaction) => transaction.status === 'waiting' ); setProductQuotation(pendingTransactions); setCountQuotation( @@ -107,20 +107,21 @@ const Quotationheader = (quotationCount) => { }; }, []); + const handleCheckout = async () => { SetButtonTerapkan(true); let checkoutAll = await odooApi( 'POST', `/api/v1/user/${auth.id}/cart/select-all` ); - router.push('/my/quotations'); + router.push('/my/transactions'); }; return ( <div className='relative group'> <div> <Link - href='/my/quotations' + href='/my/transactions' target='_blank' rel='noreferrer' className='flex items-center gap-x-2 !text-gray_r-12/80' @@ -138,7 +139,7 @@ const Quotationheader = (quotationCount) => { <span> List <br /> - Quotation + Transactions </span> </Link> </div> @@ -168,7 +169,7 @@ const Quotationheader = (quotationCount) => { > <div className='p-2 flex justify-between items-center'> <h5 className='text-base font-semibold leading-none'> - Daftar Quotation + Daftar Transaksi </h5> </div> <hr className='mt-3 mb-3 border border-gray-100' /> @@ -183,7 +184,7 @@ const Quotationheader = (quotationCount) => { > Login </Link>{' '} - Untuk Melihat Daftar Quotation Anda + Untuk Melihat Daftar Transaksi Anda </p> </div> )} @@ -209,7 +210,7 @@ const Quotationheader = (quotationCount) => { {auth && qotation.length === 0 && !isLoading && ( <div className='justify-center p-4'> <p className='text-gray-500 text-center '> - Tidak Ada Quotation + Tidak Ada Transaksi </p> </div> )} @@ -225,7 +226,7 @@ const Quotationheader = (quotationCount) => { <li className='py-1 sm:py-2'> <div className='flex justify-between border p-2 flex-col gap-y-2 hover:border-red-500'> <Link - href={`/my/quotations/${product?.id}`} + href={`/my/transactions/${product?.id}`} className='hover:border-red-500' > <div className='flex justify-between mb-2'> @@ -241,8 +242,8 @@ const Quotationheader = (quotationCount) => { <p className='text-xs opacity-80 mr-[2px]'> Status : </p> - <p className='badge-red h-fit text-xs whitespace-nowrap'> - Pending Quotation + <p className='badge-yellow h-fit text-xs whitespace-nowrap'> + Pesanan Diproses </p> </div> </div> diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx index 8b3a8dd0..2c1a6208 100644 --- a/src/lib/transaction/components/Transaction.jsx +++ b/src/lib/transaction/components/Transaction.jsx @@ -17,6 +17,7 @@ import getFileBase64 from '@/core/utils/getFileBase64'; import currencyFormat from '@/core/utils/currencyFormat'; import VariantGroupCard from '@/lib/variant/components/VariantGroupCard'; import { + EllipsisVerticalIcon, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, @@ -40,6 +41,10 @@ import rejectProductApi from '../api/rejectProductApi'; import { useRouter } from 'next/router'; import { gtagPurchase } from '@/core/utils/googleTag'; import { deleteItemCart } from '@/core/utils/cart'; +import { + downloadInvoice, + downloadTaxInvoice, +} from '@/lib/invoice/utils/invoices'; import axios from 'axios'; import InformationSection from '../../treckingAwb/component/InformationSection'; import { Button } from '@chakra-ui/react'; @@ -51,18 +56,39 @@ const Transaction = ({ id }) => { const [reason, setReason] = useState(''); const auth = useAuth(); const { transaction } = useTransaction({ id }); + console.log('transaction', transaction); const statusApprovalWeb = transaction.data?.approvalStep; const [isLoading, setIsLoading] = useState(false); const { queryAirwayBill } = useAirwayBill({ orderId: id }); const [airwayBillPopup, setAirwayBillPopup] = useState(null); - + const [isOpen, setIsOpen] = useState(false); const poNumber = useRef(null); const poFile = useRef(null); const [uploadPo, setUploadPo] = useState(false); const [idAWB, setIdAWB] = useState(null); const openUploadPo = () => setUploadPo(true); const closeUploadPo = () => setUploadPo(false); - + const [copied, setCopied] = useState(false); + const [toOthers, setToOthers] = useState(null); + const [totalAmount, setTotalAmount] = useState(0); + const [totalDiscountAmount, setTotalDiscountAmount] = useState(0); + + useEffect(() => { + if (transaction?.data?.products) { + let calculateTotalAmount = 0; + let calculateTotalDiscountAmount = 0; + transaction.data.products.forEach((product) => { + calculateTotalAmount += product.price.price * product.quantity; + calculateTotalDiscountAmount += + (product.price.price - product.price.priceDiscount) * + product.quantity; + }); + setTotalAmount(calculateTotalAmount); + setTotalDiscountAmount(calculateTotalDiscountAmount); + } + }, [transaction.data, transaction.isLoading]); + console.log('totalAmount', totalAmount); + console.log('totalDiscountAmount', totalDiscountAmount); const submitUploadPo = async () => { const file = poFile.current.files[0]; const name = poNumber.current.value; @@ -283,6 +309,34 @@ const Transaction = ({ id }) => { } }; + const handleCopyClick = (waybillNumber) => { + const textToCopy = waybillNumber; + navigator.clipboard.writeText(textToCopy); + setCopied(true); + toast.success('No Resi Berhasil di Copy'); + setTimeout(() => setCopied(false), 2000); // Reset copied state after 2 seconds + }; + + const formatDate = (dateString) => { + const months = [ + 'Januari', + 'Februari', + 'Maret', + 'April', + 'Mei', + 'Juni', + 'Juli', + 'Agustus', + 'September', + 'Oktober', + 'November', + 'Desember', + ]; + + const [day, month, year] = dateString.split('/'); + return `${day} ${months[parseInt(month, 10) - 1]} ${year}`; + }; + return ( transaction.data?.name && ( <> @@ -392,18 +446,118 @@ const Transaction = ({ id }) => { </button> </div> </BottomPopup> + + <BottomPopup + title='Lainnya' + active={toOthers} + close={() => setToOthers(null)} + > + <div className='flex flex-col gap-y-4 mt-2'> + <button + className='text-left disabled:opacity-60' + disabled={!toOthers?.purchaseOrderFile} + onClick={() => { + downloadPurchaseOrder(toOthers); + setToOthers(null); + }} + > + 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> + <Manifest idAWB={idAWB} closePopup={closePopup}></Manifest> <MobileView> - <div className='p-4'> - {auth?.feature?.soApproval && ( + <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'> + <svg + class='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 text-start'> + Pesanan anda mungkin mengalami keterlambatan tiba + </span> + </div> + </div> + <span + className='text-red-500 text-xs hover:cursor-pointer text-nowrap z-50' + onClick={() => setIdAWB(transaction?.data?.pickings[0]?.id)} + > + Lihat Detail + </span> + </div> + </div> + {auth?.feature?.soApproval && ( + <div className='p-4'> <StepApproval layer={statusApprovalWeb} status={transaction?.data?.status} className='ml-auto' /> - )} + </div> + )} + + <div className='flex flex-row justify-between items-center gap-2 px-4'> + <div className='flex flex-col justify-start items-start gap-2'> + <div className='font-medium'>Status Transaksi</div> + <TransactionStatusBadge status={transaction.data?.status} /> + </div> + <div> + <EllipsisVerticalIcon + className='w-5 h-5' + onClick={() => setToOthers(transaction?.data)} + /> + </div> </div> + + {transaction.data?.invoices?.length === 0 ? ( + <h1 className=''></h1> + ) : ( + transaction.data?.invoices?.map((invoice, index) => ( + <div + className='flex flex-row justify-between items-center gap-2 p-4' + key={index} + > + <div className=''>{invoice?.name}</div> + <span + className='text-red-500' + onClick={() => downloadInvoice(invoice)} + > + Download + </span> + </div> + )) + )} + + <Divider /> + <div className='flex flex-col gap-y-4 p-4'> <DescriptionRow label='Status Transaksi'> <div className='flex justify-end'> @@ -416,53 +570,123 @@ const Transaction = ({ id }) => { </div> </DescriptionRow> <DescriptionRow label='No Transaksi'> - {transaction.data?.name} + <p className='font-semibold'>{transaction.data?.name}</p> + </DescriptionRow> + <DescriptionRow label='Tanggal Transaksi'> + {transaction.data?.dateOrder + ? formatDate(transaction.data?.dateOrder) + : '-'} + </DescriptionRow> + <DescriptionRow label='Purchase Order'> + {transaction.data?.purchaseOrderName || '-'} </DescriptionRow> <DescriptionRow label='Ketentuan Pembayaran'> - {transaction.data?.paymentTerm} + {transaction.data?.paymentTerm || '-'} </DescriptionRow> <DescriptionRow label='Nama Sales'> {transaction.data?.sales} </DescriptionRow> - <DescriptionRow label='Waktu Transaksi'> - {transaction.data?.dateOrder} - </DescriptionRow> </div> <Divider /> <div className='p-4'> - <div className='font-medium'>Pengiriman</div> - <div className='flex flex-col gap-y-3 mt-4'> - {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' - key={airway?.id} - onClick={() => setIdAWB(airway?.id)} - > - <div> - <p className='text-sm text-gray_r-11'>{airway?.name}</p> - <span className='mt-2 font-medium'> - No Resi : {airway?.trackingNumber || '-'}{' '} - </span> - {/*biteship*/} - {/*<p className='mt-1 font-medium'>{airway?.name}</p>*/} + <div className='flex flex-row justify-between items-center'> + <div className='font-medium'>Info Pengiriman</div> + <span + className='text-red-500' + onClick={() => setIdAWB(transaction?.data?.pickings[0]?.id)} + > + Lihat Detail + </span> + </div> + <hr className='mt-4 mb-4 border border-gray-100' /> + <div className='flex flex-col gap-y-4'> + <DescriptionRow label='Dokumen Pengiriman'> + <p className='text-red-500 font-semibold text-start'> + {transaction.data?.pickings?.length == 0 + ? 'Belum ada pengiriman' + : transaction?.data?.pickings[0].name} + </p> + </DescriptionRow> + <DescriptionRow label='Kurir'> + <p className='text-start'> + {transaction?.data?.pickings[0]?.carrierName ? ( + <p className=' text-nowrap'> + {transaction?.data?.pickings[0]?.carrierName} + </p> + ) : ( + '-' + )} + </p> + </DescriptionRow> + <DescriptionRow label='Jenis Service'> + <p className='text-start'> + {transaction?.data?.pickings[0]?.serviceType && + transaction?.data?.pickings[0]?.carrierName + ? transaction?.data?.pickings[0]?.serviceType + : '-'} + </p> + </DescriptionRow> + <DescriptionRow label='Nomor Resi'> + <div className='flex flex-row gap-1 text-start'> + {transaction?.data?.pickings[0]?.trackingNumber || '-'} + {transaction?.data?.pickings[0]?.trackingNumber && ( + <button + className={`${ + copied ? 'text-gray-400' : 'text-red-600 ' + }`} + onClick={() => + handleCopyClick( + transaction?.data?.pickings[0]?.trackingNumber + ) + } + > + <svg + aria-hidden='true' + fill='none' + stroke='currentColor' + stroke-width='1.5' + viewBox='0 0 24 24' + className='w-5 h-6' + > + <path + d='M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75' + stroke-linecap='round' + stroke-linejoin='round' + ></path> + </svg> + </button> + )} + </div> + </DescriptionRow> + <DescriptionRow label='Estimasi Tiba'> + <p className='text-start'> + {transaction?.data?.pickings[0]?.eta + ? transaction?.data?.pickings[0]?.eta + : '-'} + </p> + </DescriptionRow> + <DescriptionRow label='Alamat Pengiriman'> + <div className='flex flex-col justify-start items-start'> + <div className='text-start text-nowrap truncate w-full'> + {transaction?.data?.address?.customer?.name} </div> - <div className='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 className='text-start'> + {transaction?.data?.address?.customer?.phone + ? transaction?.data?.address?.customer?.phone + : '-'} </div> - </button> - ))} + <div className='text-start'> + {transaction?.data?.address?.customer?.alamatBisnis} + </div> + </div> + </DescriptionRow> </div> - {transaction?.data?.pickings == 0 && ( - <div className='badge-red text-sm px-2'>Belum ada pengiriman</div> - )} + </div> - <Divider /> + {/* <Divider /> <div className='p-4'> <p className='font-medium'>Invoice</p> @@ -491,11 +715,11 @@ const Transaction = ({ id }) => { <div className='badge-red text-sm px-2'>Belum ada invoice</div> )} </div> - </div> + </div> */} <Divider /> - {!auth?.feature.soApproval && ( + {/* {!auth?.feature.soApproval && ( <div className='p-4 flex flex-col gap-y-4'> <DescriptionRow label='Purchase Order'> {transaction.data?.purchaseOrderName || '-'} @@ -523,11 +747,59 @@ const Transaction = ({ id }) => { </div> )} - <Divider /> + <Divider /> */} <div className='font-medium p-4'>Detail Produk</div> {transaction?.data?.products.length > 0 ? ( - <div>{memoizeVariantGroupCard}</div> + <div className='p-4 pt-0 flex flex-col gap-y-3'> + <VariantGroupCard variants={transaction.data?.products} buyMore /> + <div className='font-medium'>Rincian Pembayaran</div> + <div className='flex justify-between mt-1'> + <p className='text-gray_r-12/70'>Metode Pembayaran</p> + <p> + {transaction.data?.paymentType + ? transaction.data?.paymentType + ?.replace(/_/g, ' ') + .replace(/\b\w/g, (char) => char.toUpperCase()) + : '-'} + </p> + </div> + <div className='flex justify-between mt-1'> + <p className='text-gray_r-12/70'>Berat Barang</p> + <p>{transaction.data?.pickings[0]?.weightTotal + ' Kg'}</p> + </div> + <hr className='mt-1 border border-gray-100' /> + <div className='flex justify-between mt-1'> + <p className='text-gray_r-12/70'>Total Belanja</p> + <p>{currencyFormat(totalAmount)}</p> + </div> + <div className='flex justify-between mt-1'> + <p className='text-gray_r-12/70'>Diskon Belanja</p> + <p>{'- ' + currencyFormat(totalDiscountAmount)}</p> + </div> + <div className='flex justify-between mt-1'> + <p className='text-gray_r-12/70'>Subtotal</p> + <p>{currencyFormat(transaction.data?.amountUntaxed)}</p> + </div> + <div className='flex justify-between mt-1'> + <p className='text-gray_r-12/70'> + PPN {((PPN - 1) * 100).toFixed(0)}% + </p> + <p>{currencyFormat(transaction.data?.amountTax)}</p> + </div> + <div className='flex justify-between mt-1'> + <p className='text-gray_r-12/70'>Biaya Pengiriman</p> + <p>{currencyFormat(transaction.data?.deliveryAmount)}</p> + </div> + <div className='flex justify-between mt-1 font-medium'> + <p className='text-gray_r-12/70'>Asuransi Pengiriman</p> + <p>-</p> + </div> + <div className='flex justify-between mt-1 font-medium'> + <p>Grand Total</p> + <p>{currencyFormat(transaction.data?.amountTotal)}</p> + </div> + </div> ) : ( <div className='badge-red text-sm px-2 ml-4'> Semua produk telah di reject @@ -541,13 +813,13 @@ const Transaction = ({ id }) => { </div> )} - <Divider /> + {/* <Divider /> */} - <SectionAddress address={transaction.data?.address} /> + {/* <SectionAddress address={transaction.data?.address} /> */} - <Divider /> + {/* <Divider /> */} - <div className='p-4 pt-0'> + {/* <div className='p-4 pt-0'> {transaction.data?.status == 'draft' && auth?.feature.soApproval && ( <div className='flex gap-x-2'> @@ -604,7 +876,7 @@ const Transaction = ({ id }) => { Batalkan Transaksi </button> )} - </div> + </div> */} </MobileView> <DesktopView> @@ -646,7 +918,7 @@ const Transaction = ({ id }) => { </span> <TransactionStatusBadge status={transaction?.data?.status} /> </div> - <div className='flex gap-x-4'> + {/* <div className='flex gap-x-4'> <button type='button' className='btn-solid-red px-3 py-2 mr-auto' @@ -709,19 +981,15 @@ const Transaction = ({ id }) => { Batalkan Transaksi </button> )} - </div> + </div> */} - <div className='grid grid-cols-2 gap-x-6 mt-6'> - <div className='grid grid-cols-2 gap-y-4'> + <div className='grid grid-cols-2 gap-x-6 mt-4'> + <div className='grid grid-cols-[35%_65%] gap-y-4'> <div>Nama Sales</div> <div>: {transaction?.data?.sales}</div> <div>Tanggal Transaksi</div> <div>: {transaction?.data?.dateOrder}</div> - </div> - <div className='grid grid-cols-2 gap-y-4'> - <div>Ketentuan Pembayaran</div> - <div>: {transaction?.data?.paymentTerm}</div> {!auth?.feature?.soApproval ? ( <> @@ -754,6 +1022,193 @@ const Transaction = ({ id }) => { </> )} </div> + <div className='grid grid-cols-[35%_65%] gap-y-4'> + <div>Payment Term</div> + <div>: {transaction?.data?.paymentTerm}</div> + + <div>Dokumen Pengiriman</div> + <div> + :{' '} + {transaction.data?.pickings?.length === 0 + ? 'Belum ada pengiriman' + : transaction?.data?.pickings[0].name} + </div> + + <div>Invoice Pembelian</div> + <div> + :{' '} + {transaction.data?.invoices?.length === 0 + ? 'Belum ada invoice' + : transaction.data?.invoices?.map((invoice, index) => ( + <Link + href={`/my/invoices/${invoice.id}`} + className='contents' + 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'> + Alamat Pengiriman + </span> + <div className='grid grid-cols-[34%_2%_64%] gap-y-4'> + <div>Nama Penerima</div> + <div>: </div> + <div>{transaction?.data?.address?.customer?.name}</div> + + <div>No. Telepon</div> + <div>: </div> + <div> + {transaction?.data?.address?.customer?.phone + ? transaction?.data?.address?.customer?.phone + : '-'} + </div> + + <div>Email</div> + <div>: </div> + <div> + {transaction?.data?.address?.customer?.email + ? transaction?.data?.address?.customer?.email + : '-'} + </div> + + <div>Alamat Pengiriman</div> + <div>: </div> + <div className='text-indent-[2px]'> + {transaction?.data?.address?.customer?.alamatBisnis} + </div> + </div> + </div> + <div className='flex flex-col w-1/2 justify-start items-start'> + <span className='text-h-sm font-medium mb-2'> + Info Pengiriman + </span> + <div className='grid grid-cols-[34%_2%_64%] gap-y-4 w-full'> + <div>Nomor Resi</div> + <div>: </div> + <div className='flex flex-row gap-1 '> + {transaction?.data?.pickings[0]?.trackingNumber || '-'} + {transaction?.data?.pickings[0]?.trackingNumber && ( + <button + className={`${ + copied ? 'text-gray-400' : 'text-red-600 ' + }`} + onClick={() => + handleCopyClick( + transaction?.data?.pickings[0]?.trackingNumber + ) + } + > + <svg + aria-hidden='true' + fill='none' + stroke='currentColor' + stroke-width='1.5' + viewBox='0 0 24 24' + className='w-5 h-6' + > + <path + d='M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75' + stroke-linecap='round' + stroke-linejoin='round' + ></path> + </svg> + </button> + )} + </div> + + <div>Kurir</div> + <div>: </div> + {transaction?.data?.pickings[0]?.carrierName ? ( + <div className='flex flex-row w-full gap-1 items-center justify-start '> + <p className=' text-nowrap'> + {transaction?.data?.pickings[0]?.carrierName} + </p> + <span + className='text-red-500 text-sm font-semibold hover:cursor-pointer' + onClick={() => + setIdAWB(transaction?.data?.pickings[0]?.id) + } + > + Lacak Pengiriman + </span> + </div> + ) : ( + '-' + )} + + <div>Jenis Service</div> + <div>: </div> + <div> + {' '} + {transaction?.data?.pickings[0]?.serviceType && + transaction?.data?.pickings[0]?.carrierName + ? transaction?.data?.pickings[0]?.serviceType + : '-'} + </div> + + <div>Tanggal Kirim</div> + <div>: </div> + <div> + {transaction?.data?.pickings[0]?.date + ? formatDate(transaction?.data?.pickings[0]?.date) + : '-'} + </div> + + <div>Estimasi Tiba</div> + <div>: </div> + <div className='text-red-500'> + {transaction?.data?.pickings[0]?.eta + ? transaction?.data?.pickings[0]?.eta + : '-'} + </div> + {transaction?.data?.pickings[0] && ( + <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' + > + <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 className='flex gap-x-3'> @@ -1045,7 +1500,49 @@ const Transaction = ({ id }) => { )} {transaction?.data?.products?.length > 0 && ( - <div className='flex justify-end mt-4'> + // <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> + <div className='text-right font-medium'> + {currencyFormat(totalAmount)} + </div> + + <div className='text-right'>Total Diskon</div> + <div className='text-right font-medium'> + {'- ' + currencyFormat(totalDiscountAmount)} + </div> + </div> + + <hr className='w-full border border-gray-100 mt-4 mb-4 self-stretch' /> + <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'> @@ -1059,15 +1556,17 @@ const Transaction = ({ id }) => { {currencyFormat(transaction.data?.amountTax)} </div> - <div className='text-right whitespace-nowrap'> - Biaya Pengiriman - </div> + <div className='text-right'>Biaya Pengiriman</div> <div className='text-right font-medium'> {currencyFormat(transaction.data?.deliveryAmount)} </div> + </div> + <hr className='w-full border border-gray-100 mt-4 mb-4 self-stretch' /> + + <div className='w-1/4 grid grid-cols-2 gap-y-3 font-semibold'> <div className='text-right'>Grand Total</div> - <div className='text-right font-medium text-gray_r-12'> + <div className='text-right'> {currencyFormat(transaction.data?.amountTotal)} </div> </div> diff --git a/src/lib/transaction/components/TransactionStatusBadge.jsx b/src/lib/transaction/components/TransactionStatusBadge.jsx index e061587c..cb8cbcd9 100644 --- a/src/lib/transaction/components/TransactionStatusBadge.jsx +++ b/src/lib/transaction/components/TransactionStatusBadge.jsx @@ -14,11 +14,11 @@ const TransactionStatusBadge = ({ status }) => { break case 'waiting': badgeProps.className.push('badge-yellow') - badgeProps.text = 'Pesanan Diterima' + badgeProps.text = 'Pesanan Diproses' break case 'sale': badgeProps.className.push('badge-yellow') - badgeProps.text = 'Pesanan Diproses' + badgeProps.text = 'Pesanan Dikemas' break case 'shipping': badgeProps.className.push('badge-green') diff --git a/src/lib/transaction/components/Transactions.jsx b/src/lib/transaction/components/Transactions.jsx index 92bdd276..acb925da 100644 --- a/src/lib/transaction/components/Transactions.jsx +++ b/src/lib/transaction/components/Transactions.jsx @@ -1,12 +1,13 @@ import { useRouter } from 'next/router'; -import { useEffect, useState } 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 { downloadPurchaseOrder, downloadQuotation, @@ -28,32 +29,110 @@ import Menu from '@/lib/auth/components/Menu'; import * as XLSX from 'xlsx'; import getSite from '../api/listSiteApi'; import transactionsApi from '../api/transactionsApi'; - +import { motion } from 'framer-motion'; +import Image from '@/core/components/elements/Image/Image'; +import { upsertUserCart } from '~/services/cart'; +import { useProductCartContext } from '@/contexts/ProductCartContext'; +import { Swiper, SwiperSlide } from 'swiper/react'; +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'; const Transactions = ({ context = '' }) => { const auth = useAuth(); const router = useRouter(); - const { q = '', page = 1, site = null } = router.query; - - const limit = 15; - + const swiperRef = useRef(null); + const { + q = '', + page = 1, + site = null, + limit = 15, + status = 'all', + sort = 'none', + startDate = null, + endDate = new Date(), + } = router.query; + const { + productCart, + setRefreshCart, + setProductCart, + refreshCart, + isLoading, + setIsloading, + } = useProductCartContext(); const [inputQuery, setInputQuery] = useState(q); const [toOthers, setToOthers] = useState(null); const [toCancel, setToCancel] = useState(null); const [listSites, setListSites] = useState([]); - + const [isOpen, setIsOpen] = useState(false); 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 parseDate = (date) => { + if (!date || date === 'null') return null; + if (date instanceof Date) return date; + const [day, month, year] = date.split('/').map(Number); + return new Date(year, month - 1, day); + }; + + const [state, setState] = useState([ + { + startDate: startDate != null || 'null' ? parseDate(startDate) : null, // Gunakan `parseDate` + endDate: startDate == null ? endDate : parseDate(endDate), + key: 'selection', + }, + ]); const query = { name: q, - offset: (page - 1) * limit, - context, - limit, + offset: (pageNew - 1) * limitNew, + context: contextNew, + limit: limitNew, + status: statusNew, + sort: sortNew, + startDate: state[0].startDate + ? state[0].startDate.toLocaleDateString('id-ID') + : null, + endDate: state[0]?.endDate?.toLocaleDateString('id-ID'), site: siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null), }; + const statuses = [ + { id: 'all', label: 'Semua' }, + { id: 'quotation', label: 'Pending Quotation' }, + { id: 'diproses', label: 'Pesanan Diproses' }, + { id: 'dikemas', label: 'Pesanan Dikemas' }, + { id: 'partial', label: 'Dikirim Sebagian' }, + { id: 'dikirim', label: 'Pesanan Dikirim' }, + { id: 'selesai', label: 'Pesanan Selesai' }, + { id: 'cancel', label: 'Pesanan Dibatalkan' }, + ]; + const sortes = [ + { id: 'none', label: 'Urutkan' }, + { 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); @@ -70,7 +149,7 @@ const Transactions = ({ context = '' }) => { setToCancel(null); }; - const pageCount = Math.ceil(transactions?.data?.saleOrderTotal / limit); + const pageCount = Math.ceil(transactions?.data?.saleOrderTotal / limitNew); let pageQuery = _.omit(query, ['limit', 'offset', 'context']); pageQuery = _.pickBy( pageQuery, @@ -137,14 +216,75 @@ const Transactions = ({ context = '' }) => { XLSX.writeFile(workbook, 'transactions.xlsx'); }; + const handleExportCSV = async () => { + const dataToExport = await getAllData(); + + exportToCSV(dataToExport?.saleOrders, siteFilter); + }; + + const exportToCSV = (data, siteFilter) => { + const fieldsToExport = [ + 'No. Transaksi', + 'No. PO', + 'Tanggal', + 'Created By', + 'Salesperson', + '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), + saleOrder.status, + ]; + + if (siteFilter) { + row.push(siteFilter); + } + + return row.join(','); + }); + + const csvContent = + 'data:text/csv;charset=utf-8,' + + [fieldsToExport.join(','), ...rowsToExport].join('\n'); + + const encodedUri = encodeURI(csvContent); + const link = document.createElement('a'); + link.setAttribute('href', encodedUri); + link.setAttribute('download', 'transactions.csv'); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + const getAllData = async () => { const query = { name: q, - context, + offset: (pageNew - 1) * limitNew, + limit: limitNew, + context: contextMap[statusNew], // gunakan contextMap + 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 queryString = toQuery(query); const data = await transactionsApi({ query: queryString }); return data; }; @@ -155,25 +295,309 @@ const Transactions = ({ context = '' }) => { exportToExcel(dataToExport?.saleOrders, siteFilter); }; + const handleDownload = (format) => { + handleExport(format); + 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) + ) { + setIsOpenCalender(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + const startItem = 1 + (pageNew - 1) * limitNew; + const endItem = Math.min( + limitNew * pageNew, + transactions?.data?.saleOrderTotal + ); + useEffect(() => { fetchSite(); }, []); + + const handleBuyBack = async (products) => { + // if (status === 'success') return; + + try { + // setStatus('loading'); + + const results = await Promise.all( + products.map((product) => + upsertUserCart({ + userId: auth.id, + type: 'product', + id: product.id, + qty: product.quantity, + selected: true, + source: 'buy', // Tetap gunakan 'buy' agar bisa masuk ke halaman pembelian + qtyAppend: false, + }) + ) + ); + + // ✅ Panggil setRefreshCart(true) setiap kali satu produk berhasil ditambahkan + + setRefreshCart(true); + + // setStatus('idle'); + toast.success('Semua produk berhasil ditambahkan ke keranjang belanja'); + // Tampilkan notifikasi + // toast({ + // title: 'Tambah ke keranjang', + // description: 'Semua produk berhasil ditambahkan ke keranjang belanja', + // status: 'success', + // duration: 3000, + // isClosable: true, + // position: 'top', + // }); + + // Redirect ke halaman checkout + router.push('/shop/checkout?source=buy'); + } catch (error) { + console.error('Gagal menambahkan produk ke keranjang:', error); + // setStatus('error'); + } + }; + + + const handleStatusChange = async (status) => { + setStatusNew(status); + setPageNew(1); + + if (status === 'all' && cachedAllData) { + setCurrentData(cachedAllData); + return; + } + const data = await fetchSite(status, 1); + + if (status === 'all') { + setCachedAllData(data); + } + + setCurrentData(data); + }; + + useEffect(() => { + setCachedAllData([]); + }, []); + + + const handleReset = () => { + setState([ + { + startDate: null, + endDate: new Date(), + key: 'selection', + }, + ]); + setIsOpenCalender(false); + router.push(`${router.pathname}`); + }; + + const formatDate = (dateString) => { + const months = [ + 'Januari', + 'Februari', + 'Maret', + 'April', + 'Mei', + 'Juni', + 'Juli', + 'Agustus', + 'September', + 'Oktober', + 'November', + 'Desember', + ]; + + const [day, month, year] = dateString.split('/'); + return `${day} ${months[parseInt(month, 10) - 1]} ${year}`; + }; + return ( <> <MobileView> <div className='p-4 flex flex-col gap-y-4'> - <form className='flex gap-x-3' onSubmit={handleSubmit}> - <input - type='text' - className='form-input' - placeholder='Cari Transaksi...' - value={inputQuery} - onChange={(e) => setInputQuery(e.target.value)} - /> - <button className='btn-light bg-transparent px-3' type='submit'> - <MagnifyingGlassIcon className='w-6' /> - </button> - </form> + <div className='grid grid-cols-[30%_30%_40%] justify-between items-center gap-2 w-full '> + <select + value={statusNew} + onChange={(e) => handleStatusChange(e.target.value)} + className='border border-gray-300 rounded-lg px-2 py-1 text-xs bg-white shadow-sm focus:outline-none focus:ring-2 focus:ring-red-500' + > + {statuses.map((status) => ( + <option key={status.id} value={status.id}> + {status.label} + </option> + ))} + </select> + <select + value={sortNew} + onChange={(e) => setSortNew(e.target.value)} + className='border border-gray-300 rounded-lg px-2 py-1 text-xs bg-white shadow-sm focus:outline-none focus:ring-2 focus:ring-red-500' + > + {sortes.map((status) => ( + <option key={status.id} value={status.id}> + {status.label} + </option> + ))} + </select> + <div ref={calendarRef} className="relative inline-block"> + <button + type='button' + className='p-2 w-auto h-auto cursor-pointer hover:bg-gray-100 rounded transition duration-150 ease-in-out flex items-center justify-center' + onClick={() => setIsOpenCalender((prev) => !prev)} + > + <span className='text-nowrap px-1 truncate flex items-center gap-1'> + {state[0]?.startDate ? ( + `${state[0].startDate.toLocaleDateString()} - ${state[0].endDate.toLocaleDateString()}` + ) : ( + <Calendar size={16} className="text-gray-500" /> + )} + </span> + </button> + {isOpenCalender && ( + <div 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' + > + × + </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'> + <button + className='px-4 py-1 bg-red-500 text-white rounded' + onClick={handleReset} + > + Reset + </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> {transactions.isLoading && ( <div className='flex justify-center my-4'> @@ -190,15 +614,15 @@ const Transactions = ({ context = '' }) => { {transactions.data?.saleOrders?.map((saleOrder, index) => ( <div - className='p-4 shadow border border-gray_r-3 rounded-md' + className='p-4 shadow border border-gray_r-3 text-xs rounded-md flex flex-col gap-2' key={index} > - <div className='grid grid-cols-2'> + <div className='flex flex-row justify-between items-start'> <Link href={`${router.pathname}/${saleOrder.id}`}> - <span className='text-caption-2 text-gray_r-11'> - No. Transaksi + <h2 className='text-danger-500'>{saleOrder.name}</h2> + <span className='font-medium text-black opacity-75'> + {formatDate(saleOrder.dateOrder.split(' ')[0]) || '-'} </span> - <h2 className='text-danger-500 mt-1'>{saleOrder.name}</h2> </Link> <div className='flex gap-x-1 justify-end'> <TransactionStatusBadge status={saleOrder.status} /> @@ -208,8 +632,94 @@ const Transactions = ({ context = '' }) => { /> </div> </div> - <Link href={`${router.pathname}/${saleOrder.id}`}> - <div className='grid grid-cols-2 mt-3'> + <div className='col-span-2 h-[1px] w-full bg-gray-300'></div> + <Link + href={`${router.pathname}/${saleOrder.id}`} + className='flex flex-col gap-2' + > + <div className='flex flex-row gap-2'> + <div className='flex w-1/5'> + <Image + src={saleOrder.products[0]?.parent?.image} + alt={saleOrder.products[0]?.name} + className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md' + /> + </div> + <div className='flex w-4/5 flex-col gap-2 justify-start'> + <p className='flex flex-row gap-2'> + <span className=' text-black'>Nomor PO:</span> + <span className=' text-red-500 font-semibold'> + {saleOrder.purchaseOrderName || '-'} + </span> + </p> + <p className='line-clamp-2 leading-6 tracking-wide opacity-90 !text-gray_r-12 font-semibold truncate text-sm'> + {saleOrder.products[0]?.parent?.name} + </p> + <p className='opacity-85 !text-gray_r-12'> + {saleOrder.products[0]?.quantity} x{' '} + {currencyFormat( + saleOrder.products[0]?.price?.priceDiscount + )} + </p> + <div className='flex flex-row justify-start items-center'> + {saleOrder.products?.length > 1 && ( + <div className='flex flex-row gap-1 justify-start items-center'> + {saleOrder.products + .slice(1, 4) + .map((product, index) => ( + <Image + key={index} // Tambahkan key untuk setiap elemen dalam map() + 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> + )} + </div> + )} + </div> + <div className='flex flex-row w-full text-nowrap gap-2 text-black'> + <span className=''>Pesanan dibuat oleh:</span> + <p className=' font-semibold truncate'> + {saleOrder.address.customer?.name || '-'} + </p> + </div> + </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'>Total Harga</p> + <p className='font-semibold'> + {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 @@ -226,8 +736,8 @@ const Transactions = ({ context = '' }) => { {saleOrder.invoiceCount} Invoice </p> </div> - </div> - <div className='grid grid-cols-2 mt-3'> + </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'> @@ -242,15 +752,16 @@ const Transactions = ({ context = '' }) => { {currencyFormat(saleOrder.amountTotal)} </p> </div> - </div> + </div> */} </Link> </div> ))} <Pagination pageCount={pageCount} - currentPage={parseInt(page)} - url={router.pathname + pageQuery} + currentPage={parseInt(pageNew)} + // url={router.pathname + pageQuery} + url={`${router.pathname}?${toQuery(_.omit(query, ['page']))}`} className='mt-2 mb-2' /> @@ -328,63 +839,430 @@ const Transactions = ({ context = '' }) => { <Menu /> </div> <div className='w-9/12 p-4 bg-white border border-gray_r-6 rounded'> - <div className='flex mb-6 items-center justify-between'> + <div className='flex mb-6 items-center justify-between '> <h1 className='text-title-sm font-semibold'> Daftar Transaksi{' '} {transactions?.data?.saleOrders - ? `(${transactions?.data?.saleOrders.length})` + ? `(${transactions?.data?.saleOrderTotal})` : ''} </h1> - <div className='grid grid-cols-2 gap-2'> - {listSites?.length > 0 ? ( + <div className='relative inline-block text-left'> + <button + onClick={() => setIsOpen(!isOpen)} + type='button' + className='btn-light bg-slate-50 mt-3 w-full gap-2 items-center flex flex-row !text-gray_r-11 px-4 py-3 mb-2' + > + <p>Export</p> + <motion.div + animate={{ rotate: isOpen ? 180 : 0 }} + transition={{ duration: 0.2, ease: 'easeInOut' }} + > + <ChevronDownIcon className='w-5' /> + </motion.div> + </button> + + {isOpen && ( + <motion.div + initial={{ opacity: 0, y: -10 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -10 }} + transition={{ duration: 0.2, ease: 'easeInOut' }} + className='absolute mt-2 w-fit py-1 bg-white border border-gray-300 rounded-md shadow-lg' + > + <button + onClick={() => handleDownload('csv')} + className='block w-full px-4 py-2 text-left hover:bg-gray-200 text-nowrap' + > + Download CSV + </button> + <button + onClick={() => handleDownload('xlsx')} + className='block w-full px-4 py-2 text-left hover:bg-gray-200 text-nowrap' + > + Download XLSX + </button> + </motion.div> + )} + </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" /> + </svg> + </button> + + {/* Swiper container scrollable */} + <div className="w-full overflow-hidden"> + <Swiper + spaceBetween={10} + slidesPerView="auto" + className="status-swiper" + modules={[Navigation]} + navigation={{ + nextEl: '.custom-next', + prevEl: '.custom-prev', + }} + > + {statuses.map((status) => ( + <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' + }`} + onClick={() => handleStatusChange(status.id)} + > + {status.label} + </button> + </SwiperSlide> + ))} + </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" /> + </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 ? ( + <select + value={siteFilter} + onChange={handleSiteFilterChange} + className='form-input' + > + <option value=''>Pilih Site</option> + {listSites.map((site) => ( + <option value={site} key={site}> + {site} + </option> + ))} + </select> + ) : ( + <div></div> + )} + + <form className='flex gap-x-1' onSubmit={handleSubmit}> + <input + type='text' + className='form-input' + placeholder='Cari Transaksi...' + value={inputQuery} + onChange={(e) => setInputQuery(e.target.value)} + /> + <button + className='btn-light bg-transparent px-3' + type='submit' + > + <MagnifyingGlassIcon className='w-6' /> + </button> + </form> + </div> + <div className='flex flex-row gap-4 items-center justify-center'> + <p> + Menampilkan {startItem}- + {endItem + ? endItem + : transactions?.data?.saleOrderTotal + ? transactions?.data?.saleOrderTotal + : limitNew * pageNew}{' '} + dari{' '} + {transactions?.data?.saleOrderTotal + ? transactions?.data?.saleOrderTotal + : limitNew * pageNew} + </p> <select - value={siteFilter} - onChange={handleSiteFilterChange} - className='form-input' + id='limitSelect' + value={limitNew} + onChange={(e) => { + setLimitNew(Number(e.target.value)); + setPageNew(1); + }} + className='border p-2' > - <option value=''>Pilih Site</option> - {listSites.map((site) => ( - <option value={site} key={site}> - {site} - </option> - ))} + <option value={10}>10</option> + <option value={15}>15</option> + <option value={20}>20</option> </select> - ) : (<div></div>)} - - <form className='flex gap-x-1' onSubmit={handleSubmit}> - <input - type='text' - className='form-input' - placeholder='Cari Transaksi...' - value={inputQuery} - onChange={(e) => setInputQuery(e.target.value)} - /> - <button - className='btn-light bg-transparent px-3' - type='submit' - > - <MagnifyingGlassIcon className='w-6' /> - </button> - </form> + <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" /> + )} + </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'> + <button + className='px-4 py-1 bg-red-500 text-white rounded' + onClick={handleReset} + > + Reset + </button> + </div> + </div> + )} + </div> + </div> </div> </div> - <button - onClick={handleExportExcel} - type='button' - className='btn-solid-red px-3 py-2 mr-auto mb-2' - > - <span>Download</span> - </button> - <table className='table-data'> + <div className='flex justify-center items-center'> + {!transactions.isLoading && + transactions?.data?.saleOrders?.length == 0 && ( + <div className='justify-center p-4'> + <p className='text-gray-500 text-center '> + Tidak Ada Transaksi + </p> + </div> + )} + + {transactions.isLoading && ( + <div className='flex justify-center items-center my-2'> + <Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' /> + </div> + )} + + {!transactions.isLoading && + transactions && + transactions.data?.saleOrders?.length > 0 && ( + <div className='flex flex-col gap-4 w-full'> + {transactions.data.saleOrders.map((saleOrder, index) => ( + <div + key={index} + className='border py-2 px-4 hover:border-red-500 w-full rounded-lg data-item' + > + <Link + href={`${router.pathname}/${saleOrder?.id}`} + className='hover:border-red-500 block w-full' + > + <div className='flex flex-row justify-between items-center py-2'> + <div className='flex justify-center gap-3'> + <TransactionStatusBadge + status={saleOrder.status} + /> + <p className='text-red-500'>{saleOrder.name}</p> + <p className='text-black'> + Salesperson:{' '} + { + <span className='font-semibold'> + {saleOrder.sales} + </span> + } + </p> + </div> + <div className='text-black'> + Tanggal Pesanan:{' '} + <span className='font-semibold'> + {formatDate( + saleOrder.dateOrder.split(' ')[0] + ) || '-'} + </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'> + <div className='min-w-36 max-w-36'> + <Image + src={saleOrder.products[0]?.parent?.image} + alt={saleOrder.products[0]?.name} + className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md' + /> + </div> + <div className='flex flex-col gap-3 justify-start truncate w-full'> + <p className='flex flex-row gap-2'> + <span className='text-sm text-black'> + Nomor PO: + </span> + <span className='text-sm text-red-500 font-semibold'> + {saleOrder.purchaseOrderName || '-'} + </span> + </p> + <p className='line-clamp-2 leading-6 tracking-wide opacity-90 !text-gray_r-12 font-semibold text-nowrap '> + {saleOrder.products[0]?.parent?.name} + </p> + <p className='opacity-85 !text-gray_r-12'> + {saleOrder.products[0]?.quantity} x{' '} + {currencyFormat( + saleOrder.products[0]?.price + ?.priceDiscount + )} + </p> + <div className='flex flex-row justify-start items-center'> + {saleOrder.products?.length > 1 && ( + <div className='flex flex-row gap-1 justify-start items-center'> + {saleOrder.products + .slice(1, 4) + .map((product, index) => ( + <Image + key={index} // Tambahkan key untuk setiap elemen dalam map() + src={product?.parent?.image} + alt={product?.name} + className='object-contain object-center border border-gray_r-6 h-16 w-16 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> + )} + </div> + )} + </div> + </div> + </div> + <div className='flex flex-row w-full text-nowrap gap-2 text-black'> + <span className='text-sm'> + Pesanan dibuat oleh: + </span> + <p className='text-sm font-semibold'> + {saleOrder.address.customer?.name || '-'} + </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='flex flex-col text-black'> + <p>Total Harga</p> + <p className='font-bold'> + {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> + </Link> + </div> + ))} + </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> - )} + {auth?.feature?.soApproval && <th>Site</th>} <th className='!text-left'>Salesperson</th> <th className='!text-left'>Total</th> <th>Status</th> @@ -435,12 +1313,13 @@ const Transactions = ({ context = '' }) => { </tr> ))} </tbody> - </table> + </table> */} <Pagination pageCount={pageCount} - currentPage={parseInt(page)} - url={router.pathname + (pageQuery ? `?${pageQuery}` : '')} + currentPage={parseInt(pageNew)} + // url={router.pathname + (pageQuery ? `?${pageQuery}` : '')} + url={`${router.pathname}?${toQuery(_.omit(query, ['page']))}`} className='mt-2 mb-2' /> </div> diff --git a/src/pages/my/transactions/index.jsx b/src/pages/my/transactions/index.jsx index c1fb9a67..987d851d 100644 --- a/src/pages/my/transactions/index.jsx +++ b/src/pages/my/transactions/index.jsx @@ -15,13 +15,13 @@ export default function MyTransactions() { <MobileView> <AppLayout title='Transaksi'> - <TransactionsComponent /> + <TransactionsComponent context='transactions' /> </AppLayout> </MobileView> <DesktopView> <BasicLayout> - <TransactionsComponent /> + <TransactionsComponent context='transactions' /> </BasicLayout> </DesktopView> </IsAuth> diff --git a/src/styles/globals.css b/src/styles/globals.css index 6447284e..1860fc2b 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -46,8 +46,8 @@ button { @apply text-[11px] leading-none font-medium - px-1 - py-1 + px-2 + py-2 rounded w-fit; } @@ -696,3 +696,11 @@ button { ::-webkit-scrollbar-thumb:hover { background-color: #555; } +.status-swiper { + width: 100%; + height: 100%; +} + +.status-swiper .swiper-slide { + width: auto !important; +} |
