diff options
| -rw-r--r-- | public/images/remove.png | bin | 0 -> 16895 bytes | |||
| -rw-r--r-- | src/lib/checkout/components/CheckoutSection.jsx | 257 | ||||
| -rw-r--r-- | src/lib/quotation/components/Quotation.jsx | 492 | ||||
| -rw-r--r-- | src/lib/transaction/api/approveApi.js | 13 | ||||
| -rw-r--r-- | src/lib/transaction/api/rejectApi.js | 13 | ||||
| -rw-r--r-- | src/lib/transaction/components/Transaction.jsx | 449 | ||||
| -rw-r--r-- | src/lib/transaction/components/stepper.jsx | 83 |
7 files changed, 1086 insertions, 221 deletions
diff --git a/public/images/remove.png b/public/images/remove.png Binary files differnew file mode 100644 index 00000000..2b4c3568 --- /dev/null +++ b/public/images/remove.png diff --git a/src/lib/checkout/components/CheckoutSection.jsx b/src/lib/checkout/components/CheckoutSection.jsx new file mode 100644 index 00000000..7f9ea08a --- /dev/null +++ b/src/lib/checkout/components/CheckoutSection.jsx @@ -0,0 +1,257 @@ +import Link from 'next/link'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import { AnimatePresence, motion } from 'framer-motion'; +import { Divider, Spinner } from '@chakra-ui/react'; + +export const SectionAddress = ({ address, label, url }) => { + return ( + <div className='p-4'> + <div className='flex justify-between items-center'> + <div className='font-medium'>{label}</div> + <Link className='text-caption-1' href={url}> + Pilih Alamat Lain + </Link> + </div> + + {address && ( + <div className='mt-4 text-caption-1'> + <div className='badge-red mb-2'> + {address.type.charAt(0).toUpperCase() + + address.type.slice(1) + + ' Address'} + </div> + <p className='font-medium'>{address.name}</p> + <p className='mt-2 text-gray_r-11'>{address.mobile}</p> + <p className='mt-1 text-gray_r-11'> + {address.street}, {address?.city?.name} + </p> + </div> + )} + </div> + ); +}; + +export const SectionValidation = ({ address }) => + address?.rajaongkirCityId == 0 && ( + <BottomPopup active={true} title='Update Alamat'> + <div className='leading-7 text-gray_r-12/80'> + Mohon untuk memperbarui alamat Anda dengan mengklik tombol di bawah ini.{' '} + </div> + <div className='flex justify-center mt-6 gap-x-4'> + <Link + className='btn-solid-red w-full md:w-fit text-white' + href={`/my/address/${address?.id}/edit`} + > + Update Alamat + </Link> + </div> + </BottomPopup> + ); + +export const SectionExpedisi = ({ + address, + listExpedisi, + setSelectedExpedisi, + checkWeigth, + checkoutValidation, + expedisiValidation, + loadingRajaOngkir, +}) => + address?.rajaongkirCityId > 0 && ( + <div className='p-4' ref={expedisiValidation}> + <div className='flex justify-between items-center'> + <div className='font-medium'>Pilih Ekspedisi: </div> + <div className='w-[250px]'> + <div className='flex items-center gap-x-4'> + <select + className={`form-input ${ + checkoutValidation ? 'border-red-500 shake' : '' + }`} + onChange={(e) => setSelectedExpedisi(e.target.value)} + required + > + <option value='0,0'>Pilih Pengiriman</option> + <option value='1,32'>SELF PICKUP</option> + {checkWeigth != true && + listExpedisi.map((expedisi) => ( + <option + disabled={checkWeigth} + value={expedisi.label + ',' + expedisi.carrierId} + key={expedisi.value} + > + {' '} + {expedisi.label.toUpperCase()}{' '} + </option> + ))} + </select> + + <AnimatePresence> + {loadingRajaOngkir && ( + <motion.div + initial={{ opacity: 0, width: 0 }} + animate={{ opacity: 1, width: '28px' }} + exit={{ opacity: 0, width: 0 }} + transition={{ + duration: 0.25, + }} + className='overflow-hidden' + > + <Spinner thickness='3px' speed='0.5s' color='red.500' /> + </motion.div> + )} + </AnimatePresence> + </div> + {checkoutValidation && ( + <span className='text-sm text-red-500'> + *silahkan pilih expedisi + </span> + )} + </div> + <style jsx>{` + .shake { + animation: shake 0.4s ease-in-out; + } + `}</style> + </div> + {checkWeigth == true && ( + <p className='mt-4 text-gray_r-11 leading-6'> + Mohon maaf, pengiriman hanya tersedia untuk self pickup karena + terdapat barang yang belum diatur beratnya. Mohon atur berat barang + dengan menghubungi admin melalui{' '} + <a + className='text-danger-500 inline' + href='https://api.whatsapp.com/send?phone=628128080622' + > + tautan ini + </a> + </p> + )} + </div> + ); + +export const SectionListService = ({ + listserviceExpedisi, + setSelectedServiceType, +}) => + listserviceExpedisi?.length > 0 && ( + <> + <div className='p-4'> + <div className='flex justify-between items-center'> + <div className='font-medium'>Tipe Layanan Ekspedisi: </div> + <div> + <select + className='form-input' + onChange={(e) => setSelectedServiceType(e.target.value)} + > + {listserviceExpedisi.map((service) => ( + <option + value={ + service.cost[0].value + + ',' + + service.description + + '-' + + service.service + + ',' + + extractDuration(service.cost[0].etd) + } + key={service.service} + > + {' '} + {service.description} - {service.service.toUpperCase()} + {extractDuration(service.cost[0].etd) && + ` (Estimasi Tiba ${extractDuration( + service.cost[0].etd + )} Hari)`} + </option> + ))} + </select> + </div> + </div> + </div> + <Divider /> + </> + ); + +export const PickupAddress = ({ label }) => ( + <div className='p-4'> + <div className='flex justify-between items-center'> + <div className='font-medium'>{label}</div> + </div> + <div className='mt-4 text-caption-1'> + <p className='font-medium'>Indoteknik</p> + <p className='mt-2 mb-2 text-gray_r-11 leading-6'> + Jl. Bandengan Utara Raya No.85, RT.3/RW.16, Penjaringan, Kec. + Penjaringan, Kota Jkt Utara, Daerah Khusus Ibukota Jakarta, Indonesia + Kodepos : 14440 + </p> + <p className='mt-1 text-gray_r-11'>Telp : 021-2933 8828/29</p> + <p className='mt-1 text-gray_r-11'>Mobile : 0813 9000 7430</p> + </div> + </div> +); + +const extractDuration = (text) => { + const matches = text.match(/\d+(?:-\d+)?/g); + + if (matches && matches.length === 1) { + const parts = matches[0].split('-'); + const min = parseInt(parts[0]); + const max = parseInt(parts[1]); + + if (min === max) { + return min.toString(); + } + + return matches[0]; + } + + return ''; +}; + +export function calculateEstimatedArrival(duration) { + if (duration) { + let estimationDate = duration.split('-'); + estimationDate[0] = parseInt(estimationDate[0]); + estimationDate[1] = parseInt(estimationDate[1]); + const from = addDays(new Date(), estimationDate[0] + 3); + const to = addDays(new Date(), estimationDate[1] + 3); + + let etdText = `*Estimasi tiba ${formatDate(from)}`; + + if (estimationDate[1] > estimationDate[0]) { + etdText += ` - ${formatDate(to)}`; + } + + return etdText; + } + + return ''; +} + +function addDays(date, days) { + const result = new Date(date); + result.setDate(result.getDate() + days); + return result; +} + +function formatDate(date) { + const day = date.getDate(); + const month = date.toLocaleString('default', { month: 'short' }); + return `${day} ${month}`; +} + +export function splitDuration(duration) { + if (duration) { + let estimationDate = null; + if (duration.includes('-')) { + estimationDate = duration.split('-'); + estimationDate = parseInt(estimationDate[1]); + } else { + estimationDate = parseInt(duration); + } + + return estimationDate; + } + + return ''; +}
\ No newline at end of file diff --git a/src/lib/quotation/components/Quotation.jsx b/src/lib/quotation/components/Quotation.jsx index 8c379ead..fbb0627c 100644 --- a/src/lib/quotation/components/Quotation.jsx +++ b/src/lib/quotation/components/Quotation.jsx @@ -1,102 +1,285 @@ -import Alert from '@/core/components/elements/Alert/Alert' -import Divider from '@/core/components/elements/Divider/Divider' -import Link from '@/core/components/elements/Link/Link' -import useAuth from '@/core/hooks/useAuth' -import CartApi from '@/lib/cart/api/CartApi' -import { ExclamationCircleIcon } from '@heroicons/react/24/outline' -import { useEffect, useState } from 'react' -import _ from 'lodash' -import { deleteItemCart, getCart, getItemCart } from '@/core/utils/cart' -import currencyFormat from '@/core/utils/currencyFormat' -import { toast } from 'react-hot-toast' +import Alert from '@/core/components/elements/Alert/Alert'; +import Divider from '@/core/components/elements/Divider/Divider'; +import Link from '@/core/components/elements/Link/Link'; +import useAuth from '@/core/hooks/useAuth'; +import CartApi from '@/lib/cart/api/CartApi'; +import { ExclamationCircleIcon } from '@heroicons/react/24/outline'; +import { useEffect, useRef, useState } from 'react'; +import _ from 'lodash'; +import { deleteItemCart, getCart, getItemCart } from '@/core/utils/cart'; +import currencyFormat from '@/core/utils/currencyFormat'; +import { toast } from 'react-hot-toast'; // import checkoutApi from '@/lib/checkout/api/checkoutApi' -import { useRouter } from 'next/router' -import VariantGroupCard from '@/lib/variant/components/VariantGroupCard' -import MobileView from '@/core/components/views/MobileView' -import DesktopView from '@/core/components/views/DesktopView' -import Image from '@/core/components/elements/Image/Image' -import { useQuery } from 'react-query' -import CardProdcuctsList from '@/core/components/elements/Product/cartProductsList' +import { useRouter } from 'next/router'; +import VariantGroupCard from '@/lib/variant/components/VariantGroupCard'; +import MobileView from '@/core/components/views/MobileView'; +import DesktopView from '@/core/components/views/DesktopView'; +import Image from '@/core/components/elements/Image/Image'; +import { useQuery } from 'react-query'; +import CardProdcuctsList from '@/core/components/elements/Product/cartProductsList'; +import { Skeleton } from '@chakra-ui/react'; +import { + PickupAddress, + SectionAddress, + SectionExpedisi, + SectionListService, + SectionValidation, + calculateEstimatedArrival, + splitDuration, +} from '../../checkout/components/CheckoutSection'; +import addressesApi from '@/lib/address/api/addressesApi'; +import { getItemAddress } from '@/core/utils/address'; +import ExpedisiList from '../../checkout/api/ExpedisiList'; +import axios from 'axios'; -const { checkoutApi } = require('@/lib/checkout/api/checkoutApi') -const { getProductsCheckout } = require('@/lib/checkout/api/checkoutApi') +const { checkoutApi } = require('@/lib/checkout/api/checkoutApi'); +const { getProductsCheckout } = require('@/lib/checkout/api/checkoutApi'); const Quotation = () => { - const router = useRouter() - const auth = useAuth() + const router = useRouter(); + const auth = useAuth(); - const { data: cartCheckout } = useQuery('cartCheckout', () => getProductsCheckout()) + const { data: cartCheckout } = useQuery('cartCheckout', () => + getProductsCheckout() + ); - const [products, setProducts] = useState(null) - const [totalAmount, setTotalAmount] = useState(0) - const [totalDiscountAmount, setTotalDiscountAmount] = useState(0) + const SELF_PICKUP_ID = 32; + + const [products, setProducts] = useState(null); + const [totalAmount, setTotalAmount] = useState(0); + const [totalDiscountAmount, setTotalDiscountAmount] = useState(0); + + //start set up address and carrier + const [selectedCarrierId, setselectedCarrierId] = useState(0); + const [listExpedisi, setExpedisi] = useState([]); + const [selectedExpedisi, setSelectedExpedisi] = useState(0); + const [checkWeigth, setCheckWeight] = useState(false); + const [checkoutValidation, setCheckoutValidation] = useState(false); + const [loadingRajaOngkir, setLoadingRajaOngkir] = useState(false); + + const [listserviceExpedisi, setListServiceExpedisi] = useState([]); + const [selectedServiceType, setSelectedServiceType] = useState(null); + + const [selectedCarrier, setselectedCarrier] = useState(0); + const [totalWeight, setTotalWeight] = useState(0); + + const [biayaKirim, setBiayaKirim] = useState(0); + const [selectedExpedisiService, setselectedExpedisiService] = useState(null); + const [etd, setEtd] = useState(null); + const [etdFix, setEtdFix] = useState(null); + + const expedisiValidation = useRef(null); + + const [selectedAddress, setSelectedAddress] = useState({ + shipping: null, + invoicing: null, + }); + + const [addresses, setAddresses] = useState(null); + + useEffect(() => { + if (!auth) return; + + const getAddresses = async () => { + const dataAddresses = await addressesApi(); + setAddresses(dataAddresses); + }; + + getAddresses(); + }, [auth]); + + useEffect(() => { + if (!addresses) return; + + const matchAddress = (key) => { + const addressToMatch = getItemAddress(key); + const foundAddress = addresses.filter( + (address) => address.id == addressToMatch + ); + if (foundAddress.length > 0) { + return foundAddress[0]; + } + return addresses[0]; + }; + + setSelectedAddress({ + shipping: matchAddress('shipping'), + invoicing: matchAddress('invoicing'), + }); + }, [addresses]); + + const loadExpedisi = async () => { + let dataExpedisi = await ExpedisiList(); + dataExpedisi = dataExpedisi.map((expedisi) => ({ + value: expedisi.id, + label: expedisi.name, + carrierId: expedisi.deliveryCarrierId, + })); + setExpedisi(dataExpedisi); + }; + + const loadServiceRajaOngkir = async () => { + setLoadingRajaOngkir(true); + const body = { + origin: 2127, + destination: selectedAddress.shipping.rajaongkirCityId, + weight: totalWeight, + courier: selectedCarrier, + originType: 'subdistrict', + destinationType: 'subdistrict', + }; + setBiayaKirim(0); + const dataService = await axios( + '/api/rajaongkir-service?body=' + JSON.stringify(body) + ); + setLoadingRajaOngkir(false); + setListServiceExpedisi(dataService.data[0].costs); + if (dataService.data[0].costs[0]) { + setBiayaKirim(dataService.data[0].costs[0]?.cost[0].value); + setselectedExpedisiService( + dataService.data[0].costs[0]?.description + + '-' + + dataService.data[0].costs[0]?.service + ); + setEtd(dataService.data[0].costs[0]?.cost[0].etd); + toast.success('Harap pilih tipe layanan pengiriman'); + } else { + toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'); + } + }; + + useEffect(() => { + setCheckoutValidation(false); + + if (selectedCarrier != 0 && selectedCarrier != 1 && totalWeight > 0) { + console.log('masuk'); + loadServiceRajaOngkir(); + } else { + setListServiceExpedisi(); + setBiayaKirim(0); + setselectedExpedisiService(); + setEtd(); + } + }, [selectedCarrier, selectedAddress, totalWeight]); + + useEffect(() => { + if (selectedExpedisi) { + let serviceType = selectedExpedisi.split(','); + if (serviceType[0] === 0) return; + + setselectedCarrier(serviceType[0]); + setselectedCarrierId(serviceType[1]); + setListServiceExpedisi([]); + } + }, [selectedExpedisi]); + + useEffect(() => { + if (selectedServiceType) { + let serviceType = selectedServiceType.split(','); + setBiayaKirim(serviceType[0]); + setselectedExpedisiService(serviceType[1]); + setEtd(serviceType[2]); + } + }, [selectedServiceType]); + + useEffect(() => { + if (etd) setEtdFix(calculateEstimatedArrival(etd)); + }, [etd]); + + // end set up address and carrier useEffect(() => { const loadProducts = async () => { - const cart = getCart() + const cart = getCart(); const variantIds = _.filter(cart, (o) => o.selected == true) .map((o) => o.productId) - .join(',') - const dataProducts = await CartApi({ variantIds }) + .join(','); + const dataProducts = await CartApi({ variantIds }); const productsWithQuantity = dataProducts?.map((product) => { return { ...product, - quantity: getItemCart({ productId: product.id }).quantity - } - }) + quantity: getItemCart({ productId: product.id }).quantity, + }; + }); if (productsWithQuantity) { Promise.all(productsWithQuantity).then((resolvedProducts) => { - setProducts(resolvedProducts) - }) + setProducts(resolvedProducts); + }); } - } + }; + loadExpedisi(); // loadProducts() - }, []) + }, []); useEffect(() => { - setProducts(cartCheckout?.products) - }, [cartCheckout]) + setProducts(cartCheckout?.products); + setCheckWeight(cartCheckout?.hasProductWithoutWeight); + setTotalWeight(cartCheckout?.totalWeight.g); + console.log(cartCheckout); + }, [cartCheckout]); useEffect(() => { if (products) { - let calculateTotalAmount = 0 - let calculateTotalDiscountAmount = 0 + let calculateTotalAmount = 0; + let calculateTotalDiscountAmount = 0; products.forEach((product) => { - calculateTotalAmount += product.price.price * product.quantity + calculateTotalAmount += product.price.price * product.quantity; calculateTotalDiscountAmount += - (product.price.price - product.price.priceDiscount) * product.quantity - }) - setTotalAmount(calculateTotalAmount) - setTotalDiscountAmount(calculateTotalDiscountAmount) + (product.price.price - product.price.priceDiscount) * + product.quantity; + }); + setTotalAmount(calculateTotalAmount); + setTotalDiscountAmount(calculateTotalDiscountAmount); } - }, [products]) + }, [products]); - const [isLoading, setIsLoading] = useState(false) + const [isLoading, setIsLoading] = useState(false); const checkout = async () => { - if (!products || products.length == 0) return - setIsLoading(true) + // validation checkout + if (selectedExpedisi === 0) { + setCheckoutValidation(true); + if (expedisiValidation.current) { + const position = expedisiValidation.current.getBoundingClientRect(); + window.scrollTo({ + top: position.top - 300 + window.pageYOffset, + behavior: 'smooth', + }); + } + return; + } + if (selectedCarrier != 1 && biayaKirim == 0) { + toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'); + return; + } + + if (!products || products.length == 0) return; + setIsLoading(true); const productOrder = products.map((product) => ({ product_id: product.id, - quantity: product.quantity - })) + quantity: product.quantity, + })); let data = { partner_shipping_id: auth.partnerId, partner_invoice_id: auth.partnerId, user_id: auth.id, - order_line: JSON.stringify(productOrder) - } - const isSuccess = await checkoutApi({ data }) - setIsLoading(false) + order_line: JSON.stringify(productOrder), + delivery_amount: biayaKirim, + carrier_id: selectedCarrierId, + estimated_arrival_days: splitDuration(etd), + delivery_service_type: selectedExpedisiService, + }; + const isSuccess = await checkoutApi({ data }); + setIsLoading(false); if (isSuccess?.id) { - for (const product of products) deleteItemCart({ productId: product.id }) - router.push(`/shop/quotation/finish?id=${isSuccess.id}`) - return + for (const product of products) deleteItemCart({ productId: product.id }); + router.push(`/shop/quotation/finish?id=${isSuccess.id}`); + return; } - toast.error('Gagal melakukan transaksi, terjadi kesalahan internal') - } + toast.error('Gagal melakukan transaksi, terjadi kesalahan internal'); + }; - const taxTotal = (totalAmount - totalDiscountAmount) * 0.11 + const taxTotal = (totalAmount - totalDiscountAmount) * 0.11; return ( <> @@ -107,16 +290,80 @@ const Quotation = () => { <ExclamationCircleIcon className='w-7 text-blue-700' /> </div> <span className='leading-5'> - Jika mengalami kesulitan dalam melakukan pembelian di website Indoteknik. Hubungi kami - disini + Jika mengalami kesulitan dalam melakukan pembelian di website + Indoteknik. Hubungi kami disini </span> </Alert> </div> <Divider /> + {selectedCarrierId == SELF_PICKUP_ID && ( + <div className='p-4'> + <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-4 h-4 mr-3' + 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'> + Fitur Self Pickup, hanya berlaku untuk customer di area jakarta. + Apa bila memilih fitur ini, anda akan dihubungi setelah barang + siap diambil. + </div> + </div> + </div> + )} + + {selectedCarrierId == SELF_PICKUP_ID && ( + <PickupAddress label='Alamat Pickup' /> + )} + {selectedCarrierId != SELF_PICKUP_ID && ( + <Skeleton + isLoaded={!!selectedAddress.invoicing && !!selectedAddress.shipping} + minHeight={320} + > + <SectionAddress + address={selectedAddress.shipping} + label='Alamat Pengiriman' + url='/my/address?select=shipping' + /> + <Divider /> + <SectionAddress + address={selectedAddress.invoicing} + label='Alamat Penagihan' + url='/my/address?select=invoice' + /> + </Skeleton> + )} + <Divider /> + <SectionValidation address={selectedAddress.invoicing} /> + <SectionExpedisi + address={selectedAddress.shipping} + listExpedisi={listExpedisi} + setSelectedExpedisi={setSelectedExpedisi} + checkWeigth={checkWeigth} + checkoutValidation={checkoutValidation} + expedisiValidation={expedisiValidation} + loadingRajaOngkir={loadingRajaOngkir} + /> + <Divider /> + <SectionListService + listserviceExpedisi={listserviceExpedisi} + setSelectedServiceType={setSelectedServiceType} + /> + <div className='p-4 flex flex-col gap-y-4'> - {products && <VariantGroupCard openOnClick={false} variants={products} />} + {products && ( + <VariantGroupCard openOnClick={false} variants={products} /> + )} </div> <Divider /> @@ -124,7 +371,9 @@ const Quotation = () => { <div className='p-4'> <div className='flex justify-between items-center'> <div className='font-medium'>Ringkasan Penawaran</div> - <div className='text-gray_r-11 text-caption-1'>{products?.length} Barang</div> + <div className='text-gray_r-11 text-caption-1'> + {products?.length} Barang + </div> </div> <hr className='my-4 border-gray_r-6' /> <div className='flex flex-col gap-y-4'> @@ -134,7 +383,9 @@ const Quotation = () => { </div> <div className='flex gap-x-2 justify-between'> <div className='text-gray_r-11'>Diskon Produk</div> - <div className='text-danger-500'>- {currencyFormat(cartCheckout?.totalDiscount)}</div> + <div className='text-danger-500'> + - {currencyFormat(cartCheckout?.totalDiscount)} + </div> </div> <div className='flex gap-x-2 justify-between'> <div className='text-gray_r-11'>Subtotal</div> @@ -144,17 +395,33 @@ const Quotation = () => { <div className='text-gray_r-11'>PPN 11%</div> <div>{currencyFormat(cartCheckout?.tax)}</div> </div> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'> + Biaya Kirim <p className='text-xs mt-3'>{etdFix}</p> + </div> + <div> + {currencyFormat( + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + )} + </div> + </div> </div> <hr className='my-4 border-gray_r-6' /> <div className='flex gap-x-2 justify-between mb-4'> <div>Grand Total</div> <div className='font-semibold text-gray_r-12'> - {currencyFormat(cartCheckout?.grandTotal)} + {currencyFormat( + cartCheckout?.grandTotal + + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + )} </div> </div> - <p className='text-caption-2 text-gray_r-10 mb-2'>*) Belum termasuk biaya pengiriman</p> + <p className='text-caption-2 text-gray_r-10 mb-2'> + *) Belum termasuk biaya pengiriman + </p> <p className='text-caption-2 text-gray_r-10 leading-5'> - Dengan melakukan pembelian melalui website Indoteknik, saya menyetujui{' '} + Dengan melakukan pembelian melalui website Indoteknik, saya + menyetujui{' '} <Link href='/syarat-ketentuan' className='inline font-normal'> Syarat & Ketentuan </Link>{' '} @@ -165,7 +432,11 @@ const Quotation = () => { <Divider /> <div className='flex gap-x-3 p-4'> - <button className='flex-1 btn-yellow' onClick={checkout} disabled={isLoading}> + <button + className='flex-1 btn-yellow' + onClick={checkout} + disabled={isLoading} + > {isLoading ? 'Loading...' : 'Quotation'} </button> </div> @@ -174,15 +445,62 @@ const Quotation = () => { <DesktopView> <div className='container mx-auto py-10 flex'> <div className='w-3/4 border border-gray_r-6 rounded bg-white p-4'> - <div className='font-medium'>Detail Barang</div> - <CardProdcuctsList isLoading={isLoading} products={products} source='checkout' /> + {selectedCarrierId == SELF_PICKUP_ID && ( + <PickupAddress label='Alamat Pickup' /> + )} + {selectedCarrierId != SELF_PICKUP_ID && ( + <Skeleton + isLoaded={ + !!selectedAddress.invoicing && !!selectedAddress.shipping + } + minHeight={290} + > + <SectionAddress + address={selectedAddress.shipping} + label='Alamat Pengiriman' + url='/my/address?select=shipping' + /> + <Divider /> + <SectionAddress + address={selectedAddress.invoicing} + label='Alamat Penagihan' + url='/my/address?select=invoice' + /> + </Skeleton> + )} + <Divider /> + <SectionValidation address={selectedAddress.invoicing} /> + <SectionExpedisi + address={selectedAddress.shipping} + listExpedisi={listExpedisi} + setSelectedExpedisi={setSelectedExpedisi} + checkWeigth={checkWeigth} + checkoutValidation={checkoutValidation} + expedisiValidation={expedisiValidation} + loadingRajaOngkir={loadingRajaOngkir} + /> + <Divider /> + <SectionListService + listserviceExpedisi={listserviceExpedisi} + setSelectedServiceType={setSelectedServiceType} + /> + {/* <div className='p-4'> */} + <div className='font-medium mb-6'>Detail Barang</div> + <CardProdcuctsList + isLoading={isLoading} + products={products} + source='checkout' + /> + {/* </div> */} </div> <div className='w-1/4 pl-4'> <div className='sticky top-48 border border-gray_r-6 bg-white rounded p-4'> <div className='flex justify-between items-center'> <div className='font-medium'>Ringkasan Pesanan</div> - <div className='text-gray_r-11 text-caption-1'>{products?.length} Barang</div> + <div className='text-gray_r-11 text-caption-1'> + {products?.length} Barang + </div> </div> <hr className='my-4 border-gray_r-6' /> @@ -205,6 +523,16 @@ const Quotation = () => { <div className='text-gray_r-11'>PPN 11%</div> <div>{currencyFormat(cartCheckout?.tax)}</div> </div> + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'> + Biaya Kirim <p className='text-xs mt-3'>{etdFix}</p> + </div> + <div> + {currencyFormat( + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + )} + </div> + </div> </div> <hr className='my-4 border-gray_r-6' /> @@ -212,14 +540,18 @@ const Quotation = () => { <div className='flex gap-x-2 justify-between mb-4'> <div>Grand Total</div> <div className='font-semibold text-gray_r-12'> - {currencyFormat(cartCheckout?.grandTotal)} + {currencyFormat( + cartCheckout?.grandTotal + + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + )} </div> </div> - <p className='text-caption-2 text-gray_r-11 mb-2'> + {/* <p className='text-caption-2 text-gray_r-11 mb-2'> *) Belum termasuk biaya pengiriman - </p> + </p> */} <p className='text-caption-2 text-gray_r-11 leading-5'> - Dengan melakukan pembelian melalui website Indoteknik, saya menyetujui{' '} + Dengan melakukan pembelian melalui website Indoteknik, saya + menyetujui{' '} <Link href='/syarat-ketentuan' className='inline font-normal'> Syarat & Ketentuan </Link>{' '} @@ -240,7 +572,7 @@ const Quotation = () => { </div> </DesktopView> </> - ) -} + ); +}; -export default Quotation +export default Quotation; 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/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..8f4b2038 100644 --- a/src/lib/transaction/components/Transaction.jsx +++ b/src/lib/transaction/components/Transaction.jsx @@ -1,82 +1,117 @@ -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 { 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 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 [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 auth = useAuth(); + const { transaction } = useTransaction({ id }); + + 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 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') - } - - const [cancelTransaction, setCancelTransaction] = useState(false) - const openCancelTransaction = () => setCancelTransaction(true) - const closeCancelTransaction = () => setCancelTransaction(false) + 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 [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 +137,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 +181,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 +218,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 +237,27 @@ const Transaction = ({ id }) => { <Manifest idAWB={idAWB} closePopup={closePopup}></Manifest> <MobileView> + <div className='p-4'> + <StepApproval layer={2} status={'cancel'} 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 +293,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,7 +359,29 @@ const Transaction = ({ id }) => { <Divider /> <div className='p-4 pt-0'> - {transaction.data?.status == 'draft' && ( + {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> @@ -308,10 +411,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,12 +438,36 @@ const Transaction = ({ id }) => { > Download </button> - {transaction.data?.status == 'draft' && ( - <button className='btn-yellow' onClick={checkout}> - Lanjutkan Transaksi - </button> - )} - {transaction.data?.status != 'draft' && ( + {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'} @@ -350,33 +490,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 +555,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> @@ -483,7 +637,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 +734,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 +776,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; |
