diff options
Diffstat (limited to 'src/lib')
21 files changed, 1414 insertions, 474 deletions
diff --git a/src/lib/auth/components/LoginDesktop.jsx b/src/lib/auth/components/LoginDesktop.jsx index 1333db14..9a68dc53 100644 --- a/src/lib/auth/components/LoginDesktop.jsx +++ b/src/lib/auth/components/LoginDesktop.jsx @@ -8,6 +8,7 @@ import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner'; +import Image from 'next/image'; const LoginDesktop = () => { const { @@ -108,7 +109,7 @@ const LoginDesktop = () => { {!isLoading ? 'Masuk' : 'Loading...'} </button> </form> - {/* <div className='flex items-center mt-3 mb-3'> + <div className='flex items-center mt-3 mb-3'> <hr className='flex-1' /> <p className='text-gray-400'>ATAU</p> <hr className='flex-1' /> @@ -127,7 +128,7 @@ const LoginDesktop = () => { height={10} /> <p>Masuk dengan Google</p> - </button> */} + </button> <div className='text-gray_r-11 mt-10'> Belum punya akun Indoteknik?{' '} diff --git a/src/lib/auth/components/LoginMobile.jsx b/src/lib/auth/components/LoginMobile.jsx index 40924fbe..d2bf704f 100644 --- a/src/lib/auth/components/LoginMobile.jsx +++ b/src/lib/auth/components/LoginMobile.jsx @@ -117,7 +117,7 @@ const LoginMobile = () => { {!isLoading ? 'Masuk' : 'Loading...'} </button> </form> - {/* <div className='flex items-center mt-3 mb-3'> + <div className='flex items-center mt-3 mb-3'> <hr className='flex-1' /> <p className='text-gray-400'>ATAU</p> <hr className='flex-1' /> @@ -136,7 +136,7 @@ const LoginMobile = () => { height={10} /> <p>Masuk dengan Google</p> - </button> */} + </button> <div className='text-gray_r-11 mt-4'> Belum punya akun Indoteknik?{' '} diff --git a/src/lib/auth/hooks/useLogin.js b/src/lib/auth/hooks/useLogin.js index dc9580ea..dd5a4b03 100644 --- a/src/lib/auth/hooks/useLogin.js +++ b/src/lib/auth/hooks/useLogin.js @@ -74,7 +74,7 @@ const useLogin = () => { if (data.isAuth) { session.odooUser = data.user; setCookie('auth', JSON.stringify(session?.odooUser)); - router.push(decodeURIComponent(router?.query?.next) ?? '/'); + router.push(router?.query?.next || '/'); return; } }; diff --git a/src/lib/brand/components/BrandCard.jsx b/src/lib/brand/components/BrandCard.jsx index bb1a17f7..731214ff 100644 --- a/src/lib/brand/components/BrandCard.jsx +++ b/src/lib/brand/components/BrandCard.jsx @@ -1,4 +1,4 @@ -import Image from '@/core/components/elements/Image/Image' +import Image from '~/components/ui/image' import Link from '@/core/components/elements/Link/Link' import useDevice from '@/core/hooks/useDevice' import { createSlug } from '@/core/utils/slug' @@ -16,6 +16,8 @@ const BrandCard = ({ brand }) => { <Image src={brand.logo} alt={brand.name} + width={128} + height={128} className='h-full w-full object-contain object-center' /> )} diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 042fc258..4355c7bd 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -119,6 +119,8 @@ const Checkout = () => { const [checkoutValidation, setCheckoutValidation] = useState(false); const [loadingVoucher, setLoadingVoucher] = useState(true); const [loadingRajaOngkir, setLoadingRajaOngkir] = useState(false); + const [grandTotal, setGrandTotal] = useState(0); + const [hasFlashSale, setHasFlashSale] = useState(false); const expedisiValidation = useRef(null); @@ -241,7 +243,9 @@ const Checkout = () => { setProducts(cartCheckout?.products); setCheckWeight(cartCheckout?.hasProductWithoutWeight); setTotalWeight(cartCheckout?.totalWeight.g); + setHasFlashSale(cartCheckout?.products[0]?.hasFlashsale ? cartCheckout.products[0].hasFlashsale : false); }, [cartCheckout]); + useEffect(() => { setCheckoutValidation(false); @@ -313,6 +317,14 @@ const Checkout = () => { const [isLoading, setIsLoading] = useState(false); + useEffect(() => { + const GT = + cartCheckout?.grandTotal + + Math.round(parseInt(finalShippingAmt * 1.1) / 1000) * 1000; + const finalGT = GT < 0 ? 0 : GT; + setGrandTotal(finalGT); + }, [biayaKirim, cartCheckout?.grandTotal, activeVoucher]); + const checkout = async () => { const file = poFile.current.files[0]; if (typeof file !== 'undefined' && file.size > 5000000) { @@ -342,14 +354,17 @@ const Checkout = () => { quantity: product.quantity, })); let data = { - partner_shipping_id: auth.partnerId, - partner_invoice_id: auth.partnerId, + // partner_shipping_id: auth.partnerId, + // partner_invoice_id: auth.partnerId, + partner_shipping_id: selectedAddress?.shipping?.id || auth.partnerId, + partner_invoice_id: selectedAddress?.invoicing?.id || auth.partnerId, user_id: auth.id, order_line: JSON.stringify(productOrder), delivery_amount: biayaKirim, carrier_id: selectedCarrierId, estimated_arrival_days: splitDuration(etd), delivery_service_type: selectedExpedisiService, + flash_sale : hasFlashSale, // dibuat negasi untuk ngetest kebalikan nilai false voucher: activeVoucher, voucher_shipping: activeVoucherShipping, type: 'sale_order', @@ -366,16 +381,25 @@ const Checkout = () => { toast.error('Gagal melakukan transaksi, terjadi kesalahan internal'); return; } - + gtagPurchase(products, biayaKirim, isCheckouted.name); const midtrans = async () => { for (const product of products) deleteItemCart({ productId: product.id }); - const payment = await axios.post( - `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/midtrans-payment?transactionId=${isCheckouted.id}` - ); - setIsLoading(false); - window.location.href = payment.data.redirectUrl; + if (grandTotal > 0) { + const payment = await axios.post( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/midtrans-payment?transactionId=${isCheckouted.id}` + ); + setIsLoading(false); + window.location.href = payment.data.redirectUrl; + } else { + window.location.href = `${ + process.env.NEXT_PUBLIC_SELF_HOST + }/shop/checkout/success?order_id=${isCheckouted.name.replace( + /\//g, + '-' + )}`; + } }; gtag('event', 'conversion', { @@ -1071,7 +1095,7 @@ const Checkout = () => { <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 + finalShippingAmt)} + {currencyFormat(grandTotal)} </div> </div> )} @@ -1367,9 +1391,13 @@ const Checkout = () => { <div className='flex gap-x-2 justify-between mb-4'> <div>Grand Total</div> <div className='font-semibold text-gray_r-12'> +<<<<<<< HEAD + {currencyFormat(grandTotal)} +======= {currencyFormat( cartCheckout?.grandTotal + finalShippingAmt )} +>>>>>>> dev/voucher-shipment </div> </div> )} @@ -1600,7 +1628,7 @@ const SectionExpedisi = ({ dengan menghubungi admin melalui{' '} <a className='text-danger-500 inline' - href='https://api.whatsapp.com/send?phone=628128080622' + href='https://api.whatsapp.com/send?phone=6281717181922' > tautan ini </a> diff --git a/src/lib/checkout/components/CheckoutOld.jsx b/src/lib/checkout/components/CheckoutOld.jsx index d57fbd66..e2c45ce6 100644 --- a/src/lib/checkout/components/CheckoutOld.jsx +++ b/src/lib/checkout/components/CheckoutOld.jsx @@ -696,7 +696,7 @@ const SectionExpedisi = ({ address, listExpedisi, setSelectedExpedisi, checkWeig diatur beratnya. Mohon atur berat barang dengan menghubungi admin melalui{' '} <a className='text-danger-500 inline' - href='https://api.whatsapp.com/send?phone=628128080622' + href='https://api.whatsapp.com/send?phone=6281717181922' > tautan ini </a> diff --git a/src/lib/checkout/components/CheckoutSection.jsx b/src/lib/checkout/components/CheckoutSection.jsx index 7f9ea08a..affe6138 100644 --- a/src/lib/checkout/components/CheckoutSection.jsx +++ b/src/lib/checkout/components/CheckoutSection.jsx @@ -120,7 +120,7 @@ export const SectionExpedisi = ({ dengan menghubungi admin melalui{' '} <a className='text-danger-500 inline' - href='https://api.whatsapp.com/send?phone=628128080622' + href='https://api.whatsapp.com/send?phone=6281717181922' > tautan ini </a> diff --git a/src/lib/checkout/email/FinishCheckoutEmail.jsx b/src/lib/checkout/email/FinishCheckoutEmail.jsx index d40ce7d4..d19ba1ca 100644 --- a/src/lib/checkout/email/FinishCheckoutEmail.jsx +++ b/src/lib/checkout/email/FinishCheckoutEmail.jsx @@ -14,8 +14,10 @@ import { Section, Text } from '@react-email/components' +import FinishCheckout from '../components/FinishCheckout' const FinishCheckoutEmail = ({ transaction, payment, statusPayment }) => { + return ( <Html> <Head /> @@ -38,7 +40,10 @@ const FinishCheckoutEmail = ({ transaction, payment, statusPayment }) => { </Heading> <Text style={style.text}>Hai {transaction.address.customer.name},</Text> - <Text style={style.text}> + + {transaction.amountTotal > 0 ? + <div> + <Text style={style.text}> {statusPayment == 'success' && ( <> Terima kasih atas kepercayaan anda berbelanja di Indoteknik. Dengan ini kami @@ -71,202 +76,204 @@ const FinishCheckoutEmail = ({ transaction, payment, statusPayment }) => { & Solution </> )} - </Text> - <Text style={style.text}> - {['pending', 'failed'].includes(statusPayment) && ( - <> - Jika anda mengalami kesulitan, dapat menghubungi Customer Service kami untuk - menanyakan transaksi anda lakukan melalui Whatsapp kami. - </> - )} - {statusPayment == 'success' && ( - <> - Anda dapat menghubungi Customer Service kami untuk menanyakan status pesanan yang - sudah berhasil anda lakukan melalui Whatsapp kami. - </> - )} - {statusPayment == 'manual' && ( + </Text> + <Text style={style.text}> + {['pending', 'failed'].includes(statusPayment) && ( + <> + Jika anda mengalami kesulitan, dapat menghubungi Customer Service kami untuk + menanyakan transaksi anda lakukan melalui Whatsapp kami. + </> + )} + {statusPayment == 'success' && ( + <> + Anda dapat menghubungi Customer Service kami untuk menanyakan status pesanan yang + sudah berhasil anda lakukan melalui Whatsapp kami. + </> + )} + {statusPayment == 'manual' && ( + <> + Kami mohon kepada {transaction.address.customer.name} untuk dapat segera + menyelesaikan transaksi dengan detail dibawah ini: + <ul> + <li>Nomor Pembelian: {transaction.name}</li> + <li>Nominal: {currencyFormat(transaction.amountTotal)}</li> + <li>Tanggal: {transaction.dateOrder}</li> + </ul> + </> + )} + </Text> + + {['pending', 'failed', 'success'].includes(statusPayment) && ( <> - Kami mohon kepada {transaction.address.customer.name} untuk dapat segera - menyelesaikan transaksi dengan detail dibawah ini: - <ul> - <li>Nomor Pembelian: {transaction.name}</li> - <li>Nominal: {currencyFormat(transaction.amountTotal)}</li> - <li>Tanggal: {transaction.dateOrder}</li> - </ul> - </> - )} - </Text> + <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}> + <strong>Detail Transaksi</strong> + </Text> + + <Hr style={style.hr} /> - {['pending', 'failed', 'success'].includes(statusPayment) && ( - <> - <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}> - <strong>Detail Transaksi</strong> - </Text> + <Section style={style.alert}> + {statusPayment == 'success' && + 'Struk ini dapat anda simpan sebagai bukti tambahan dalam transaksi yang telah dilakukan.'} + {statusPayment == 'pending' && + 'Kami akan menginformasikan melalui email setelah anda berhasil melakukan pembayaran.'} + {statusPayment == 'failed' && + 'Dimohon untuk tidak melakukan pembayaran. Karena transaksi anda tidak berhasil dibuat.'} + </Section> - <Hr style={style.hr} /> + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>No Transaksi (SO)</Column> + <Column style={style.descriptionRCol}>{transaction.name}</Column> + </Row> + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>Tanggal Transaksi</Column> + <Column style={style.descriptionRCol}>{payment.transactionTime}</Column> + </Row> + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>Status Pembayaran</Column> + <Column style={{ ...style.descriptionRCol }}> + {statusPayment == 'success' && ( + <div style={{ ...style.badge, ...style.badgeGreen }}>Berhasil</div> + )} + {statusPayment == 'pending' && ( + <div style={{ ...style.badge, ...style.badgeRed }}>Pending</div> + )} + {statusPayment == 'failed' && ( + <div style={{ ...style.badge, ...style.badgeRed }}>Tidak Berhasil</div> + )} + </Column> + </Row> + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>Metode Pembayaran</Column> + <Column style={style.descriptionRCol}> + {toTitleCase(payment.paymentType.replaceAll('_', ' '))} + </Column> + </Row> + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>Batas Akhir Pembayaran</Column> + <Column style={style.descriptionRCol}>{payment.expiryTime}</Column> + </Row> + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>Nominal Transfer</Column> + <Column style={style.descriptionRCol}> + <span style={{ fontWeight: '600' }}>{currencyFormat(payment.grossAmount)}</span> + </Column> + </Row> - <Section style={style.alert}> - {statusPayment == 'success' && - 'Struk ini dapat anda simpan sebagai bukti tambahan dalam transaksi yang telah dilakukan.'} - {statusPayment == 'pending' && - 'Kami akan menginformasikan melalui email setelah anda berhasil melakukan pembayaran.'} - {statusPayment == 'failed' && - 'Dimohon untuk tidak melakukan pembayaran. Karena transaksi anda tidak berhasil dibuat.'} - </Section> + <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}> + <strong>Detail Produk</strong> + </Text> - <Row style={style.descriptionRow}> - <Column style={style.descriptionLCol}>No Transaksi (SO)</Column> - <Column style={style.descriptionRCol}>{transaction.name}</Column> - </Row> - <Row style={style.descriptionRow}> - <Column style={style.descriptionLCol}>Tanggal Transaksi</Column> - <Column style={style.descriptionRCol}>{payment.transactionTime}</Column> - </Row> - <Row style={style.descriptionRow}> - <Column style={style.descriptionLCol}>Status Pembayaran</Column> - <Column style={{ ...style.descriptionRCol }}> - {statusPayment == 'success' && ( - <div style={{ ...style.badge, ...style.badgeGreen }}>Berhasil</div> - )} - {statusPayment == 'pending' && ( - <div style={{ ...style.badge, ...style.badgeRed }}>Pending</div> - )} - {statusPayment == 'failed' && ( - <div style={{ ...style.badge, ...style.badgeRed }}>Tidak Berhasil</div> - )} - </Column> - </Row> - <Row style={style.descriptionRow}> - <Column style={style.descriptionLCol}>Metode Pembayaran</Column> - <Column style={style.descriptionRCol}> - {toTitleCase(payment.paymentType.replaceAll('_', ' '))} - </Column> - </Row> - <Row style={style.descriptionRow}> - <Column style={style.descriptionLCol}>Batas Akhir Pembayaran</Column> - <Column style={style.descriptionRCol}>{payment.expiryTime}</Column> - </Row> - <Row style={style.descriptionRow}> - <Column style={style.descriptionLCol}>Nominal Transfer</Column> - <Column style={style.descriptionRCol}> - <span style={{ fontWeight: '600' }}>{currencyFormat(payment.grossAmount)}</span> - </Column> - </Row> + <Hr style={style.hr} /> - <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}> - <strong>Detail Produk</strong> - </Text> + {transaction.products.map((product) => ( + <Row style={style.productRow} key={product.id}> + <Column style={style.productLCol}> + <Img src={product.parent.image} width='100%' /> + </Column> + <Column style={style.productRCol}> + <Text style={style.productName}>{product.name}</Text> + <Text style={style.productCode}>{product.code}</Text> + <div style={{ dislay: 'flex' }}> + <span style={style.productPriceA}> + {currencyFormat(product.price.priceDiscount)} + </span> + {product.price.discountPercentage > 0 && ( + <> + + <span style={style.productPriceB}> + {currencyFormat(product.price.price)} + </span> + </> + )} + x {product.quantity} barang + </div> + </Column> + </Row> + ))} - <Hr style={style.hr} /> + <Hr style={style.hr} /> - {transaction.products.map((product) => ( - <Row style={style.productRow} key={product.id}> - <Column style={style.productLCol}> - <Img src={product.parent.image} width='100%' /> + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>Subtotal</Column> + <Column style={style.descriptionRCol}> + {currencyFormat(transaction.subtotal)} </Column> - <Column style={style.productRCol}> - <Text style={style.productName}>{product.name}</Text> - <Text style={style.productCode}>{product.code}</Text> - <div style={{ dislay: 'flex' }}> - <span style={style.productPriceA}> - {currencyFormat(product.price.priceDiscount)} - </span> - {product.price.discountPercentage > 0 && ( - <> - - <span style={style.productPriceB}> - {currencyFormat(product.price.price)} - </span> - </> - )} - x {product.quantity} barang - </div> + </Row> + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>Total Diskon</Column> + <Column style={{ ...style.descriptionRCol, color: '#E20613' }}> + {currencyFormat(transaction.discountTotal)} + </Column> + </Row> + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>PPN 11% (Incl.)</Column> + <Column style={style.descriptionRCol}> + {currencyFormat(transaction.subtotal * 0.11)} </Column> </Row> - ))} - - <Hr style={style.hr} /> - - <Row style={style.descriptionRow}> - <Column style={style.descriptionLCol}>Subtotal</Column> - <Column style={style.descriptionRCol}> - {currencyFormat(transaction.subtotal)} - </Column> - </Row> - <Row style={style.descriptionRow}> - <Column style={style.descriptionLCol}>Total Diskon</Column> - <Column style={{ ...style.descriptionRCol, color: '#E20613' }}> - {currencyFormat(transaction.discountTotal)} - </Column> - </Row> - <Row style={style.descriptionRow}> - <Column style={style.descriptionLCol}>PPN 11% (Incl.)</Column> - <Column style={style.descriptionRCol}> - {currencyFormat(transaction.subtotal * 0.11)} - </Column> - </Row> - - <Hr style={style.hr} /> - <Row style={style.descriptionRow}> - <Column style={style.descriptionLCol}>Grand Total</Column> - <Column style={style.descriptionRCol}> - <span style={{ fontWeight: '600' }}> - {currencyFormat(transaction.amountTotal)} - </span> - </Column> - </Row> + <Hr style={style.hr} /> - <Hr style={style.hr} /> - </> - )} + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>Grand Total</Column> + <Column style={style.descriptionRCol}> + <span style={{ fontWeight: '600' }}> + {transaction.amountTotal > 0 ? currencyFormat(transaction.amountTotal) : '0'} + </span> + </Column> + </Row> - {statusPayment == 'manual' && ( - <> - <Text style={style.text}> - Dengan cara dibawah ini: - <ul> - <li> - Lakukan pembayaran manual via mobile app perbankan{' '} - {transaction.address.customer.name} - <br /> - Nama Bank: Bank Central Asia (BCA) - <br /> - No. Rek: 8870400081 - <br /> - A/N: INDOTEKNIK DOTCOM GEMILANG PT - </li> - <li> - Setelah berhasil melakukan pembayaran, mohon agar melakukan Screen Capture bukti - bayar sebagai bukti untuk kami bahwa {transaction.address.customer.name} telah - melakukan transaksi pembayaran - </li> - <li> - Kirimkan bukti transaksi pembayaran anda dengan melakukan reply / balas email - ini dengan melampirkan bukti di attachment / lampiran - </li> - <li> - Transaksi {transaction.address.customer.name} akan segera diproses oleh salah - satu Account Representative Indoteknik - </li> - </ul> - </Text> - <Text style={style.text}> - Jika ada pertanyaan seputar teknis pembayaran {transaction.address.customer.name}{' '} - dapat hubungi kami melalui Email{' '} - <a href='mailto:sales@indoteknik.com'>(sales@indoteknik.com)</a> atau Whatsapp{' '} - <a href={whatsappUrl()} target='_blank' rel='noreferrer'> - (+62 812-8080-622) - </a> - . - </Text> - <Text style={style.text}> - Terima kasih atas perhatiannya, selamat kembali beraktifitas - </Text> - </> - )} + <Hr style={style.hr} /> + </> + )} + {statusPayment == 'manual' && ( + <> + <Text style={style.text}> + Dengan cara dibawah ini: + <ul> + <li> + Lakukan pembayaran manual via mobile app perbankan{' '} + {transaction.address.customer.name} + <br /> + Nama Bank: Bank Central Asia (BCA) + <br /> + No. Rek: 8870400081 + <br /> + A/N: INDOTEKNIK DOTCOM GEMILANG PT + </li> + <li> + Setelah berhasil melakukan pembayaran, mohon agar melakukan Screen Capture bukti + bayar sebagai bukti untuk kami bahwa {transaction.address.customer.name} telah + melakukan transaksi pembayaran + </li> + <li> + Kirimkan bukti transaksi pembayaran anda dengan melakukan reply / balas email + ini dengan melampirkan bukti di attachment / lampiran + </li> + <li> + Transaksi {transaction.address.customer.name} akan segera diproses oleh salah + satu Account Representative Indoteknik + </li> + </ul> + </Text> + <Text style={style.text}> + Jika ada pertanyaan seputar teknis pembayaran {transaction.address.customer.name}{' '} + dapat hubungi kami melalui Email{' '} + <a href='mailto:sales@indoteknik.com'>(sales@indoteknik.com)</a> atau Whatsapp{' '} + <a href={whatsappUrl()} target='_blank' rel='noreferrer'> + (+62 812-8080-622) + </a> + . + </Text> + <Text style={style.text}> + Terima kasih atas perhatiannya, selamat kembali beraktifitas + </Text> + </> + )} + </div> + : <FinishCheckout query={{order_id:transaction.name}}/> + } <Text style={{ ...style.text, margin: '12px 0 3px' }}>Best regards,</Text> <Text style={{ ...style.text, margin: '3px 0 0' }}> diff --git a/src/lib/home/components/PreferredBrand.jsx b/src/lib/home/components/PreferredBrand.jsx index 571c4745..ec09aa4e 100644 --- a/src/lib/home/components/PreferredBrand.jsx +++ b/src/lib/home/components/PreferredBrand.jsx @@ -1,13 +1,41 @@ import { Swiper, SwiperSlide } from 'swiper/react' +import { useCallback, useEffect, useState } from 'react' import usePreferredBrand from '../hooks/usePreferredBrand' import PreferredBrandSkeleton from './Skeleton/PreferredBrandSkeleton' import BrandCard from '@/lib/brand/components/BrandCard' import useDevice from '@/core/hooks/useDevice' import Link from '@/core/components/elements/Link/Link' +import axios from 'axios' const PreferredBrand = () => { let query = 'level_s' let params = 'prioritas' + const [isLoading, setIsLoading] = useState(true) + const [startWith, setStartWith] = useState(null) + const [manufactures, setManufactures] = useState([]) + + const loadBrand = useCallback(async () => { + setIsLoading(true) + const name = startWith ? `${startWith}*` : '' + const result = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/brands?params=${name}`) + + setIsLoading(false) + setManufactures((manufactures) => [...result.data]) + }, [startWith]) + + const toggleStartWith = (alphabet) => { + setManufactures([]) + if (alphabet == startWith) { + setStartWith(null) + return + } + setStartWith(alphabet) + } + + useEffect(() => { + loadBrand() + }, [loadBrand]) + const { preferredBrands } = usePreferredBrand(query) const { isMobile, isDesktop } = useDevice() @@ -21,12 +49,12 @@ const PreferredBrand = () => { </Link> )} </div> - {preferredBrands.isLoading && <PreferredBrandSkeleton />} - {!preferredBrands.isLoading && ( + {manufactures.isLoading && <PreferredBrandSkeleton />} + {!manufactures.isLoading && ( <Swiper slidesPerView={isMobile ? 3.5 : 7.5} spaceBetween={isMobile ? 12 : 24} freeMode> - {preferredBrands.data?.data.map((brand) => ( - <SwiperSlide key={brand.id}> - <BrandCard brand={brand} /> + {manufactures.map((manufacture) => ( + <SwiperSlide key={manufacture.id}> + <BrandCard brand={manufacture} /> </SwiperSlide> ))} </Swiper> diff --git a/src/lib/home/components/PromotionProgram.jsx b/src/lib/home/components/PromotionProgram.jsx new file mode 100644 index 00000000..5034fd28 --- /dev/null +++ b/src/lib/home/components/PromotionProgram.jsx @@ -0,0 +1,65 @@ +import Link from '@/core/components/elements/Link/Link' +import Image from 'next/image' +import { bannerApi } from '@/api/bannerApi'; +import useDevice from '@/core/hooks/useDevice' +import { Swiper, SwiperSlide } from 'swiper/react'; +const { useQuery } = require('react-query') +const BannerSection = () => { + const promotionProgram = useQuery('promotionProgram', bannerApi({ type: 'banner-promotion' })); + const { isMobile, isDesktop } = useDevice() + + return ( + <div className='px-4 sm:px-0'> + <div className='flex justify-between items-center mb-4 '> + <div className='font-semibold sm:text-h-lg'>Promo Tersedia</div> + {isDesktop && ( + <Link href='/shop/promo' className='!text-red-500 font-semibold'> + Lihat Semua + </Link> + )} + </div> + {isDesktop && (promotionProgram.data && + promotionProgram.data?.length > 0 && ( + <div className='grid grid-cols-3 sm:grid-cols-3 gap-4 rounded-md'> + {promotionProgram.data?.map((banner) => ( + <Link key={banner.id} href={banner.url}> + <Image + width={439} + height={150} + quality={100} + src={banner.image} + alt={banner.name} + className='h-auto w-full rounded hover:scale-105 transition duration-500 ease-in-out' + /> + </Link> + ))} + </div> + + ))} + +{isMobile && ( + + <Swiper slidesPerView={1.1} spaceBetween={8} freeMode> + {promotionProgram.data?.map((banner) => ( + <SwiperSlide key={banner.id}> + <Link key={banner.id} href={banner.url}> + <Image + width={439} + height={150} + quality={100} + src={banner.image} + alt={banner.name} + className='h-auto w-full rounded ' + /> + </Link> + </SwiperSlide> + ))} + </Swiper> + + )} + </div> + + ) +} + +export default BannerSection diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx index 38ed35f8..98732407 100644 --- a/src/lib/product/components/ProductCard.jsx +++ b/src/lib/product/components/ProductCard.jsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import ImageNext from 'next/image'; import { useRouter } from 'next/router'; -import { useMemo } from 'react'; +import { useMemo, useEffect, useState } from 'react'; import Image from '@/core/components/elements/Image/Image'; import Link from '@/core/components/elements/Link/Link'; @@ -15,6 +15,7 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { const router = useRouter(); const utmSource = useUtmSource(); + const callForPriceWhatsapp = whatsappUrl('product', { name: product.name, manufacture: product.manufacture?.name, @@ -41,11 +42,39 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { return ( <div className='rounded shadow-sm border border-gray_r-4 bg-white h-[300px] md:h-[350px]'> <Link href={URL.product} className='border-b border-gray_r-4 relative'> + <div className="relative"> <Image src={image} alt={product?.name} - className='w-full object-contain object-center h-36 sm:h-48' + className="gambarA w-full object-contain object-center h-36 sm:h-48" /> + <div className="absolute top-0 right-0 flex mt-3"> + <div className="gambarB "> + {product?.isSni && ( + <ImageNext + src="/images/sni-logo.png" + alt="SNI Logo" + className="w-4 h-5 object-contain object-top sm:h-6" + width={50} + height={50} + /> + )} + </div> + <div className="gambarC "> + {product?.isTkdn && ( + <ImageNext + src="/images/TKDN.png" + alt="TKDN" + className="w-11 h-6 object-contain object-top ml-1 mr-1 sm:h-6" + width={50} + height={50} + /> + )} + </div> + </div> + </div> + + {router.pathname != '/' && product?.flashSale?.id > 0 && ( <div className='absolute bottom-0 w-full grid'> <div className='absolute bottom-0 w-full h-full'> @@ -171,11 +200,37 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { <div className='flex bg-white'> <div className='w-4/12'> <Link href={URL.product} className='relative'> + <div className="relative"> <Image src={image} alt={product?.name} - className='w-full object-contain object-center h-36' + className="gambarA w-full object-contain object-center h-36 sm:h-48" /> + <div className="absolute top-0 right-0 flex mt-3"> + <div className="gambarB "> + {product?.isSni && ( + <ImageNext + src="/images/sni-logo.png" + alt="SNI Logo" + className="w-4 h-5 object-contain object-top sm:h-6" + width={50} + height={50} + /> + )} + </div> + <div className="gambarC "> + {product?.isTkdn && ( + <ImageNext + src="/images/TKDN.png" + alt="TKDN" + className="w-11 h-6 object-contain object-top ml-1 sm:h-6" + width={50} + height={50} + /> + )} + </div> + </div> + </div> {product.variantTotal > 1 && ( <div className='absolute badge-gray bottom-1.5 left-1.5'> {product.variantTotal} Varian diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx index e4a62abb..a8073036 100644 --- a/src/lib/product/components/ProductFilterDesktop.jsx +++ b/src/lib/product/components/ProductFilterDesktop.jsx @@ -21,6 +21,7 @@ import Image from '@/core/components/elements/Image/Image' import { formatCurrency } from '@/core/utils/formatValue' const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = null }) => { + const router = useRouter() const { query } = router const [order, setOrder] = useState(query?.orderBy) @@ -102,7 +103,14 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu } params = _.pickBy(params, _.identity) params = toQuery(params) - router.push(`${prefixUrl}?${params}`) + + const slug = Array.isArray(router.query.slug) ? router.query.slug[0] : router.query.slug; + + if (slug) { + router.push(`${prefixUrl}/${slug}?${params}`) + } else { + router.push(`${prefixUrl}?${params}`) + } } diff --git a/src/lib/product/components/ProductFilterDesktopPromotion.jsx b/src/lib/product/components/ProductFilterDesktopPromotion.jsx new file mode 100644 index 00000000..0815b881 --- /dev/null +++ b/src/lib/product/components/ProductFilterDesktopPromotion.jsx @@ -0,0 +1,132 @@ +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import _ from 'lodash'; +import { toQuery } from 'lodash-contrib'; +import { Button } from '@chakra-ui/react'; +import { MultiSelect } from 'react-multi-select-component'; + +const ProductFilterDesktop = ({ brands, categories, prefixUrl }) => { + const router = useRouter(); + const { query } = router; + const [order, setOrder] = useState(query?.orderBy); + const [brandValues, setBrand] = useState([]); + const [categoryValues, setCategory] = useState([]); + const [priceFrom, setPriceFrom] = useState(query?.priceFrom); + const [priceTo, setPriceTo] = useState(query?.priceTo); + const [stock, setStock] = useState(query?.stock); + const [activeRange, setActiveRange] = useState(null); + const [isBrandDropdownClicked, setIsBrandDropdownClicked] = useState(false); + const [isCategoryDropdownClicked, setIsCategoryDropdownClicked] = useState(false); + + // Effect to set brandValues from query parameter 'brand' + useEffect(() => { + const brandParam = query?.brand; + if (brandParam) { + const brandsArray = brandParam.split(',').map((b) => ({ + label: b, + value: b, + })); + setBrand(brandsArray); + } + + }, [query.brand]); // Trigger effect whenever query.brand changes + + useEffect(() => { + const categoryParam = query?.category; + if (categoryParam) { + const categoriesArray = categoryParam.split(',').map((c) => ({ + label: c, + value: c, + })); + setCategory(categoriesArray); + } + }, [query.category]); // Trigger effect whenever query.category changes + + const handleSubmit = () => { + let params = { + q: router.query.q, + orderBy: order, + brand: brandValues.map((b) => b.value).join(','), + category: categoryValues.map((c) => c.value).join(','), + priceFrom, + priceTo, + stock: stock, + }; + params = _.pickBy(params, _.identity); + params = toQuery(params); + + const slug = Array.isArray(router.query.slug) + ? router.query.slug[0] + : router.query.slug; + + if (slug) { + router.push(`${prefixUrl}/${slug}?${params}`); + } else { + router.push(`${prefixUrl}?${params}`); + } + }; + + + const brandOptions = brands.map((brand) => ({ + label: `${brand.brand} (${brand.qty})`, + value: brand.brand, + })); + + const categoryOptions = categories.map((category) => ({ + label: `${category.name} (${category.qty})`, + value: category.name, + })); + + return ( + <> + <div className='flex h-full w-[100%] justify-end '> + {/* Brand MultiSelect */} + <div className='mb-[20px] mr-4 w-64 h-full flex justify-start '> + <div className='relative'> + <label>Brand</label> + <div className='h-auto z-50 w-64 '> + <MultiSelect + options={brandOptions} + value={brandValues} + onChange={setBrand} + labelledBy='Select Brand' + onMenuToggle={(isOpen) => setIsBrandDropdownClicked(isOpen)} + hasSelectAll={false} + /> + </div> + </div> + </div> + + {/* Category MultiSelect */} + <div className='mb-[20px] mr-4 w-64 h-full flex justify-start '> + <div className='relative'> + <label>Kategori</label> + <div className=' h-auto w-64'> + <MultiSelect + options={categoryOptions} + value={categoryValues} + onChange={setCategory} + labelledBy='Select Kategori' + onMenuToggle={() => + setIsCategoryDropdownClicked(!isCategoryDropdownClicked) + } + hasSelectAll={false} + /> + </div> + </div> + </div> + + {/* Apply Button */} + <div className='TOMBOL mb-1 h-24 flex justify-center items-center w-24'> + <div className=' bottom-1 pb-1 left-0 right-0 flex justify-center rounded' > + <Button colorScheme='red' width={"full"} onClick={handleSubmit}> + Terapkan + </Button> + </div> + </div> + </div> + </> + ); +}; + +export default ProductFilterDesktop; diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index ee4ec2de..b1a5d409 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -24,6 +24,9 @@ import ProductFilter from './ProductFilter'; import ProductFilterDesktop from './ProductFilterDesktop'; import ProductSearchSkeleton from './Skeleton/ProductSearchSkeleton'; +import SideBanner from '~/modules/side-banner'; +import FooterBanner from '~/modules/footer-banner'; + const ProductSearch = ({ query, prefixUrl, @@ -127,6 +130,7 @@ const ProductSearch = ({ brands.push({ brand, qty }); } } + const categories = []; for ( @@ -141,6 +145,7 @@ const ProductSearch = ({ categories.push({ name, qty }); } } + const orderOptions = [ { value: 'price-asc', label: 'Harga Terendah' }, @@ -401,6 +406,10 @@ const ProductSearch = ({ prefixUrl={prefixUrl} defaultBrand={defaultBrand} /> + + <div className='h-6' /> + + <SideBanner /> </div> <div className='w-9/12 pl-6'> {bannerPromotionHeader && bannerPromotionHeader?.image && ( @@ -552,6 +561,7 @@ const ProductSearch = ({ /> </div> )} + <FooterBanner /> </div> </div> </DesktopView> diff --git a/src/lib/promo/components/Promocrumb.jsx b/src/lib/promo/components/Promocrumb.jsx new file mode 100644 index 00000000..4f5cf346 --- /dev/null +++ b/src/lib/promo/components/Promocrumb.jsx @@ -0,0 +1,40 @@ +import { Breadcrumb as ChakraBreadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react' +import Link from 'next/link' +import React from 'react' + +/** + * Renders a breadcrumb component with links to navigate through different pages. + * + * @param {Object} props - The props object containing the brand name. + * @param {string} props.brandName - The name of the brand to display in the breadcrumb. + * @return {JSX.Element} The rendered breadcrumb component. + */ +const Breadcrumb = ({ brandName }) => { + return ( + <div className='container mx-auto py-4 md:py-6'> + <ChakraBreadcrumb> + <BreadcrumbItem> + <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'> + Shop + </BreadcrumbLink> + </BreadcrumbItem> + + {/* <BreadcrumbItem> + <BreadcrumbLink + as={Link} + href='/shop/promo' + className='!text-danger-500 whitespace-nowrap' + > + Promo + </BreadcrumbLink> + </BreadcrumbItem> */} + + <BreadcrumbItem isCurrentPage> + <BreadcrumbLink className='whitespace-nowrap'>{brandName}</BreadcrumbLink> + </BreadcrumbItem> + </ChakraBreadcrumb> + </div> + ) +} + +export default Breadcrumb diff --git a/src/lib/quotation/components/Quotation.jsx b/src/lib/quotation/components/Quotation.jsx index 09d55e92..8855c6c4 100644 --- a/src/lib/quotation/components/Quotation.jsx +++ b/src/lib/quotation/components/Quotation.jsx @@ -68,6 +68,8 @@ const Quotation = () => { const [etd, setEtd] = useState(null); const [etdFix, setEtdFix] = useState(null); + const [isApproval, setIsApproval] = useState(false); + const expedisiValidation = useRef(null); const [selectedAddress, setSelectedAddress] = useState({ @@ -86,6 +88,7 @@ const Quotation = () => { }; getAddresses(); + setIsApproval(auth?.feature?.soApproval); }, [auth]); useEffect(() => { @@ -185,6 +188,13 @@ const Quotation = () => { if (etd) setEtdFix(calculateEstimatedArrival(etd)); }, [etd]); + useEffect(() => { + if (isApproval) { + setselectedCarrierId(1); + setselectedExpedisiService('indoteknik'); + } + }, [isApproval]); + // end set up address and carrier useEffect(() => { @@ -235,7 +245,7 @@ const Quotation = () => { const checkout = async () => { // validation checkout - if (selectedExpedisi === 0) { + if (selectedExpedisi === 0 && !isApproval) { setCheckoutValidation(true); if (expedisiValidation.current) { const position = expedisiValidation.current.getBoundingClientRect(); @@ -246,7 +256,7 @@ const Quotation = () => { } return; } - if (selectedCarrier != 1 && biayaKirim == 0) { + if (selectedCarrier != 1 && biayaKirim == 0 && !isApproval) { toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'); return; } @@ -267,7 +277,9 @@ const Quotation = () => { estimated_arrival_days: splitDuration(etd), delivery_service_type: selectedExpedisiService, }; + console.log('data checkout', data); const isSuccess = await checkoutApi({ data }); + console.log('isSuccess', isSuccess); setIsLoading(false); if (isSuccess?.id) { for (const product of products) deleteItemCart({ productId: product.id }); @@ -343,16 +355,21 @@ const Quotation = () => { )} <Divider /> <SectionValidation address={selectedAddress.invoicing} /> - <SectionExpedisi - address={selectedAddress.shipping} - listExpedisi={listExpedisi} - setSelectedExpedisi={setSelectedExpedisi} - checkWeigth={checkWeigth} - checkoutValidation={checkoutValidation} - expedisiValidation={expedisiValidation} - loadingRajaOngkir={loadingRajaOngkir} - /> - <Divider /> + {!isApproval && ( + <> + <SectionExpedisi + address={selectedAddress.shipping} + listExpedisi={listExpedisi} + setSelectedExpedisi={setSelectedExpedisi} + checkWeigth={checkWeigth} + checkoutValidation={checkoutValidation} + expedisiValidation={expedisiValidation} + loadingRajaOngkir={loadingRajaOngkir} + /> + <Divider /> + </> + )} + <SectionListService listserviceExpedisi={listserviceExpedisi} setSelectedServiceType={setSelectedServiceType} @@ -468,15 +485,18 @@ const Quotation = () => { )} <Divider /> <SectionValidation address={selectedAddress.invoicing} /> - <SectionExpedisi - address={selectedAddress.shipping} - listExpedisi={listExpedisi} - setSelectedExpedisi={setSelectedExpedisi} - checkWeigth={checkWeigth} - checkoutValidation={checkoutValidation} - expedisiValidation={expedisiValidation} - loadingRajaOngkir={loadingRajaOngkir} - /> + {!isApproval && ( + <SectionExpedisi + address={selectedAddress.shipping} + listExpedisi={listExpedisi} + setSelectedExpedisi={setSelectedExpedisi} + checkWeigth={checkWeigth} + checkoutValidation={checkoutValidation} + expedisiValidation={expedisiValidation} + loadingRajaOngkir={loadingRajaOngkir} + /> + )} + <Divider /> <SectionListService listserviceExpedisi={listserviceExpedisi} 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/rejectProductApi.js b/src/lib/transaction/api/rejectProductApi.js new file mode 100644 index 00000000..e03c7975 --- /dev/null +++ b/src/lib/transaction/api/rejectProductApi.js @@ -0,0 +1,9 @@ +import odooApi from '@/core/api/odooApi' + +const rejectProductApi = async ({ idSo, idProduct, reason }) => { + const dataCheckout = await odooApi('POST', `/api/v1/sale_order/${idSo}/reject/${idProduct}`, { + reason_reject: reason + }); + return dataCheckout +} +export default rejectProductApi
\ No newline at end of file diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx index 8f4b2038..cd3613c0 100644 --- a/src/lib/transaction/components/Transaction.jsx +++ b/src/lib/transaction/components/Transaction.jsx @@ -2,7 +2,8 @@ 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 { useEffect, useMemo, useRef, useState } from 'react'; +import ImageNext from 'next/image'; import { downloadPurchaseOrder, downloadQuotation, @@ -33,12 +34,18 @@ import useAuth from '@/core/hooks/useAuth'; import StepApproval from './stepper'; import aprpoveApi from '../api/approveApi'; import rejectApi from '../api/rejectApi'; +import rejectProductApi from '../api/rejectProductApi'; +import { useRouter } from 'next/router'; const Transaction = ({ id }) => { + const router = useRouter() + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedProduct, setSelectedProduct] = useState(null); + const [reason, setReason] = useState(''); const auth = useAuth(); const { transaction } = useTransaction({ id }); - - const statusApprovalWeb = transaction.data?.approvalStep + + const statusApprovalWeb = transaction.data?.approvalStep; const { queryAirwayBill } = useAirwayBill({ orderId: id }); const [airwayBillPopup, setAirwayBillPopup] = useState(null); @@ -49,6 +56,7 @@ const Transaction = ({ id }) => { 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; @@ -78,7 +86,7 @@ const Transaction = ({ id }) => { const closeCancelTransaction = () => setCancelTransaction(false); const [rejectTransaction, setRejectTransaction] = useState(false); - + const openRejectTransaction = () => setRejectTransaction(true); const closeRejectTransaction = () => setRejectTransaction(false); const submitCancelTransaction = async () => { @@ -106,13 +114,13 @@ const Transaction = ({ id }) => { await aprpoveApi({ id }); toast.success('Berhasil melanjutkan approval'); transaction.refetch(); - } + }; const handleReject = async () => { - await rejectApi({ id }); - closeRejectTransaction() - transaction.refetch(); - } + await rejectApi({ id }); + closeRejectTransaction(); + transaction.refetch(); + }; const memoizeVariantGroupCard = useMemo( () => ( @@ -139,6 +147,15 @@ const Transaction = ({ id }) => { [transaction.data] ); + const memoizeVariantGroupCardReject = useMemo( + () => ( + <div className='p-4 pt-0 flex flex-col gap-y-3'> + <VariantGroupCard variants={transaction.data?.productsRejectLine} buyMore /> + </div> + ), + [transaction.data] + ); + if (transaction.isLoading) { return ( <div className='flex justify-center my-6'> @@ -151,6 +168,38 @@ const Transaction = ({ id }) => { setIdAWB(null); }; + const openModal = (product) => { + setSelectedProduct(product); + setIsModalOpen(true); + }; + + const closeModal = () => { + setIsModalOpen(false); + setSelectedProduct(null); + setReason(''); + }; + + const handleRejectProduct = async () => { + try{ + if (!reason.trim()) { + toast.error('Masukkan alasan terlebih dahulu'); + return; + }else{ + let idSo = transaction?.data.id + let idProduct = selectedProduct?.id + await rejectProductApi({ idSo, idProduct, reason}); + closeModal(); + toast.success("Produk berhasil di reject") + setTimeout(() => { + window.location.reload(); + }, 1500); + } + }catch(error){ + toast.error('Gagal reject produk. Silakan coba lagi.'); + } + }; + + return ( transaction.data?.name && ( <> @@ -238,7 +287,13 @@ const Transaction = ({ id }) => { <MobileView> <div className='p-4'> - <StepApproval layer={2} status={'cancel'} className='ml-auto' /> + {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'> @@ -293,40 +348,6 @@ const Transaction = ({ id }) => { <Divider /> - {!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> - )} - - <Divider /> - - <div className='font-medium p-4'>Detail Produk</div> - - {memoizeVariantGroupCard} - - <Divider /> - - <SectionAddress address={transaction.data?.address} /> - - <Divider /> - <div className='p-4'> <p className='font-medium'>Invoice</p> <div className='flex flex-col gap-y-3 mt-4'> @@ -358,34 +379,90 @@ const Transaction = ({ id }) => { <Divider /> - <div className='p-4 pt-0'> - {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> + {!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 - className='btn-solid-red px-7 w-full' - onClick={checkout} - disabled={ - transaction.data?.status === 'cancel' ? true : false || auth?.webRole === statusApprovalWeb ? true : false + type='button' + className='btn-light py-1.5 px-3 ml-auto' + onClick={ + transaction.data?.purchaseOrderFile + ? () => downloadPurchaseOrder(transaction.data) + : openUploadPo } > - Reject + {transaction.data?.purchaseOrderFile ? 'Download' : 'Upload'} </button> </div> + </div> + )} + + <Divider /> + + <div className='font-medium p-4'>Detail Produk</div> + {transaction?.data?.products.length > 0? ( + <div> + {memoizeVariantGroupCard} + </div> + ) : ( + <div className='badge-red text-sm px-2 ml-4'>Semua produk telah di reject</div> )} - {transaction.data?.status == 'draft' && !auth?.feature?.soApproval && ( - <button className='btn-yellow w-full mt-4' onClick={checkout}> - Lanjutkan Transaksi - </button> - )} + + {transaction?.data?.productsRejectLine.length > 0 && ( + <div> + <div className='font-medium p-4'>Detail Produk Reject</div> + {memoizeVariantGroupCardReject} + </div> + )} + + <Divider /> + + <SectionAddress address={transaction.data?.address} /> + + <Divider /> + + <div className='p-4 pt-0'> + {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'} @@ -439,13 +516,20 @@ const Transaction = ({ id }) => { Download </button> {transaction.data?.status == 'draft' && - auth?.feature?.soApproval && auth?.webRole && ( + 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 + transaction.data?.status === 'cancel' + ? true + : false || auth?.webRole === statusApprovalWeb + ? true + : false || statusApprovalWeb < 1 + ? true + : false } > Approve @@ -454,7 +538,13 @@ const Transaction = ({ id }) => { className='btn-solid-red px-7' onClick={openRejectTransaction} disabled={ - transaction.data?.status === 'cancel' ? true : false || auth?.webRole === statusApprovalWeb ? true : false || statusApprovalWeb < 1 ? true : false + transaction.data?.status === 'cancel' + ? true + : false || auth?.webRole === statusApprovalWeb + ? true + : false || statusApprovalWeb < 1 + ? true + : false } > Reject @@ -467,15 +557,16 @@ const Transaction = ({ id }) => { Lanjutkan Transaksi </button> )} - {transaction.data?.status != 'draft' && !auth?.feature.soApproval && ( - <button - className='btn-light' - disabled={transaction.data?.status != 'waiting'} - onClick={openCancelTransaction} - > - Batalkan 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'> @@ -490,7 +581,7 @@ const Transaction = ({ id }) => { <div>Ketentuan Pembayaran</div> <div>: {transaction?.data?.paymentTerm}</div> - {!auth?.feature?.soApproval && ( + {!auth?.feature?.soApproval ? ( <> <div>Purchase Order</div> <div> @@ -510,6 +601,11 @@ const Transaction = ({ id }) => { </button> </div> </> + ) : ( + <> + <div>Site</div> + <div>: {transaction?.data?.sitePartner}</div> + </> )} </div> </div> @@ -525,50 +621,83 @@ const Transaction = ({ id }) => { /> </div> </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 - className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left' - key={airway?.id} - onClick={() => setIdAWB(airway?.id)} - > - <div> - <span className='text-sm text-gray_r-11'> - No Resi : {airway?.trackingNumber || '-'}{' '} - </span> - <p className='mt-1 font-medium'>{airway?.name}</p> - </div> - <div className='flex gap-x-2'> - <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1'> - {airway?.delivered ? 'Pesanan Tiba' : 'Sedang Dikirim'} - </div> - <ChevronRightIcon className='w-5 stroke-2' /> + <div className='flex '> + <div className='w-1/2'> + <div className='text-h-sm font-semibold mt-10 mb-4'> + Pengiriman + </div> + {transaction?.data?.pickings.length == 0 && ( + <div className='badge-red text-sm'>Belum ada pengiriman</div> + )} + <div className='grid grid-cols-1 gap-1 w-2/3'> + {transaction?.data?.pickings?.map((airway) => ( + <button + className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left h-20' + key={airway?.id} + onClick={() => setIdAWB(airway?.id)} + > + <div> + <span className='text-sm text-gray_r-11'> + No Resi : {airway?.trackingNumber || '-'}{' '} + </span> + <p className='mt-1 font-medium'>{airway?.name}</p> + </div> + <div className='flex gap-x-2'> + <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1'> + {airway?.delivered ? 'Pesanan Tiba' : 'Sedang Dikirim'} + </div> + <ChevronRightIcon className='w-5 stroke-2' /> + </div> + </button> + ))} + </div> + </div> + <div className='invoice w-1/2 '> + <div className='text-h-sm font-semibold mt-10 mb-4 '>Invoice</div> + {transaction.data?.invoices?.length === 0 && ( + <div className='badge-red text-sm'>Belum ada invoice</div> + )} + <div className='grid grid-cols-1 gap-1 w-2/3 '> + {transaction.data?.invoices?.map((invoice, index) => ( + <Link href={`/my/invoices/${invoice.id}`} key={index}> + <div className='shadow rounded-md p-4 text-gray_r-12 font-normal flex justify-between'> + <div> + <p className='mb-1'>{invoice?.name}</p> + <div className='flex items-center gap-x-1'> + {invoice.amountResidual > 0 ? ( + <div className='badge-red'>Belum Lunas</div> + ) : ( + <div className='badge-green'>Lunas</div> + )} + <p className='text-caption-2 text-gray_r-11'> + {currencyFormat(invoice.amountTotal)} + </p> + </div> + </div> + <ChevronRightIcon className='w-5 stroke-2' /> + </div> + </Link> + ))} </div> - </button> - ))} + </div> </div> - {transaction?.data?.pickings.length == 0 && ( - <div className='badge-red text-sm'>Belum ada pengiriman</div> - )} - <div className='text-h-sm font-semibold mt-10 mb-4'> + <div className='text-h-sm font-semibold mt-4 mb-4'> Rincian Pembelian </div> - <table className='table-data'> - <thead> - <tr> - <th>Nama Produk</th> - <th>Diskon</th> - <th>Jumlah</th> - <th>Harga</th> - <th>Subtotal</th> - </tr> - </thead> - <tbody> + {transaction?.data?.products?.length > 0? ( + <table className='table-data'> + <thead> + <tr> + <th>Nama Produk</th> + {/* <th>Diskon</th> */} + <th>Jumlah</th> + <th>Harga</th> + <th>Subtotal</th> + <th></th> + </tr> + </thead> + <tbody> {transaction?.data?.products?.map((product) => ( <tr key={product.id}> <td className='flex'> @@ -580,11 +709,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 @@ -605,26 +760,71 @@ 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> + {/* {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (transaction.data.isReaject == false) && ( */} + {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (router.asPath.includes("/my/quotations/")) && ( + <td> + <button + className="bg-red-500 text-white py-1 px-3 rounded" + onClick={() => openModal(product)} + > + Reject + </button> + </td> + )} </tr> ))} </tbody> </table> + ) : ( + <div className='badge-red text-sm'>Semua produk telah di reject</div> + )} + + {isModalOpen && ( + <div className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center'> + <div className='bg-white p-4 rounded w-96 + ease-in-out opacity-100 + transform transition-transform duration-300 scale-100'> + <h2 className='text-lg mb-2'>Berikan Alasan</h2> + <textarea + value={reason} + onChange={(e) => setReason(e.target.value)} + className='w-full p-2 border rounded' + rows='4' + ></textarea> + <div className='mt-4 flex justify-end'> + <button + className='bg-gray-300 text-black py-1 px-3 rounded mr-2' + onClick={closeModal} + > + Batal + </button> + <button + className='bg-red-500 text-white py-1 px-3 rounded' + onClick={handleRejectProduct} + > + Reject + </button> + </div> + </div> + </div> + )} + {transaction?.data?.products?.map((product) => ( <div className='flex justify-end mt-4'> <div className='w-1/4 grid grid-cols-2 gap-y-3 text-gray_r-12/80'> <div className='text-right'>Subtotal</div> @@ -650,33 +850,84 @@ const Transaction = ({ id }) => { </div> </div> </div> + ))} - <div className='text-h-sm font-semibold mt-10 mb-4'>Invoice</div> - <div className='grid grid-cols-3 gap-4'> - {transaction.data?.invoices?.map((invoice, index) => ( - <Link href={`/my/invoices/${invoice.id}`} key={index}> - <div className='shadow rounded-md p-4 text-gray_r-12 font-normal flex justify-between'> - <div> - <p className='mb-2'>{invoice?.name}</p> - <div className='flex items-center gap-x-1'> - {invoice.amountResidual > 0 ? ( - <div className='badge-red'>Belum Lunas</div> - ) : ( - <div className='badge-green'>Lunas</div> - )} - <p className='text-caption-2 text-gray_r-11'> - {currencyFormat(invoice.amountTotal)} - </p> - </div> - </div> - <ChevronRightIcon className='w-5 stroke-2' /> - </div> - </Link> - ))} - </div> - {transaction.data?.invoices?.length === 0 && ( - <div className='badge-red text-sm'>Belum ada invoice</div> + + + {transaction?.data?.productsRejectLine.length > 0 && ( + <div className='text-h-sm font-semibold mt-10 mb-4'> + Rincian Produk Reject + </div> + )} + {transaction?.data?.productsRejectLine.length > 0 && ( + <table className='table-data'> + <thead> + <tr> + <th>Nama Produk</th> + {/* <th>Diskon</th> */} + <th>Jumlah</th> + <th>Harga</th> + <th>Subtotal</th> + </tr> + </thead> + <tbody> + {transaction?.data?.productsRejectLine?.map((product) => ( + <tr key={product.id}> + <td className='flex'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.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' + /> + </Link> + <div className='px-2 text-left'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + {product?.parent?.name} + </Link> + <div className='text-gray_r-11 mt-2'> + {product?.code}{' '} + {product?.attributes.length > 0 + ? `| ${product?.attributes.join(', ')}` + : ''} + </div> + </div> + </td> + {/* <td> + {product.price.discountPercentage > 0 + ? `${product.price.discountPercentage}%` + : ''} + </td> */} + <td>{product.quantity}</td> + <td> + {/* {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.quantity * product.price.priceDiscount)}</td> + </tr> + ))} + </tbody> + </table> )} + </div> </div> </DesktopView> 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/variant/components/VariantCard.jsx b/src/lib/variant/components/VariantCard.jsx index 9f1b5733..68cdf54f 100644 --- a/src/lib/variant/components/VariantCard.jsx +++ b/src/lib/variant/components/VariantCard.jsx @@ -1,15 +1,28 @@ import { useRouter } from 'next/router' import { toast } from 'react-hot-toast' - +import useAuth from '@/core/hooks/useAuth'; import Image from '@/core/components/elements/Image/Image' import Link from '@/core/components/elements/Link/Link' import { createSlug } from '@/core/utils/slug' import currencyFormat from '@/core/utils/currencyFormat' import { updateItemCart } from '@/core/utils/cart' import whatsappUrl from '@/core/utils/whatsappUrl' +import {useState } from 'react'; +import rejectProductApi from '../../../lib/transaction/api/rejectProductApi' +// import {useTransaction} from 'C:\Users\Indoteknik\next-indoteknik\src\lib\transaction\hooks\useTransaction.js' +import useTransaction from '../../../lib/transaction/hooks/useTransaction'; +import ImageNext from 'next/image'; const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { const router = useRouter() + const id = router.query.id + const auth = useAuth(); + const { transaction } = useTransaction({id}); + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedProduct, setSelectedProduct] = useState(null); + const [reason, setReason] = useState(''); + + const addItemToCart = () => { toast.success('Berhasil menambahkan ke keranjang', { duration: 1500 }) @@ -19,19 +32,78 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { }) return } - + const checkoutItem = () => { router.push(`/shop/checkout?product_id=${product.id}&qty=${product.quantity}`) } - + + const openModal = (product) => { + setSelectedProduct(product); + setIsModalOpen(true); + }; + + const closeModal = () => { + setIsModalOpen(false); + setSelectedProduct(null); + setReason(''); + }; + + const handleRejectProduct = async () => { + try { + if (!reason.trim()) { + toast.error('Masukkan alasan terlebih dahulu'); + return; + }else{ + let idSo = transaction?.data.id + let idProduct = selectedProduct.id + await rejectProductApi({ idSo, idProduct, reason}); + closeModal(); + toast.success("Produk berhasil di reject") + setTimeout(() => { + window.location.reload(); + }, 1500); + } + } catch (error) { + toast.error('Gagal reject produk. Silakan coba lagi.'); + } + }; + const Card = () => ( <div className='flex gap-x-3'> <div className='w-4/12 flex items-center gap-x-2'> - <Image + + <div className="relative"> + <Image src={product.parent.image} alt={product.parent.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-5 object-contain object-top sm:h-6" + width={50} + height={50} + /> + )} + </div> + <div className="gambarC "> + {product.isTkdn && ( + <ImageNext + src="/images/TKDN.png" + alt="TKDN" + className="w-5 h-6 object-contain object-top ml-1 mr-1 sm:h-6" + width={50} + height={50} + /> + )} + </div> + </div> + </div> + </div> <div className='w-8/12 flex flex-col'> <p className='product-card__title wrap-line-ellipsis-2'>{product.parent.name}</p> @@ -82,7 +154,7 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id)}> <Card /> </Link> - {buyMore && ( + {buyMore && (!transaction?.data?.productsRejectLine.some(pr => pr.id === product.id)) && ( <div className='flex justify-end gap-x-2 mb-2'> <button type='button' @@ -91,14 +163,48 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { > Tambah Keranjang </button> - <button - type='button' - onClick={checkoutItem} - className='btn-solid-red py-2 px-3 text-caption-1' - > - Beli Lagi - </button> + {/* {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (transaction.data.isReaject == false) && ( */} + {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (router.asPath.includes("/my/quotations/")) && ( + !transaction?.data?.productsRejectLine.some(pr => pr.id === product.id) && ( + <button + className="bg-red-500 text-white py-1 px-3 rounded" + onClick={() => openModal(product)} + > + Reject + </button> + ) + )} + {isModalOpen && ( + <div className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center'> + <div className='bg-white p-4 rounded w-96 + ease-in-out opacity-100 + transform transition-transform duration-300 scale-100'> + <h2 className='text-lg mb-2'>Berikan Alasan</h2> + <textarea + value={reason} + onChange={(e) => setReason(e.target.value)} + className='w-full p-2 border rounded' + rows='4' + ></textarea> + <div className='mt-4 flex justify-end'> + <button + className='bg-gray-300 text-black py-1 px-3 rounded mr-2' + onClick={closeModal} + > + Batal + </button> + <button + className='bg-red-500 text-white py-1 px-3 rounded' + onClick={handleRejectProduct} + > + Reject + </button> + </div> + </div> + </div> + )} </div> + )} </> ) |
