diff options
| author | trisusilo48 <tri.susilo@altama.co.id> | 2024-07-02 14:33:25 +0700 |
|---|---|---|
| committer | trisusilo48 <tri.susilo@altama.co.id> | 2024-07-02 14:33:25 +0700 |
| commit | e8ad23dbad5e96dddcd6b10bdc46400c6721e80b (patch) | |
| tree | fafea81669ea00f824260ecb4a0acc9e1096499f /src | |
| parent | c6eec1fcd70c878f9fa4911ae4ebf1a1c97a18b7 (diff) | |
| parent | 66d787499d0751365c1cda9d79b31e9f3c3c28bc (diff) | |
Merge branch 'release' into feature/generate_recomendation
Diffstat (limited to 'src')
42 files changed, 2971 insertions, 724 deletions
diff --git a/src/api/bannerApi.js b/src/api/bannerApi.js index 8bae131d..431225a5 100644 --- a/src/api/bannerApi.js +++ b/src/api/bannerApi.js @@ -3,3 +3,6 @@ import odooApi from '@/core/api/odooApi' export const bannerApi = ({ type }) => { return async () => await odooApi('GET', `/api/v1/banner?type=${type}`) } + +// ubah ke SOLR + diff --git a/src/api/promoApi.js b/src/api/promoApi.js new file mode 100644 index 00000000..4c386fba --- /dev/null +++ b/src/api/promoApi.js @@ -0,0 +1,70 @@ +// src/api/promoApi.js +import odooApi from '@/core/api/odooApi'; +import { type } from 'os'; +// import { SolrResponse } from "../../../../src-migrate/types/solr.ts"; + +export const fetchPromoItems = async (type) => { + try { + const response = await odooApi('GET', `/api/v1/program-line?type=${type}&limit=3`); + return response.map((item) => ({ value: item.id, label: item.name, product: item.products ,price:item.price})); + } catch (error) { + console.error('Error fetching promo items:', error); + return []; + } +}; + +export const fetchPromoItemsSolr = async (type) => { + // let query = type ? `type_value_s:${type}` : '*:*'; + let start = 0 + let rows = 100 + try { + const queryParams = new URLSearchParams({ q: type }); + const response = await fetch(`/solr/promotion_program_lines/select?${queryParams.toString()}&rows=${rows}&start=${start}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + const promotions = await map(data.response.docs); + return promotions; + } catch (error) { + console.error("Error fetching promotion data:", error); + return []; + } +}; + +export const fetchVariantSolr = async(data)=>{ + try { + const queryParams = new URLSearchParams({ q: data }); + const response = await fetch(`/solr/variants/select?${queryParams.toString()}`); + const responseData = await response.json(); + return responseData; + } catch (error) { + console.error("Error fetching promotion data:", error); + return []; + } +}; + +const map = async (promotions) => { + const result = []; + for (const promotion of promotions) { + const data = { + id: promotion.id, + program_id: promotion.program_id_i, + name: promotion.name_s, + type: { + value: promotion.type_value_s, + label: promotion.type_label_s, + }, + limit: promotion.package_limit_i, + limit_user: promotion.package_limit_user_i, + limit_trx: promotion.package_limit_trx_i, + price: promotion.price_f, + total_qty: promotion.total_qty_i, + products: JSON.parse(promotion.products_s), + product_id: promotion.product_ids[0], + free_products: JSON.parse(promotion.free_products_s), + }; + result.push(data); + } + return result; +};
\ No newline at end of file diff --git a/src/core/components/elements/Footer/BasicFooter.jsx b/src/core/components/elements/Footer/BasicFooter.jsx index 28a3764c..6129143d 100644 --- a/src/core/components/elements/Footer/BasicFooter.jsx +++ b/src/core/components/elements/Footer/BasicFooter.jsx @@ -41,6 +41,8 @@ const BasicFooter = () => { <Form /> <CustomerGuide /> <Payments /> + <Shippings /> + <Secures /> </div> <div className='w-full mt-8 leading-5 text-caption-2 text-gray_r-12/80'> @@ -53,7 +55,7 @@ const BasicFooter = () => { <DesktopView> <footer className='bg-gray_r-3 py-6'> <div className='container mx-auto flex flex-wrap justify-between'> - <div className='w-3/12'> + <div className='w-4/12'> <NextImage src={IndoteknikLogo} alt='Logo Indoteknik' @@ -64,27 +66,40 @@ const BasicFooter = () => { PT. Indoteknik Dotcom Gemilang </div> <InformationCenter /> + <div className='h-4' /> + <OfficeLocation /> + <div className='h-4' /> + <OpenHours /> </div> - <CustomerGuide /> - <Form /> - <AboutUs /> + + <div className='w-2/12'> + <CustomerGuide /> + <div className='h-6' /> + <Form /> + </div> + + <div className='w-2/12'> + <AboutUs /> + </div> + <div className='w-3/12'> - <div className='grid grid-cols-1 gap-y-4'> - <OfficeLocation /> - {/* <WarehouseLocation /> */} - <OpenHours /> + <div className='grid grid-cols-1 gap-y-6'> <Payments /> + <Shippings /> + <Secures /> </div> </div> + <hr className='w-full my-4 border-gray_r-7' /> + <div className='w-full flex justify-between items-center'> <div className='text-caption-1'> Copyright © 2007 - {new Date().getFullYear()}, PT. Indoteknik Dotcom Gemilang </div> - <div> + {/* <div> <SocialMedias /> - </div> + </div> */} </div> </div> </footer> @@ -256,7 +271,7 @@ const InformationCenter = () => ( <li className='text-gray_r-12/80 flex items-center'> <DevicePhoneMobileIcon className='w-[18px] mr-2' /> <a href={whatsappUrl()} target='_blank' rel='noreferrer'> - 0812-8080-622 + 0817-1718-1922 </a> </li> </ul> @@ -286,7 +301,7 @@ const SocialMedias = () => ( <a target='_blank' rel='noreferrer' - href='https://www.youtube.com/@indoteknikb2bindustriale-c778' + href='https://www.youtube.com/@indoteknikcom' > <NextImage src='/images/socials/youtube.webp' @@ -369,57 +384,43 @@ const SocialMedias = () => ( const Payments = () => ( <div> - <div className={headerClassName}>Pembayaran</div> - <div className='flex flex-wrap gap-2'> - <NextImage - src='/images/payments/bca.png' - alt='Bank BCA Logo' - width={48} - height={24} - /> - <NextImage - src='/images/payments/bni.png' - alt='Bank BNI Logo' - width={48} - height={24} - /> - <NextImage - src='/images/payments/bri.png' - alt='Bank BRI Logo' - width={48} - height={24} - /> - <NextImage - src='/images/payments/gopay.png' - alt='Gopay Logo' - width={48} - height={24} - /> - <NextImage - src='/images/payments/mandiri.png' - alt='Bank Mandiri Logo' - width={48} - height={24} - /> - <NextImage - src='/images/payments/mastercard.png' - alt='Mastercard Logo' - width={48} - height={24} - /> - <NextImage - src='/images/payments/permata.png' - alt='Bank Permata Logo' - width={48} - height={24} - /> - <NextImage - src='/images/payments/visa.png' - alt='Visa Logo' - width={48} - height={24} - /> - </div> + <div className={headerClassName}>Metode Pembayaran</div> + <NextImage + src='/images/footer/payment-method-new.png' + alt='Metode Pembayaran - Indoteknik' + width={512} + height={512} + quality={100} + className='w-full' + /> + </div> +); + +const Shippings = () => ( + <div> + <div className={headerClassName}>Jasa Pengiriman</div> + <NextImage + src='/images/footer/shippings.png' + alt='Jasa Pengiriman - Indoteknik' + width={512} + height={512} + quality={100} + className='w-full' + /> + </div> +); + +const Secures = () => ( + <div> + <div className={headerClassName}>Keamanan Belanja</div> + <NextImage + src='/images/footer/secures.png' + alt='Keamanan Belanja - Indoteknik' + width={512} + height={512} + quality={100} + className='w-full' + /> </div> ); diff --git a/src/core/components/elements/Footer/SimpleFooter.jsx b/src/core/components/elements/Footer/SimpleFooter.jsx index 26f7f786..371b1652 100644 --- a/src/core/components/elements/Footer/SimpleFooter.jsx +++ b/src/core/components/elements/Footer/SimpleFooter.jsx @@ -22,7 +22,7 @@ const SimpleFooter = () => ( <li className='text-gray_r-12/80 flex items-center'> <DevicePhoneMobileIcon className='w-[18px] mr-2' /> <a href={whatsappUrl()} target='_blank' rel='noreferrer'> - 0812-8080-622 + 081717181922 </a> </li> </ul> diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx index e11ad214..308f2623 100644 --- a/src/core/components/elements/Navbar/NavbarDesktop.jsx +++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx @@ -18,6 +18,7 @@ import { useEffect, useState } from 'react'; import DesktopView from '../../views/DesktopView'; import Link from '../Link/Link'; import NavbarUserDropdown from './NavbarUserDropdown'; +import NextImage from 'next/image'; const Search = dynamic(() => import('./Search'), { ssr: false }); const TopBanner = dynamic(() => import('./TopBanner'), { ssr: false }); @@ -69,12 +70,17 @@ const NavbarDesktop = () => { return ( <DesktopView> <TopBanner /> - <div className='py-3 bg-warning-400' id='desktop-nav-top'> + <div className='py-2 bg-warning-400' id='desktop-nav-top'> <div className='container mx-auto flex justify-between'> - <Link href='/tentang-kami' className='!text-gray_r-12'> - Tentang Indoteknik.com - </Link> + <div className='flex items-start gap-5'> + <div> + <SocialMedias /> + </div> + </div> <div className='flex gap-x-6'> + <Link href='/tentang-kami' className='!text-gray_r-12'> + Tentang Indoteknik.com + </Link> <Link href='/my/pembayaran-tempo' className='!text-gray_r-12'> Pembayaran Tempo </Link> @@ -136,7 +142,7 @@ const NavbarDesktop = () => { /> <div> <div className='font-semibold'>Whatsapp</div> - 0812 8080 622 (Chat) + 0817 1718 1922 (Chat) </div> </a> </div> @@ -166,7 +172,9 @@ const NavbarDesktop = () => { <div className='w-6/12 flex px-1 divide-x divide-gray_r-6'> <Link href='/shop/brands' - className={`${router.asPath === '/shop/brands' && 'bg-gray_r-3'} p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`} + className={`${ + router.asPath === '/shop/brands' && 'bg-gray_r-3' + } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`} target='_blank' rel='noreferrer' > @@ -174,7 +182,10 @@ const NavbarDesktop = () => { </Link> <Link href='/shop/search?orderBy=stock' - className={`${router.asPath === '/shop/search?orderBy=stock' && 'bg-gray_r-3'} p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`} + className={`${ + router.asPath === '/shop/search?orderBy=stock' && + 'bg-gray_r-3' + } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`} target='_blank' rel='noreferrer' > @@ -190,7 +201,9 @@ const NavbarDesktop = () => { </Link> <Link href='/video' - className={`${router.asPath === '/video' && 'bg-gray_r-3'} p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`} + className={`${ + router.asPath === '/video' && 'bg-gray_r-3' + } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`} target='_blank' rel='noreferrer' > @@ -234,4 +247,92 @@ const NavbarDesktop = () => { ); }; +const SocialMedias = () => ( + <div> + {/* <div className={headerClassName + 'block md:hidden'}>Temukan Kami</div> */} + <div className='flex flex-wrap gap-3 items-start'> + <a + target='_blank' + rel='noreferrer' + href='https://www.youtube.com/@indoteknikcom' + > + <NextImage + src='/images/socials/youtube.webp' + alt='Youtube - Indoteknik.com' + width={24} + height={24} + /> + </a> + <a + target='_blank' + rel='noreferrer' + href='https://www.tiktok.com/@indoteknikcom' + > + <NextImage + src='/images/socials/tiktok.png' + alt='TikTok - Indoteknik.com' + width={24} + height={24} + /> + </a> + {/* <a target='_blank' rel='noreferrer' href={whatsappUrl(null)}> + <NextImage + src='/images/socials/Whatsapp.png' + alt='Whatsapp - Indoteknik.com' + width={24} + height={24} + /> + </a> */} + <a + target='_blank' + rel='noreferrer' + href='https://www.facebook.com/indoteknikcom' + > + <NextImage + src='/images/socials/Facebook.png' + alt='Facebook - Indoteknik.com' + width={24} + height={24} + /> + </a> + <a + target='_blank' + rel='noreferrer' + href='https://www.instagram.com/indoteknikcom/' + > + <NextImage + src='/images/socials/Instagram.png' + alt='Instagram - Indoteknik.com' + width={24} + height={24} + /> + </a> + <a + target='_blank' + rel='noreferrer' + href='https://www.linkedin.com/company/pt-indoteknik-dotcom-gemilang/' + > + <NextImage + src='/images/socials/Linkedin.png' + alt='Linkedin - Indoteknik.com' + width={24} + height={24} + /> + </a> + <a + target='_blank' + rel='noreferrer' + href='https://goo.gl/maps/GF8EmDjpQTHZPsJ1A' + > + <NextImage + src='/images/socials/g_maps.png' + alt='Maps - Indoteknik.com' + width={24} + height={24} + /> + </a> + </div> + </div> +); + export default NavbarDesktop; diff --git a/src/core/components/layouts/BasicLayout.jsx b/src/core/components/layouts/BasicLayout.jsx index fa41a8ed..a4f3a856 100644 --- a/src/core/components/layouts/BasicLayout.jsx +++ b/src/core/components/layouts/BasicLayout.jsx @@ -1,16 +1,13 @@ import dynamic from 'next/dynamic'; import Image from 'next/image'; -import { useCallback, useEffect, useState } from 'react'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; import { useProductContext } from '@/contexts/ProductContext'; import odooApi from '@/core/api/odooApi'; import whatsappUrl from '@/core/utils/whatsappUrl'; -import { useRouter } from 'next/router'; +import Navbar from '../elements/Navbar/Navbar'; -const Navbar = dynamic(() => import('../elements/Navbar/Navbar'), { - ssr: false, - loading: () => <div className='h-[156px]' />, -}); const AnimationLayout = dynamic(() => import('./AnimationLayout'), { ssr: false, }); @@ -42,13 +39,10 @@ const BasicLayout = ({ children }) => { } }, [product, router]); - const recordActivity = useCallback(async () => { - const recordedPath = [ - '/shop/product/[slug]', - '/shop/product/variant/[slug]', - ]; - - if (!recordedPath.includes(router.pathname)) return; + const recordActivity = async (pathname) => { + const ONLY_ON_PATH = false; + const recordedPath = []; + if (ONLY_ON_PATH && !recordedPath.includes(pathname)) return; const ip = await odooApi('GET', '/api/ip-address'); const data = new URLSearchParams({ @@ -58,11 +52,11 @@ const BasicLayout = ({ children }) => { }); fetch(`/api/user-activity?${data.toString()}`); - }, [router.pathname]); + }; useEffect(() => { - recordActivity(); - }, [recordActivity]); + recordActivity(router.pathname); + }, [router.pathname]); return ( <> diff --git a/src/core/utils/auth.js b/src/core/utils/auth.js index a7244747..03b20ae2 100644 --- a/src/core/utils/auth.js +++ b/src/core/utils/auth.js @@ -29,7 +29,7 @@ const setAuth = (user) => { * @returns {boolean} - Returns `true`. */ const deleteAuth = async() => { - // await signOut() + await signOut() deleteCookie('auth') return true } diff --git a/src/core/utils/googleTag.js b/src/core/utils/googleTag.js index cc6d1283..96a6bd2e 100644 --- a/src/core/utils/googleTag.js +++ b/src/core/utils/googleTag.js @@ -33,6 +33,20 @@ const sumTotal = (variants) => { } } +const mapProducts = (product) => { + const res = { + item_id: product.id, + item_name: product.name, + discount: product.lowest_price.price_discount || 0, + // index: 0, + item_brand: product.manufacture.name, + item_category: product.categories, + item_variant: product.variants, + price: product.lowest_price.price, + quantity: product.stock_total + } + return res +} export const gtagAddToCart = (variant, quantity) => { const param = { currency: 'IDR', @@ -77,3 +91,13 @@ export const gtagPurchase = (variants, shipping, transactionId) => { } gtag('event', 'purchase', param) } + +export const gtagProductDetail = (product) => { + const items = mapProducts(product) + const param = { + currency: 'IDR', + value: product.id, + items + } + gtag('event', 'view_item', param) +}
\ No newline at end of file diff --git a/src/core/utils/whatsappUrl.js b/src/core/utils/whatsappUrl.js index 9a92f424..7a129aa6 100644 --- a/src/core/utils/whatsappUrl.js +++ b/src/core/utils/whatsappUrl.js @@ -7,7 +7,7 @@ const whatsappUrl = (template = 'default', payload, urlPath = null) => { if(!urlPath) return '/login' } let parentName = user.parentName || '-' - let url = 'https://wa.me/628128080622' + let url = 'https://wa.me/6281717181922' let text = 'Hallo Indoteknik.com,' switch (template) { case 'product': diff --git a/src/images/logo-idul-fitri.png b/src/images/logo-idul-fitri.png Binary files differnew file mode 100644 index 00000000..db04faa5 --- /dev/null +++ b/src/images/logo-idul-fitri.png 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 52edbd05..4aafdece 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -111,6 +111,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); @@ -223,7 +225,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); @@ -295,6 +299,14 @@ const Checkout = () => { const [isLoading, setIsLoading] = useState(false); + useEffect(() => { + const GT = + cartCheckout?.grandTotal + + Math.round(parseInt(biayaKirim * 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) { @@ -324,14 +336,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, type: 'sale_order', }; @@ -347,16 +362,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', { @@ -913,10 +937,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 + - Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 - )} + {currencyFormat(grandTotal)} </div> </div> )} @@ -1208,10 +1229,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 + - Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 - )} + {currencyFormat(grandTotal)} </div> </div> )} @@ -1442,7 +1460,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 new file mode 100644 index 00000000..affe6138 --- /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=6281717181922' + > + 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/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/flashSale/components/FlashSale.jsx b/src/lib/flashSale/components/FlashSale.jsx index 3d5c4e0e..5be6d4e3 100644 --- a/src/lib/flashSale/components/FlashSale.jsx +++ b/src/lib/flashSale/components/FlashSale.jsx @@ -1,26 +1,28 @@ -import { useEffect, useState } from 'react' -import flashSaleApi from '../api/flashSaleApi' -import Image from 'next/image' -import CountDown from '@/core/components/elements/CountDown/CountDown' -import productSearchApi from '@/lib/product/api/productSearchApi' -import ProductSlider from '@/lib/product/components/ProductSlider' -import { FlashSaleSkeleton } from '../skeleton/FlashSaleSkeleton' +import Image from 'next/image'; +import { useEffect, useState } from 'react'; + +import CountDown from '@/core/components/elements/CountDown/CountDown'; +import productSearchApi from '@/lib/product/api/productSearchApi'; +import ProductSlider from '@/lib/product/components/ProductSlider'; + +import flashSaleApi from '../api/flashSaleApi'; +import { FlashSaleSkeleton } from '../skeleton/FlashSaleSkeleton'; const FlashSale = () => { - const [flashSales, setFlashSales] = useState(null) - const [isLoading, setIsLoading] = useState(true) + const [flashSales, setFlashSales] = useState(null); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { const loadFlashSales = async () => { - const dataFlashSales = await flashSaleApi() - setFlashSales(dataFlashSales) - setIsLoading(false) - } - loadFlashSales() - }, []) + const dataFlashSales = await flashSaleApi(); + setFlashSales(dataFlashSales); + setIsLoading(false); + }; + loadFlashSales(); + }, []); if (isLoading) { - return <FlashSaleSkeleton /> + return <FlashSaleSkeleton />; } return ( @@ -29,7 +31,9 @@ const FlashSale = () => { {flashSales.map((flashSale, index) => ( <div key={index}> <div className='flex gap-x-3 mb-4 justify-between sm:justify-start'> - <div className='font-medium sm:text-h-lg mt-1.5'>{flashSale.name}</div> + <div className='font-medium sm:text-h-lg mt-1.5'> + {flashSale.name} + </div> <CountDown initialTime={flashSale.duration} /> </div> @@ -54,24 +58,24 @@ const FlashSale = () => { ))} </div> ) - ) -} + ); +}; const FlashSaleProduct = ({ flashSaleId }) => { - const [products, setProducts] = useState(null) + const [products, setProducts] = useState(null); useEffect(() => { const loadProducts = async () => { const dataProducts = await productSearchApi({ query: `fq=flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=500&orderBy=flashsale-price-asc`, - operation: 'AND' - }) - setProducts(dataProducts.response) - } - loadProducts() - }, [flashSaleId]) + operation: 'AND', + }); + setProducts(dataProducts.response); + }; + loadProducts(); + }, [flashSaleId]); - return <ProductSlider products={products} /> -} + return <ProductSlider products={products} />; +}; -export default FlashSale +export default FlashSale; diff --git a/src/lib/home/components/PromotionProgram.jsx b/src/lib/home/components/PromotionProgram.jsx new file mode 100644 index 00000000..b204df8e --- /dev/null +++ b/src/lib/home/components/PromotionProgram.jsx @@ -0,0 +1,66 @@ +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 && ( + <div></div> + // <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/Product/ProductDesktop.jsx b/src/lib/product/components/Product/ProductDesktop.jsx index a12b8609..444ddd8e 100644 --- a/src/lib/product/components/Product/ProductDesktop.jsx +++ b/src/lib/product/components/Product/ProductDesktop.jsx @@ -235,7 +235,7 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { <ImageNext src={ backgorundFlashSale || - '/images/GAMBAR-BG-FLASH-SALE.jpg' + '/images/BG-FLASH-SALE.jpg' } width={1000} height={100} diff --git a/src/lib/product/components/Product/ProductDesktopVariant.jsx b/src/lib/product/components/Product/ProductDesktopVariant.jsx index bae00b87..09b30a44 100644 --- a/src/lib/product/components/Product/ProductDesktopVariant.jsx +++ b/src/lib/product/components/Product/ProductDesktopVariant.jsx @@ -1,3 +1,4 @@ + import { Box, Skeleton, Tooltip } from '@chakra-ui/react'; import { HeartIcon } from '@heroicons/react/24/outline'; import { Info } from 'lucide-react'; @@ -264,41 +265,19 @@ const ProductDesktopVariant = ({ </div> </div> - {/* <div className='w-full'> - <div className='mt-12'> - <div className='text-h-lg font-semibold'>Informasi Produk</div> - <div className='flex gap-x-4 mt-6 mb-4'> - {informationTabOptions.map((option) => ( - <TabButton - value={option.value} - key={option.value} - active={informationTab == option.value} - onClick={() => setInformationTab(option.value)} - > - {option.label} - </TabButton> - ))} - </div> - <div className='flex'> - <div className='w-3/4 leading-7 product__description'> - <TabContent active={informationTab == 'description'}> - <span - dangerouslySetInnerHTML={{ - __html: - product.description != '' - ? product.description - : 'Belum ada deskripsi produk.' - }} - /> - </TabContent> - - <TabContent active={informationTab == 'information'}> - Belum ada informasi. - </TabContent> - </div> - </div> - </div> - </div> */} + <div className='p-4 md:p-6 md:bg-gray-50 rounded-xl'> + <h2 className='text-h-md md:text-h-lg font-medium'>Informasi Produk</h2> + <div className='h-4' /> + <div + className='leading-relaxed text-gray-700' + dangerouslySetInnerHTML={{ + __html: + !product.parent.description || product.parent.description == '<p><br></p>' + ? 'Belum ada deskripsi' + : product.parent.description, + }} + /> + </div> </div> <div className='w-[25%]'> {product?.isFlashsale > 0 && diff --git a/src/lib/product/components/Product/ProductMobile.jsx b/src/lib/product/components/Product/ProductMobile.jsx index e9e64469..113a1e42 100644 --- a/src/lib/product/components/Product/ProductMobile.jsx +++ b/src/lib/product/components/Product/ProductMobile.jsx @@ -202,9 +202,7 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { <div className={`absolute bottom-0 w-full`}> <div className='absolute bottom-0 w-full'> <ImageNext - src={ - backgorundFlashSale || '/images/GAMBAR-BG-FLASH-SALE.jpg' - } + src={backgorundFlashSale || '/images/BG-FLASH-SALE.jpg'} width={1000} height={100} /> diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx index 1cec0804..98732407 100644 --- a/src/lib/product/components/ProductCard.jsx +++ b/src/lib/product/components/ProductCard.jsx @@ -1,44 +1,85 @@ -import Image from '@/core/components/elements/Image/Image' -import Link from '@/core/components/elements/Link/Link' -import currencyFormat from '@/core/utils/currencyFormat' -import { sellingProductFormat } from '@/core/utils/formatValue' -import { createSlug } from '@/core/utils/slug' -import whatsappUrl from '@/core/utils/whatsappUrl' -import ImageNext from 'next/image' -import { useRouter } from 'next/router' -import { useMemo } from 'react' +import clsx from 'clsx'; +import ImageNext from 'next/image'; +import { useRouter } from 'next/router'; +import { useMemo, useEffect, useState } from 'react'; + +import Image from '@/core/components/elements/Image/Image'; +import Link from '@/core/components/elements/Link/Link'; +import currencyFormat from '@/core/utils/currencyFormat'; +import { sellingProductFormat } from '@/core/utils/formatValue'; +import { createSlug } from '@/core/utils/slug'; +import whatsappUrl from '@/core/utils/whatsappUrl'; +import useUtmSource from '~/hooks/useUtmSource'; const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { - const router = useRouter() + const router = useRouter(); + const utmSource = useUtmSource(); + const callForPriceWhatsapp = whatsappUrl('product', { name: product.name, manufacture: product.manufacture?.name, - url: createSlug('/shop/product/', product.name, product.id, true) - }) + url: createSlug('/shop/product/', product.name, product.id, true), + }); const image = useMemo(() => { - if (product.image) return product.image + '?ratio=square' - return '/images/noimage.jpeg' - }, [product.image]) + if (product.image) return product.image + '?ratio=square'; + return '/images/noimage.jpeg'; + }, [product.image]); + + const URL = { + product: + createSlug('/shop/product/', product?.name, product?.id) + + `?utm_source=${utmSource}`, + manufacture: createSlug( + '/shop/brands/', + product?.manufacture?.name, + product?.manufacture.id + ), + }; if (variant == 'vertical') { return ( <div className='rounded shadow-sm border border-gray_r-4 bg-white h-[300px] md:h-[350px]'> - <Link - href={createSlug('/shop/product/', product?.name, product?.id)} - className='border-b border-gray_r-4 relative' - > + <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'> <ImageNext - src='/images/GAMBAR-BG-FLASH-SALE.jpg' + src='/images/BG-FLASH-SALE.jpg' className='h-full' width={1000} height={100} @@ -58,7 +99,8 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { height={5} /> <span className='text-white text-[9px] md:text-[10px] font-semibold'> - {product?.flashSale?.tag != 'false' || product?.flashSale?.tag + {product?.flashSale?.tag != 'false' || + product?.flashSale?.tag ? product?.flashSale?.tag : 'FLASH SALE'} </span> @@ -75,27 +117,21 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { </Link> <div className='p-2 sm:p-3 pb-3 text-caption-2 sm:text-body-2 leading-5'> {product?.manufacture?.name ? ( - <Link - href={createSlug( - '/shop/brands/', - product?.manufacture?.name, - product?.manufacture.id - )} - className='mb-1' - > + <Link href={URL.manufacture} className='mb-1'> {product.manufacture.name} </Link> ) : ( <div>-</div> )} <Link - href={createSlug('/shop/product/', product?.name, product?.id)} + href={URL.product} className={`mb-2 !text-gray_r-12 leading-6 block line-clamp-3 h-[64px]`} title={product?.name} > {product?.name} </Link> - {product?.flashSale?.id > 0 && product?.lowestPrice.discountPercentage > 0 ? ( + {product?.flashSale?.id > 0 && + product?.lowestPrice.discountPercentage > 0 ? ( <> <div className='flex gap-x-1 mb-1 items-center'> <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'> @@ -109,7 +145,11 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { {product?.lowestPrice.priceDiscount > 0 ? ( currencyFormat(product?.lowestPrice.priceDiscount) ) : ( - <a rel='noopener noreferrer' target='_blank' href={callForPriceWhatsapp}> + <a + rel='noopener noreferrer' + target='_blank' + href={callForPriceWhatsapp} + > Call for Inquiry </a> )} @@ -122,11 +162,17 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { {currencyFormat(product?.lowestPrice.price)} <div className='text-gray_r-9 text-[10px] font-normal mt-2'> Inc. PPN:{' '} - {currencyFormat(product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN)} + {currencyFormat( + product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN + )} </div> </> ) : ( - <a rel='noopener noreferrer' target='_blank' href={callForPriceWhatsapp}> + <a + rel='noopener noreferrer' + target='_blank' + href={callForPriceWhatsapp} + > Call for Inquiry </a> )} @@ -134,7 +180,9 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { )} <div className='flex w-full items-center gap-x-1 '> - {product?.stockTotal > 0 && <div className='badge-solid-red'>Ready Stock</div>} + {product?.stockTotal > 0 && ( + <div className='badge-solid-red'>Ready Stock</div> + )} {/* <div className='badge-gray'>{product?.stockTotal > 5 ? '> 5' : '< 5'}</div> */} {product?.qtySold > 0 && ( <div className='text-gray_r-9 text-[11px]'> @@ -144,22 +192,45 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { </div> </div> </div> - ) + ); } if (variant == 'horizontal') { return ( <div className='flex bg-white'> <div className='w-4/12'> - <Link - href={createSlug('/shop/product/', product?.name, product?.id)} - className='relative' - > + <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 @@ -184,26 +255,20 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { </div> )} {product?.manufacture?.name ? ( - <Link - href={createSlug( - '/shop/brands/', - product?.manufacture?.name, - product?.manufacture.id - )} - className='mb-1' - > + <Link href={URL.manufacture} className='mb-1'> {product.manufacture.name} </Link> ) : ( <div>-</div> )} <Link - href={createSlug('/shop/product/', product?.name, product?.id)} + href={URL.product} className={`mb-3 !text-gray_r-12 leading-6 line-clamp-3`} > {product?.name} </Link> - {product?.flashSale?.id > 0 && product?.lowestPrice?.discountPercentage > 0 ? ( + {product?.flashSale?.id > 0 && + product?.lowestPrice?.discountPercentage > 0 ? ( <> {product?.lowestPrice.discountPercentage > 0 && ( <div className='flex gap-x-1 mb-1 items-center'> @@ -220,7 +285,11 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { {product?.lowestPrice?.priceDiscount > 0 ? ( currencyFormat(product?.lowestPrice?.priceDiscount) ) : ( - <a rel='noopener noreferrer' target='_blank' href={callForPriceWhatsapp}> + <a + rel='noopener noreferrer' + target='_blank' + href={callForPriceWhatsapp} + > Call for Inquiry </a> )} @@ -233,11 +302,17 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { {currencyFormat(product?.lowestPrice.price)} <div className='text-gray_r-9 text-[11px] sm:text-caption-2 font-normal mt-2'> Inc. PPN:{' '} - {currencyFormat(product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN)} + {currencyFormat( + product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN + )} </div> </> ) : ( - <a rel='noopener noreferrer' target='_blank' href={callForPriceWhatsapp}> + <a + rel='noopener noreferrer' + target='_blank' + href={callForPriceWhatsapp} + > Call for Inquiry </a> )} @@ -245,7 +320,9 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { )} <div className='flex w-full items-center gap-x-1 '> - {product?.stockTotal > 0 && <div className='badge-solid-red'>Ready Stock</div>} + {product?.stockTotal > 0 && ( + <div className='badge-solid-red'>Ready Stock</div> + )} {/* <div className='badge-gray'>{product?.stockTotal > 5 ? '> 5' : '< 5'}</div> */} {product?.qtySold > 0 && ( <div className='text-gray_r-9 text-[11px]'> @@ -255,8 +332,8 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { </div> </div> </div> - ) + ); } -} +}; -export default ProductCard +export default ProductCard; 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 8c379ead..09d55e92 100644 --- a/src/lib/quotation/components/Quotation.jsx +++ b/src/lib/quotation/components/Quotation.jsx @@ -1,102 +1,283 @@ -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) { + 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); + }, [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, + partner_shipping_id: selectedAddress.shipping.id, + partner_invoice_id: selectedAddress.invoicing.id, 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 +288,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 +369,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 +381,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 +393,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 +430,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 +443,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 +521,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 +538,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 +570,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..9962b46e 100644 --- a/src/lib/transaction/components/Transaction.jsx +++ b/src/lib/transaction/components/Transaction.jsx @@ -1,83 +1,123 @@ -import Spinner from '@/core/components/elements/Spinner/Spinner' -import useTransaction from '../hooks/useTransaction' -import TransactionStatusBadge from './TransactionStatusBadge' -import Divider from '@/core/components/elements/Divider/Divider' -import { useMemo, useRef, useState } from 'react' -import { downloadPurchaseOrder, downloadQuotation } from '../utils/transactions' -import BottomPopup from '@/core/components/elements/Popup/BottomPopup' -import uploadPoApi from '../api/uploadPoApi' -import { toast } from 'react-hot-toast' -import getFileBase64 from '@/core/utils/getFileBase64' -import currencyFormat from '@/core/utils/currencyFormat' -import VariantGroupCard from '@/lib/variant/components/VariantGroupCard' -import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon } from '@heroicons/react/24/outline' -import Link from '@/core/components/elements/Link/Link' -import checkoutPoApi from '../api/checkoutPoApi' -import cancelTransactionApi from '../api/cancelTransactionApi' -import MobileView from '@/core/components/views/MobileView' -import DesktopView from '@/core/components/views/DesktopView' -import Menu from '@/lib/auth/components/Menu' -import Image from '@/core/components/elements/Image/Image' -import { createSlug } from '@/core/utils/slug' -import toTitleCase from '@/core/utils/toTitleCase' -import useAirwayBill from '../hooks/useAirwayBill' -import Manifest from '@/lib/treckingAwb/component/Manifest' +import Spinner from '@/core/components/elements/Spinner/Spinner'; +import useTransaction from '../hooks/useTransaction'; +import TransactionStatusBadge from './TransactionStatusBadge'; +import Divider from '@/core/components/elements/Divider/Divider'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import ImageNext from 'next/image'; +import { + downloadPurchaseOrder, + downloadQuotation, +} from '../utils/transactions'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import uploadPoApi from '../api/uploadPoApi'; +import { toast } from 'react-hot-toast'; +import getFileBase64 from '@/core/utils/getFileBase64'; +import currencyFormat from '@/core/utils/currencyFormat'; +import VariantGroupCard from '@/lib/variant/components/VariantGroupCard'; +import { + ChevronDownIcon, + ChevronRightIcon, + ChevronUpIcon, +} from '@heroicons/react/24/outline'; +import Link from '@/core/components/elements/Link/Link'; +import checkoutPoApi from '../api/checkoutPoApi'; +import cancelTransactionApi from '../api/cancelTransactionApi'; +import MobileView from '@/core/components/views/MobileView'; +import DesktopView from '@/core/components/views/DesktopView'; +import Menu from '@/lib/auth/components/Menu'; +import Image from '@/core/components/elements/Image/Image'; +import { createSlug } from '@/core/utils/slug'; +import toTitleCase from '@/core/utils/toTitleCase'; +import useAirwayBill from '../hooks/useAirwayBill'; +import Manifest from '@/lib/treckingAwb/component/Manifest'; +import useAuth from '@/core/hooks/useAuth'; +import StepApproval from './stepper'; +import aprpoveApi from '../api/approveApi'; +import rejectApi from '../api/rejectApi'; const Transaction = ({ id }) => { - const { transaction } = useTransaction({ id }) - const { queryAirwayBill } = useAirwayBill({ orderId: id }) + const auth = useAuth(); + const { transaction } = useTransaction({ id }); - const [airwayBillPopup, setAirwayBillPopup] = useState(null) + const statusApprovalWeb = transaction.data?.approvalStep; + + const { queryAirwayBill } = useAirwayBill({ orderId: id }); + const [airwayBillPopup, setAirwayBillPopup] = useState(null); + + const poNumber = useRef(null); + const poFile = useRef(null); + const [uploadPo, setUploadPo] = useState(false); + const [idAWB, setIdAWB] = useState(null); + const openUploadPo = () => setUploadPo(true); + const closeUploadPo = () => setUploadPo(false); + + + - const poNumber = useRef(null) - const poFile = useRef(null) - const [uploadPo, setUploadPo] = useState(false) - const [idAWB, setIdAWB] = useState(null) - const openUploadPo = () => setUploadPo(true) - const closeUploadPo = () => setUploadPo(false) const submitUploadPo = async () => { - const file = poFile.current.files[0] - const name = poNumber.current.value + const file = poFile.current.files[0]; + const name = poNumber.current.value; if (typeof file === 'undefined' || !name) { - toast.error('Nomor dan Dokumen PO harus diisi') - return + toast.error('Nomor dan Dokumen PO harus diisi'); + return; } if (file.size > 5000000) { - toast.error('Maksimal ukuran file adalah 5MB') - return + toast.error('Maksimal ukuran file adalah 5MB'); + return; } - const data = { name, file: await getFileBase64(file) } - const isUploaded = await uploadPoApi({ id, data }) + const data = { name, file: await getFileBase64(file) }; + const isUploaded = await uploadPoApi({ id, data }); if (isUploaded) { - toast.success('Berhasil upload PO') - transaction.refetch() - closeUploadPo() - return + toast.success('Berhasil upload PO'); + transaction.refetch(); + closeUploadPo(); + return; } - toast.error('Terjadi kesalahan internal, coba lagi nanti atau hubungi kami') - } + toast.error( + 'Terjadi kesalahan internal, coba lagi nanti atau hubungi kami' + ); + }; - const [cancelTransaction, setCancelTransaction] = useState(false) - const openCancelTransaction = () => setCancelTransaction(true) - const closeCancelTransaction = () => setCancelTransaction(false) + const [cancelTransaction, setCancelTransaction] = useState(false); + const openCancelTransaction = () => setCancelTransaction(true); + const closeCancelTransaction = () => setCancelTransaction(false); + + const [rejectTransaction, setRejectTransaction] = useState(false); + + const openRejectTransaction = () => setRejectTransaction(true); + const closeRejectTransaction = () => setRejectTransaction(false); const submitCancelTransaction = async () => { - const isCancelled = await cancelTransactionApi({ transaction: transaction.data }) + const isCancelled = await cancelTransactionApi({ + transaction: transaction.data, + }); if (isCancelled) { - toast.success('Berhasil batalkan transaksi') - transaction.refetch() + toast.success('Berhasil batalkan transaksi'); + transaction.refetch(); } - closeCancelTransaction() - } + closeCancelTransaction(); + }; const checkout = async () => { if (!transaction.data?.purchaseOrderFile) { - toast.error('Mohon upload dokumen PO anda sebelum melanjutkan pesanan') - return + toast.error('Mohon upload dokumen PO anda sebelum melanjutkan pesanan'); + return; } - await checkoutPoApi({ id }) - toast.success('Berhasil melanjutkan pesanan') - transaction.refetch() - } + await checkoutPoApi({ id }); + toast.success('Berhasil melanjutkan pesanan'); + transaction.refetch(); + }; + + const handleApproval = async () => { + await aprpoveApi({ id }); + toast.success('Berhasil melanjutkan approval'); + transaction.refetch(); + }; + + const handleReject = async () => { + await rejectApi({ id }); + closeRejectTransaction(); + transaction.refetch(); + }; const memoizeVariantGroupCard = useMemo( () => ( @@ -102,19 +142,19 @@ const Transaction = ({ id }) => { </div> ), [transaction.data] - ) + ); if (transaction.isLoading) { return ( <div className='flex justify-center my-6'> <Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' /> </div> - ) + ); } const closePopup = () => { - setIdAWB(null) - } + setIdAWB(null); + }; return ( transaction.data?.name && ( @@ -146,6 +186,33 @@ const Transaction = ({ id }) => { </div> </BottomPopup> + <BottomPopup + active={rejectTransaction} + close={closeRejectTransaction} + title='Batalkan Transaksi' + > + <div className='leading-7 text-gray_r-12/80'> + Apakah anda yakin Membatalkan transaksi{' '} + <span className='underline'>{transaction.data?.name}</span>? + </div> + <div className='flex justify-end mt-6 gap-x-4'> + <button + className='btn-solid-red w-full md:w-fit' + type='button' + onClick={handleReject} + > + Ya, Batalkan + </button> + <button + className='btn-light w-full md:w-fit' + type='button' + onClick={closeRejectTransaction} + > + Batal + </button> + </div> + </BottomPopup> + <BottomPopup title='Upload PO' close={closeUploadPo} active={uploadPo}> <div> <label>Nomor PO</label> @@ -156,10 +223,18 @@ const Transaction = ({ id }) => { <input type='file' className='form-input mt-3 py-2' ref={poFile} /> </div> <div className='grid grid-cols-2 gap-x-3 mt-6'> - <button type='button' className='btn-light w-full' onClick={closeUploadPo}> + <button + type='button' + className='btn-light w-full' + onClick={closeUploadPo} + > Batal </button> - <button type='button' className='btn-solid-red w-full' onClick={submitUploadPo}> + <button + type='button' + className='btn-solid-red w-full' + onClick={submitUploadPo} + > Upload </button> </div> @@ -167,18 +242,33 @@ const Transaction = ({ id }) => { <Manifest idAWB={idAWB} closePopup={closePopup}></Manifest> <MobileView> + <div className='p-4'> + {auth?.feature?.soApproval && ( + <StepApproval + layer={statusApprovalWeb} + status={transaction?.data?.status} + className='ml-auto' + /> + )} + </div> <div className='flex flex-col gap-y-4 p-4'> <DescriptionRow label='Status Transaksi'> <div className='flex justify-end'> <TransactionStatusBadge status={transaction.data?.status} /> </div> </DescriptionRow> - <DescriptionRow label='No Transaksi'>{transaction.data?.name}</DescriptionRow> + <DescriptionRow label='No Transaksi'> + {transaction.data?.name} + </DescriptionRow> <DescriptionRow label='Ketentuan Pembayaran'> {transaction.data?.paymentTerm} </DescriptionRow> - <DescriptionRow label='Nama Sales'>{transaction.data?.sales}</DescriptionRow> - <DescriptionRow label='Waktu Transaksi'>{transaction.data?.dateOrder}</DescriptionRow> + <DescriptionRow label='Nama Sales'> + {transaction.data?.sales} + </DescriptionRow> + <DescriptionRow label='Waktu Transaksi'> + {transaction.data?.dateOrder} + </DescriptionRow> </div> <Divider /> @@ -214,25 +304,27 @@ const Transaction = ({ id }) => { <Divider /> - <div className='p-4 flex flex-col gap-y-4'> - <DescriptionRow label='Purchase Order'> - {transaction.data?.purchaseOrderName || '-'} - </DescriptionRow> - <div className='flex items-center'> - <p className='text-gray_r-11 leading-none'>Dokumen PO</p> - <button - type='button' - className='btn-light py-1.5 px-3 ml-auto' - onClick={ - transaction.data?.purchaseOrderFile - ? () => downloadPurchaseOrder(transaction.data) - : openUploadPo - } - > - {transaction.data?.purchaseOrderFile ? 'Download' : 'Upload'} - </button> + {!auth?.feature.soApproval && ( + <div className='p-4 flex flex-col gap-y-4'> + <DescriptionRow label='Purchase Order'> + {transaction.data?.purchaseOrderName || '-'} + </DescriptionRow> + <div className='flex items-center'> + <p className='text-gray_r-11 leading-none'>Dokumen PO</p> + <button + type='button' + className='btn-light py-1.5 px-3 ml-auto' + onClick={ + transaction.data?.purchaseOrderFile + ? () => downloadPurchaseOrder(transaction.data) + : openUploadPo + } + > + {transaction.data?.purchaseOrderFile ? 'Download' : 'Upload'} + </button> + </div> </div> - </div> + )} <Divider /> @@ -278,11 +370,43 @@ const Transaction = ({ id }) => { <Divider /> <div className='p-4 pt-0'> - {transaction.data?.status == 'draft' && ( - <button className='btn-yellow w-full mt-4' onClick={checkout}> - Lanjutkan Transaksi - </button> - )} + {transaction.data?.status == 'draft' && + auth?.feature.soApproval && ( + <div className='flex gap-x-2'> + <button + className='btn-yellow w-full' + onClick={checkout} + disabled={ + transaction.data?.status === 'cancel' + ? true + : false || auth?.webRole === statusApprovalWeb + ? true + : false + } + > + Approve + </button> + <button + className='btn-solid-red px-7 w-full' + onClick={checkout} + disabled={ + transaction.data?.status === 'cancel' + ? true + : false || auth?.webRole === statusApprovalWeb + ? true + : false + } + > + Reject + </button> + </div> + )} + {transaction.data?.status == 'draft' && + !auth?.feature?.soApproval && ( + <button className='btn-yellow w-full mt-4' onClick={checkout}> + Lanjutkan Transaksi + </button> + )} <button className='btn-light w-full mt-4' disabled={transaction.data?.status != 'draft'} @@ -308,10 +432,23 @@ const Transaction = ({ id }) => { <Menu /> </div> <div className='w-9/12 p-4 py-6 bg-white border border-gray_r-6 rounded'> - <h1 className='text-title-sm font-semibold mb-6'>Detail Transaksi</h1> + <div className='flex justify-between'> + <h1 className='text-title-sm font-semibold mb-6'> + Detail Transaksi + </h1> + {auth?.feature?.soApproval && ( + <StepApproval + layer={statusApprovalWeb} + status={transaction?.data?.status} + className='ml-auto' + /> + )} + </div> <div className='flex items-center gap-x-2 mb-3'> - <span className='text-h-sm font-medium'>{transaction?.data?.name}</span> + <span className='text-h-sm font-medium'> + {transaction?.data?.name} + </span> <TransactionStatusBadge status={transaction?.data?.status} /> </div> <div className='flex gap-x-4'> @@ -322,20 +459,58 @@ const Transaction = ({ id }) => { > Download </button> - {transaction.data?.status == 'draft' && ( - <button className='btn-yellow' onClick={checkout}> - Lanjutkan Transaksi - </button> - )} - {transaction.data?.status != 'draft' && ( - <button - className='btn-light' - disabled={transaction.data?.status != 'waiting'} - onClick={openCancelTransaction} - > - Batalkan Transaksi - </button> - )} + {transaction.data?.status == 'draft' && + auth?.feature?.soApproval && + auth?.webRole && ( + <div className='flex gap-x-2'> + <button + className='btn-yellow' + onClick={handleApproval} + disabled={ + transaction.data?.status === 'cancel' + ? true + : false || auth?.webRole === statusApprovalWeb + ? true + : false || statusApprovalWeb < 1 + ? true + : false + } + > + Approve + </button> + <button + className='btn-solid-red px-7' + onClick={openRejectTransaction} + disabled={ + transaction.data?.status === 'cancel' + ? true + : false || auth?.webRole === statusApprovalWeb + ? true + : false || statusApprovalWeb < 1 + ? true + : false + } + > + Reject + </button> + </div> + )} + {transaction.data?.status == 'draft' && + !auth?.feature.soApproval && ( + <button className='btn-yellow' onClick={checkout}> + Lanjutkan Transaksi + </button> + )} + {transaction.data?.status != 'draft' && + !auth?.feature.soApproval && ( + <button + className='btn-light' + disabled={transaction.data?.status != 'waiting'} + onClick={openCancelTransaction} + > + Batalkan Transaksi + </button> + )} </div> <div className='grid grid-cols-2 gap-x-6 mt-6'> @@ -350,33 +525,45 @@ const Transaction = ({ id }) => { <div>Ketentuan Pembayaran</div> <div>: {transaction?.data?.paymentTerm}</div> - <div>Purchase Order</div> - <div> - : {transaction?.data?.purchaseOrderName}{' '} - <button - type='button' - className='inline-block text-danger-500' - onClick={ - transaction.data?.purchaseOrderFile - ? () => downloadPurchaseOrder(transaction.data) - : openUploadPo - } - > - {transaction?.data?.purchaseOrderFile ? 'Download' : 'Upload'} - </button> - </div> + {!auth?.feature?.soApproval && ( + <> + <div>Purchase Order</div> + <div> + : {transaction?.data?.purchaseOrderName}{' '} + <button + type='button' + className='inline-block text-danger-500' + onClick={ + transaction.data?.purchaseOrderFile + ? () => downloadPurchaseOrder(transaction.data) + : openUploadPo + } + > + {transaction?.data?.purchaseOrderFile + ? 'Download' + : 'Upload'} + </button> + </div> + </> + )} </div> </div> - <div className='text-h-sm font-semibold mt-10 mb-4'>Informasi Pelanggan</div> + <div className='text-h-sm font-semibold mt-10 mb-4'> + Informasi Pelanggan + </div> <div className='grid grid-cols-2 gap-x-4'> <div className='border border-gray_r-6 rounded p-3'> <div className='font-medium mb-4'>Detail Pelanggan</div> - <SectionContent address={transaction?.data?.address?.customer} /> + <SectionContent + address={transaction?.data?.address?.customer} + /> </div> </div> - <div className='text-h-sm font-semibold mt-10 mb-4'>Pengiriman</div> + <div className='text-h-sm font-semibold mt-10 mb-4'> + Pengiriman + </div> <div className='grid grid-cols-3 gap-1'> {transaction?.data?.pickings?.map((airway) => ( <button @@ -403,7 +590,9 @@ const Transaction = ({ id }) => { <div className='badge-red text-sm'>Belum ada pengiriman</div> )} - <div className='text-h-sm font-semibold mt-10 mb-4'>Rincian Pembelian</div> + <div className='text-h-sm font-semibold mt-10 mb-4'> + Rincian Pembelian + </div> <table className='table-data'> <thead> <tr> @@ -426,11 +615,39 @@ const Transaction = ({ id }) => { )} className='w-[20%] flex-shrink-0' > - <Image + + <div className="relative"> + <Image src={product?.parent?.image} alt={product?.name} className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md' /> + <div className="absolute top-0 right-4 flex mt-3"> + <div className="gambarB "> + {product.isSni && ( + <ImageNext + src="/images/sni-logo.png" + alt="SNI Logo" + className="w-2 h-4 object-contain object-top sm:h-4" + width={50} + height={50} + /> + )} + </div> + <div className="gambarC "> + {product.isTkdn && ( + <ImageNext + src="/images/TKDN.png" + alt="TKDN" + className="w-5 h-4 object-contain object-top ml-1 sm:h-4" + width={50} + height={50} + /> + )} + </div> + </div> + </div> + </Link> <div className='px-2 text-left'> <Link @@ -483,7 +700,9 @@ const Transaction = ({ id }) => { {currencyFormat(transaction.data?.amountTax)} </div> - <div className='text-right whitespace-nowrap'>Biaya Pengiriman</div> + <div className='text-right whitespace-nowrap'> + Biaya Pengiriman + </div> <div className='text-right font-medium'> {currencyFormat(transaction.data?.deliveryAmount)} </div> @@ -578,18 +797,18 @@ const Transaction = ({ id }) => { ))} */} </> ) - ) -} + ); +}; const SectionAddress = ({ address }) => { const [section, setSection] = useState({ customer: false, invoice: false, - shipping: false - }) + shipping: false, + }); const toggleSection = (name) => { - setSection({ ...section, [name]: !section[name] }) - } + setSection({ ...section, [name]: !section[name] }); + }; return ( <> @@ -620,39 +839,50 @@ const SectionAddress = ({ address }) => { /> {section.invoice && <SectionContent address={address?.invoice} />} */} </> - ) -} + ); +}; const SectionButton = ({ label, active, toggle }) => ( - <button className='p-4 font-medium flex justify-between w-full' onClick={toggle}> + <button + className='p-4 font-medium flex justify-between w-full' + onClick={toggle} + > <span>{label}</span> - {active ? <ChevronUpIcon className='w-5' /> : <ChevronDownIcon className='w-5' />} + {active ? ( + <ChevronUpIcon className='w-5' /> + ) : ( + <ChevronDownIcon className='w-5' /> + )} </button> -) +); const SectionContent = ({ address }) => { - let fullAddress = [] - if (address?.street) fullAddress.push(address.street) - if (address?.subDistrict?.name) fullAddress.push(toTitleCase(address.subDistrict.name)) - if (address?.district?.name) fullAddress.push(toTitleCase(address.district.name)) - if (address?.city?.name) fullAddress.push(toTitleCase(address.city.name)) - fullAddress = fullAddress.join(', ') + let fullAddress = []; + if (address?.street) fullAddress.push(address.street); + if (address?.subDistrict?.name) + fullAddress.push(toTitleCase(address.subDistrict.name)); + if (address?.district?.name) + fullAddress.push(toTitleCase(address.district.name)); + if (address?.city?.name) fullAddress.push(toTitleCase(address.city.name)); + fullAddress = fullAddress.join(', '); return ( <div className='flex flex-col gap-y-4 p-4 md:p-0 border-t border-gray_r-6 md:border-0'> <DescriptionRow label='Nama'>{address.name}</DescriptionRow> <DescriptionRow label='Email'>{address.email || '-'}</DescriptionRow> - <DescriptionRow label='No Telepon'>{address.mobile || '-'}</DescriptionRow> + <DescriptionRow label='No Telepon'> + {address.mobile || '-'} + </DescriptionRow> <DescriptionRow label='Alamat'>{fullAddress}</DescriptionRow> </div> - ) -} + ); +}; const DescriptionRow = ({ children, label }) => ( <div className='grid grid-cols-2'> <span className='text-gray_r-11'>{label}</span> <span className='text-right leading-6'>{children}</span> </div> -) +); -export default Transaction +export default Transaction; diff --git a/src/lib/transaction/components/stepper.jsx b/src/lib/transaction/components/stepper.jsx new file mode 100644 index 00000000..9b0da0d9 --- /dev/null +++ b/src/lib/transaction/components/stepper.jsx @@ -0,0 +1,83 @@ +import { + Box, + Step, + StepDescription, + StepIcon, + StepIndicator, + StepNumber, + StepSeparator, + StepStatus, + StepTitle, + Stepper, + useSteps, +} from '@chakra-ui/react'; +import Image from 'next/image'; + +const StepApproval = ({ layer, status }) => { + const steps = [ + { title: 'Indoteknik', layer_approval: 1 }, + { title: 'Manager', layer_approval: 2 }, + { title: 'Director', layer_approval: 3 }, + ]; + const { activeStep } = useSteps({ + index: layer, + count: steps.length, + }); + return ( + <Stepper size='md' index={layer} colorScheme='green'> + {steps.map((step, index) => ( + <Step key={index}> + <StepIndicator> + {layer === step.layer_approval && status === 'cancel' ? ( + <StepStatus + complete={ + <Image + src='/images/remove.png' + width={20} + height={20} + alt='' + className='w-full' + /> + } + incomplete={<StepNumber />} + active={<StepNumber />} + /> + ) : ( + <StepStatus + complete={<StepIcon />} + incomplete={<StepNumber />} + active={<StepNumber />} + /> + )} + </StepIndicator> + + <Box flexShrink='0'> + <StepTitle className='md:text-xs'>{step.title}</StepTitle> + {status === 'cancel' ? ( + layer > step.layer_approval ? ( + <StepDescription className='md:text-[8px]'> + Approved + </StepDescription> + ) : ( + <StepDescription className='md:text-[8px]'> + Rejected + </StepDescription> + ) + ) : layer >= step.layer_approval ? ( + <StepDescription className='md:text-[8px]'> + Approved + </StepDescription> + ) : ( + <StepDescription className='md:text-[8px]'> + Pending + </StepDescription> + )} + </Box> + <StepSeparator _horizontal={{ ml: '0' }} /> + </Step> + ))} + </Stepper> + ); +}; + +export default StepApproval; diff --git a/src/lib/variant/components/VariantCard.jsx b/src/lib/variant/components/VariantCard.jsx index 9f1b5733..9f65fc3c 100644 --- a/src/lib/variant/components/VariantCard.jsx +++ b/src/lib/variant/components/VariantCard.jsx @@ -7,9 +7,14 @@ 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 ImageNext from 'next/image'; +import { useMemo, useEffect, useState } from 'react'; const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { const router = useRouter() + + + const addItemToCart = () => { toast.success('Berhasil menambahkan ke keranjang', { duration: 1500 }) @@ -27,11 +32,39 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { 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> diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index 01dec611..bcb41dd6 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -89,9 +89,9 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }) { <AnimatePresence> {animateLoader && ( <motion.div - initial={{ opacity: 0.4 }} + initial={{ opacity: 0.25 }} animate={{ opacity: 1 }} - exit={{ opacity: 0.4 }} + exit={{ opacity: 0.25 }} transition={{ duration: 0.1, }} diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index adb23511..b6b8c795 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -1,6 +1,6 @@ -import { productMappingSolr } from '@/utils/solrMapping' -import axios from 'axios' -import camelcaseObjectDeep from 'camelcase-object-deep' +import { productMappingSolr } from '@/utils/solrMapping'; +import axios from 'axios'; +import camelcaseObjectDeep from 'camelcase-object-deep'; export default async function handler(req, res) { const { @@ -14,35 +14,36 @@ export default async function handler(req, res) { operation = 'AND', fq = '', limit = 30, - stock = '' - } = req.query + } = req.query; - let paramOrderBy = '' + let { stock = '' } = req.query; + + let paramOrderBy = ''; switch (orderBy) { case 'price-asc': - paramOrderBy += 'price_tier1_v2_f ASC' - break + paramOrderBy += 'price_tier1_v2_f ASC'; + break; case 'price-desc': - paramOrderBy += 'price_tier1_v2_f DESC' - break + paramOrderBy += 'price_tier1_v2_f DESC'; + break; case 'popular': - paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,' - break + paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,'; + break; case 'popular-weekly': - paramOrderBy += 'search_rank_weekly_i DESC' - break + paramOrderBy += 'search_rank_weekly_i DESC'; + break; case 'stock': - paramOrderBy += 'product_rating_f DESC, stock_total_f DESC' - break + paramOrderBy += 'product_rating_f DESC, stock_total_f DESC'; + break; case 'flashsale-price-asc': - paramOrderBy += 'flashsale_price_f ASC' - break + paramOrderBy += 'flashsale_price_f ASC'; + break; default: - paramOrderBy += 'product_rating_f DESC, price_discount_f DESC' - break + paramOrderBy += 'product_rating_f DESC, price_discount_f DESC'; + break; } - let offset = (page - 1) * limit + let offset = (page - 1) * limit; let parameter = [ 'facet.field=manufacture_name_s', 'facet.field=category_name', @@ -55,59 +56,82 @@ export default async function handler(req, res) { `start=${parseInt(offset)}`, `rows=${limit}`, `sort=${paramOrderBy}`, - `fq=-publish_b:false` - ] + `fq=-publish_b:false`, + ]; if (priceFrom > 0 || priceTo > 0) { parameter.push( `fq=price_tier1_v2_f:[${priceFrom == '' ? '*' : priceFrom} TO ${ priceTo == '' ? '*' : priceTo }]` - ) + ); + } + + let { auth } = req.cookies; + if (auth) { + auth = JSON.parse(auth); + if (auth.feature.onlyReadyStock) stock = true; } - if (brand) parameter.push(`fq=${brand.split(',').map(manufacturer => `manufacture_name:"${manufacturer}"`).join(" OR ")}`) - if (category) parameter.push(`fq=${category.split(',').map(cat => `category_name:"${cat}"`).join(' OR ')}`) + if (brand) + parameter.push( + `fq=${brand + .split(',') + .map((manufacturer) => `manufacture_name:"${encodeURIComponent(manufacturer)}"`) + .join(' OR ')}` + ); + if (category) + parameter.push( + `fq=${category + .split(',') + .map((cat) => `category_name:"${encodeURIComponent(cat)}"`) + .join(' OR ')}` + ); // if (category) parameter.push(`fq=category_name:${capitalizeFirstLetter(category.replace(/,/g, ' OR '))}`) - if (stock) parameter.push(`fq=stock_total_f:{1 TO *}`) + if (stock) parameter.push(`fq=stock_total_f:{1 TO *}`); // Single fq in url params - if (typeof fq === 'string') parameter.push(`fq=${fq}`) + if (typeof fq === 'string') parameter.push(`fq=${fq}`); // Multi fq in url params - if (Array.isArray(fq)) parameter = parameter.concat(fq.map((val) => `fq=${val}`)) + if (Array.isArray(fq)) + parameter = parameter.concat(fq.map((val) => `fq=${val}`)); - let result = await axios(process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&')) + let result = await axios( + process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&') + ); try { - let { auth } = req.cookies - if (auth) auth = JSON.parse(auth) result.data.response.products = productMappingSolr( result.data.response.docs, auth?.pricelist || false - ) - result.data.responseHeader.params.start = parseInt(result.data.responseHeader.params.start) - result.data.responseHeader.params.rows = parseInt(result.data.responseHeader.params.rows) - delete result.data.response.docs - result.data = camelcaseObjectDeep(result.data) - res.status(200).json(result.data) + ); + result.data.responseHeader.params.start = parseInt( + result.data.responseHeader.params.start + ); + result.data.responseHeader.params.rows = parseInt( + result.data.responseHeader.params.rows + ); + delete result.data.response.docs; + result.data = camelcaseObjectDeep(result.data); + res.status(200).json(result.data); } catch (error) { - res.status(400).json({ error: error.message }) + res.status(400).json({ error: error.message }); } } const escapeSolrQuery = (query) => { - if (query == '*') return query + if (query == '*') return query; - const specialChars = /([\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g - const words = query.split(/\s+/) + const specialChars = /([\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g; + const words = query.split(/\s+/); const escapedWords = words.map((word) => { if (specialChars.test(word)) { - return `"${word.replace(specialChars, '\\$1')}"` + return `"${word.replace(specialChars, '\\$1')}"`; } - return word - }) + return word; + }); - return escapedWords.join(' ') -} + return escapedWords.join(' '); +}; /*const productResponseMap = (products, pricelist) => { return products.map((product) => { diff --git a/src/pages/api/shop/variant-detail.js b/src/pages/api/shop/variant-detail.js index fadbe000..08ce75b8 100644 --- a/src/pages/api/shop/variant-detail.js +++ b/src/pages/api/shop/variant-detail.js @@ -8,7 +8,10 @@ export default async function handler(req, res) { `/solr/variants/select?q=id:${req.query.id}&q.op=OR&indent=true` ) let auth = req.query.auth === 'false' ? JSON.parse(req.query.auth) : req.query.auth - let result = variantsMappingSolr('',productVariants.data.response.docs, auth || false) + let productTemplate = await axios( + process.env.SOLR_HOST + `/solr/product/select?q=id:${req.query.id}&q.op=OR&indent=true` + ) + let result = variantsMappingSolr(productTemplate.data.response.docs, productVariants.data.response.docs, auth || false) res.status(200).json(result) } catch (error) { diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 65d953d2..3da381b6 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -1,14 +1,15 @@ import dynamic from 'next/dynamic'; -import MobileView from '@/core/components/views/MobileView'; -import DesktopView from '@/core/components/views/DesktopView'; import { useRef } from 'react'; -import Seo from '@/core/components/Seo'; -import DelayRender from '@/core/components/elements/DelayRender/DelayRender'; + import { HeroBannerSkeleton } from '@/components/skeleton/BannerSkeleton'; import { PopularProductSkeleton } from '@/components/skeleton/PopularProductSkeleton'; -import PromotinProgram from '@/lib/promotinProgram/components/HomePage'; -import PreferredBrandSkeleton from '@/lib/home/components/Skeleton/PreferredBrandSkeleton'; +import Seo from '@/core/components/Seo'; +import DelayRender from '@/core/components/elements/DelayRender/DelayRender'; +import DesktopView from '@/core/components/views/DesktopView'; +import MobileView from '@/core/components/views/MobileView'; import { FlashSaleSkeleton } from '@/lib/flashSale/skeleton/FlashSaleSkeleton'; +import PreferredBrandSkeleton from '@/lib/home/components/Skeleton/PreferredBrandSkeleton'; +import PromotinProgram from '@/lib/promotinProgram/components/HomePage'; import PagePopupIformation from '~/modules/popup-information'; const BasicLayout = dynamic(() => @@ -40,6 +41,11 @@ const FlashSale = dynamic( loading: () => <FlashSaleSkeleton />, } ); + +const ProgramPromotion = dynamic(() => + import('@/lib/home/components/PromotionProgram') +); + const BannerSection = dynamic(() => import('@/lib/home/components/BannerSection') ); @@ -74,8 +80,9 @@ export default function Home() { ]} /> + <PagePopupIformation /> + <DesktopView> - <PagePopupIformation /> <div className='container mx-auto'> <div className='flex min-h-[400px] h-[460px]' @@ -95,9 +102,12 @@ export default function Home() { </div> </div> - <div className='my-16 flex flex-col gap-y-16'> + <div className='my-16 flex flex-col gap-y-8'> <ServiceList /> - <PreferredBrand /> + <div id='flashsale'> + <PreferredBrand /> + </div> + <ProgramPromotion/> <FlashSale /> <PromotinProgram /> <CategoryHomeId /> @@ -108,7 +118,6 @@ export default function Home() { </DesktopView> <MobileView> - <PagePopupIformation /> <DelayRender renderAfter={200}> <HeroBanner /> </DelayRender> @@ -117,7 +126,12 @@ export default function Home() { <ServiceList /> </DelayRender> <DelayRender renderAfter={400}> - <PreferredBrand /> + <div id='flashsale'> + <PreferredBrand /> + </div> + </DelayRender> + <DelayRender renderAfter={400}> + <ProgramPromotion/> </DelayRender> <DelayRender renderAfter={600}> <FlashSale /> diff --git a/src/pages/shop/promo/[slug].tsx b/src/pages/shop/promo/[slug].tsx new file mode 100644 index 00000000..bd69c071 --- /dev/null +++ b/src/pages/shop/promo/[slug].tsx @@ -0,0 +1,523 @@ +import dynamic from 'next/dynamic' +import NextImage from 'next/image'; +import { useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import Seo from '../../../core/components/Seo' +import Promocrumb from '../../../lib/promo/components/Promocrumb' +import { fetchPromoItemsSolr, fetchVariantSolr } from '../../../api/promoApi' +import LogoSpinner from '../../../core/components/elements/Spinner/LogoSpinner.jsx' +import ProductPromoCard from '../../../../src-migrate/modules/product-promo/components/Card' +import { IPromotion } from '../../../../src-migrate/types/promotion' +import React from 'react' +import { SolrResponse } from "../../../../src-migrate/types/solr.ts"; +import DesktopView from '../../../core/components/views/DesktopView'; +import MobileView from '../../../core/components/views/MobileView'; +import 'swiper/swiper-bundle.css'; +import useDevice from '../../../core/hooks/useDevice' +import ProductFilterDesktop from '../../../lib/product/components/ProductFilterDesktopPromotion'; +import ProductFilter from '../../../lib/product/components/ProductFilter'; +import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react'; +import { formatCurrency } from '../../../core/utils/formatValue'; +import Pagination from '../../../core/components/elements/Pagination/Pagination'; +import SideBanner from '../../../../src-migrate/modules/side-banner'; +import whatsappUrl from '../../../core/utils/whatsappUrl'; +import { cons, toQuery } from 'lodash-contrib'; +import _ from 'lodash'; +import useActive from '../../../core/hooks/useActive'; + +const BasicLayout = dynamic(() => import('../../../core/components/layouts/BasicLayout')) + +export default function PromoDetail() { + const router = useRouter() + const { slug = '', brand ='', category='', priceFrom = '', priceTo = '', page = '1' } = router.query + const [promoItems, setPromoItems] = useState<any[]>([]) + const [promoData, setPromoData] = useState<IPromotion[] | null>(null) + const [currentPage, setCurrentPage] = useState(parseInt(page as string, 10) || 1); + const itemsPerPage = 12; // Jumlah item yang ingin ditampilkan per halaman + const [loading, setLoading] = useState(true); + const { isMobile, isDesktop } = useDevice() + const [brands, setBrands] = useState<Brand[]>([]); + const [categories, setCategories] = useState<Category[]>([]); + const [brandValues, setBrandValues] = useState<string[]>([]); + const [categoryValues, setCategoryValues] = useState<string[]>([]); + const [orderBy, setOrderBy] = useState(router.query?.orderBy || 'popular'); + const popup = useActive(); + const prefixUrl = `/shop/promo/${slug}` + + useEffect(() => { + if (router.query.brand) { + let brandsArray: string[] = []; + if (Array.isArray(router.query.brand)) { + brandsArray = router.query.brand; + } else if (typeof router.query.brand === 'string') { + brandsArray = router.query.brand.split(',').map((brand) => brand.trim()); + } + setBrandValues(brandsArray); + } else { + setBrandValues([]); + } + + if (router.query.category) { + let categoriesArray: string[] = []; + + if (Array.isArray(router.query.category)) { + categoriesArray = router.query.category; + } else if (typeof router.query.category === 'string') { + categoriesArray = router.query.category.split(',').map((category) => category.trim()); + } + setCategoryValues(categoriesArray); + } else { + setCategoryValues([]); + } + }, [router.query.brand, router.query.category]); + + interface Brand { + brand: string; + qty: number; + } + + interface Category { + name: string; + qty: number; + } + + useEffect(() => { + const loadPromo = async () => { + setLoading(true); + const brandsData: Brand[] = []; + const categoriesData: Category[] = []; + + const pageNumber = Array.isArray(page) ? parseInt(page[0], 10) : parseInt(page, 10); + setCurrentPage(pageNumber) + + try { + const items = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug}`); + setPromoItems(items); + + if (items.length === 0) { + setPromoData([]) + setLoading(false); + return; + } + + const brandArray = Array.isArray(brand) ? brand : brand.split(','); + const categoryArray = Array.isArray(category) ? category : category.split(','); + + const promoDataPromises = items.map(async (item) => { + + try { + let brandQuery = ''; + if (brand) { + brandQuery = brandArray.map(b => `manufacture_name_s:${b}`).join(' OR '); + brandQuery = `(${brandQuery})`; + } + + let categoryQuery = ''; + if (category) { + categoryQuery = categoryArray.map(c => `category_name:${c}`).join(' OR '); + categoryQuery = `(${categoryQuery})`; + } + + let priceQuery = ''; + if (priceFrom && priceTo) { + priceQuery = `price_f:[${priceFrom} TO ${priceTo}]`; + } else if (priceFrom) { + priceQuery = `price_f:[${priceFrom} TO *]`; + } else if (priceTo) { + priceQuery = `price_f:[* TO ${priceTo}]`; + } + + let combinedQuery = ''; + let combinedQueryPrice = `${priceQuery}`; + if (brand && category && priceFrom || priceTo) { + combinedQuery = `${brandQuery} AND ${categoryQuery} `; + } else if (brand && category) { + combinedQuery = `${brandQuery} AND ${categoryQuery}`; + } else if (brand && priceFrom || priceTo) { + combinedQuery = `${brandQuery}`; + } else if (category && priceFrom || priceTo) { + combinedQuery = `${categoryQuery}`; + } else if (brand) { + combinedQuery = brandQuery; + } else if (category) { + combinedQuery = categoryQuery; + } + + if (combinedQuery && priceFrom || priceTo) { + const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`); + const product = response.response.docs[0]; + const product_id = product.id; + const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} AND ${combinedQueryPrice}`); + return response2; + }else if(combinedQuery){ + const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`); + const product = response.response.docs[0]; + const product_id = product.id; + const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} `); + return response2; + } else { + const response = await fetchPromoItemsSolr(`id:${item.id}`); + return response; + } + } catch (fetchError) { + return []; + } + }); + + const promoDataArray = await Promise.all(promoDataPromises); + const mergedPromoData = promoDataArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []); + setPromoData(mergedPromoData); + + const dataBrandCategoryPromises = promoDataArray.map(async (promoData) => { + if (promoData) { + const dataBrandCategory = promoData.map(async (item) => { + let response; + if(category){ + const categoryQuery = categoryArray.map(c => `category_name:${c}`).join(' OR '); + response = await fetchVariantSolr(`id:${item.products[0].product_id} AND (${categoryQuery})`); + }else{ + response = await fetchVariantSolr(`id:${item.products[0].product_id}`) + } + + + if (response.response?.docs?.length > 0) { + const product = response.response.docs[0]; + const manufactureNameS = product.manufacture_name; + if (Array.isArray(manufactureNameS)) { + for (let i = 0; i < manufactureNameS.length; i += 2) { + const brand = manufactureNameS[i]; + const qty = 1; + const existingBrandIndex = brandsData.findIndex(b => b.brand === brand); + if (existingBrandIndex !== -1) { + brandsData[existingBrandIndex].qty += qty; + } else { + brandsData.push({ brand, qty }); + } + } + } + + const categoryNameS = product.category_name; + if (Array.isArray(categoryNameS)) { + for (let i = 0; i < categoryNameS.length; i += 2) { + const name = categoryNameS[i]; + const qty = 1; + const existingCategoryIndex = categoriesData.findIndex(c => c.name === name); + if (existingCategoryIndex !== -1) { + categoriesData[existingCategoryIndex].qty += qty; + } else { + categoriesData.push({ name, qty }); + } + } + } + } + }); + + return Promise.all(dataBrandCategory); + } + }); + + await Promise.all(dataBrandCategoryPromises); + setBrands(brandsData); + setCategories(categoriesData); + setLoading(false); + + } catch (loadError) { + // console.error("Error loading promo items:", loadError) + setLoading(false); + } + } + + if (slug) { + loadPromo() + } + },[slug, brand, category, priceFrom, priceTo, currentPage]); + + + function capitalizeFirstLetter(string) { + string = string.replace(/_/g, ' '); + return string.replace(/(^\w|\s\w)/g, function(match) { + return match.toUpperCase(); + }); + } + + const handleDeleteFilter = async (source, value) => { + let params = { + q: router.query.q, + orderBy: '', + brand: brandValues.join(','), + category: categoryValues.join(','), + priceFrom: priceFrom || '', + priceTo: priceTo || '', + }; + + let brands = brandValues; + let catagories = categoryValues; + switch (source) { + case 'brands': + brands = brandValues.filter((item) => item !== value); + params.brand = brands.join(','); + await setBrandValues(brands); + break; + case 'category': + catagories = categoryValues.filter((item) => item !== value); + params.category = catagories.join(','); + await setCategoryValues(catagories); + break; + case 'price': + params.priceFrom = ''; + params.priceTo = ''; + break; + case 'delete': + params = { + q: router.query.q, + orderBy: '', + brand: '', + category: '', + priceFrom: '', + priceTo: '', + }; + break; + } + + handleSubmitFilter(params); + }; + const handleSubmitFilter = (params) => { + params = _.pickBy(params, _.identity); + params = toQuery(params); + router.push(`${slug}?${params}`); + }; + + const visiblePromotions = promoData?.slice( (currentPage-1) * itemsPerPage, currentPage * 12) + + const toQuery = (obj) => { + const str = Object.keys(obj) + .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`) + .join('&') + return str + } + + const whatPromo = capitalizeFirstLetter(slug) + const queryWithoutSlug = _.omit(router.query, ['slug']) + const queryString = toQuery(queryWithoutSlug) + + return ( + <BasicLayout> + <Seo + title={`Promo ${Array.isArray(slug) ? slug[0] : slug} Terkini`} + description='B2B Marketplace MRO & Industri dengan Layanan Pembayaran Tempo, Faktur Pajak, Online Quotation, Garansi Resmi & Harga Kompetitif' + /> + <Promocrumb brandName={whatPromo} /> + <MobileView> + <div className='p-4 pt-0'> + <h1 className='mb-2 font-semibold text-h-sm'>Promo {whatPromo}</h1> + + <FilterChoicesComponent + brandValues={brandValues} + categoryValues={categoryValues} + priceFrom={priceFrom} + priceTo={priceTo} + handleDeleteFilter={handleDeleteFilter} + /> + {promoItems.length >= 1 && ( + <div className='flex items-center gap-x-2 mb-5 justify-between'> + <div> + <button + className='btn-light py-2 px-5 h-[40px]' + onClick={popup.activate} + > + Filter + </button> + </div> + </div> + )} + + {loading ? ( + <div className='container flex justify-center my-4'> + <LogoSpinner width={48} height={48} /> + </div> + ) : promoData && promoItems.length >= 1 ? ( + <> + <div className='grid grid-cols-1 gap-x-1 gap-y-1'> + {visiblePromotions?.map((promotion) => ( + <div key={promotion.id} className="min-w-36 max-w-[400px] mb-[20px] sm:w-full md:w-1/2 lg:w-1/3 xl:w-1/4 "> + <ProductPromoCard promotion={promotion}/> + </div> + ))} + </div> + </> + ) : ( + <div className="text-center my-8"> + <p>Belum ada promo pada kategori ini</p> + </div> + )} + + <Pagination + pageCount={Math.ceil((promoData?.length ?? 0) / itemsPerPage)} + currentPage={currentPage} + url={`${prefixUrl}?${toQuery(_.omit(queryWithoutSlug, ['page']))}`} + className='mt-6 mb-2' + /> + <ProductFilter + active={popup.active} + close={popup.deactivate} + brands={brands || []} + categories={categories || []} + prefixUrl={router.asPath.includes('?') ? `${router.asPath}` : `${router.asPath}?`} + defaultBrand={null} + /> + </div> + + </MobileView> + <DesktopView> + <div className='container mx-auto flex mb-3 flex-col'> + <div className='w-full pl-6'> + <h1 className='text-2xl mb-2 font-semibold'>Promo {whatPromo}</h1> + <div className=' w-full h-full flex flex-row items-center '> + + <div className='detail-filter w-1/2 flex justify-start items-center mt-4'> + + <FilterChoicesComponent + brandValues={brandValues} + categoryValues={categoryValues} + priceFrom={priceFrom} + priceTo={priceTo} + handleDeleteFilter={handleDeleteFilter} + /> + </div> + <div className='Filter w-1/2 flex flex-col'> + + <ProductFilterDesktop + brands={brands || []} + categories={categories || []} + prefixUrl={'/shop/promo'} + // defaultBrand={null} + /> + </div> + </div> + {loading ? ( + <div className='container flex justify-center my-4'> + <LogoSpinner width={48} height={48} /> + </div> + ) : promoData && promoItems.length >= 1 ? ( + <> + <div className='grid grid-cols-1 gap-x-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3'> + {visiblePromotions?.map((promotion) => ( + <div key={promotion.id} className="min-w-[400px] max-w-[400px] mb-[20px] sm:min-w-[350px] md:min-w-[380px] lg:min-w-[400px] xl:min-w-[400px] "> + <ProductPromoCard promotion={promotion}/> + </div> + ))} + </div> + </> + ) : ( + <div className="text-center my-8"> + <p>Belum ada promo pada kategori ini</p> + </div> + )} + <div className='flex justify-between items-center mt-6 mb-2'> + <div className='pt-2 pb-6 flex items-center gap-x-3'> + <NextImage + src='/images/logo-question.png' + alt='Logo Question Indoteknik' + width={60} + height={60} + /> + <div className='text-gray_r-12/90'> + <span> + Barang yang anda cari tidak ada?{' '} + <a + href={ + router.query?.q + ? whatsappUrl('productSearch', { + name: router.query.q, + }) + : whatsappUrl() + } + className='text-danger-500' + > + Hubungi Kami + </a> + </span> + </div> + </div> + + + + <Pagination + pageCount={Math.ceil((promoData?.length ?? 0) / itemsPerPage)} + currentPage={currentPage} + url={`${prefixUrl}?${toQuery(_.omit(queryWithoutSlug, ['page']))}`} + className='mt-6 mb-2' + /> + </div> + + </div> + </div> + </DesktopView> + </BasicLayout> + ) + } + +const FilterChoicesComponent = ({ + brandValues, + categoryValues, + priceFrom, + priceTo, + handleDeleteFilter, + }) => ( + <div className='flex items-center mb-4'> + <HStack spacing={2} className='flex-wrap'> + {brandValues?.map((value, index) => ( + <Tag + size='lg' + key={index} + borderRadius='lg' + variant='outline' + colorScheme='gray' + > + <TagLabel>{value}</TagLabel> + <TagCloseButton onClick={() => handleDeleteFilter('brands', value)} /> + </Tag> + ))} + + {categoryValues?.map((value, index) => ( + <Tag + size='lg' + key={index} + borderRadius='lg' + variant='outline' + colorScheme='gray' + > + <TagLabel>{value}</TagLabel> + <TagCloseButton + onClick={() => handleDeleteFilter('category', value)} + /> + </Tag> + ))} + {priceFrom && priceTo && ( + <Tag size='lg' borderRadius='lg' variant='outline' colorScheme='gray'> + <TagLabel> + {formatCurrency(priceFrom) + '-' + formatCurrency(priceTo)} + </TagLabel> + <TagCloseButton + onClick={() => handleDeleteFilter('price', priceFrom)} + /> + </Tag> + )} + {brandValues?.length > 0 || + categoryValues?.length > 0 || + priceFrom || + priceTo ? ( + <span> + <button + className='btn-transparent py-2 px-5 h-[40px] text-red-700' + onClick={() => handleDeleteFilter('delete')} + > + Hapus Semua + </button> + </span> + ) : ( + '' + )} + </HStack> + </div> +); diff --git a/src/pages/shop/promo/index.tsx b/src/pages/shop/promo/index.tsx new file mode 100644 index 00000000..7ec4f6b0 --- /dev/null +++ b/src/pages/shop/promo/index.tsx @@ -0,0 +1,186 @@ +import dynamic from 'next/dynamic' +import { useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import Seo from '../../../core/components/Seo.jsx' +import Promocrumb from '../../../lib/promo/components/Promocrumb.jsx' +import { fetchPromoItemsSolr } from '../../../api/promoApi.js' +import LogoSpinner from '../../../core/components/elements/Spinner/LogoSpinner.jsx' +import ProductPromoCard from '../../../../src-migrate/modules/product-promo/components/Card.tsx' +import { IPromotion } from '../../../../src-migrate/types/promotion.ts' +import React from 'react' +import { SolrResponse } from "../../../../src-migrate/types/solr.ts"; + +const BasicLayout = dynamic(() => import('../../../core/components/layouts/BasicLayout.jsx')) + +export default function Promo() { + const router = useRouter() + const { slug = '' } = router.query + const [promoItems, setPromoItems] = useState<any[]>([]) + const [promoData, setPromoData] = useState<IPromotion[] | null>(null) + const [loading, setLoading] = useState(true) + const [currentPage, setCurrentPage] = useState(1) + const [fetchingData, setFetchingData] = useState(false) + + useEffect(() => { + const loadPromo = async () => { + try { + const items = await fetchPromoItemsSolr(`*:*`) + + + setPromoItems(items) + + + if (items.length === 0) { + setPromoData([]) + setLoading(false); + return; + } + + const promoDataPromises = items.map(async (item) => { + const queryParams = new URLSearchParams({ q: `id:${item.id}` }) + + + try { + const response = await fetch(`/solr/promotion_program_lines/select?${queryParams.toString()}`) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data: SolrResponse<any[]> = await response.json() + + + const promotions = await map(data.response.docs) + return promotions; + } catch (fetchError) { + console.error("Error fetching promotion data:", fetchError) + return []; + } + }); + + const promoDataArray = await Promise.all(promoDataPromises); + const mergedPromoData = promoDataArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []); + setPromoData(mergedPromoData); + setTimeout(() => setLoading(false), 120); // Menambahkan delay 200ms sebelum mengubah status loading + } catch (loadError) { + console.error("Error loading promo items:", loadError) + setLoading(false); + } + } + + if (slug) { + loadPromo() + } + }, [slug]) + + const map = async (promotions: any[]): Promise<IPromotion[]> => { + const result: IPromotion[] = [] + + for (const promotion of promotions) { + const data: IPromotion = { + id: promotion.id, + program_id: promotion.program_id_i, + name: promotion.name_s, + type: { + value: promotion.type_value_s, + label: promotion.type_label_s, + }, + limit: promotion.package_limit_i, + limit_user: promotion.package_limit_user_i, + limit_trx: promotion.package_limit_trx_i, + price: promotion.price_f, + total_qty: promotion.total_qty_i, + products: JSON.parse(promotion.products_s), + free_products: JSON.parse(promotion.free_products_s), + } + + result.push(data) + } + + return result + } + + + + + useEffect(() => { + const handleScroll = () => { + if ( + !fetchingData && + window.innerHeight + document.documentElement.scrollTop >= 0.95 * document.documentElement.offsetHeight + ) { + // User has scrolled to 95% of page height + + setTimeout(() => setFetchingData(true), 120); + setCurrentPage((prevPage) => prevPage + 1) + } + } + + window.addEventListener('scroll', handleScroll) + return () => window.removeEventListener('scroll', handleScroll) + }, [fetchingData]) + + useEffect(() => { + if (fetchingData) { + // Fetch more data + // You may need to adjust this logic according to your API + fetchMoreData() + } + }, [fetchingData]) + + const fetchMoreData = async () => { + try { + // Add a delay of approximately 150ms + setTimeout(async () => { + // Fetch more data + // Update promoData state with the new data + }, 150) + } catch (error) { + console.error('Error fetching more data:', error) + } finally { + setTimeout(() => setFetchingData(false), 120); + + } + } + + const visiblePromotions = promoData?.slice(0, currentPage * 12) + + return ( + <BasicLayout> + <Seo + title={`Promo ${Array.isArray(slug) ? slug[0] : slug} Terkini`} + description='B2B Marketplace MRO & Industri dengan Layanan Pembayaran Tempo, Faktur Pajak, Online Quotation, Garansi Resmi & Harga Kompetitif' + /> + {/* <Promocrumb brandName={capitalizeFirstLetter(Array.isArray(slug) ? slug[0] : slug)} /> */} + <div className='container mx-auto mt-1 flex mb-1'> + <div className=''> + <h1 className='font-semibold'>Semua Promo di Indoteknik</h1> + </div> + </div> + {loading ? ( + <div className='container flex justify-center my-4'> + <LogoSpinner width={48} height={48} /> + </div> + ) : promoData && promoItems.length >= 1 ? ( + <> + <div className='flex flex-wrap justify-center'> + {visiblePromotions?.map((promotion) => ( + <div key={promotion.id} className="min-w-[40px] max-w-[400px] mr-[20px] mb-[20px] sm:w-full md:w-1/2 lg:w-1/3 xl:w-1/4"> + <ProductPromoCard promotion={promotion} /> + </div> + ))} + </div> + {fetchingData && ( + <div className='container flex justify-center my-4'> + <LogoSpinner width={48} height={48} /> + </div> + )} + </> + ) : ( + <div className="text-center my-8"> + <p>Belum ada promo pada kategori ini</p> + </div> + )} + </BasicLayout> + ) +} diff --git a/src/pages/video.jsx b/src/pages/video.jsx index 61790dbb..7d1f8372 100644 --- a/src/pages/video.jsx +++ b/src/pages/video.jsx @@ -44,7 +44,7 @@ export default function Video() { </LazyLoadComponent> <div className='p-3'> <a - href='https://www.youtube.com/@indoteknikb2bindustriale-c778' + href='https://www.youtube.com/@indoteknikcom' className='text-danger-500 mb-2 block' > {video.channelName} diff --git a/src/utils/solrMapping.js b/src/utils/solrMapping.js index 7e887253..dd90ac7d 100644 --- a/src/utils/solrMapping.js +++ b/src/utils/solrMapping.js @@ -36,6 +36,8 @@ export const productMappingSolr = (products, pricelist) => { tag: product?.flashsale_tag_s || 'FLASH SALE', }, qtySold: product?.qty_sold_f || 0, + isTkdn:product?.tkdn_b || false, + isSni:product?.sni_b || false, }; if (product.manufacture_id_i && product.manufacture_name_s) { @@ -98,9 +100,10 @@ export const variantsMappingSolr = (parent, products, pricelist) => { }; } productMapped.parent = { - id: parent.product_id_i || '', - image: parent.image_s || '', - name: parent.name_s || '', + id: parent[0]?.product_id_i || '', + image: parent[0]?.image_s || '', + name: parent[0]?.name_s || '', + description: parent[0]?.description_t || '', }; return productMapped; }); |
