diff options
Diffstat (limited to 'src/lib/transaction/components')
| -rw-r--r-- | src/lib/transaction/components/Transaction.jsx | 534 | ||||
| -rw-r--r-- | src/lib/transaction/components/stepper.jsx | 83 |
2 files changed, 465 insertions, 152 deletions
diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx index 82eb1775..9962b46e 100644 --- a/src/lib/transaction/components/Transaction.jsx +++ b/src/lib/transaction/components/Transaction.jsx @@ -1,83 +1,123 @@ -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 { useMemo, 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 checkoutPoApi from '../api/checkoutPoApi' -import cancelTransactionApi from '../api/cancelTransactionApi' -import MobileView from '@/core/components/views/MobileView' -import DesktopView from '@/core/components/views/DesktopView' -import Menu from '@/lib/auth/components/Menu' -import Image from '@/core/components/elements/Image/Image' -import { createSlug } from '@/core/utils/slug' -import toTitleCase from '@/core/utils/toTitleCase' -import useAirwayBill from '../hooks/useAirwayBill' -import Manifest from '@/lib/treckingAwb/component/Manifest' +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 { useEffect, useMemo, useRef, useState } from 'react'; +import ImageNext from 'next/image'; +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 checkoutPoApi from '../api/checkoutPoApi'; +import cancelTransactionApi from '../api/cancelTransactionApi'; +import MobileView from '@/core/components/views/MobileView'; +import DesktopView from '@/core/components/views/DesktopView'; +import Menu from '@/lib/auth/components/Menu'; +import Image from '@/core/components/elements/Image/Image'; +import { createSlug } from '@/core/utils/slug'; +import toTitleCase from '@/core/utils/toTitleCase'; +import useAirwayBill from '../hooks/useAirwayBill'; +import Manifest from '@/lib/treckingAwb/component/Manifest'; +import useAuth from '@/core/hooks/useAuth'; +import StepApproval from './stepper'; +import aprpoveApi from '../api/approveApi'; +import rejectApi from '../api/rejectApi'; const Transaction = ({ id }) => { - const { transaction } = useTransaction({ id }) - const { queryAirwayBill } = useAirwayBill({ orderId: id }) + const auth = useAuth(); + const { transaction } = useTransaction({ id }); - const [airwayBillPopup, setAirwayBillPopup] = useState(null) + const statusApprovalWeb = transaction.data?.approvalStep; + + const { queryAirwayBill } = useAirwayBill({ orderId: id }); + const [airwayBillPopup, setAirwayBillPopup] = useState(null); + + 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 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 submitUploadPo = async () => { - const file = poFile.current.files[0] - const name = poNumber.current.value + const file = poFile.current.files[0]; + const name = poNumber.current.value; if (typeof file === 'undefined' || !name) { - toast.error('Nomor dan Dokumen PO harus diisi') - return + toast.error('Nomor dan Dokumen PO harus diisi'); + return; } if (file.size > 5000000) { - toast.error('Maksimal ukuran file adalah 5MB') - return + toast.error('Maksimal ukuran file adalah 5MB'); + return; } - const data = { name, file: await getFileBase64(file) } - const isUploaded = await uploadPoApi({ id, data }) + 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.success('Berhasil upload PO'); + transaction.refetch(); + closeUploadPo(); + return; } - toast.error('Terjadi kesalahan internal, coba lagi nanti atau hubungi kami') - } + 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 [cancelTransaction, setCancelTransaction] = useState(false); + const openCancelTransaction = () => setCancelTransaction(true); + const closeCancelTransaction = () => setCancelTransaction(false); + + const [rejectTransaction, setRejectTransaction] = useState(false); + + const openRejectTransaction = () => setRejectTransaction(true); + const closeRejectTransaction = () => setRejectTransaction(false); const submitCancelTransaction = async () => { - const isCancelled = await cancelTransactionApi({ transaction: transaction.data }) + const isCancelled = await cancelTransactionApi({ + transaction: transaction.data, + }); if (isCancelled) { - toast.success('Berhasil batalkan transaksi') - transaction.refetch() + toast.success('Berhasil batalkan transaksi'); + transaction.refetch(); } - closeCancelTransaction() - } + closeCancelTransaction(); + }; const checkout = async () => { if (!transaction.data?.purchaseOrderFile) { - toast.error('Mohon upload dokumen PO anda sebelum melanjutkan pesanan') - return + toast.error('Mohon upload dokumen PO anda sebelum melanjutkan pesanan'); + return; } - await checkoutPoApi({ id }) - toast.success('Berhasil melanjutkan pesanan') - transaction.refetch() - } + await checkoutPoApi({ id }); + toast.success('Berhasil melanjutkan pesanan'); + transaction.refetch(); + }; + + const handleApproval = async () => { + await aprpoveApi({ id }); + toast.success('Berhasil melanjutkan approval'); + transaction.refetch(); + }; + + const handleReject = async () => { + await rejectApi({ id }); + closeRejectTransaction(); + transaction.refetch(); + }; const memoizeVariantGroupCard = useMemo( () => ( @@ -102,19 +142,19 @@ const Transaction = ({ id }) => { </div> ), [transaction.data] - ) + ); 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> - ) + ); } const closePopup = () => { - setIdAWB(null) - } + setIdAWB(null); + }; return ( transaction.data?.name && ( @@ -146,6 +186,33 @@ const Transaction = ({ id }) => { </div> </BottomPopup> + <BottomPopup + active={rejectTransaction} + close={closeRejectTransaction} + 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 justify-end mt-6 gap-x-4'> + <button + className='btn-solid-red w-full md:w-fit' + type='button' + onClick={handleReject} + > + Ya, Batalkan + </button> + <button + className='btn-light w-full md:w-fit' + type='button' + onClick={closeRejectTransaction} + > + Batal + </button> + </div> + </BottomPopup> + <BottomPopup title='Upload PO' close={closeUploadPo} active={uploadPo}> <div> <label>Nomor PO</label> @@ -156,10 +223,18 @@ const Transaction = ({ id }) => { <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}> + <button + type='button' + className='btn-light w-full' + onClick={closeUploadPo} + > Batal </button> - <button type='button' className='btn-solid-red w-full' onClick={submitUploadPo}> + <button + type='button' + className='btn-solid-red w-full' + onClick={submitUploadPo} + > Upload </button> </div> @@ -167,18 +242,33 @@ const Transaction = ({ id }) => { <Manifest idAWB={idAWB} closePopup={closePopup}></Manifest> <MobileView> + <div className='p-4'> + {auth?.feature?.soApproval && ( + <StepApproval + layer={statusApprovalWeb} + status={transaction?.data?.status} + className='ml-auto' + /> + )} + </div> <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='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> + <DescriptionRow label='Nama Sales'> + {transaction.data?.sales} + </DescriptionRow> + <DescriptionRow label='Waktu Transaksi'> + {transaction.data?.dateOrder} + </DescriptionRow> </div> <Divider /> @@ -214,25 +304,27 @@ const Transaction = ({ id }) => { <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> + {!auth?.feature.soApproval && ( + <div className='p-4 flex flex-col gap-y-4'> + <DescriptionRow label='Purchase Order'> + {transaction.data?.purchaseOrderName || '-'} + </DescriptionRow> + <div className='flex items-center'> + <p className='text-gray_r-11 leading-none'>Dokumen PO</p> + <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> - </div> + )} <Divider /> @@ -278,11 +370,43 @@ const Transaction = ({ id }) => { <Divider /> <div className='p-4 pt-0'> - {transaction.data?.status == 'draft' && ( - <button className='btn-yellow w-full mt-4' onClick={checkout}> - Lanjutkan Transaksi - </button> - )} + {transaction.data?.status == 'draft' && + auth?.feature.soApproval && ( + <div className='flex gap-x-2'> + <button + className='btn-yellow w-full' + onClick={checkout} + disabled={ + transaction.data?.status === 'cancel' + ? true + : false || auth?.webRole === statusApprovalWeb + ? true + : false + } + > + Approve + </button> + <button + className='btn-solid-red px-7 w-full' + onClick={checkout} + disabled={ + transaction.data?.status === 'cancel' + ? true + : false || auth?.webRole === statusApprovalWeb + ? true + : false + } + > + Reject + </button> + </div> + )} + {transaction.data?.status == 'draft' && + !auth?.feature?.soApproval && ( + <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'} @@ -308,10 +432,23 @@ const Transaction = ({ id }) => { <Menu /> </div> <div className='w-9/12 p-4 py-6 bg-white border border-gray_r-6 rounded'> - <h1 className='text-title-sm font-semibold mb-6'>Detail Transaksi</h1> + <div className='flex justify-between'> + <h1 className='text-title-sm font-semibold mb-6'> + Detail Transaksi + </h1> + {auth?.feature?.soApproval && ( + <StepApproval + layer={statusApprovalWeb} + status={transaction?.data?.status} + className='ml-auto' + /> + )} + </div> <div className='flex items-center gap-x-2 mb-3'> - <span className='text-h-sm font-medium'>{transaction?.data?.name}</span> + <span className='text-h-sm font-medium'> + {transaction?.data?.name} + </span> <TransactionStatusBadge status={transaction?.data?.status} /> </div> <div className='flex gap-x-4'> @@ -322,20 +459,58 @@ const Transaction = ({ id }) => { > Download </button> - {transaction.data?.status == 'draft' && ( - <button className='btn-yellow' onClick={checkout}> - Lanjutkan Transaksi - </button> - )} - {transaction.data?.status != 'draft' && ( - <button - className='btn-light' - disabled={transaction.data?.status != 'waiting'} - onClick={openCancelTransaction} - > - Batalkan Transaksi - </button> - )} + {transaction.data?.status == 'draft' && + auth?.feature?.soApproval && + auth?.webRole && ( + <div className='flex gap-x-2'> + <button + className='btn-yellow' + onClick={handleApproval} + disabled={ + transaction.data?.status === 'cancel' + ? true + : false || auth?.webRole === statusApprovalWeb + ? true + : false || statusApprovalWeb < 1 + ? true + : false + } + > + Approve + </button> + <button + className='btn-solid-red px-7' + onClick={openRejectTransaction} + disabled={ + transaction.data?.status === 'cancel' + ? true + : false || auth?.webRole === statusApprovalWeb + ? true + : false || statusApprovalWeb < 1 + ? true + : false + } + > + Reject + </button> + </div> + )} + {transaction.data?.status == 'draft' && + !auth?.feature.soApproval && ( + <button className='btn-yellow' onClick={checkout}> + Lanjutkan Transaksi + </button> + )} + {transaction.data?.status != 'draft' && + !auth?.feature.soApproval && ( + <button + className='btn-light' + disabled={transaction.data?.status != 'waiting'} + onClick={openCancelTransaction} + > + Batalkan Transaksi + </button> + )} </div> <div className='grid grid-cols-2 gap-x-6 mt-6'> @@ -350,33 +525,45 @@ const Transaction = ({ id }) => { <div>Ketentuan Pembayaran</div> <div>: {transaction?.data?.paymentTerm}</div> - <div>Purchase Order</div> - <div> - : {transaction?.data?.purchaseOrderName}{' '} - <button - type='button' - className='inline-block text-danger-500' - onClick={ - transaction.data?.purchaseOrderFile - ? () => downloadPurchaseOrder(transaction.data) - : openUploadPo - } - > - {transaction?.data?.purchaseOrderFile ? 'Download' : 'Upload'} - </button> - </div> + {!auth?.feature?.soApproval && ( + <> + <div>Purchase Order</div> + <div> + : {transaction?.data?.purchaseOrderName}{' '} + <button + type='button' + className='inline-block text-danger-500' + onClick={ + transaction.data?.purchaseOrderFile + ? () => downloadPurchaseOrder(transaction.data) + : openUploadPo + } + > + {transaction?.data?.purchaseOrderFile + ? 'Download' + : 'Upload'} + </button> + </div> + </> + )} </div> </div> - <div className='text-h-sm font-semibold mt-10 mb-4'>Informasi Pelanggan</div> + <div className='text-h-sm font-semibold mt-10 mb-4'> + Informasi Pelanggan + </div> <div className='grid grid-cols-2 gap-x-4'> <div className='border border-gray_r-6 rounded p-3'> <div className='font-medium mb-4'>Detail Pelanggan</div> - <SectionContent address={transaction?.data?.address?.customer} /> + <SectionContent + address={transaction?.data?.address?.customer} + /> </div> </div> - <div className='text-h-sm font-semibold mt-10 mb-4'>Pengiriman</div> + <div className='text-h-sm font-semibold mt-10 mb-4'> + Pengiriman + </div> <div className='grid grid-cols-3 gap-1'> {transaction?.data?.pickings?.map((airway) => ( <button @@ -403,7 +590,9 @@ const Transaction = ({ id }) => { <div className='badge-red text-sm'>Belum ada pengiriman</div> )} - <div className='text-h-sm font-semibold mt-10 mb-4'>Rincian Pembelian</div> + <div className='text-h-sm font-semibold mt-10 mb-4'> + Rincian Pembelian + </div> <table className='table-data'> <thead> <tr> @@ -426,11 +615,39 @@ const Transaction = ({ id }) => { )} className='w-[20%] flex-shrink-0' > - <Image + + <div className="relative"> + <Image src={product?.parent?.image} alt={product?.name} className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md' /> + <div className="absolute top-0 right-4 flex mt-3"> + <div className="gambarB "> + {product.isSni && ( + <ImageNext + src="/images/sni-logo.png" + alt="SNI Logo" + className="w-2 h-4 object-contain object-top sm:h-4" + width={50} + height={50} + /> + )} + </div> + <div className="gambarC "> + {product.isTkdn && ( + <ImageNext + src="/images/TKDN.png" + alt="TKDN" + className="w-5 h-4 object-contain object-top ml-1 sm:h-4" + width={50} + height={50} + /> + )} + </div> + </div> + </div> + </Link> <div className='px-2 text-left'> <Link @@ -483,7 +700,9 @@ const Transaction = ({ id }) => { {currencyFormat(transaction.data?.amountTax)} </div> - <div className='text-right whitespace-nowrap'>Biaya Pengiriman</div> + <div className='text-right whitespace-nowrap'> + Biaya Pengiriman + </div> <div className='text-right font-medium'> {currencyFormat(transaction.data?.deliveryAmount)} </div> @@ -578,18 +797,18 @@ const Transaction = ({ id }) => { ))} */} </> ) - ) -} + ); +}; const SectionAddress = ({ address }) => { const [section, setSection] = useState({ customer: false, invoice: false, - shipping: false - }) + shipping: false, + }); const toggleSection = (name) => { - setSection({ ...section, [name]: !section[name] }) - } + setSection({ ...section, [name]: !section[name] }); + }; return ( <> @@ -620,39 +839,50 @@ const SectionAddress = ({ address }) => { /> {section.invoice && <SectionContent address={address?.invoice} />} */} </> - ) -} + ); +}; const SectionButton = ({ label, active, toggle }) => ( - <button className='p-4 font-medium flex justify-between w-full' onClick={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' />} + {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(toTitleCase(address.subDistrict.name)) - if (address?.district?.name) fullAddress.push(toTitleCase(address.district.name)) - if (address?.city?.name) fullAddress.push(toTitleCase(address.city.name)) - fullAddress = fullAddress.join(', ') + let fullAddress = []; + if (address?.street) fullAddress.push(address.street); + if (address?.subDistrict?.name) + fullAddress.push(toTitleCase(address.subDistrict.name)); + if (address?.district?.name) + fullAddress.push(toTitleCase(address.district.name)); + if (address?.city?.name) fullAddress.push(toTitleCase(address.city.name)); + fullAddress = fullAddress.join(', '); return ( <div className='flex flex-col gap-y-4 p-4 md:p-0 border-t border-gray_r-6 md:border-0'> <DescriptionRow label='Nama'>{address.name}</DescriptionRow> <DescriptionRow label='Email'>{address.email || '-'}</DescriptionRow> - <DescriptionRow label='No Telepon'>{address.mobile || '-'}</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 leading-6'>{children}</span> </div> -) +); -export default Transaction +export default Transaction; diff --git a/src/lib/transaction/components/stepper.jsx b/src/lib/transaction/components/stepper.jsx new file mode 100644 index 00000000..9b0da0d9 --- /dev/null +++ b/src/lib/transaction/components/stepper.jsx @@ -0,0 +1,83 @@ +import { + Box, + Step, + StepDescription, + StepIcon, + StepIndicator, + StepNumber, + StepSeparator, + StepStatus, + StepTitle, + Stepper, + useSteps, +} from '@chakra-ui/react'; +import Image from 'next/image'; + +const StepApproval = ({ layer, status }) => { + const steps = [ + { title: 'Indoteknik', layer_approval: 1 }, + { title: 'Manager', layer_approval: 2 }, + { title: 'Director', layer_approval: 3 }, + ]; + const { activeStep } = useSteps({ + index: layer, + count: steps.length, + }); + return ( + <Stepper size='md' index={layer} colorScheme='green'> + {steps.map((step, index) => ( + <Step key={index}> + <StepIndicator> + {layer === step.layer_approval && status === 'cancel' ? ( + <StepStatus + complete={ + <Image + src='/images/remove.png' + width={20} + height={20} + alt='' + className='w-full' + /> + } + incomplete={<StepNumber />} + active={<StepNumber />} + /> + ) : ( + <StepStatus + complete={<StepIcon />} + incomplete={<StepNumber />} + active={<StepNumber />} + /> + )} + </StepIndicator> + + <Box flexShrink='0'> + <StepTitle className='md:text-xs'>{step.title}</StepTitle> + {status === 'cancel' ? ( + layer > step.layer_approval ? ( + <StepDescription className='md:text-[8px]'> + Approved + </StepDescription> + ) : ( + <StepDescription className='md:text-[8px]'> + Rejected + </StepDescription> + ) + ) : layer >= step.layer_approval ? ( + <StepDescription className='md:text-[8px]'> + Approved + </StepDescription> + ) : ( + <StepDescription className='md:text-[8px]'> + Pending + </StepDescription> + )} + </Box> + <StepSeparator _horizontal={{ ml: '0' }} /> + </Step> + ))} + </Stepper> + ); +}; + +export default StepApproval; |
