summaryrefslogtreecommitdiff
path: root/src/lib/transaction
diff options
context:
space:
mode:
authortrisusilo48 <tri.susilo@altama.co.id>2024-07-10 15:58:51 +0700
committertrisusilo48 <tri.susilo@altama.co.id>2024-07-10 15:58:51 +0700
commit2e3c726bc8217f3960cfecec44b81303b03de72b (patch)
tree1b85ced7f61f3e4c3f1f27b577b37aa161615065 /src/lib/transaction
parent2b3bd9c0a454dbad69ce29cee877bfb1fca5dfa6 (diff)
parenta99bf6480eea556e53b85e6db45f3b8c2361e693 (diff)
Merge branch 'release' into development
# Conflicts: # src/pages/shop/product/variant/[slug].jsx
Diffstat (limited to 'src/lib/transaction')
-rw-r--r--src/lib/transaction/api/approveApi.js13
-rw-r--r--src/lib/transaction/api/listSiteApi.js10
-rw-r--r--src/lib/transaction/api/rejectApi.js13
-rw-r--r--src/lib/transaction/components/Transaction.jsx552
-rw-r--r--src/lib/transaction/components/Transactions.jsx328
-rw-r--r--src/lib/transaction/components/stepper.jsx83
6 files changed, 758 insertions, 241 deletions
diff --git a/src/lib/transaction/api/approveApi.js b/src/lib/transaction/api/approveApi.js
new file mode 100644
index 00000000..891f0235
--- /dev/null
+++ b/src/lib/transaction/api/approveApi.js
@@ -0,0 +1,13 @@
+import odooApi from '@/core/api/odooApi'
+import { getAuth } from '@/core/utils/auth'
+
+const aprpoveApi = async ({ id }) => {
+ const auth = getAuth()
+ const dataCheckout = await odooApi(
+ 'POST',
+ `/api/v1/partner/${auth?.partnerId}/sale_order/${id}/approve`
+ )
+ return dataCheckout
+}
+
+export default aprpoveApi
diff --git a/src/lib/transaction/api/listSiteApi.js b/src/lib/transaction/api/listSiteApi.js
new file mode 100644
index 00000000..8b7740c5
--- /dev/null
+++ b/src/lib/transaction/api/listSiteApi.js
@@ -0,0 +1,10 @@
+import odooApi from '@/core/api/odooApi'
+import { getAuth } from '@/core/utils/auth'
+
+const getSite = async () => {
+ const auth = getAuth()
+ const dataSite = await odooApi('GET', `/api/v1/partner/${auth?.partnerId}/list/site`)
+ return dataSite
+}
+
+export default getSite \ No newline at end of file
diff --git a/src/lib/transaction/api/rejectApi.js b/src/lib/transaction/api/rejectApi.js
new file mode 100644
index 00000000..127c0d38
--- /dev/null
+++ b/src/lib/transaction/api/rejectApi.js
@@ -0,0 +1,13 @@
+import odooApi from '@/core/api/odooApi'
+import { getAuth } from '@/core/utils/auth'
+
+const rejectApi = async ({ id }) => {
+ const auth = getAuth()
+ const dataCheckout = await odooApi(
+ 'POST',
+ `/api/v1/partner/${auth?.partnerId}/sale_order/${id}/reject`
+ )
+ return dataCheckout
+}
+
+export default rejectApi
diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx
index 82eb1775..c6152ca9 100644
--- a/src/lib/transaction/components/Transaction.jsx
+++ b/src/lib/transaction/components/Transaction.jsx
@@ -1,83 +1,120 @@
-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 +139,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 +183,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 +220,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 +239,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 +301,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 +367,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 +429,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 +456,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 +522,50 @@ 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>Site</div>
+ <div>: {transaction?.data?.sitePartner}</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,12 +592,14 @@ 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>
<th>Nama Produk</th>
- <th>Diskon</th>
+ {/* <th>Diskon</th> */}
<th>Jumlah</th>
<th>Harga</th>
<th>Subtotal</th>
@@ -426,11 +617,37 @@ const Transaction = ({ id }) => {
)}
className='w-[20%] flex-shrink-0'
>
- <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='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
@@ -451,18 +668,18 @@ const Transaction = ({ id }) => {
</div>
</div>
</td>
- <td>
+ {/* <td>
{product.price.discountPercentage > 0
? `${product.price.discountPercentage}%`
: ''}
- </td>
+ </td> */}
<td>{product.quantity}</td>
<td>
- {product.price.discountPercentage > 0 && (
+ {/* {product.price.discountPercentage > 0 && (
<div className='line-through mb-1 text-caption-1 text-gray_r-12/70'>
{currencyFormat(product.price.price)}
</div>
- )}
+ )} */}
<div>{currencyFormat(product.price.priceDiscount)}</div>
</td>
<td>{currencyFormat(product.price.subtotal)}</td>
@@ -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/Transactions.jsx b/src/lib/transaction/components/Transactions.jsx
index be63effd..92bdd276 100644
--- a/src/lib/transaction/components/Transactions.jsx
+++ b/src/lib/transaction/components/Transactions.jsx
@@ -1,63 +1,163 @@
-import { useRouter } from 'next/router'
-import { useState } from 'react'
-import { toast } from 'react-hot-toast'
-import { EllipsisVerticalIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-hot-toast';
+import {
+ EllipsisVerticalIcon,
+ MagnifyingGlassIcon,
+} from '@heroicons/react/24/outline';
+import useAuth from '@/core/hooks/useAuth';
-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'
-import MobileView from '@/core/components/views/MobileView'
-import DesktopView from '@/core/components/views/DesktopView'
-import Menu from '@/lib/auth/components/Menu'
+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';
+import MobileView from '@/core/components/views/MobileView';
+import DesktopView from '@/core/components/views/DesktopView';
+import Menu from '@/lib/auth/components/Menu';
+import * as XLSX from 'xlsx';
+import getSite from '../api/listSiteApi';
+import transactionsApi from '../api/transactionsApi';
const Transactions = ({ context = '' }) => {
- const router = useRouter()
- const { q = '', page = 1 } = router.query
+ const auth = useAuth();
+ const router = useRouter();
+ const { q = '', page = 1, site = null } = router.query;
- const limit = 15
+ const limit = 15;
+
+ const [inputQuery, setInputQuery] = useState(q);
+ const [toOthers, setToOthers] = useState(null);
+ const [toCancel, setToCancel] = useState(null);
+ const [listSites, setListSites] = useState([]);
+
+ const [siteFilter, setSiteFilter] = useState(site);
const query = {
name: q,
offset: (page - 1) * limit,
context,
- limit
- }
- const { transactions } = useTransactions({ query })
+ limit,
+ site:
+ siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null),
+ };
+
+ const { transactions } = useTransactions({ query });
- const [inputQuery, setInputQuery] = useState(q)
- const [toOthers, setToOthers] = useState(null)
- const [toCancel, setToCancel] = useState(null)
+ const fetchSite = async () => {
+ const site = await getSite();
+ setListSites(site.sites);
+ };
const submitCancelTransaction = async () => {
const isCancelled = await cancelTransactionApi({
- transaction: toCancel
- })
+ transaction: toCancel,
+ });
if (isCancelled) {
- toast.success('Berhasil batalkan transaksi')
- transactions.refetch()
+ toast.success('Berhasil batalkan transaksi');
+ transactions.refetch();
}
- setToCancel(null)
- }
+ setToCancel(null);
+ };
- const pageCount = Math.ceil(transactions?.data?.saleOrderTotal / limit)
- let pageQuery = _.omit(query, ['limit', 'offset', 'context'])
- pageQuery = _.pickBy(pageQuery, _.identity)
- pageQuery = toQuery(pageQuery)
+ const pageCount = Math.ceil(transactions?.data?.saleOrderTotal / limit);
+ let pageQuery = _.omit(query, ['limit', 'offset', 'context']);
+ pageQuery = _.pickBy(
+ pageQuery,
+ (value, key) => value !== '' && !(key === 'page' && value === '1')
+ );
+ pageQuery = toQuery(pageQuery);
const handleSubmit = (e) => {
- e.preventDefault()
- router.push(`${router.pathname}?q=${inputQuery}`)
- }
+ e.preventDefault();
+ const queryParams = {};
+ if (inputQuery) queryParams.q = inputQuery;
+ if (siteFilter) queryParams.site = siteFilter;
+ router.push({
+ pathname: router.pathname,
+ query: queryParams,
+ });
+ };
+
+ const handleSiteFilterChange = (e) => {
+ setSiteFilter(e.target.value);
+ const queryParams = {};
+ if (inputQuery) queryParams.q = inputQuery;
+ if (e.target.value) queryParams.site = e.target.value;
+ router.push({
+ pathname: router.pathname,
+ query: queryParams,
+ });
+ };
+
+ const exportToExcel = (data, siteFilter) => {
+ const fieldsToExport = [
+ 'No. Transaksi',
+ 'No. PO',
+ 'Tanggal',
+ 'Created By',
+ 'Salesperson',
+ 'Total',
+ 'Status',
+ ];
+ const rowsToExport = [];
+
+ data.forEach((saleOrder) => {
+ const row = {
+ 'No. Transaksi': saleOrder.name,
+ 'No. PO': saleOrder.purchaseOrderName || '-',
+ Tanggal: saleOrder.dateOrder || '-',
+ 'Created By': saleOrder.address.customer?.name || '-',
+ Salesperson: saleOrder.sales,
+ Total: currencyFormat(saleOrder.amountTotal),
+ Status: saleOrder.status,
+ };
+ if (siteFilter) {
+ row['Site'] = siteFilter;
+ }
+ rowsToExport.push(row);
+ });
+ const worksheet = XLSX.utils.json_to_sheet(rowsToExport, {
+ header: fieldsToExport,
+ });
+
+ const workbook = XLSX.utils.book_new();
+ XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
+ XLSX.writeFile(workbook, 'transactions.xlsx');
+ };
+
+ const getAllData = async () => {
+ const query = {
+ name: q,
+ context,
+ site:
+ siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null),
+ };
+ const queryString = toQuery(query)
+ const data = await transactionsApi({ query: queryString });
+ return data;
+ };
+
+ const handleExportExcel = async () => {
+ const dataToExport = await getAllData();
+
+ exportToExcel(dataToExport?.saleOrders, siteFilter);
+ };
+
+ useEffect(() => {
+ fetchSite();
+ }, []);
return (
<>
<MobileView>
@@ -81,17 +181,23 @@ const Transactions = ({ context = '' }) => {
</div>
)}
- {!transactions.isLoading && transactions.data?.saleOrders?.length === 0 && (
- <Alert type='info' className='text-center'>
- Tidak ada transaksi
- </Alert>
- )}
+ {!transactions.isLoading &&
+ transactions.data?.saleOrders?.length === 0 && (
+ <Alert type='info' className='text-center'>
+ Tidak ada transaksi
+ </Alert>
+ )}
{transactions.data?.saleOrders?.map((saleOrder, index) => (
- <div className='p-4 shadow border border-gray_r-3 rounded-md' key={index}>
+ <div
+ className='p-4 shadow border border-gray_r-3 rounded-md'
+ key={index}
+ >
<div className='grid grid-cols-2'>
<Link href={`${router.pathname}/${saleOrder.id}`}>
- <span className='text-caption-2 text-gray_r-11'>No. Transaksi</span>
+ <span className='text-caption-2 text-gray_r-11'>
+ No. Transaksi
+ </span>
<h2 className='text-danger-500 mt-1'>{saleOrder.name}</h2>
</Link>
<div className='flex gap-x-1 justify-end'>
@@ -105,13 +211,17 @@ const Transactions = ({ context = '' }) => {
<Link href={`${router.pathname}/${saleOrder.id}`}>
<div className='grid grid-cols-2 mt-3'>
<div>
- <span className='text-caption-2 text-gray_r-11'>No. Purchase Order</span>
+ <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>
+ <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>
@@ -120,10 +230,14 @@ const Transactions = ({ context = '' }) => {
<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>
+ <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>
+ <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>
@@ -140,14 +254,18 @@ const Transactions = ({ context = '' }) => {
className='mt-2 mb-2'
/>
- <BottomPopup title='Lainnya' active={toOthers} close={() => setToOthers(null)}>
+ <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)
+ downloadPurchaseOrder(toOthers);
+ setToOthers(null);
}}
>
Download PO
@@ -156,8 +274,8 @@ const Transactions = ({ context = '' }) => {
className='text-left disabled:opacity-60'
disabled={toOthers?.status != 'draft'}
onClick={() => {
- downloadQuotation(toOthers)
- setToOthers(null)
+ downloadQuotation(toOthers);
+ setToOthers(null);
}}
>
Download Quotation
@@ -166,8 +284,8 @@ const Transactions = ({ context = '' }) => {
className='text-left disabled:opacity-60'
disabled={toOthers?.status != 'waiting'}
onClick={() => {
- setToCancel(toOthers)
- setToOthers(null)
+ setToCancel(toOthers);
+ setToOthers(null);
}}
>
Batalkan Transaksi
@@ -175,7 +293,11 @@ const Transactions = ({ context = '' }) => {
</div>
</BottomPopup>
- <BottomPopup active={toCancel} close={() => setToCancel(null)} title='Batalkan Transaksi'>
+ <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>?
@@ -188,7 +310,11 @@ const Transactions = ({ context = '' }) => {
>
Ya, Batalkan
</button>
- <button className='btn-light flex-1' type='button' onClick={() => setToCancel(null)}>
+ <button
+ className='btn-light flex-1'
+ type='button'
+ onClick={() => setToCancel(null)}
+ >
Batal
</button>
</div>
@@ -205,21 +331,50 @@ const Transactions = ({ context = '' }) => {
<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?.saleOrders
+ ? `(${transactions?.data?.saleOrders.length})`
+ : ''}
</h1>
- <form className='flex gap-x-2' 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-2 gap-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>
+ <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'>
<thead>
<tr>
@@ -227,6 +382,9 @@ const Transactions = ({ context = '' }) => {
<th>No. PO</th>
<th>Tanggal</th>
<th>Created By</th>
+ {auth?.feature?.soApproval && (
+ <th>Site</th>
+ )}
<th className='!text-left'>Salesperson</th>
<th className='!text-left'>Total</th>
<th>Status</th>
@@ -252,13 +410,23 @@ const Transactions = ({ context = '' }) => {
{transactions.data?.saleOrders?.map((saleOrder) => (
<tr key={saleOrder.id}>
<td>
- <Link className='whitespace-nowrap' href={`${router.pathname}/${saleOrder.id}`}>{saleOrder.name}</Link>
+ <Link
+ className='whitespace-nowrap'
+ href={`${router.pathname}/${saleOrder.id}`}
+ >
+ {saleOrder.name}
+ </Link>
</td>
<td>{saleOrder.purchaseOrderName || '-'}</td>
<td>{saleOrder.dateOrder || '-'}</td>
<td>{saleOrder.address.customer?.name || '-'}</td>
+ {auth?.feature?.soApproval && (
+ <td>{saleOrder.sitePartner || '-'}</td>
+ )}
<td className='!text-left'>{saleOrder.sales}</td>
- <td className='!text-left'>{currencyFormat(saleOrder.amountTotal)}</td>
+ <td className='!text-left'>
+ {currencyFormat(saleOrder.amountTotal)}
+ </td>
<td>
<div className='flex justify-center'>
<TransactionStatusBadge status={saleOrder.status} />
@@ -272,14 +440,14 @@ const Transactions = ({ context = '' }) => {
<Pagination
pageCount={pageCount}
currentPage={parseInt(page)}
- url={router.pathname + pageQuery}
+ url={router.pathname + (pageQuery ? `?${pageQuery}` : '')}
className='mt-2 mb-2'
/>
</div>
</div>
</DesktopView>
</>
- )
-}
+ );
+};
-export default Transactions
+export default Transactions;
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;