diff options
Diffstat (limited to 'src/lib/transaction/components')
| -rw-r--r-- | src/lib/transaction/components/Transaction.jsx | 352 | ||||
| -rw-r--r-- | src/lib/transaction/components/TransactionStatusBadge.jsx | 41 | ||||
| -rw-r--r-- | src/lib/transaction/components/Transactions.jsx | 216 |
3 files changed, 609 insertions, 0 deletions
diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx new file mode 100644 index 00000000..7da33551 --- /dev/null +++ b/src/lib/transaction/components/Transaction.jsx @@ -0,0 +1,352 @@ +import Spinner from '@/core/components/elements/Spinner/Spinner' +import useTransaction from '../hooks/useTransaction' +import TransactionStatusBadge from './TransactionStatusBadge' +import Divider from '@/core/components/elements/Divider/Divider' +import { useRef, useState } from 'react' +import { downloadPurchaseOrder, downloadQuotation } from '../utils/transactions' +import BottomPopup from '@/core/components/elements/Popup/BottomPopup' +import uploadPoApi from '../api/uploadPoApi' +import { toast } from 'react-hot-toast' +import getFileBase64 from '@/core/utils/getFileBase64' +import currencyFormat from '@/core/utils/currencyFormat' +import VariantGroupCard from '@/lib/variant/components/VariantGroupCard' +import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon } from '@heroicons/react/24/outline' +import Link from '@/core/components/elements/Link/Link' +import Alert from '@/core/components/elements/Alert/Alert' +import checkoutPoApi from '../api/checkoutPoApi' +import cancelTransactionApi from '../api/cancelTransactionApi' + +const Transaction = ({ id }) => { + const { transaction } = useTransaction({ id }) + + const poNumber = useRef('') + const poFile = useRef('') + const [uploadPo, setUploadPo] = useState(false) + const openUploadPo = () => setUploadPo(true) + const closeUploadPo = () => setUploadPo(false) + const submitUploadPo = async () => { + const file = poFile.current.files[0] + const name = poNumber.current.value + if (typeof file === 'undefined' || !name) { + toast.error('Nomor dan Dokumen PO harus diisi', { position: 'bottom-center' }) + return + } + if (file.size > 5000000) { + toast.error('Maksimal ukuran file adalah 5MB', { position: 'bottom-center' }) + return + } + const data = { name, file: await getFileBase64(file) } + const isUploaded = await uploadPoApi({ id, data }) + if (isUploaded) { + toast.success('Berhasil upload PO') + transaction.refetch() + closeUploadPo() + return + } + toast.error('Terjadi kesalahan internal, coba lagi nanti atau hubungi kami') + } + + const [cancelTransaction, setCancelTransaction] = useState(false) + const openCancelTransaction = () => setCancelTransaction(true) + const closeCancelTransaction = () => setCancelTransaction(false) + const submitCancelTransaction = async () => { + const isCancelled = await cancelTransactionApi({ transaction: transaction.data }) + if (isCancelled) { + toast.success('Berhasil batalkan transaksi') + transaction.refetch() + } + closeCancelTransaction() + } + + const checkout = async () => { + if (!transaction.data?.purchaseOrderFile) { + toast.error('Mohon upload dokumen PO anda sebelum melanjutkan pesanan') + return + } + await checkoutPoApi({ id }) + toast.success('Berhasil melanjutkan pesanan') + transaction.refetch() + } + + if (transaction.isLoading) { + return ( + <div className='flex justify-center my-6'> + <Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' /> + </div> + ) + } + + return ( + transaction.data?.name && ( + <> + <div className='flex flex-col gap-y-4 p-4'> + <DescriptionRow label='Status Transaksi'> + <div className='flex justify-end'> + <TransactionStatusBadge status={transaction.data?.status} /> + </div> + </DescriptionRow> + <DescriptionRow label='No Transaksi'>{transaction.data?.name}</DescriptionRow> + <DescriptionRow label='Ketentuan Pembayaran'> + {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 flex flex-col gap-y-4'> + <DescriptionRow label='Purchase Order'> + {transaction.data?.purchaseOrderName || '-'} + </DescriptionRow> + <div className='flex items-center'> + <p className='text-gray_r-11 leading-none'>Dokumen PO</p> + <button + type='button' + className='btn-light py-1.5 px-3 ml-auto' + onClick={ + transaction.data?.purchaseOrderFile + ? () => downloadPurchaseOrder(transaction.data) + : openUploadPo + } + > + {transaction.data?.purchaseOrderFile ? 'Download' : 'Upload'} + </button> + </div> + </div> + + <Divider /> + + <div className='font-medium p-4'>Detail Produk</div> + + <div className='p-4 pt-0 flex flex-col gap-y-3'> + <VariantGroupCard + variants={transaction.data?.products} + buyMore + /> + <div className='flex justify-between mt-3 font-medium'> + <p>Total Belanja</p> + <p>{currencyFormat(transaction.data?.amountTotal)}</p> + </div> + </div> + + <Divider /> + + <SectionAddress address={transaction.data?.address} /> + + <Divider /> + + <div className='p-4'> + <p className='font-medium'>Invoice</p> + <div className='flex flex-col gap-y-3 mt-4'> + {transaction.data?.invoices?.map((invoice, index) => ( + <Link + href={`/my/invoice/${invoice.id}`} + key={index} + > + <div className='shadow rounded-md p-4 text-gray_r-12 font-normal flex justify-between'> + <div> + <p className='mb-2'>{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> + ))} + {transaction.data?.invoices?.length === 0 && ( + <Alert + type='info' + className='text-center' + > + Belum ada Invoice + </Alert> + )} + </div> + </div> + + <Divider /> + + <div className='p-4 pt-0'> + {transaction.data?.status == 'draft' && ( + <button + className='btn-yellow w-full mt-4' + onClick={checkout} + > + Lanjutkan Transaksi + </button> + )} + <button + className='btn-light w-full mt-4' + disabled={transaction.data?.status != 'draft'} + onClick={downloadQuotation} + > + Download Quotation + </button> + {transaction.data?.status != 'draft' && ( + <button + className='btn-light w-full mt-4' + disabled={transaction.data?.status != 'waiting'} + onClick={openCancelTransaction} + > + Batalkan Transaksi + </button> + )} + </div> + + <BottomPopup + active={cancelTransaction} + close={closeCancelTransaction} + title='Batalkan Transaksi' + > + <div className='leading-7 text-gray_r-12/80'> + Apakah anda yakin membatalkan transaksi{' '} + <span className='underline'>{transaction.data?.name}</span>? + </div> + <div className='flex mt-6 gap-x-4'> + <button + className='btn-solid-red flex-1' + type='button' + onClick={submitCancelTransaction} + > + Ya, Batalkan + </button> + <button + className='btn-light flex-1' + type='button' + onClick={closeCancelTransaction} + > + Batal + </button> + </div> + </BottomPopup> + + <BottomPopup + title='Upload PO' + close={closeUploadPo} + active={uploadPo} + > + <div> + <label>Nomor PO</label> + <input + type='text' + className='form-input mt-3' + ref={poNumber} + /> + </div> + <div className='mt-4'> + <label>Dokumen PO</label> + <input + type='file' + className='form-input mt-3 py-2' + ref={poFile} + /> + </div> + <div className='grid grid-cols-2 gap-x-3 mt-6'> + <button + type='button' + className='btn-light w-full' + onClick={closeUploadPo} + > + Batal + </button> + <button + type='button' + className='btn-solid-red w-full' + onClick={submitUploadPo} + > + Upload + </button> + </div> + </BottomPopup> + </> + ) + ) +} + +const SectionAddress = ({ address }) => { + const [section, setSection] = useState({ + customer: false, + invoice: false, + shipping: false + }) + const toggleSection = (name) => { + setSection({ ...section, [name]: !section[name] }) + } + + return ( + <> + <SectionButton + label='Detail Pelanggan' + active={section.customer} + toggle={() => toggleSection('customer')} + /> + + {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} />} + </> + ) +} + +const SectionButton = ({ label, active, toggle }) => ( + <button + className='p-4 font-medium flex justify-between w-full' + onClick={toggle} + > + <span>{label}</span> + {active ? <ChevronUpIcon className='w-5' /> : <ChevronDownIcon className='w-5' />} + </button> +) + +const SectionContent = ({ address }) => { + let fullAddress = [] + if (address?.street) fullAddress.push(address.street) + if (address?.subDistrict?.name) fullAddress.push(address.subDistrict.name) + if (address?.district?.name) fullAddress.push(address.district.name) + if (address?.city?.name) fullAddress.push(address.city.name) + fullAddress = fullAddress.join(', ') + + return ( + <div className='flex flex-col gap-y-4 p-4 border-t border-gray_r-6'> + <DescriptionRow label='Nama'>{address.name}</DescriptionRow> + <DescriptionRow label='Email'>{address.email || '-'}</DescriptionRow> + <DescriptionRow label='No Telepon'>{address.mobile || '-'}</DescriptionRow> + <DescriptionRow label='Alamat'>{fullAddress}</DescriptionRow> + </div> + ) +} + +const DescriptionRow = ({ children, label }) => ( + <div className='grid grid-cols-2'> + <span className='text-gray_r-11'>{label}</span> + <span className='text-right'>{children}</span> + </div> +) + +export default Transaction diff --git a/src/lib/transaction/components/TransactionStatusBadge.jsx b/src/lib/transaction/components/TransactionStatusBadge.jsx new file mode 100644 index 00000000..7372e4da --- /dev/null +++ b/src/lib/transaction/components/TransactionStatusBadge.jsx @@ -0,0 +1,41 @@ +const TransactionStatusBadge = ({ status }) => { + let badgeProps = { + className: ['h-fit'], + text: '' + } + switch (status) { + case 'cancel': + badgeProps.className.push('badge-solid-red') + badgeProps.text = 'Pesanan batal' + break + case 'draft': + badgeProps.className.push('badge-red') + badgeProps.text = 'Pending quotation' + break + case 'waiting': + badgeProps.className.push('badge-yellow') + badgeProps.text = 'Pesanan diterima' + break + case 'sale': + badgeProps.className.push('badge-yellow') + badgeProps.text = 'Pesanan diproses' + break + case 'shipping': + badgeProps.className.push('badge-green') + badgeProps.text = 'Pesanan dikirim' + break + case 'partial_shipping': + badgeProps.className.push('badge-green') + badgeProps.text = 'Dikirim sebagian' + break + case 'done': + badgeProps.className.push('badge-solid-green') + badgeProps.text = 'Pesanan selesai' + break + } + badgeProps.className = badgeProps.className.join(' ') + + return <div className={badgeProps.className}>{badgeProps.text}</div> +} + +export default TransactionStatusBadge diff --git a/src/lib/transaction/components/Transactions.jsx b/src/lib/transaction/components/Transactions.jsx new file mode 100644 index 00000000..ccbdede2 --- /dev/null +++ b/src/lib/transaction/components/Transactions.jsx @@ -0,0 +1,216 @@ +import { useRouter } from 'next/router' +import { useState } from 'react' +import { toast } from 'react-hot-toast' +import { EllipsisVerticalIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline' + +import { downloadPurchaseOrder, downloadQuotation } from '../utils/transactions' +import useTransactions from '../hooks/useTransactions' +import currencyFormat from '@/core/utils/currencyFormat' +import cancelTransactionApi from '../api/cancelTransactionApi' +import TransactionStatusBadge from './TransactionStatusBadge' +import Spinner from '@/core/components/elements/Spinner/Spinner' +import Link from '@/core/components/elements/Link/Link' +import BottomPopup from '@/core/components/elements/Popup/BottomPopup' +import Pagination from '@/core/components/elements/Pagination/Pagination' +import { toQuery } from 'lodash-contrib' +import _ from 'lodash' +import Alert from '@/core/components/elements/Alert/Alert' + +const Transactions = () => { + const router = useRouter() + const { q = '', page = 1 } = router.query + + const limit = 10 + + const query = { + name: q, + offset: (page - 1) * limit, + limit + } + const { transactions } = useTransactions({ query }) + + const [inputQuery, setInputQuery] = useState(q) + const [toOthers, setToOthers] = useState(null) + const [toCancel, setToCancel] = useState(null) + + const submitCancelTransaction = async () => { + const isCancelled = await cancelTransactionApi({ + transaction: toCancel + }) + if (isCancelled) { + toast.success('Berhasil batalkan transaksi') + transactions.refetch() + } + setToCancel(null) + } + + const pageCount = Math.ceil(transactions?.data?.saleOrderTotal / limit) + let pageQuery = _.omit(query, ['limit', 'offset']) + pageQuery = _.pickBy(pageQuery, _.identity) + pageQuery = toQuery(pageQuery) + + const handleSubmit = (e) => { + e.preventDefault() + router.push(`/my/transactions?q=${inputQuery}`) + } + + return ( + <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> + + {transactions.isLoading && ( + <div className='flex justify-center my-4'> + <Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' /> + </div> + )} + + {!transactions.isLoading && transactions.data?.saleOrders?.length === 0 && ( + <Alert + type='info' + className='text-center' + > + Tidak ada data transaksi + </Alert> + )} + + {transactions.data?.saleOrders?.map((saleOrder, index) => ( + <div + className='p-4 shadow border border-gray_r-3 rounded-md' + key={index} + > + <div className='grid grid-cols-2'> + <Link href={`/my/transaction/${saleOrder.id}`}> + <span className='text-caption-2 text-gray_r-11'>No. Transaksi</span> + <h2 className='text-red_r-11 mt-1'>{saleOrder.name}</h2> + </Link> + <div className='flex gap-x-1 justify-end'> + <TransactionStatusBadge status={saleOrder.status} /> + <EllipsisVerticalIcon + className='w-5 h-5' + onClick={() => setToOthers(saleOrder)} + /> + </div> + </div> + <Link href={`/my/transaction/${saleOrder.id}`}> + <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> + </div> + </Link> + </div> + ))} + + <Pagination + pageCount={pageCount} + currentPage={parseInt(page)} + url={`/my/transactions${pageQuery}`} + className='mt-2 mb-2' + /> + + <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> + + <BottomPopup + active={toCancel} + close={() => setToCancel(null)} + title='Batalkan Transaksi' + > + <div className='leading-7 text-gray_r-12/80'> + Apakah anda yakin membatalkan transaksi{' '} + <span className='underline'>{toCancel?.name}</span>? + </div> + <div className='flex mt-6 gap-x-4'> + <button + className='btn-solid-red flex-1' + type='button' + onClick={submitCancelTransaction} + > + Ya, Batalkan + </button> + <button + className='btn-light flex-1' + type='button' + onClick={() => setToCancel(null)} + > + Batal + </button> + </div> + </BottomPopup> + </div> + ) +} + +export default Transactions |
