diff options
Diffstat (limited to 'src')
32 files changed, 1620 insertions, 550 deletions
diff --git a/src/api/promoApi.js b/src/api/promoApi.js index 0e82c8b9..3f85db8e 100644 --- a/src/api/promoApi.js +++ b/src/api/promoApi.js @@ -13,11 +13,13 @@ export const fetchPromoItems = async (type) => { } }; -export const fetchPromoItemsSolr = async (type) => { +export const fetchPromoItemsSolr = async (type, start, rows) => { // let query = type ? `type_value_s:${type}` : '*:*'; let sort ='sort=if(exists(sequence_i),0,1) asc, sequence_i asc, if(exists(total_qty_sold_f), total_qty_sold_f, -1) desc'; - let start = 0 - let rows = 100 + // let start = 0 + // let rows = 100 + // let start = 0 + // let rows = 10 try { const queryParams = new URLSearchParams({ q: type }); const response = await fetch(`/solr/promotion_program_lines/select?${queryParams.toString()}&rows=${rows}&start=${start}&${sort}`); diff --git a/src/core/components/elements/Footer/PromoOffer.tsx b/src/core/components/elements/Footer/PromoOffer.tsx new file mode 100644 index 00000000..a5432b6a --- /dev/null +++ b/src/core/components/elements/Footer/PromoOffer.tsx @@ -0,0 +1,112 @@ + +import React from "react"; +// import { useGeneralSetting } from "@/common/state-management/general-setting"; +import { FormEvent, useEffect, useState } from "react"; +import toast from "react-hot-toast"; +import style from "../Footer/style/promoOffer.module.css" +const PromoOffer = ()=>{ + // const { data, isLoading, fetchData } = useGeneralSetting(); + const [formData, setFormData] = useState<FormData>({ + email: "", + name: "", + telephone: "", + message: "", + }); + + useEffect(() => { + // fetchData(); + }, []); + + type FormData = { + email: string; + name: string; + telephone: string; + message: string; + }; + + const [errors, setErrors] = useState({ + email: false, + name: false, + message: false, + }); + + +const handleGetOffer = async (e: FormEvent) => { + e.preventDefault(); + let loadingToast; + try { + loadingToast = toast.loading("Mengirimkan formulir..."); + const response = await fetch("/api/contactus", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ...formData, from: "newsletter" }), + }); + + if (response.ok) { + toast.dismiss(loadingToast); + toast.success("Terima kasih telah menghubungi kami"); + setFormData({ + email: "", + name: "", + telephone: "", + message: "", + }); + } else { + toast.dismiss(loadingToast); + toast.error("Gagal mengirimkan formulir. Silakan coba lagi nanti."); + } + } catch (error) { + toast.dismiss(loadingToast); + console.error("Gagal mengirimkan formulir", error); + toast.error("Terjadi kesalahan. Silakan coba lagi nanti."); + } + }; + + + + + return( + <div className=" py-3 mb-3"> + <div className="md:flex container mx-auto md:justify-between md:items-center md:gap-x-20"> + <div className=""> + <span className="text-black font-semibold text-sm md:text-lg"> + Dapatkan Promo Menarik Setiap Bulan{" "} + </span> + <p> + Promo produk dengan penawaran terbatas setiap bulannya! + </p> + </div> + <div className=" flex-1 flex items-center h-full justify-end text-sm text-slate-950"> + <form onSubmit={handleGetOffer} className={style['form-input']}> + <div className="flex justify-start w-full"> + <div className="flex justify-end"> + <input + type="email" + value={formData.email} + onChange={(e) => + setFormData({ ...formData, email: e.target.value }) + } + className={style['input']} + placeholder="Masukkan email anda disini" + /> + <button + type="submit" + className={style['button']} + > + Dapatkan + </button> + </div> + + + </div> + </form> + + </div> + </div> + </div> + ) + }; + + export default PromoOffer; diff --git a/src/core/components/elements/Footer/style/promoOffer.module.css b/src/core/components/elements/Footer/style/promoOffer.module.css new file mode 100644 index 00000000..3184182d --- /dev/null +++ b/src/core/components/elements/Footer/style/promoOffer.module.css @@ -0,0 +1,39 @@ +.form-input { + @apply + h-full + w-[514px] + text-slate-950 + flex + justify-center; +} + +.input{ + @apply w-[320px] + sm:w-[320px] + md:w-[500px] + xl:w-[514px] + lg:w-[514px] + 2xl:w-[514px] + text-black + py-2 + h-11 + md:py-3 + px-4 + bg-[#FDF1C7] + rounded-3xl + focus:outline-none + ; +} + +.button{ + @apply bg-[#FAD147] + absolute + py-1.5 + rounded-3xl + text-black + md:py-2.5 + px-4 + h-11 + z-0 + ; +}
\ No newline at end of file diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx index 9de761a2..f157297e 100644 --- a/src/core/components/elements/Navbar/NavbarDesktop.jsx +++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx @@ -19,6 +19,7 @@ import DesktopView from '../../views/DesktopView'; import Link from '../Link/Link'; import NavbarUserDropdown from './NavbarUserDropdown'; import NextImage from 'next/image'; +import style from "./style/NavbarDesktop.module.css"; import { Button, Menu, @@ -203,45 +204,65 @@ const NavbarDesktop = () => { </div> </button> <div className='w-6/12 flex px-1 divide-x divide-gray_r-6'> + <Link + href="/shop/promo" + className={`${ + router.asPath === '/shop/promo' && 'bg-gray_r-3' + } flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition group relative`} // Added relative position + target="_blank" + rel="noreferrer" + > + <p className="absolute inset-0 flex justify-center items-center group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200 z-10">Semua Promo</p> + <div className='w-full h-full flex justify-end items-start'> + <Image + src='/images/ICON PROMO DISKON.svg' + alt='promo' + width={100} + height={100} + quality={100} + className={`inline-block z-20`} + /> + </div> + </Link> + + <Link href='/shop/brands' - className={`${ + 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`} + } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition group`} target='_blank' rel='noreferrer' > - Semua Brand + <p className="group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200">Semua Brand</p> </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`} + } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition group`} target='_blank' rel='noreferrer' > - Ready Stock + <p className="group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200">Ready Stock</p> </Link> <Link href='https://blog.indoteknik.com/' - className='p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition' + className='p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition group' target='_blank' rel='noreferrer noopener' > - Blog Indoteknik + <p className="group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200">Blog Indoteknik</p> </Link> - <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='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' > Indoteknik TV - </Link> + </Link> */} </div> <div className='w-3/12 flex gap-x-1 relative'> diff --git a/src/core/components/elements/Navbar/style/NavbarDesktop.module.css b/src/core/components/elements/Navbar/style/NavbarDesktop.module.css new file mode 100644 index 00000000..9cddb127 --- /dev/null +++ b/src/core/components/elements/Navbar/style/NavbarDesktop.module.css @@ -0,0 +1,14 @@ +/* navbarDesktop.module.css */ +@keyframes blink { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0; + } +} + +.blink { + animation: blink 0.8s infinite; +} + diff --git a/src/core/components/elements/Sidebar/Sidebar.jsx b/src/core/components/elements/Sidebar/Sidebar.jsx index 38fcdef8..55838890 100644 --- a/src/core/components/elements/Sidebar/Sidebar.jsx +++ b/src/core/components/elements/Sidebar/Sidebar.jsx @@ -117,6 +117,9 @@ const Sidebar = ({ active, close }) => { </> )} </div> + <SidebarLink className={itemClassName} href='/shop/promo'> + Semua Promo + </SidebarLink> <SidebarLink className={itemClassName} href='/shop/brands'> Semua Brand </SidebarLink> @@ -128,9 +131,9 @@ const Sidebar = ({ active, close }) => { > Blog Indoteknik </SidebarLink> - <SidebarLink className={itemClassName} href='/video'> + {/* <SidebarLink className={itemClassName} href='/video'> Indoteknik TV - </SidebarLink> + </SidebarLink> */} <SidebarLink className={itemClassName} href='/tentang-kami'> Tentang Indoteknik </SidebarLink> diff --git a/src/lib/brand/components/BrandCard.jsx b/src/lib/brand/components/BrandCard.jsx index 731214ff..2d78d956 100644 --- a/src/lib/brand/components/BrandCard.jsx +++ b/src/lib/brand/components/BrandCard.jsx @@ -8,7 +8,7 @@ const BrandCard = ({ brand }) => { return ( <Link href={createSlug('/shop/brands/', brand.name, brand.id)} - className={`py-1 px-2 rounded border border-gray_r-6 flex justify-center items-center ${ + className={`py-1 px-2 border-gray_r-6 flex justify-center items-center hover:scale-110 transition duration-500 ease-in-out ${ isMobile ? 'h-16' : 'h-24' }`} > @@ -16,9 +16,9 @@ const BrandCard = ({ brand }) => { <Image src={brand.logo} alt={brand.name} - width={128} - height={128} - className='h-full w-full object-contain object-center' + width={50} + height={50} + className='h-full w-[122px] object-contain object-center' /> )} {!brand.logo && ( diff --git a/src/lib/category/components/Category.jsx b/src/lib/category/components/Category.jsx index e6ea5acf..c147a3b3 100644 --- a/src/lib/category/components/Category.jsx +++ b/src/lib/category/components/Category.jsx @@ -2,10 +2,14 @@ import odooApi from '@/core/api/odooApi' import Link from '@/core/components/elements/Link/Link' import DesktopView from '@/core/components/views/DesktopView' import { createSlug } from '@/core/utils/slug' +import { ChevronRightIcon } from '@heroicons/react/24/outline' +import Image from 'next/image' import { useEffect, useState } from 'react' const Category = () => { const [categories, setCategories] = useState([]) + const [openCategories, setOpenCategory] = useState([]); + useEffect(() => { const loadCategories = async () => { @@ -31,7 +35,7 @@ const Category = () => { <DesktopView> <div className='category-mega-box'> {categories?.map((category) => ( - <div key={category.id}> + <div key={category.id} className='flex'> <Link href={createSlug('/shop/category/', category.name, category.id)} className='category-mega-box__parent' @@ -39,33 +43,74 @@ const Category = () => { {category.name} </Link> <div className='category-mega-box__child-wrapper'> - <div className='grid grid-cols-3 gap-x-4 gap-y-6 max-h-full overflow-auto'> + <div className='grid grid-cols-3 gap-x-4 gap-y-6 max-h-full !w-[590px] overflow-auto'> {category.childs.map((child1Category) => ( - <div key={child1Category.id}> + <div key={child1Category.id} className='w-full'> <Link href={createSlug('/shop/category/', child1Category.name, child1Category.id)} - className='category-mega-box__child-one mb-4' + className='category-mega-box__child-one mb-4 w-full h-8 flex justify-center line-clamp-2' > {child1Category.name} </Link> - <div className='flex flex-col gap-y-3'> - {child1Category.childs.map((child2Category) => ( - <Link - href={createSlug( - '/shop/category/', - child2Category.name, - child2Category.id - )} - className='category-mega-box__child-two' - key={child2Category.id} - > - {child2Category.name} - </Link> + <div className='flex flex-col gap-y-3 w-full'> + {child1Category.childs.map((child2Category, index) => ( + (index < 4) && ( + <Link + href={createSlug('/shop/category/', child2Category.name, child2Category.id)} + className='category-mega-box__child-two truncate' + key={child2Category.id} + > + {child2Category.name} + </Link> + ) ))} + {child1Category.childs.length > 5 && ( + <div className='flex hover:bg-gray_r-8/35 rounded-10'> + <Link + href={createSlug('/shop/category/', child1Category.name, child1Category.id)} + className='category-mega-box__child-one flex items-center gap-4 font-bold hover:ml-4' + > + <p className='mt-2 mb-0 text-danger-500 font-semibold'>Lihat Semua</p> + <ChevronRightIcon className='w-4 text-danger-500 font-bold' /> + </Link> + </div> + )} </div> </div> ))} </div> + <div className='category-mega-box__child-wrapper !w-[260px] !flex !flex-col !gap-4'> + <div className='flex flex-col'> + <div className='grid grid-cols-2 max-h-full w-full gap-2'> + {category.childs.map((brand, index) => ( + (index < 8 ) && ( + <div key={brand.id} className='w-full flex items-center justify-center pb-2'> + <Link + href={createSlug('/shop/category/', brand.name, brand.id)} + className='category-mega-box__child-one w-fit h-full flex items-center justify-center ' + > + <Image src='https://erp.indoteknik.com/api/image/x_manufactures/x_logo_manufacture/661' alt='' width={104} height={44} objectFit='cover' /> + </Link> + </div> + ) + ))} + </div> + {category.childs.length > 8 && ( + <div className='flex hover:bg-gray_r-8/35 rounded-10'> + <Link + href={createSlug('/shop/category/', category.name, category.id)} + className='category-mega-box__child-one flex items-center gap-4 font-bold hover:ml-4' + > + <p className='mt-2 mb-0 text-danger-500 font-semibold'>Lihat Semua Brand</p> + <ChevronRightIcon className='w-4 text-danger-500 font-bold' /> + </Link> + </div> + )} + </div> + <div className='flex w-60 h-20 object-cover'> + <Image src='https://erp.indoteknik.com/api/image/x_banner.banner/x_banner_image/397' alt='' width={275} height={4} /> + </div> + </div> </div> </div> ))} diff --git a/src/lib/checkout/api/checkoutApi.js b/src/lib/checkout/api/checkoutApi.js index 24f1868a..fd982fff 100644 --- a/src/lib/checkout/api/checkoutApi.js +++ b/src/lib/checkout/api/checkoutApi.js @@ -1,28 +1,20 @@ -import odooApi from '@/core/api/odooApi' -import { getAuth } from '@/core/utils/auth' +import odooApi from '@/core/api/odooApi'; +import { getAuth } from '@/core/utils/auth'; export const checkoutApi = async ({ data }) => { - const auth = getAuth() + const auth = getAuth(); const dataCheckout = await odooApi( 'POST', `/api/v1/partner/${auth.partnerId}/sale_order/checkout`, data - ) - return dataCheckout -} + ); + return dataCheckout; +}; -export const getProductsCheckout = async (voucher, query) => { - const id = getAuth()?.id - let products - if(voucher && query){ - products = await odooApi('GET',`/api/v1/user/${id}/sale_order/checkout?voucher=${voucher}&source=buy`) - }else if (voucher){ - products = await odooApi('GET',`/api/v1/user/${id}/sale_order/checkout?voucher=${voucher}`) - }else if (query) { - products = await odooApi('GET',`/api/v1/user/${id}/sale_order/checkout?source=buy`) - }else{ - products = await odooApi('GET',`/api/v1/user/${id}/sale_order/checkout`) - } - - return products -} +export const getProductsCheckout = async (query) => { + const queryParam = new URLSearchParams(query); + const userId = getAuth()?.id; + const url = `/api/v1/user/${userId}/sale_order/checkout?${queryParam.toString()}`; + const result = await odooApi('GET', url); + return result; +}; diff --git a/src/lib/checkout/api/getVoucher.js b/src/lib/checkout/api/getVoucher.js index 07cf376e..54c8cce5 100644 --- a/src/lib/checkout/api/getVoucher.js +++ b/src/lib/checkout/api/getVoucher.js @@ -1,21 +1,24 @@ -import odooApi from '@/core/api/odooApi' +import odooApi from '@/core/api/odooApi'; -export const getVoucher = async (id, source) => { - let dataVoucher = null - if(source){ - dataVoucher = await odooApi('GET', `/api/v1/user/${id}/voucher?source=${source}`) - }else { - dataVoucher = await odooApi('GET', `/api/v1/user/${id}/voucher`) - } - return dataVoucher -} +export const getVoucher = async (id, query) => { + const queryParam = new URLSearchParams(query); + const url = `/api/v1/user/${id}/voucher?${queryParam.toString()}`; + const dataVoucher = await odooApi('GET', url); + return dataVoucher; +}; export const findVoucher = async (code, id, source) => { - let dataVoucher = null - if(source){ - dataVoucher = await odooApi('GET', `/api/v1/user/${id}/voucher?code=${code}&source=${source}`) - }else{ - dataVoucher = await odooApi('GET', `/api/v1/user/${id}/voucher?code=${code}`) + let dataVoucher = null; + if (source) { + dataVoucher = await odooApi( + 'GET', + `/api/v1/user/${id}/voucher?code=${code}&source=${source}` + ); + } else { + dataVoucher = await odooApi( + 'GET', + `/api/v1/user/${id}/voucher?code=${code}` + ); } - return dataVoucher -} + return dataVoucher; +}; diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 92e14db6..fde6f6de 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -43,9 +43,16 @@ const Checkout = () => { const auth = useAuth(); const [activeVoucher, SetActiveVoucher] = useState(null); - - const { data: cartCheckout } = useQuery('cartCheckout-' + activeVoucher, () => - getProductsCheckout(activeVoucher, query) + const [activeVoucherShipping, setActiveVoucherShipping] = useState(null); + + const { data: cartCheckout } = useQuery( + ['cartCheckout', activeVoucher, activeVoucherShipping], + () => + getProductsCheckout({ + source: query, + voucher: activeVoucher, + voucher_shipping: activeVoucherShipping, + }) ); const [selectedAddress, setSelectedAddress] = useState({ @@ -103,6 +110,7 @@ const Checkout = () => { const [bottomPopupTnC, SetBottomPopupTnC] = useState(null); const [itemTnC, setItemTnC] = useState(null); const [listVouchers, SetListVoucher] = useState(null); + const [listVoucherShippings, SetListVoucherShipping] = useState(null); const [discountVoucher, SetDiscountVoucher] = useState(0); const [codeVoucher, SetCodeVoucher] = useState(null); const [findCodeVoucher, SetFindVoucher] = useState(null); @@ -119,13 +127,23 @@ const Checkout = () => { const voucher = async () => { if (!listVouchers) { try { - let dataVoucher = await getVoucher(auth?.id, query); + setLoadingVoucher(true); + let dataVoucher = await getVoucher(auth?.id, { + source: query, + }); SetListVoucher(dataVoucher); + + let dataVoucherShipping = await getVoucher(auth?.id, { + source: query, + type: 'shipping', + }); + SetListVoucherShipping(dataVoucherShipping); } finally { setLoadingVoucher(false); } } }; + const VoucherCode = async (code) => { let dataVoucher = await findVoucher(code, auth.id, query); if (dataVoucher.length <= 0) { @@ -228,7 +246,6 @@ const Checkout = () => { const hasFlashSale = cartCheckout?.products.some(product => product.hasFlashsale); setHasFlashSale(hasFlashSale); }, [cartCheckout]); - useEffect(() => { setCheckoutValidation(false); @@ -303,7 +320,7 @@ const Checkout = () => { useEffect(() => { const GT = cartCheckout?.grandTotal + - Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000; + Math.round(parseInt(finalShippingAmt * 1.1) / 1000) * 1000; const finalGT = GT < 0 ? 0 : GT; setGrandTotal(finalGT); }, [biayaKirim, cartCheckout?.grandTotal, activeVoucher]); @@ -349,6 +366,7 @@ const Checkout = () => { delivery_service_type: selectedExpedisiService, flash_sale : hasFlashSale, // dibuat negasi untuk ngetest kebalikan nilai false voucher: activeVoucher, + voucher_shipping: activeVoucherShipping, type: 'sale_order', }; @@ -444,6 +462,11 @@ const Checkout = () => { return false; }, [products]); + const voucherShippingAmt = cartCheckout?.discountVoucherShipping || 0; + const discShippingAmt = Math.min(biayaKirim, voucherShippingAmt); + + const finalShippingAmt = biayaKirim - discShippingAmt; + return ( <> <BottomPopup @@ -553,8 +576,145 @@ const Checkout = () => { </div> )} - <hr className='mt-10 my-4 border-gray_r-10' /> - <div className=''> + <hr className='mt-8 mb-4 border-gray_r-8' /> + + {listVoucherShippings && listVoucherShippings?.length > 0 && ( + <div> + <h3 className='font-semibold mb-4'>Promo Gratis Ongkir</h3> + {listVoucherShippings?.map((item) => ( + <div key={item.id} className='relative'> + <div + className={`border border-solid mb-5 w-full hover:cursor-pointer p-2 pl-4 pr-4 `} + > + {item.canApply && ( + <div + class='p-2 mb-4 text-sm text-green-800 rounded-lg bg-green-50 dark:text-green-400' + role='alert' + > + <p> + Potensi potongan sebesar{' '} + <span className='text-green font-bold'> + {currencyFormat(item.discountVoucher)} + </span> + </p> + </div> + )} + {!item.canApply && ( + <div + class='p-2 mb-4 text-sm text-red-800 rounded-lg bg-red-50' + role='alert' + onClick={() => handlingTnC(item)} + > + <p> + Voucher tidak bisa di gunakan,{' '} + <span className='text-red font-bold'> + Baca Selengkapnya ! + </span> + </p> + </div> + )} + + <div className={`flex gap-x-3 relative`}> + {item.canApply === false && ( + <div className='absolute w-full h-full bg-gray_r-3/40 top-0 left-0 z-50' /> + )} + <div className='hidden md:w-[250px] md:block'> + <Image + src={item.image} + alt={item.name} + className={`object-cover`} + /> + </div> + <div className='w-full'> + <div className='flex justify-between gap-x-2 mb-1 items-center'> + <div className=''> + <h3 className='font-semibold'>{item.name}</h3> + <div className='mt-1'> + <span className='text-sm line-clamp-3'> + {item.description}{' '} + </span> + </div> + </div> + <div className='flex justify-end'> + <label class='relative inline-flex items-center cursor-pointer'> + <input + type='checkbox' + value='' + class='sr-only peer' + checked={activeVoucherShipping === item.code} + onChange={() => + setActiveVoucherShipping( + activeVoucherShipping === item.code + ? null + : item.code + ) + } + /> + <div class="w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-focus:ring-4 peer-focus:ring-green-300 dark:peer-focus:ring-green-800 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-green-600"></div> + </label> + </div> + </div> + <hr className='mt-2 my-2 border-gray_r-8' /> + <div className='flex justify-between items-center'> + <p className='text-justify text-sm md:text-xs'> + Kode Voucher :{' '} + <span className='text-red-500 font-semibold'> + {item.code} + </span> + </p> + <p className='text-sm md:text-xs'> + {activeVoucher === item.code && ( + <span className=' text-green-600'> + Voucher digunakan{' '} + </span> + )} + </p> + </div> + <div className='flex items-center mt-3'> + <svg + aria-hidden='true' + fill='none' + stroke='currentColor' + stroke-width='1.5' + viewBox='0 0 24 24' + className='w-5 text-black' + > + <path + d='M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z' + stroke-linecap='round' + stroke-linejoin='round' + ></path> + </svg> + <div className='flex justify-between items-center'> + <div className='text-left ml-3 text-sm '> + Berakhir dalam{' '} + <span className='text-red-600'> + {item.remainingTime} + </span>{' '} + lagi,{' '} + </div> + <div + className='text-sm ml-2 text-red-600' + onClick={() => handlingTnC(item)} + > + Baca S&K + </div> + </div> + </div> + </div> + </div> + <div className='mt-3'> + <p className='text-justify text-sm '></p> + </div> + </div> + </div> + ))} + </div> + )} + + <hr className='mt-8 mb-4 border-gray_r-8' /> + + <div> {!loadingVoucher && listVouchers?.length === 0 ? ( <div className='flex items-center justify-center mt-4 mb-4'> <div className='text-center'> @@ -739,14 +899,7 @@ const Checkout = () => { </div> </div> <div className='mt-3'> - <p className='text-justify text-sm '> - {/* {item.canApply === false - ? 'Tambah ' + - currencyFormat(item.differenceToApply) + - ' untuk pakai promo ini' - : 'Potensi potongan sebesar ' + - currencyFormat(hitungDiscountVoucher(item.code))} */} - </p> + <p className='text-justify text-sm '></p> </div> </div> </div> @@ -912,14 +1065,18 @@ const Checkout = () => { </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 - )} + Biaya Kirim <p className='text-xs mt-1'>{etdFix}</p> </div> + <div>{currencyFormat(biayaKirim)}</div> </div> + {activeVoucherShipping && voucherShippingAmt && ( + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Diskon Kirim</div> + <div className='text-danger-500'> + - {currencyFormat(discShippingAmt)} + </div> + </div> + )} </div> )} @@ -948,7 +1105,7 @@ const Checkout = () => { <div className='mt-4 mb-4'> <button type='button' - onClick={() => { + onClick={async () => { SetBottomPopup(true); voucher(); }} @@ -1204,14 +1361,18 @@ const Checkout = () => { <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 - )} + <p className='text-xs mt-1'>{etdFix}</p> </div> + <div>{currencyFormat(biayaKirim)}</div> </div> + {activeVoucherShipping && voucherShippingAmt && ( + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Diskon Kirim</div> + <div className='text-danger-500'> + - {currencyFormat(discShippingAmt)} + </div> + </div> + )} </div> )} diff --git a/src/lib/home/api/categoryManagementApi.js b/src/lib/home/api/categoryManagementApi.js new file mode 100644 index 00000000..b70d60ce --- /dev/null +++ b/src/lib/home/api/categoryManagementApi.js @@ -0,0 +1,8 @@ +import odooApi from '@/core/api/odooApi' + +const categoryManagementApi = async () => { + const dataCategoryManagement = await odooApi('GET', '/api/v1/categories_management') + return dataCategoryManagement +} + +export default categoryManagementApi diff --git a/src/lib/home/components/CategoryDynamic.jsx b/src/lib/home/components/CategoryDynamic.jsx new file mode 100644 index 00000000..fa1df286 --- /dev/null +++ b/src/lib/home/components/CategoryDynamic.jsx @@ -0,0 +1,84 @@ +import React, { useEffect, useState } from 'react'; +import useCategoryManagement from '../hooks/useCategoryManagement'; +import NextImage from 'next/image'; +import Link from "next/link"; +import router from 'next/router'; +import { createSlug } from '@/core/utils/slug'; + +const CategoryDynamic = () => { + const { categoryManagement } = useCategoryManagement(); + + const calculateLevel3Products = (category) => { + return category.childFrontendIdI.reduce((total, child) => total + (child.numFound || 0), 0); + }; + + const calculateLevel2Products = (category) => { + return category.categories.reduce((total, subCategory) => { + const level3Products = calculateLevel3Products(subCategory); + return total + (subCategory.numFound || 0) + level3Products; + }, 0); + }; + + return ( + <div> + {categoryManagement && categoryManagement.data?.map((category) => { + const countLevel2 = calculateLevel2Products(category); + + return ( + <div key={category.id}> + <div className='bagian-judul flex flex-row justify-start items-center gap-3 mb-4 mt-4'> + <div className='font-semibold sm:text-h-lg mr-2'>{category.name}</div> + <p className='text-gray_r-10 text-sm'>{countLevel2} Produk tersedia</p> + <Link href={createSlug('/shop/category/', category?.name, category?.categoryIdI)} className="!text-red-500 font-semibold">Lihat Semua</Link> + </div> + <div className='grid grid-cols-3 gap-2'> + {category.categories.map((subCategory) => { + const countLevel3 = calculateLevel3Products(subCategory); + + return ( + <div key={subCategory.id} className='border rounded justify-start items-start'> + <div className='p-3'> + <div className='flex flex-row border rounded mb-2 justify-start items-center'> + <NextImage + src={subCategory.image ? subCategory.image : "/images/noimage.jpeg"} + alt={subCategory.name} + width={90} + height={30} + className='object-fit' + /> + <div className='bagian-judul flex flex-col justify-center items-start gap-2 ml-2'> + <div className='font-semibold text-lg mr-2'>{subCategory.name}</div> + <p className='text-gray_r-10 text-sm'>{(subCategory.numFound || 0) + countLevel3} Produk</p> + <Link href={createSlug('/shop/category/', subCategory?.name, subCategory?.idLevel2)} className="!text-red-500 font-semibold">Lihat Semua</Link> + </div> + </div> + <div className='grid grid-cols-2 gap-2 overflow-y-auto max-h-[240px]'> + {subCategory.childFrontendIdI.map((childCategory) => ( + <div key={childCategory.id}> + <Link href={createSlug('/shop/category/', childCategory?.name, childCategory?.idLevel3)} className="flex flex-row gap-2 border rounded group hover:border-red-500"> + <NextImage + src={childCategory.image ? childCategory.image : "/images/noimage.jpeg"} + alt={childCategory.name} + width={40} + height={40} + /> + <div className='bagian-judul flex flex-col justify-center items-center gap-2 break-words line-clamp-2 group-hover:text-red-500'> + <div className='font-semibold line-clamp-2 group-hover:text-red-500 text-sm mr-2'>{childCategory.name}</div> + </div> + </Link> + </div> + ))} + </div> + </div> + </div> + ); + })} + </div> + </div> + ); + })} + </div> + ); +}; + +export default CategoryDynamic; diff --git a/src/lib/home/components/CategoryDynamicMobile.jsx b/src/lib/home/components/CategoryDynamicMobile.jsx new file mode 100644 index 00000000..c1433a2d --- /dev/null +++ b/src/lib/home/components/CategoryDynamicMobile.jsx @@ -0,0 +1,101 @@ +import React, { useEffect, useState } from 'react'; +import useCategoryManagement from '../hooks/useCategoryManagement'; +import NextImage from 'next/image'; +import Link from "next/link"; +import { createSlug } from '@/core/utils/slug'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; + +const CategoryDynamicMobile = () => { + const { categoryManagement } = useCategoryManagement() + const [selectedCategory, setSelectedCategory] = useState({}); + + useEffect(() => { + const loadPromo = async () => { + try { + if (categoryManagement.data?.length > 0) { + const initialSelections = categoryManagement.data.reduce((acc, category) => { + if (category.categories.length > 0) { + acc[category.id] = category.categories[0].idLevel2; + } + return acc; + }, {}); + setSelectedCategory(initialSelections); + } + } catch (loadError) { + // console.error("Error loading promo items:", loadError); + } + }; + + loadPromo(); + }, [categoryManagement.data]); + + const handleCategoryLevel2Click = (categoryIdI, idLevel2) => { + setSelectedCategory(prev => ({ + ...prev, + [categoryIdI]: idLevel2 + })); + }; + + return ( + <div className='p-4'> + {categoryManagement.data && categoryManagement.data.map((category) => ( + <div key={category.id}> + <div className='bagian-judul flex flex-row justify-between items-center gap-3 mb-4 mt-4'> + <div className='font-semibold sm:text-h-sm mr-2'>{category.name}</div> + <Link href={createSlug('/shop/category/', category?.name, category?.categoryIdI)} className="!text-red-500 font-semibold text-sm">Lihat Semua</Link> + </div> + <Swiper slidesPerView={2.3} spaceBetween={10}> + {category.categories.map((index) => ( + <SwiperSlide key={index.id}> + <div + onClick={() => handleCategoryLevel2Click(category.id, index?.idLevel2)} + className={`border flex justify-start items-center max-w-48 max-h-16 rounded ${selectedCategory[category.id] === index?.idLevel2 ? 'bg-red-50 border-red-500 text-red-500' : 'border-gray-200 text-gray-900'}`} + > + <div className='p-1 flex justify-start items-center'> + <div className='flex flex-row justify-center items-center'> + <NextImage + src={index.image ? index.image : "/images/noimage.jpeg"} + alt={index.name} + width={30} + height={30} + className='object-' + /> + <div className='bagian-judul flex flex-col justify-center items-start gap-1 ml-2'> + <div className='font-semibold text-[10px] line-clamp-1'>{index.name}</div> + <p className='text-gray_r-10 text-[10px]'>999 rb+ Produk</p> + </div> + </div> + </div> + </div> + </SwiperSlide> + ))} + </Swiper> + <div className='p-3 mt-2 border'> + <div className='grid grid-cols-2 gap-2 overflow-y-auto max-h-[240px]'> + {category.categories.map((index) => ( + selectedCategory[category.id] === index?.idLevel2 && index.childFrontendIdI.map((x) => ( + <div key={x.id}> + <Link href={createSlug('/shop/category/', x?.name, x?.idLevel3)} className="flex flex-row gap-1 border rounded group hover:border-red-500"> + <NextImage + src={x.image ? x.image : "/images/noimage.jpeg"} + alt={x.name} + width={40} + height={40} + /> + <div className='bagian-judul flex flex-col justify-center items-start gap-1 break-words line-clamp-2 group-hover:text-red-500'> + <div className='font-semibold line-clamp-2 group-hover:text-red-500 text-[10px]'>{x.name}</div> + </div> + </Link> + </div> + )) + ))} + </div> + </div> + </div> + ))} + </div> + ); +}; + +export default CategoryDynamicMobile; diff --git a/src/lib/home/components/CategoryPilihan.jsx b/src/lib/home/components/CategoryPilihan.jsx new file mode 100644 index 00000000..5aa1fbcc --- /dev/null +++ b/src/lib/home/components/CategoryPilihan.jsx @@ -0,0 +1,66 @@ +import Image from 'next/image' +import useCategoryHome from '../hooks/useCategoryHome' +import Link from '@/core/components/elements/Link/Link' +import { createSlug } from '@/core/utils/slug' +import { useEffect, useState } from 'react'; +import { bannerApi } from '../../../api/bannerApi'; +const { useQuery } = require('react-query') +import { HeroBannerSkeleton } from '../../../components/skeleton/BannerSkeleton'; + + +const CategoryPilihan = ({ id, categories }) => { + const heroBanner = useQuery('categoryPilihan', bannerApi({ type: 'banner-category-list' })); + + return ( + <section> + <div className='flex flex-row items-center mb-4'> + <div className='font-semibold sm:text-h-lg mr-2'>Kategori Pilihan</div> + <p className='text-gray_r-10 text-sm'>200 Rb+ Produk Unggulan & 800+ Brand Rekomendasi tersedia!</p> + </div> + <div className='flex w-full h-48 bg-red-300 mb-4'> + {heroBanner.data && + heroBanner.data?.length > 0 && ( + <div key={heroBanner.data.id} className='px-4 sm:px-0 object-fill '> + {/* {heroBanner.data?.map((banner) => ( */} + <Link key={heroBanner.data[0].id} href={heroBanner.data[0].url}> + <Image + width={439} + height={150} + quality={100} + src={heroBanner.data[0].image} + alt={heroBanner.data[0].name} + className='h-48 object-fill w-full' + /> + </Link> + {/* ))} */} + </div> + + )} + </div> + <div className="group/item grid grid-cols-6 gap-y-2 w-full h-full col-span-2 "> + {categories.map((category) => ( + <div key={category.id} className="KartuInti h-48 w-60 max-w-sm lg:max-w-full flex flex-col border-[1px] border-gray-200 relative group"> + <div className='KartuB absolute h-48 w-60 inset-0 flex items-center justify-center '> + <div className="group/edit flex items-center justify-end h-48 w-60 flex-col group-hover/item:visible"> + <div className=' h-36 flex justify-end items-end'> + <Image className='group-hover:scale-105 transition-transform duration-300 ' src={category?.image1920? category?.image1920 : '/images/noimage.jpeg'} width={120} height={120} alt={category?.name} /> + </div> + <h2 className="text-gray-700 content-center h-12 border-t-[1px] px-1 w-60 border-gray-200 font-normal text-sm text-center">{category?.name}</h2> + </div> + </div> + <div className='KartuA relative inset-0 flex h-36 w-60 items-center justify-center opacity-0 group-hover:opacity-75 group-hover:bg-[#E20613] transition-opacity '> + <Link + href={createSlug('/shop/category/', category?.name, category?.id)} + className='category-mega-box__parent text-white rounded-lg' + > + Lihat semua + </Link> + </div> + </div> + ))} + </div> + </section> + ) +} + +export default CategoryPilihan diff --git a/src/lib/home/components/PreferredBrand.jsx b/src/lib/home/components/PreferredBrand.jsx index ec09aa4e..ae12505d 100644 --- a/src/lib/home/components/PreferredBrand.jsx +++ b/src/lib/home/components/PreferredBrand.jsx @@ -1,4 +1,5 @@ import { Swiper, SwiperSlide } from 'swiper/react' +import { Navigation, Pagination, Autoplay } from 'swiper'; import { useCallback, useEffect, useState } from 'react' import usePreferredBrand from '../hooks/usePreferredBrand' import PreferredBrandSkeleton from './Skeleton/PreferredBrandSkeleton' @@ -38,7 +39,23 @@ const PreferredBrand = () => { const { preferredBrands } = usePreferredBrand(query) const { isMobile, isDesktop } = useDevice() - + const swiperBanner = { + modules:[Navigation, Pagination, Autoplay], + autoplay: { + delay: 4000, + disableOnInteraction: false + }, + loop: true, + className: 'h-[70px] md:h-[100px] w-full', + slidesPerView: isMobile ? 4 : 8, + spaceBetween: isMobile ? 12 : 0, + pagination: { + dynamicBullets: true, + dynamicMainBullets: isMobile ? 6 : 8, + clickable: true, + } + } + const preferredBrandsData = manufactures ? manufactures.slice(0, 20) : [] return ( <div className='px-4 sm:px-0'> <div className='flex justify-between items-center mb-4'> @@ -49,18 +66,20 @@ const PreferredBrand = () => { </Link> )} </div> - {manufactures.isLoading && <PreferredBrandSkeleton />} - {!manufactures.isLoading && ( - <Swiper slidesPerView={isMobile ? 3.5 : 7.5} spaceBetween={isMobile ? 12 : 24} freeMode> - {manufactures.map((manufacture) => ( - <SwiperSlide key={manufacture.id}> - <BrandCard brand={manufacture} /> - </SwiperSlide> - ))} - </Swiper> - )} + <div className='border rounded border-gray_r-6'> + {manufactures.isLoading && <PreferredBrandSkeleton />} + {!manufactures.isLoading && ( + <Swiper {...swiperBanner}> + {preferredBrandsData.map((manufacture) => ( + <SwiperSlide key={manufacture.id}> + <BrandCard brand={manufacture} /> + </SwiperSlide> + ))} + </Swiper> + )} + </div> </div> ) } -export default PreferredBrand +export default PreferredBrand
\ No newline at end of file diff --git a/src/lib/home/components/PromotionProgram.jsx b/src/lib/home/components/PromotionProgram.jsx index b204df8e..99258d94 100644 --- a/src/lib/home/components/PromotionProgram.jsx +++ b/src/lib/home/components/PromotionProgram.jsx @@ -13,10 +13,14 @@ const BannerSection = () => { <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> + <Link href='/shop/promo' className='!text-red-500 font-semibold'> + Lihat Semua + </Link> + )} + {isMobile && ( + <Link href='/shop/promo' className='!text-red-500 font-semibold sm:text-h-sm'> + Lihat Semua + </Link> )} </div> {isDesktop && (promotionProgram.data && diff --git a/src/lib/home/hooks/useCategoryManagement.js b/src/lib/home/hooks/useCategoryManagement.js new file mode 100644 index 00000000..c1dda585 --- /dev/null +++ b/src/lib/home/hooks/useCategoryManagement.js @@ -0,0 +1,13 @@ +import categoryManagementApi from '../api/categoryManagementApi' +import { useQuery } from 'react-query' + +const useCategoryManagement = () => { + const fetchCategoryManagement = async () => await categoryManagementApi() + const { isLoading, data } = useQuery('categoryManagementApi', fetchCategoryManagement) + + return { + categoryManagement: { data, isLoading } + } +} + +export default useCategoryManagement
\ No newline at end of file diff --git a/src/lib/product/components/CategorySection.jsx b/src/lib/product/components/CategorySection.jsx new file mode 100644 index 00000000..14a39e7e --- /dev/null +++ b/src/lib/product/components/CategorySection.jsx @@ -0,0 +1,111 @@ +import Image from "next/image"; +import Link from 'next/link'; +import { createSlug } from '@/core/utils/slug'; +import useDevice from '@/core/hooks/useDevice'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; +import { useQuery } from 'react-query'; +import { useRouter } from 'next/router'; +import { + ChevronDownIcon, + ChevronUpIcon, // Import ChevronUpIcon for toggling + DocumentCheckIcon, + HeartIcon, +} from '@heroicons/react/24/outline'; +import { useState } from 'react'; // Import useState + +const CategorySection = ({ categories }) => { + const { isDesktop, isMobile } = useDevice(); + const router = useRouter(); + const [isOpenCategory, setIsOpenCategory] = useState(false); // State to manage category visibility + + let teks = router.query.slug; + let hasil = teks?.match(/(\d+)$/)[0]; + + const breadcrumbs = useQuery( + `category-breadcrumbs/${hasil}`, + async () => await odooApi('GET', `/api/v1/category/${hasil}/category-breadcrumb`) + ); + + const handleToggleCategories = () => { + setIsOpenCategory(!isOpenCategory); + }; + + const displayedCategories = isOpenCategory ? categories : categories.slice(0, 10); + + return ( + <section> + {isDesktop && ( + <div className="group/item grid grid-cols-5 gap-y-2 gap-x-2 w-full h-full col-span-2 "> + {displayedCategories.map((category) => ( + <Link href={createSlug('/shop/category/', category?.name, category?.id)} key={category?.id} passHref> + <div className="group transition-colors duration-300 "> + <div className="KartuInti h-12 w-26 max-w-sm lg:max-w-full flex flex-col border-[2px] border-gray-200 group-hover:border-red-400 rounded relative "> + <div className="flex items-center justify-start h-full px-1 flex-row "> + <Image className="h-full" src={category?.image1920 ? category?.image1920 : '/images/noimage.jpeg'} width={56} height={48} alt={category?.name} /> + <h2 className="text-gray-700 group-hover:text-[#E20613] line-clamp-2 content-center h-fit w-60 px-1 font-semibold text-sm text-start">{category?.name}</h2> + </div> + </div> + </div> + </Link> + ))} + </div> + )} + {isDesktop && categories.length > 10 && ( + <div className="w-full flex justify-center mt-4"> + <button + onClick={handleToggleCategories} + className="flex justify-end mt-4 text-red-500 font-bold px-4 py-2 rounded" + > + {isOpenCategory ? 'Sembunyikan' : 'Lihat semua'} + {isOpenCategory ? ( + <ChevronUpIcon className="ml-auto w-5 font-bold" /> + ) : ( + <ChevronDownIcon className="ml-auto w-5 font-bold" /> + )} + </button> + </div> + )} + + {isMobile && ( + <div className="py-4"> + <Swiper slidesPerView={2.3} spaceBetween={10}> + {displayedCategories.map((category) => ( + <SwiperSlide key={category?.id}> + <Link href={createSlug('/shop/category/', category?.name, category?.id)} passHref> + <div className="group transition-colors duration-300"> + <div className="KartuInti min-h-16 max-h-16 w-26 max-w-sm lg:max-w-full flex flex-col border-[2px] border-gray-200 group-hover:bg-red-200 group-hover:border-red-400 rounded relative"> + <div className="flex items-center justify-center h-full px-1 flex-row"> + <Image + src={category?.image1920 ? category?.image1920 : '/images/noimage.jpeg'} + width={56} + height={48} + alt={category?.name} + /> + <h2 className="text-gray-700 group-hover:text-[#E20613] line-clamp-2 content-center h-fit w-60 px-1 font-semibold text-sm text-start"> + {category?.name} + </h2> + </div> + </div> + </div> + </Link> + </SwiperSlide> + ))} + </Swiper> + {categories.length > 10 && ( + <div className="w-full flex justify-end mt-4"> + <button + onClick={handleToggleCategories} + className="flex justify-end mt-4 bg-red-500 text-white text-sm px-4 py-2 rounded" + > + {isOpenCategory ? 'Sembunyikan Semua' : 'Lihat Semua'} + </button> + </div> + )} + </div> + )} + </section> + ) +} + +export default CategorySection diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx index 98732407..3b5fab28 100644 --- a/src/lib/product/components/ProductCard.jsx +++ b/src/lib/product/components/ProductCard.jsx @@ -14,7 +14,15 @@ import useUtmSource from '~/hooks/useUtmSource'; const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { const router = useRouter(); const utmSource = useUtmSource(); + const [discount, setDiscount] = useState(0); + let voucherPastiHemat = 0; + + if (product?.voucherPastiHemat?.length > 0) { + const stringVoucher = product?.voucherPastiHemat[0]; + const validJsonString = stringVoucher.replace(/'/g, '"'); + voucherPastiHemat = JSON.parse(validJsonString); + } const callForPriceWhatsapp = whatsappUrl('product', { name: product.name, @@ -38,42 +46,59 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { ), }; + const hitungDiscountVoucher = () => { + let countDiscount = 0; + if (voucherPastiHemat.discount_type === 'percentage') { + countDiscount = product?.lowestPrice.priceDiscount * (voucherPastiHemat.discount_amount / 100); + if (voucherPastiHemat.max_discount > 0 && countDiscount > voucherPastiHemat.max_discount) { + countDiscount = voucherPastiHemat.max_discount; + } + } else { + countDiscount = voucherPastiHemat.discount_amount; + } + + setDiscount(countDiscount); + }; + + useEffect(() => { + hitungDiscountVoucher(); + }, []); + if (variant == 'vertical') { return ( - <div className='rounded shadow-sm border border-gray_r-4 bg-white h-[300px] md:h-[350px]'> + <div className='rounded shadow-sm border border-gray_r-4 bg-white h-[310px] md:h-[380px]'> <Link href={URL.product} className='border-b border-gray_r-4 relative'> - <div className="relative"> - <Image - src={image} - alt={product?.name} - 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 className='relative'> + <Image + src={image} + alt={product?.name} + 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> - </div> - {router.pathname != '/' && product?.flashSale?.id > 0 && ( <div className='absolute bottom-0 w-full grid'> @@ -178,6 +203,13 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { )} </div> )} + {discount > 0 && ( + <div className='flex gap-x-1 mb-1 text-sm'> + <div className='inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20'> + Hemat : {currencyFormat(discount)} + </div> + </div> + )} <div className='flex w-full items-center gap-x-1 '> {product?.stockTotal > 0 && ( @@ -200,37 +232,37 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { <div className='flex bg-white'> <div className='w-4/12'> <Link href={URL.product} className='relative'> - <div className="relative"> - <Image - src={image} - alt={product?.name} - className="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 className='relative'> + <Image + src={image} + alt={product?.name} + 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> - </div> {product.variantTotal > 1 && ( <div className='absolute badge-gray bottom-1.5 left-1.5'> {product.variantTotal} Varian diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index b1a5d409..34018ffe 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -26,6 +26,8 @@ import ProductSearchSkeleton from './Skeleton/ProductSearchSkeleton'; import SideBanner from '~/modules/side-banner'; import FooterBanner from '~/modules/footer-banner'; +import CategorySection from './CategorySection'; +import { getIdFromSlug } from '@/core/utils/slug' const ProductSearch = ({ query, @@ -68,6 +70,9 @@ const ProductSearch = ({ const productStart = productSearch.data?.responseHeader.params.start; const productRows = limit; const productFound = productSearch.data?.response.numFound; + const [dataCategories, setDataCategories] = useState([]) + + const categoryId = getIdFromSlug(prefixUrl) useEffect(() => { if (productFound == 0 && query.q && !spellings) { @@ -115,6 +120,16 @@ const ProductSearch = ({ checkIfBrand(); } }, [q]); + + useEffect(() => { + const loadCategories = async () => { + const getCategories = await odooApi('GET', `/api/v1/category/child?parent_id=${categoryId}`) + if(getCategories){ + setDataCategories(getCategories) + } + } + loadCategories() + }, []) const brands = []; for ( @@ -323,6 +338,7 @@ const ProductSearch = ({ SpellingComponent )} </div> + <CategorySection categories={dataCategories}/> {productFound > 0 && ( <div className='flex items-center gap-x-2 mb-5 justify-between'> @@ -411,7 +427,9 @@ const ProductSearch = ({ <SideBanner /> </div> + <div className='w-9/12 pl-6'> + <CategorySection categories={dataCategories}/> {bannerPromotionHeader && bannerPromotionHeader?.image && ( <div className='mb-3'> <Image diff --git a/src/lib/quotation/components/Quotation.jsx b/src/lib/quotation/components/Quotation.jsx index 8855c6c4..df234dc2 100644 --- a/src/lib/quotation/components/Quotation.jsx +++ b/src/lib/quotation/components/Quotation.jsx @@ -67,17 +67,19 @@ const Quotation = () => { const [selectedExpedisiService, setselectedExpedisiService] = useState(null); const [etd, setEtd] = useState(null); const [etdFix, setEtdFix] = useState(null); - + const [isApproval, setIsApproval] = useState(false); - + const expedisiValidation = useRef(null); - + const [selectedAddress, setSelectedAddress] = useState({ shipping: null, invoicing: null, }); - + const [addresses, setAddresses] = useState(null); + + const [note_websiteText, setselectedNote_websiteText] = useState(''); useEffect(() => { if (!auth) return; @@ -262,6 +264,12 @@ const Quotation = () => { } if (!products || products.length == 0) return; + + if (isApproval && note_websiteText == '') { + toast.error('Maaf, Note wajib dimasukkan.'); + return; + } + setIsLoading(true); const productOrder = products.map((product) => ({ product_id: product.id, @@ -276,16 +284,18 @@ const Quotation = () => { carrier_id: selectedCarrierId, estimated_arrival_days: splitDuration(etd), delivery_service_type: selectedExpedisiService, + note_website : note_websiteText, }; - console.log('data checkout', data); + const isSuccess = await checkoutApi({ data }); - console.log('isSuccess', isSuccess); + ; setIsLoading(false); if (isSuccess?.id) { for (const product of products) deleteItemCart({ productId: product.id }); router.push(`/shop/quotation/finish?id=${isSuccess.id}`); return; } + toast.error('Gagal melakukan transaksi, terjadi kesalahan internal'); }; @@ -442,8 +452,25 @@ const Quotation = () => { </Link>{' '} yang berlaku </p> + <hr className='my-4 border-gray_r-6' /> + + <div className='flex gap-x-2 justify-start mb-4'> + <div className=''>Note</div> + {isApproval && ( + <div className='text-caption-1 text-red-500 items-center flex'>*harus diisi</div> + )} + </div> + <div className='text-caption-2 text-gray_r-11'> + <textarea + rows="4" + cols="50" + className={`w-full p-1 rounded border border-gray_r-6`} + onChange={(e) => setselectedNote_websiteText(e.target.value)} + /> + </div> </div> - + + <Divider /> <div className='flex gap-x-3 p-4'> @@ -576,6 +603,27 @@ const Quotation = () => { yang berlaku </p> + <div> + <hr className='my-4 border-gray_r-6' /> + + <div className='flex gap-x-1 flex-col mb-4'> + <div className='flex flex-row gap-x-1'> + <div className=''>Note</div> + {isApproval && ( + <div className='text-caption-1 text-red-500 items-center flex'>*harus diisi</div> + )} + </div> + <div className='text-caption-2 text-gray_r-11'> + <textarea + rows="4" + cols="50" + className={`w-full p-1 rounded border border-gray_r-6`} + onChange={(e) => setselectedNote_websiteText(e.target.value)} + /> + </div> + </div> + </div> + <hr className='my-4 border-gray_r-6' /> <button diff --git a/src/lib/transaction/api/rejectProductApi.js b/src/lib/transaction/api/rejectProductApi.js new file mode 100644 index 00000000..e03c7975 --- /dev/null +++ b/src/lib/transaction/api/rejectProductApi.js @@ -0,0 +1,9 @@ +import odooApi from '@/core/api/odooApi' + +const rejectProductApi = async ({ idSo, idProduct, reason }) => { + const dataCheckout = await odooApi('POST', `/api/v1/sale_order/${idSo}/reject/${idProduct}`, { + reason_reject: reason + }); + return dataCheckout +} +export default rejectProductApi
\ No newline at end of file diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx index c6152ca9..9bef895a 100644 --- a/src/lib/transaction/components/Transaction.jsx +++ b/src/lib/transaction/components/Transaction.jsx @@ -1,4 +1,6 @@ import Spinner from '@/core/components/elements/Spinner/Spinner'; +import NextImage from 'next/image'; +import rejectImage from "../../../../public/images/reject.png" import useTransaction from '../hooks/useTransaction'; import TransactionStatusBadge from './TransactionStatusBadge'; import Divider from '@/core/components/elements/Divider/Divider'; @@ -34,8 +36,14 @@ import useAuth from '@/core/hooks/useAuth'; import StepApproval from './stepper'; import aprpoveApi from '../api/approveApi'; import rejectApi from '../api/rejectApi'; +import rejectProductApi from '../api/rejectProductApi'; +import { useRouter } from 'next/router'; const Transaction = ({ id }) => { + const router = useRouter() + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedProduct, setSelectedProduct] = useState(null); + const [reason, setReason] = useState(''); const auth = useAuth(); const { transaction } = useTransaction({ id }); @@ -141,6 +149,15 @@ const Transaction = ({ id }) => { [transaction.data] ); + const memoizeVariantGroupCardReject = useMemo( + () => ( + <div className='p-4 pt-0 flex flex-col gap-y-3'> + <VariantGroupCard variants={transaction.data?.productsRejectLine} buyMore /> + </div> + ), + [transaction.data] + ); + if (transaction.isLoading) { return ( <div className='flex justify-center my-6'> @@ -153,6 +170,38 @@ const Transaction = ({ id }) => { setIdAWB(null); }; + const openModal = (product) => { + setSelectedProduct(product); + setIsModalOpen(true); + }; + + const closeModal = () => { + setIsModalOpen(false); + setSelectedProduct(null); + setReason(''); + }; + + const handleRejectProduct = async () => { + try{ + if (!reason.trim()) { + toast.error('Masukkan alasan terlebih dahulu'); + return; + }else{ + let idSo = transaction?.data.id + let idProduct = selectedProduct?.id + await rejectProductApi({ idSo, idProduct, reason}); + closeModal(); + toast.success("Produk berhasil di reject") + setTimeout(() => { + window.location.reload(); + }, 1500); + } + }catch(error){ + toast.error('Gagal reject produk. Silakan coba lagi.'); + } + }; + + return ( transaction.data?.name && ( <> @@ -301,6 +350,37 @@ const Transaction = ({ id }) => { <Divider /> + <div className='p-4'> + <p className='font-medium'>Invoice</p> + <div className='flex flex-col gap-y-3 mt-4'> + {transaction.data?.invoices?.map((invoice, index) => ( + <Link href={`/my/invoices/${invoice.id}`} key={index}> + <div className='shadow rounded-md p-4 text-gray_r-12 font-normal flex justify-between'> + <div> + <p className='mb-2'>{invoice?.name}</p> + <div className='flex items-center gap-x-1'> + {invoice.amountResidual > 0 ? ( + <div className='badge-red'>Belum Lunas</div> + ) : ( + <div className='badge-green'>Lunas</div> + )} + <p className='text-caption-2 text-gray_r-11'> + {currencyFormat(invoice.amountTotal)} + </p> + </div> + </div> + <ChevronRightIcon className='w-5 stroke-2' /> + </div> + </Link> + ))} + {transaction.data?.invoices?.length === 0 && ( + <div className='badge-red text-sm px-2'>Belum ada invoice</div> + )} + </div> + </div> + + <Divider /> + {!auth?.feature.soApproval && ( <div className='p-4 flex flex-col gap-y-4'> <DescriptionRow label='Purchase Order'> @@ -326,8 +406,20 @@ const Transaction = ({ id }) => { <Divider /> <div className='font-medium p-4'>Detail Produk</div> + {transaction?.data?.products.length > 0? ( + <div> + {memoizeVariantGroupCard} + </div> + ) : ( + <div className='badge-red text-sm px-2 ml-4'>Semua produk telah di reject</div> + )} - {memoizeVariantGroupCard} + {transaction?.data?.productsRejectLine.length > 0 && ( + <div> + <div className='font-medium p-4'>Detail Produk Reject</div> + {memoizeVariantGroupCardReject} + </div> + )} <Divider /> @@ -335,37 +427,6 @@ const Transaction = ({ id }) => { <Divider /> - <div className='p-4'> - <p className='font-medium'>Invoice</p> - <div className='flex flex-col gap-y-3 mt-4'> - {transaction.data?.invoices?.map((invoice, index) => ( - <Link href={`/my/invoices/${invoice.id}`} key={index}> - <div className='shadow rounded-md p-4 text-gray_r-12 font-normal flex justify-between'> - <div> - <p className='mb-2'>{invoice?.name}</p> - <div className='flex items-center gap-x-1'> - {invoice.amountResidual > 0 ? ( - <div className='badge-red'>Belum Lunas</div> - ) : ( - <div className='badge-green'>Lunas</div> - )} - <p className='text-caption-2 text-gray_r-11'> - {currencyFormat(invoice.amountTotal)} - </p> - </div> - </div> - <ChevronRightIcon className='w-5 stroke-2' /> - </div> - </Link> - ))} - {transaction.data?.invoices?.length === 0 && ( - <div className='badge-red text-sm px-2'>Belum ada invoice</div> - )} - </div> - </div> - - <Divider /> - <div className='p-4 pt-0'> {transaction.data?.status == 'draft' && auth?.feature.soApproval && ( @@ -562,67 +623,100 @@ const Transaction = ({ id }) => { /> </div> </div> - - <div className='text-h-sm font-semibold mt-10 mb-4'> - Pengiriman - </div> - <div className='grid grid-cols-3 gap-1'> - {transaction?.data?.pickings?.map((airway) => ( - <button - className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left' - key={airway?.id} - onClick={() => setIdAWB(airway?.id)} - > - <div> - <span className='text-sm text-gray_r-11'> - No Resi : {airway?.trackingNumber || '-'}{' '} - </span> - <p className='mt-1 font-medium'>{airway?.name}</p> - </div> - <div className='flex gap-x-2'> - <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1'> - {airway?.delivered ? 'Pesanan Tiba' : 'Sedang Dikirim'} - </div> - <ChevronRightIcon className='w-5 stroke-2' /> + <div className='flex '> + <div className='w-1/2'> + <div className='text-h-sm font-semibold mt-10 mb-4'> + Pengiriman + </div> + {transaction?.data?.pickings.length == 0 && ( + <div className='badge-red text-sm'>Belum ada pengiriman</div> + )} + <div className='grid grid-cols-1 gap-1 w-2/3'> + {transaction?.data?.pickings?.map((airway) => ( + <button + className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left h-20' + key={airway?.id} + onClick={() => setIdAWB(airway?.id)} + > + <div> + <span className='text-sm text-gray_r-11'> + No Resi : {airway?.trackingNumber || '-'}{' '} + </span> + <p className='mt-1 font-medium'>{airway?.name}</p> + </div> + <div className='flex gap-x-2'> + <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1'> + {airway?.delivered ? 'Pesanan Tiba' : 'Sedang Dikirim'} + </div> + <ChevronRightIcon className='w-5 stroke-2' /> + </div> + </button> + ))} + </div> + </div> + <div className='invoice w-1/2 '> + <div className='text-h-sm font-semibold mt-10 mb-4 '>Invoice</div> + {transaction.data?.invoices?.length === 0 && ( + <div className='badge-red text-sm'>Belum ada invoice</div> + )} + <div className='grid grid-cols-1 gap-1 w-2/3 '> + {transaction.data?.invoices?.map((invoice, index) => ( + <Link href={`/my/invoices/${invoice.id}`} key={index}> + <div className='shadow rounded-md p-4 text-gray_r-12 font-normal flex justify-between'> + <div> + <p className='mb-1'>{invoice?.name}</p> + <div className='flex items-center gap-x-1'> + {invoice.amountResidual > 0 ? ( + <div className='badge-red'>Belum Lunas</div> + ) : ( + <div className='badge-green'>Lunas</div> + )} + <p className='text-caption-2 text-gray_r-11'> + {currencyFormat(invoice.amountTotal)} + </p> + </div> + </div> + <ChevronRightIcon className='w-5 stroke-2' /> + </div> + </Link> + ))} </div> - </button> - ))} + </div> </div> - {transaction?.data?.pickings.length == 0 && ( - <div className='badge-red text-sm'>Belum ada pengiriman</div> - )} - <div className='text-h-sm font-semibold mt-10 mb-4'> + <div className='text-h-sm font-semibold mt-4 mb-4'> Rincian Pembelian </div> - <table className='table-data'> - <thead> - <tr> - <th>Nama Produk</th> - {/* <th>Diskon</th> */} - <th>Jumlah</th> - <th>Harga</th> - <th>Subtotal</th> - </tr> - </thead> - <tbody> - {transaction?.data?.products?.map((product) => ( - <tr key={product.id}> - <td className='flex'> - <Link - href={createSlug( - '/shop/product/', - product?.parent.name, - product?.parent.id - )} - className='w-[20%] flex-shrink-0' - > + {transaction?.data?.products?.length > 0? ( + <table className='table-data'> + <thead> + <tr> + <th>Nama Produk</th> + {/* <th>Diskon</th> */} + <th>Jumlah</th> + <th>Harga</th> + <th>Subtotal</th> + <th></th> + </tr> + </thead> + <tbody> + {transaction?.data?.products?.map((product) => ( + <tr key={product.id}> + <td className='flex'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='w-[20%] flex-shrink-0' + > <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' - /> + <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 && ( @@ -648,47 +742,92 @@ const Transaction = ({ id }) => { </div> </div> </div> - </Link> - <div className='px-2 text-left'> - <Link - href={createSlug( - '/shop/product/', - product?.parent.name, - product?.parent.id - )} - className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' - > - {product?.parent?.name} </Link> - <div className='text-gray_r-11 mt-2'> - {product?.code}{' '} - {product?.attributes.length > 0 - ? `| ${product?.attributes.join(', ')}` - : ''} - </div> - </div> - </td> - {/* <td> - {product.price.discountPercentage > 0 - ? `${product.price.discountPercentage}%` - : ''} - </td> */} - <td>{product.quantity}</td> - <td> - {/* {product.price.discountPercentage > 0 && ( - <div className='line-through mb-1 text-caption-1 text-gray_r-12/70'> - {currencyFormat(product.price.price)} + <div className='px-2 text-left'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + {product?.parent?.name} + </Link> + <div className='text-gray_r-11 mt-2'> + {product?.code}{' '} + {product?.attributes.length > 0 + ? `| ${product?.attributes.join(', ')}` + : ''} + </div> </div> - )} */} - <div>{currencyFormat(product.price.priceDiscount)}</div> - </td> - <td>{currencyFormat(product.price.subtotal)}</td> - </tr> - ))} - </tbody> - </table> - - <div className='flex justify-end mt-4'> + </td> + {/* <td> + {product.price.discountPercentage > 0 + ? `${product.price.discountPercentage}%` + : ''} + </td> */} + <td>{product.quantity}</td> + <td> + {/* {product.price.discountPercentage > 0 && ( + <div className='line-through mb-1 text-caption-1 text-gray_r-12/70'> + {currencyFormat(product.price.price)} + </div> + )} */} + <div>{currencyFormat(product.price.priceDiscount)}</div> + </td> + <td>{currencyFormat(product.price.subtotal)}</td> + {/* {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (transaction.data.isReaject == false) && ( */} + {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (router.asPath.includes("/my/quotations/")) && transaction.data?.status == 'draft' && ( + <td> + <button + className="bg-red-500 text-white py-1 px-3 rounded" + onClick={() => openModal(product)} + > + Reject + </button> + </td> + )} + </tr> + ))} + </tbody> + </table> + ) : ( + <div className='badge-red text-sm'>Semua produk telah di reject</div> + )} + + {isModalOpen && ( + <div className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center'> + <div className='bg-white p-4 rounded w-96 + ease-in-out opacity-100 + transform transition-transform duration-300 scale-100'> + <h2 className='text-lg mb-2'>Berikan Alasan</h2> + <textarea + value={reason} + onChange={(e) => setReason(e.target.value)} + className='w-full p-2 border rounded' + rows='4' + ></textarea> + <div className='mt-4 flex justify-end'> + <button + className='bg-gray-300 text-black py-1 px-3 rounded mr-2' + onClick={closeModal} + > + Batal + </button> + <button + className='bg-red-500 text-white py-1 px-3 rounded' + onClick={handleRejectProduct} + > + Reject + </button> + </div> + </div> + </div> + )} + + {transaction?.data?.products?.map((product) => ( + <div className='flex justify-end mt-4' key={product.id}> <div className='w-1/4 grid grid-cols-2 gap-y-3 text-gray_r-12/80'> <div className='text-right'>Subtotal</div> <div className='text-right font-medium'> @@ -713,33 +852,91 @@ const Transaction = ({ id }) => { </div> </div> </div> + ))} - <div className='text-h-sm font-semibold mt-10 mb-4'>Invoice</div> - <div className='grid grid-cols-3 gap-4'> - {transaction.data?.invoices?.map((invoice, index) => ( - <Link href={`/my/invoices/${invoice.id}`} key={index}> - <div className='shadow rounded-md p-4 text-gray_r-12 font-normal flex justify-between'> - <div> - <p className='mb-2'>{invoice?.name}</p> - <div className='flex items-center gap-x-1'> - {invoice.amountResidual > 0 ? ( - <div className='badge-red'>Belum Lunas</div> - ) : ( - <div className='badge-green'>Lunas</div> - )} - <p className='text-caption-2 text-gray_r-11'> - {currencyFormat(invoice.amountTotal)} - </p> - </div> - </div> - <ChevronRightIcon className='w-5 stroke-2' /> - </div> - </Link> - ))} - </div> - {transaction.data?.invoices?.length === 0 && ( - <div className='badge-red text-sm'>Belum ada invoice</div> + + + {transaction?.data?.productsRejectLine.length > 0 && ( + <div className='text-h-sm font-semibold mt-10 mb-4'> + Rincian Produk Reject + </div> + )} + {transaction?.data?.productsRejectLine.length > 0 && ( + <table className='table-data'> + <thead> + <tr> + <th>Nama Produk</th> + {/* <th>Diskon</th> */} + <th>Jumlah</th> + <th>Harga</th> + <th>Subtotal</th> + </tr> + </thead> + <tbody> + {transaction?.data?.productsRejectLine?.map((product) => ( + <tr key={product.id}> + <td className='flex'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='w-[20%] flex-shrink-0' + > + <Image + src={product?.parent?.image} + alt={product?.name} + className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md' + /> + </Link> + <div className='px-2 text-left'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + {product?.parent?.name} + </Link> + <div className='text-gray_r-11 mt-2'> + {product?.code}{' '} + {product?.attributes.length > 0 + ? `| ${product?.attributes.join(', ')}` + : ''} + </div> + </div> + </td> + {/* <td> + {product.price.discountPercentage > 0 + ? `${product.price.discountPercentage}%` + : ''} + </td> */} + <td>{product.quantity}</td> + <td> + {/* {product.price.discountPercentage > 0 && ( + <div className='line-through mb-1 text-caption-1 text-gray_r-12/70'> + {currencyFormat(product.price.price)} + </div> + )} */} + <div>{currencyFormat(product.price.priceDiscount)}</div> + </td> + <td className='flex justify-center'> + <NextImage + src={rejectImage} + alt='Reject' + width={90} + height={30} + /> + </td> + </tr> + ))} + </tbody> + </table> )} + </div> </div> </DesktopView> diff --git a/src/lib/variant/components/VariantCard.jsx b/src/lib/variant/components/VariantCard.jsx index 9f65fc3c..68cdf54f 100644 --- a/src/lib/variant/components/VariantCard.jsx +++ b/src/lib/variant/components/VariantCard.jsx @@ -1,18 +1,26 @@ import { useRouter } from 'next/router' import { toast } from 'react-hot-toast' - +import useAuth from '@/core/hooks/useAuth'; import Image from '@/core/components/elements/Image/Image' import Link from '@/core/components/elements/Link/Link' import { createSlug } from '@/core/utils/slug' import currencyFormat from '@/core/utils/currencyFormat' import { updateItemCart } from '@/core/utils/cart' import whatsappUrl from '@/core/utils/whatsappUrl' +import {useState } from 'react'; +import rejectProductApi from '../../../lib/transaction/api/rejectProductApi' +// import {useTransaction} from 'C:\Users\Indoteknik\next-indoteknik\src\lib\transaction\hooks\useTransaction.js' +import useTransaction from '../../../lib/transaction/hooks/useTransaction'; import ImageNext from 'next/image'; -import { useMemo, useEffect, useState } from 'react'; const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { const router = useRouter() - + const id = router.query.id + const auth = useAuth(); + const { transaction } = useTransaction({id}); + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedProduct, setSelectedProduct] = useState(null); + const [reason, setReason] = useState(''); @@ -24,11 +32,42 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { }) return } - + const checkoutItem = () => { router.push(`/shop/checkout?product_id=${product.id}&qty=${product.quantity}`) } - + + const openModal = (product) => { + setSelectedProduct(product); + setIsModalOpen(true); + }; + + const closeModal = () => { + setIsModalOpen(false); + setSelectedProduct(null); + setReason(''); + }; + + const handleRejectProduct = async () => { + try { + if (!reason.trim()) { + toast.error('Masukkan alasan terlebih dahulu'); + return; + }else{ + let idSo = transaction?.data.id + let idProduct = selectedProduct.id + await rejectProductApi({ idSo, idProduct, reason}); + closeModal(); + toast.success("Produk berhasil di reject") + setTimeout(() => { + window.location.reload(); + }, 1500); + } + } catch (error) { + toast.error('Gagal reject produk. Silakan coba lagi.'); + } + }; + const Card = () => ( <div className='flex gap-x-3'> <div className='w-4/12 flex items-center gap-x-2'> @@ -115,7 +154,7 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id)}> <Card /> </Link> - {buyMore && ( + {buyMore && (!transaction?.data?.productsRejectLine.some(pr => pr.id === product.id)) && ( <div className='flex justify-end gap-x-2 mb-2'> <button type='button' @@ -124,14 +163,48 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { > Tambah Keranjang </button> - <button - type='button' - onClick={checkoutItem} - className='btn-solid-red py-2 px-3 text-caption-1' - > - Beli Lagi - </button> + {/* {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (transaction.data.isReaject == false) && ( */} + {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (router.asPath.includes("/my/quotations/")) && ( + !transaction?.data?.productsRejectLine.some(pr => pr.id === product.id) && ( + <button + className="bg-red-500 text-white py-1 px-3 rounded" + onClick={() => openModal(product)} + > + Reject + </button> + ) + )} + {isModalOpen && ( + <div className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center'> + <div className='bg-white p-4 rounded w-96 + ease-in-out opacity-100 + transform transition-transform duration-300 scale-100'> + <h2 className='text-lg mb-2'>Berikan Alasan</h2> + <textarea + value={reason} + onChange={(e) => setReason(e.target.value)} + className='w-full p-2 border rounded' + rows='4' + ></textarea> + <div className='mt-4 flex justify-end'> + <button + className='bg-gray-300 text-black py-1 px-3 rounded mr-2' + onClick={closeModal} + > + Batal + </button> + <button + className='bg-red-500 text-white py-1 px-3 rounded' + onClick={handleRejectProduct} + > + Reject + </button> + </div> + </div> + </div> + )} </div> + )} </> ) diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 8af963fb..30a7ac1f 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -1,6 +1,5 @@ import dynamic from 'next/dynamic'; -import { useRef } from 'react'; - +import { useEffect, useRef, useState } from 'react'; import { HeroBannerSkeleton } from '@/components/skeleton/BannerSkeleton'; import { PopularProductSkeleton } from '@/components/skeleton/PopularProductSkeleton'; import Seo from '@/core/components/Seo'; @@ -11,8 +10,10 @@ 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'; -import useProductDetail from '~/modules/product-detail/stores/useProductDetail'; +import CategoryPilihan from '../lib/home/components/CategoryPilihan'; +import odooApi from '@/core/api/odooApi'; import { getAuth } from '~/libs/auth'; +// import { getAuth } from '~/libs/auth'; const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout') @@ -54,12 +55,23 @@ const BannerSection = dynamic(() => const CategoryHomeId = dynamic(() => import('@/lib/home/components/CategoryHomeId') ); + +const CategoryDynamic = dynamic(() => + import('@/lib/home/components/CategoryDynamic') +); + +const CategoryDynamicMobile = dynamic(() => + import('@/lib/home/components/CategoryDynamicMobile') +); + const CustomerReviews = dynamic(() => import('@/lib/review/components/CustomerReviews') ); const ServiceList = dynamic(() => import('@/lib/home/components/ServiceList')); -export default function Home() { + + +export default function Home({categoryId}) { const bannerRef = useRef(null); const wrapperRef = useRef(null); @@ -70,6 +82,18 @@ export default function Home() { bannerRef.current?.querySelector(':first-child')?.clientHeight + 'px'; }; + useEffect(() => { + const loadCategories = async () => { + const getCategories = await odooApi('GET', '/api/v1/category/child?partner_id='+{categoryId}) + if(getCategories){ + setDataCategories(getCategories) + } + } + loadCategories() + }, []) + + const [dataCategories, setDataCategories] = useState([]) + return ( <BasicLayout> <Seo @@ -107,6 +131,7 @@ export default function Home() { </div> <div className='my-16 flex flex-col gap-y-8'> + <div className='my-16 flex flex-col gap-y-8'> <ServiceList /> <div id='flashsale'> <PreferredBrand /> @@ -117,10 +142,13 @@ export default function Home() { </> )} <PromotinProgram /> + <CategoryPilihan categories={dataCategories}/> + <CategoryDynamic/> <CategoryHomeId /> <BannerSection /> <CustomerReviews /> </div> + </div> </div> </DesktopView> @@ -150,6 +178,10 @@ export default function Home() { <DelayRender renderAfter={600}> <PromotinProgram /> </DelayRender> + <DelayRender renderAfter={600}> + <CategoryPilihan categories={dataCategories}/> + <CategoryDynamicMobile/> + </DelayRender> <DelayRender renderAfter={800}> <PopularProduct /> </DelayRender> @@ -164,4 +196,4 @@ export default function Home() { </MobileView> </BasicLayout> ); -} +}
\ No newline at end of file diff --git a/src/pages/shop/category/[slug].jsx b/src/pages/shop/category/[slug].jsx index 1afe30bf..11840d47 100644 --- a/src/pages/shop/category/[slug].jsx +++ b/src/pages/shop/category/[slug].jsx @@ -5,6 +5,8 @@ import { useRouter } from 'next/router'; import Seo from '@/core/components/Seo'; import { getIdFromSlug, getNameFromSlug } from '@/core/utils/slug'; import Breadcrumb from '@/lib/category/components/Breadcrumb'; +import { useEffect, useState } from 'react'; +import odooApi from '@/core/api/odooApi'; const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout') @@ -12,10 +14,14 @@ const BasicLayout = dynamic(() => const ProductSearch = dynamic(() => import('@/lib/product/components/ProductSearch') ); +const CategorySection = dynamic(() => + import('@/lib/product/components/CategorySection') +) export default function CategoryDetail() { const router = useRouter(); const { slug = '', page = 1 } = router.query; + const [dataCategories, setDataCategories] = useState([]) const categoryName = getNameFromSlug(slug); const categoryId = getIdFromSlug(slug); @@ -43,8 +49,9 @@ export default function CategoryDetail() { <Breadcrumb categoryId={categoryId} /> + {!_.isEmpty(router.query) && ( - <ProductSearch query={query} prefixUrl={`/shop/category/${slug}`} /> + <ProductSearch query={query} categories ={categoryId} prefixUrl={`/shop/category/${slug}`} /> )} </BasicLayout> ); diff --git a/src/pages/shop/promo/[slug].tsx b/src/pages/shop/promo/[slug].tsx index bd69c071..aaee1249 100644 --- a/src/pages/shop/promo/[slug].tsx +++ b/src/pages/shop/promo/[slug].tsx @@ -91,7 +91,7 @@ export default function PromoDetail() { setCurrentPage(pageNumber) try { - const items = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug}`); + const items = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug}`,0,100); setPromoItems(items); if (items.length === 0) { @@ -147,16 +147,16 @@ export default function PromoDetail() { 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}`); + const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} AND ${combinedQueryPrice}`,0,100); 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} `); + const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} `,0,100); return response2; } else { - const response = await fetchPromoItemsSolr(`id:${item.id}`); + const response = await fetchPromoItemsSolr(`id:${item.id}`,0,100); return response; } } catch (fetchError) { diff --git a/src/pages/shop/promo/index.jsx b/src/pages/shop/promo/index.jsx new file mode 100644 index 00000000..01a11aad --- /dev/null +++ b/src/pages/shop/promo/index.jsx @@ -0,0 +1,40 @@ +import Seo from '@/core/components/Seo' +import BasicLayout from '@/core/components/layouts/BasicLayout'; +import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'; +import Link from 'next/link'; +import Promo from '~/pages/shop/promo'; + +import React from 'react'; + +const PromoPage = () => { + return ( + <BasicLayout> + <Seo title='Promo Indoteknik.com' /> + <div className='container mx-auto py-4 md:py-6 pb-0'> + <Breadcrumb> + <BreadcrumbItem> + <BreadcrumbLink + as={Link} + href='/' + className='!text-danger-500 whitespace-nowrap' + > + Home + </BreadcrumbLink> + </BreadcrumbItem> + + <BreadcrumbItem isCurrentPage> + <BreadcrumbLink className='whitespace-nowrap'> + Promo + </BreadcrumbLink> + </BreadcrumbItem> + </Breadcrumb> + + <div className='h-10' /> + + <Promo /> + </div> + </BasicLayout> + ); +}; + +export default PromoPage; diff --git a/src/pages/shop/promo/index.tsx b/src/pages/shop/promo/index.tsx deleted file mode 100644 index 7ec4f6b0..00000000 --- a/src/pages/shop/promo/index.tsx +++ /dev/null @@ -1,186 +0,0 @@ -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/styles/globals.css b/src/styles/globals.css index f6561b00..505dcab4 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -583,12 +583,11 @@ button { @apply absolute left-[100%] top-12 - w-[40vw] bg-gray_r-1/90 backdrop-blur-md border border-gray_r-6 - p-6 + p-6 opacity-0 h-full transition-all @@ -604,6 +603,7 @@ button { transition-colors ease-linear duration-100 + w-fit font-semibold; } @@ -613,6 +613,7 @@ button { transition-colors ease-linear duration-100 + w-full font-normal; } diff --git a/src/utils/solrMapping.js b/src/utils/solrMapping.js index dd90ac7d..d4694eb2 100644 --- a/src/utils/solrMapping.js +++ b/src/utils/solrMapping.js @@ -38,6 +38,7 @@ export const productMappingSolr = (products, pricelist) => { qtySold: product?.qty_sold_f || 0, isTkdn:product?.tkdn_b || false, isSni:product?.sni_b || false, + voucherPastiHemat:product?.voucher_pastihemat || [] }; if (product.manufacture_id_i && product.manufacture_name_s) { |
