summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/auth/components/LoginDesktop.jsx5
-rw-r--r--src/lib/auth/components/LoginMobile.jsx4
-rw-r--r--src/lib/auth/hooks/useLogin.js2
-rw-r--r--src/lib/brand/components/BrandCard.jsx4
-rw-r--r--src/lib/cart/components/Cartheader.jsx181
-rw-r--r--src/lib/checkout/components/Checkout.jsx1000
-rw-r--r--src/lib/checkout/components/CheckoutOld.jsx2
-rw-r--r--src/lib/checkout/components/CheckoutSection.jsx257
-rw-r--r--src/lib/checkout/email/FinishCheckoutEmail.jsx371
-rw-r--r--src/lib/flashSale/components/FlashSale.jsx62
-rw-r--r--src/lib/form/components/KunjunganSales.jsx13
-rw-r--r--src/lib/form/components/KunjunganService.jsx12
-rw-r--r--src/lib/form/components/Merchant.jsx11
-rw-r--r--src/lib/form/components/PembayaranTempo.jsx14
-rw-r--r--src/lib/form/components/RequestForQuotation.jsx12
-rw-r--r--src/lib/form/components/SuratDukungan.jsx11
-rw-r--r--src/lib/home/api/categoryHomeApi.js15
-rw-r--r--src/lib/home/components/PreferredBrand.jsx38
-rw-r--r--src/lib/home/components/PromotionProgram.jsx66
-rw-r--r--src/lib/product/components/Product/ProductDesktop.jsx764
-rw-r--r--src/lib/product/components/Product/ProductDesktopVariant.jsx324
-rw-r--r--src/lib/product/components/Product/ProductMobile.jsx409
-rw-r--r--src/lib/product/components/Product/ProductMobileVariant.jsx240
-rw-r--r--src/lib/product/components/ProductCard.jsx197
-rw-r--r--src/lib/product/components/ProductFilterDesktop.jsx10
-rw-r--r--src/lib/product/components/ProductFilterDesktopPromotion.jsx132
-rw-r--r--src/lib/product/components/ProductSearch.jsx405
-rw-r--r--src/lib/promo/components/Promocrumb.jsx40
-rw-r--r--src/lib/quotation/components/Quotation.jsx514
-rw-r--r--src/lib/transaction/api/approveApi.js13
-rw-r--r--src/lib/transaction/api/listSiteApi.js10
-rw-r--r--src/lib/transaction/api/rejectApi.js13
-rw-r--r--src/lib/transaction/components/Transaction.jsx552
-rw-r--r--src/lib/transaction/components/Transactions.jsx328
-rw-r--r--src/lib/transaction/components/stepper.jsx83
-rw-r--r--src/lib/variant/components/VariantCard.jsx35
36 files changed, 3960 insertions, 2189 deletions
diff --git a/src/lib/auth/components/LoginDesktop.jsx b/src/lib/auth/components/LoginDesktop.jsx
index 1333db14..9a68dc53 100644
--- a/src/lib/auth/components/LoginDesktop.jsx
+++ b/src/lib/auth/components/LoginDesktop.jsx
@@ -8,6 +8,7 @@ import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner';
+import Image from 'next/image';
const LoginDesktop = () => {
const {
@@ -108,7 +109,7 @@ const LoginDesktop = () => {
{!isLoading ? 'Masuk' : 'Loading...'}
</button>
</form>
- {/* <div className='flex items-center mt-3 mb-3'>
+ <div className='flex items-center mt-3 mb-3'>
<hr className='flex-1' />
<p className='text-gray-400'>ATAU</p>
<hr className='flex-1' />
@@ -127,7 +128,7 @@ const LoginDesktop = () => {
height={10}
/>
<p>Masuk dengan Google</p>
- </button> */}
+ </button>
<div className='text-gray_r-11 mt-10'>
Belum punya akun Indoteknik?{' '}
diff --git a/src/lib/auth/components/LoginMobile.jsx b/src/lib/auth/components/LoginMobile.jsx
index 40924fbe..d2bf704f 100644
--- a/src/lib/auth/components/LoginMobile.jsx
+++ b/src/lib/auth/components/LoginMobile.jsx
@@ -117,7 +117,7 @@ const LoginMobile = () => {
{!isLoading ? 'Masuk' : 'Loading...'}
</button>
</form>
- {/* <div className='flex items-center mt-3 mb-3'>
+ <div className='flex items-center mt-3 mb-3'>
<hr className='flex-1' />
<p className='text-gray-400'>ATAU</p>
<hr className='flex-1' />
@@ -136,7 +136,7 @@ const LoginMobile = () => {
height={10}
/>
<p>Masuk dengan Google</p>
- </button> */}
+ </button>
<div className='text-gray_r-11 mt-4'>
Belum punya akun Indoteknik?{' '}
diff --git a/src/lib/auth/hooks/useLogin.js b/src/lib/auth/hooks/useLogin.js
index dc9580ea..dd5a4b03 100644
--- a/src/lib/auth/hooks/useLogin.js
+++ b/src/lib/auth/hooks/useLogin.js
@@ -74,7 +74,7 @@ const useLogin = () => {
if (data.isAuth) {
session.odooUser = data.user;
setCookie('auth', JSON.stringify(session?.odooUser));
- router.push(decodeURIComponent(router?.query?.next) ?? '/');
+ router.push(router?.query?.next || '/');
return;
}
};
diff --git a/src/lib/brand/components/BrandCard.jsx b/src/lib/brand/components/BrandCard.jsx
index bb1a17f7..731214ff 100644
--- a/src/lib/brand/components/BrandCard.jsx
+++ b/src/lib/brand/components/BrandCard.jsx
@@ -1,4 +1,4 @@
-import Image from '@/core/components/elements/Image/Image'
+import Image from '~/components/ui/image'
import Link from '@/core/components/elements/Link/Link'
import useDevice from '@/core/hooks/useDevice'
import { createSlug } from '@/core/utils/slug'
@@ -16,6 +16,8 @@ const BrandCard = ({ brand }) => {
<Image
src={brand.logo}
alt={brand.name}
+ width={128}
+ height={128}
className='h-full w-full object-contain object-center'
/>
)}
diff --git a/src/lib/cart/components/Cartheader.jsx b/src/lib/cart/components/Cartheader.jsx
index 580dfc8c..19f79bc9 100644
--- a/src/lib/cart/components/Cartheader.jsx
+++ b/src/lib/cart/components/Cartheader.jsx
@@ -1,14 +1,9 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { getCartApi } from '../api/CartApi'
-import currencyFormat from '@/core/utils/currencyFormat'
-import Image from '@/core/components/elements/Image/Image'
-import { createSlug } from '@/core/utils/slug'
import useAuth from '@/core/hooks/useAuth'
import { useRouter } from 'next/router'
import odooApi from '@/core/api/odooApi'
import { useProductCartContext } from '@/contexts/ProductCartContext'
-import whatsappUrl from '@/core/utils/whatsappUrl'
-import { AnimatePresence, motion } from 'framer-motion'
const { ShoppingCartIcon, PhotoIcon } = require('@heroicons/react/24/outline')
const { default: Link } = require('next/link')
@@ -114,182 +109,6 @@ const Cardheader = (cartCount) => {
</span>
</Link>
</div>
-
- <AnimatePresence>
- {isHovered && (
- <>
- <motion.div
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- exit={{ opacity: 0 }}
- transition={{ duration: 0.15 }}
- className='fixed top-[155px] left-0 w-full h-full bg-black/50 z-10'
- />
-
- <motion.div
- initial={{ opacity: 0 }}
- animate={{ opacity: 1, transition: { duration: 0.2 } }}
- exit={{ opacity: 0, transition: { duration: 0.3 } }}
- className='absolute z-10 left-0 w-96'
- onMouseEnter={handleMouseEnter}
- onMouseLeave={handleMouseLeave}
- >
- <motion.div
- initial={{ height: 0 }}
- animate={{ height: 'auto' }}
- exit={{ height: 0 }}
- className='w-full max-w-md p-2 bg-white border border-gray-200 rounded-lg shadow overflow-hidden'
- >
- <div className='p-2 flex justify-between items-center'>
- <h5 className='text-base font-semibold leading-none'>Keranjang Belanja</h5>
- <Link href='/shop/cart' class='text-sm font-medium text-red-600 underline'>
- Lihat Semua
- </Link>
- </div>
- <hr className='mt-3 mb-3 border border-gray-100' />
- <div className='flow-root max-h-[250px] overflow-y-auto'>
- {!auth && (
- <div className='justify-center p-4'>
- <p className='text-gray-500 text-center '>
- Silahkan{' '}
- <Link href='/login' className='text-red-600 underline leading-6'>
- Login
- </Link>{' '}
- Untuk Melihat Daftar Keranjang Belanja Anda
- </p>
- </div>
- )}
- {isLoading &&
- itemLoading.map((item) => (
- <div key={item} role='status' className='max-w-sm animate-pulse'>
- <div className='flex items-center space-x-4 mb- 2'>
- <div className='flex-shrink-0'>
- <PhotoIcon className='h-16 w-16 text-gray-500' />
- </div>
- <div className='flex-1 min-w-0'>
- <div className='h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4'></div>
- <div className='h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px] mb-2.5'></div>
- <div className='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div>
- </div>
- </div>
- </div>
- ))}
- {auth && products.length === 0 && !isLoading && (
- <div className='justify-center p-4'>
- <p className='text-gray-500 text-center '>
- Tidak Ada Produk di Keranjang Belanja Anda
- </p>
- </div>
- )}
- {auth && products.length > 0 && !isLoading && (
- <>
- <ul role='list' className='divide-y divide-gray-200 dark:divide-gray-700'>
- {products &&
- products?.map((product, index) => (
- <>
- <li className='py-1 sm:py-2'>
- <div className='flex items-center space-x-4'>
- <div className='flex-shrink-0'>
- <Link
- href={createSlug(
- '/shop/product/',
- product?.parent.name,
- product?.parent.id
- )}
- className='line-clamp-2 leading-6 !text-gray_r-12 font-normal'
- >
- <Image
- src={product?.parent?.image}
- alt={product?.name}
- className='object-contain object-center border border-gray_r-6 h-16 w-16 rounded-md'
- />
- </Link>
- </div>
- <div className='flex-1 min-w-0'>
- <Link
- href={createSlug(
- '/shop/product/',
- product?.parent.name,
- product?.parent.id
- )}
- className='line-clamp-2 leading-6 !text-gray_r-12 font-normal'
- >
- {' '}
- <p className='text-caption-2 font-medium text-gray-900 truncate dark:text-white'>
- {product.parent.name}
- </p>
- </Link>
-
- {product?.hasFlashsale && (
- <div className='flex gap-x-1 items-center mb-2 mt-1'>
- <div className='badge-solid-red'>
- {product?.price?.discountPercentage}%
- </div>
- <div className='text-gray_r-11 line-through text-caption-2'>
- {currencyFormat(product?.price?.price)}
- </div>
- </div>
- )}
- <div className='flex justify-between items-center'>
- <div className='font-semibold text-sm text-red-600'>
- {product?.price?.priceDiscount > 0 ? (
- currencyFormat(product?.price?.priceDiscount)
- ) : (
- <span className='text-gray_r-12/90 font-normal text-caption-1'>
- <a
- href={whatsappUrl('product', {
- name: product.name,
- manufacture: product.manufacture?.name,
- url: createSlug(
- '/shop/product/',
- product.name,
- product.id,
- true
- )
- })}
- className='text-danger-500 underline'
- rel='noopener noreferrer'
- target='_blank'
- >
- Call For Price
- </a>
- </span>
- )}
- </div>
- </div>
- </div>
- </div>
- </li>
- </>
- ))}
- </ul>
- <hr />
- </>
- )}
- </div>
- {auth && products.length > 0 && !isLoading && (
- <>
- <div className='mt-3'>
- <span className='text-gray-400 text-caption-2'>Subtotal Sebelum PPN : </span>
- <span className='font-semibold text-red-600'>{currencyFormat(subTotal)}</span>
- </div>
- <div className='mt-5 mb-2'>
- <button
- type='button'
- className='btn-solid-red rounded-lg w-full'
- onClick={handleCheckout}
- disabled={buttonLoading}
- >
- {buttonLoading ? 'Loading...' : 'Lanjutkan Ke Pembayaran'}
- </button>
- </div>
- </>
- )}
- </motion.div>
- </motion.div>
- </>
- )}
- </AnimatePresence>
</div>
)
}
diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx
index 9a799010..4aafdece 100644
--- a/src/lib/checkout/components/Checkout.jsx
+++ b/src/lib/checkout/components/Checkout.jsx
@@ -1,191 +1,201 @@
-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 { getItemAddress } from '@/core/utils/address'
-import addressesApi from '@/lib/address/api/addressesApi'
+import { Skeleton, Spinner } from '@chakra-ui/react';
import {
BanknotesIcon,
ChevronLeftIcon,
ClockIcon,
- ExclamationCircleIcon
-} from '@heroicons/react/24/outline'
-import React, { useEffect, useRef, useState } from 'react'
-import _ from 'lodash'
-import { deleteItemCart, getCartApi } from '@/core/utils/cart'
-import currencyFormat from '@/core/utils/currencyFormat'
-import { toast } from 'react-hot-toast'
-import getFileBase64 from '@/core/utils/getFileBase64'
-import { useRouter } from 'next/router'
-import VariantGroupCard from '@/lib/variant/components/VariantGroupCard'
-import axios from 'axios'
-import Image from '@/core/components/elements/Image/Image'
-import MobileView from '@/core/components/views/MobileView'
-import DesktopView from '@/core/components/views/DesktopView'
-import ExpedisiList from '../api/ExpedisiList'
-import whatsappUrl from '@/core/utils/whatsappUrl'
-import BottomPopup from '@/core/components/elements/Popup/BottomPopup'
-import { useQuery } from 'react-query'
-import { gtagPurchase } from '@/core/utils/googleTag'
-import { findVoucher, getVoucher } from '../api/getVoucher'
-import CardProdcuctsList from '@/core/components/elements/Product/cartProductsList'
-import { Spinner } from '@chakra-ui/react'
-import { AnimatePresence, motion } from 'framer-motion'
-
-const SELF_PICKUP_ID = 32
-
-const { checkoutApi } = require('../api/checkoutApi')
-const { getProductsCheckout } = require('../api/checkoutApi')
+ ExclamationCircleIcon,
+} from '@heroicons/react/24/outline';
+import axios from 'axios';
+import { AnimatePresence, motion } from 'framer-motion';
+import { useRouter } from 'next/router';
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import { toast } from 'react-hot-toast';
+import { useQuery } from 'react-query';
+import snakecaseKeys from 'snakecase-keys';
+
+import Alert from '@/core/components/elements/Alert/Alert';
+import Divider from '@/core/components/elements/Divider/Divider';
+import Image from '@/core/components/elements/Image/Image';
+import Link from '@/core/components/elements/Link/Link';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import DesktopView from '@/core/components/views/DesktopView';
+import MobileView from '@/core/components/views/MobileView';
+import useAuth from '@/core/hooks/useAuth';
+import { getItemAddress } from '@/core/utils/address';
+import { deleteItemCart } from '@/core/utils/cart';
+import currencyFormat from '@/core/utils/currencyFormat';
+import getFileBase64 from '@/core/utils/getFileBase64';
+import { gtagPurchase } from '@/core/utils/googleTag';
+import whatsappUrl from '@/core/utils/whatsappUrl';
+import addressesApi from '@/lib/address/api/addressesApi';
+import CartItem from '~/modules/cart/components/Item.tsx';
+import ExpedisiList from '../api/ExpedisiList';
+import { findVoucher, getVoucher } from '../api/getVoucher';
+
+const SELF_PICKUP_ID = 32;
+
+const { checkoutApi } = require('../api/checkoutApi');
+const { getProductsCheckout } = require('../api/checkoutApi');
const Checkout = () => {
- const router = useRouter()
- const query = router.query.source ?? null
- const auth = useAuth()
+ const router = useRouter();
+ const query = router.query.source ?? null;
+ const auth = useAuth();
- const [activeVoucher, SetActiveVoucher] = useState(null)
+ const [activeVoucher, SetActiveVoucher] = useState(null);
const { data: cartCheckout } = useQuery('cartCheckout-' + activeVoucher, () =>
getProductsCheckout(activeVoucher, query)
- )
+ );
const [selectedAddress, setSelectedAddress] = useState({
shipping: null,
- invoicing: null
- })
- const [addresses, setAddresses] = useState(null)
+ invoicing: null,
+ });
+ const [addresses, setAddresses] = useState(null);
useEffect(() => {
- if (!auth) return
+ if (!auth) return;
const getAddresses = async () => {
- const dataAddresses = await addressesApi()
- setAddresses(dataAddresses)
- }
+ const dataAddresses = await addressesApi();
+ setAddresses(dataAddresses);
+ };
- getAddresses()
- }, [auth])
+ getAddresses();
+ }, [auth]);
useEffect(() => {
- if (!addresses) return
+ if (!addresses) return;
const matchAddress = (key) => {
- const addressToMatch = getItemAddress(key)
- const foundAddress = addresses.filter((address) => address.id == addressToMatch)
+ const addressToMatch = getItemAddress(key);
+ const foundAddress = addresses.filter(
+ (address) => address.id == addressToMatch
+ );
if (foundAddress.length > 0) {
- return foundAddress[0]
+ return foundAddress[0];
}
- return addresses[0]
- }
+ return addresses[0];
+ };
setSelectedAddress({
shipping: matchAddress('shipping'),
- invoicing: matchAddress('invoicing')
- })
- }, [addresses])
-
- const [products, setProducts] = useState(null)
- const [totalWeight, setTotalWeight] = useState(0)
- const [priceCheck, setPriceCheck] = useState(false)
- const [listExpedisi, setExpedisi] = useState([])
- const [listserviceExpedisi, setListServiceExpedisi] = useState([])
- const [selectedExpedisi, setSelectedExpedisi] = useState(0)
- const [selectedCarrierId, setselectedCarrierId] = useState(0)
- const [selectedCarrier, setselectedCarrier] = useState(0)
- const [biayaKirim, setBiayaKirim] = useState(0)
- const [checkWeigth, setCheckWeight] = useState(false)
- const [selectedServiceType, setSelectedServiceType] = useState(null)
- const [selectedExpedisiService, setselectedExpedisiService] = useState(null)
- const [etd, setEtd] = useState(null)
- const [etdFix, setEtdFix] = useState(null)
- const [bottomPopup, SetBottomPopup] = useState(null)
- const [bottomPopupTnC, SetBottomPopupTnC] = useState(null)
- const [itemTnC, setItemTnC] = useState(null)
- const [listVouchers, SetListVoucher] = useState(null)
- const [discountVoucher, SetDiscountVoucher] = useState(0)
- const [codeVoucher, SetCodeVoucher] = useState(null)
- const [findCodeVoucher, SetFindVoucher] = useState(null)
- const [selisihHargaCode, SetSelisihHargaCode] = useState(null)
- const [buttonTerapkan, SetButtonTerapkan] = useState(false)
- const [checkoutValidation, setCheckoutValidation] = useState(false)
- const [loadingVoucher, setLoadingVoucher] = useState(true)
- const [loadingRajaOngkir, setLoadingRajaOngkir] = useState(false)
-
- const expedisiValidation = useRef(null)
+ invoicing: matchAddress('invoicing'),
+ });
+ }, [addresses]);
+
+ const [products, setProducts] = useState(null);
+ const [totalWeight, setTotalWeight] = useState(0);
+ const [priceCheck, setPriceCheck] = useState(false);
+ const [listExpedisi, setExpedisi] = useState([]);
+ const [listserviceExpedisi, setListServiceExpedisi] = useState([]);
+ const [selectedExpedisi, setSelectedExpedisi] = useState(0);
+ const [selectedCarrierId, setselectedCarrierId] = useState(0);
+ const [selectedCarrier, setselectedCarrier] = useState(0);
+ const [biayaKirim, setBiayaKirim] = useState(0);
+ const [checkWeigth, setCheckWeight] = useState(false);
+ const [selectedServiceType, setSelectedServiceType] = useState(null);
+ const [selectedExpedisiService, setselectedExpedisiService] = useState(null);
+ const [etd, setEtd] = useState(null);
+ const [etdFix, setEtdFix] = useState(null);
+ const [bottomPopup, SetBottomPopup] = useState(null);
+ const [bottomPopupTnC, SetBottomPopupTnC] = useState(null);
+ const [itemTnC, setItemTnC] = useState(null);
+ const [listVouchers, SetListVoucher] = useState(null);
+ const [discountVoucher, SetDiscountVoucher] = useState(0);
+ const [codeVoucher, SetCodeVoucher] = useState(null);
+ const [findCodeVoucher, SetFindVoucher] = useState(null);
+ const [selisihHargaCode, SetSelisihHargaCode] = useState(null);
+ const [buttonTerapkan, SetButtonTerapkan] = useState(false);
+ 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);
const voucher = async () => {
if (!listVouchers) {
try {
- let dataVoucher = await getVoucher(auth?.id, query)
- SetListVoucher(dataVoucher)
+ let dataVoucher = await getVoucher(auth?.id, query);
+ SetListVoucher(dataVoucher);
} finally {
- setLoadingVoucher(false)
+ setLoadingVoucher(false);
}
}
- }
+ };
const VoucherCode = async (code) => {
- let dataVoucher = await findVoucher(code, auth.id, query)
+ let dataVoucher = await findVoucher(code, auth.id, query);
if (dataVoucher.length <= 0) {
- SetFindVoucher(1)
- return
+ SetFindVoucher(1);
+ return;
}
- let addNewLine = dataVoucher[0]
- let checkList = listVouchers.findIndex((voucher) => voucher.code == addNewLine.code)
+ let addNewLine = dataVoucher[0];
+ let checkList = listVouchers.findIndex(
+ (voucher) => voucher.code == addNewLine.code
+ );
if (checkList >= 0) {
if (listVouchers[checkList].canApply) {
- ToggleSwitch(code)
- SetCodeVoucher(null)
+ ToggleSwitch(code);
+ SetCodeVoucher(null);
} else {
- SetSelisihHargaCode(listVouchers[checkList].differenceToApply)
- SetFindVoucher(2)
+ SetSelisihHargaCode(listVouchers[checkList].differenceToApply);
+ SetFindVoucher(2);
}
- return
+ return;
}
if (cartCheckout?.subtotal < addNewLine.minPurchaseAmount) {
- SetSelisihHargaCode(currencyFormat(addNewLine.minPurchaseAmount - cartCheckout?.subtotal))
- SetFindVoucher(2)
- return
+ SetSelisihHargaCode(
+ currencyFormat(addNewLine.minPurchaseAmount - cartCheckout?.subtotal)
+ );
+ SetFindVoucher(2);
+ return;
} else {
- SetFindVoucher(3)
- SetButtonTerapkan(true)
+ SetFindVoucher(3);
+ SetButtonTerapkan(true);
}
- SetListVoucher((prevList) => [addNewLine, ...prevList])
- SetActiveVoucher(addNewLine.code)
- }
+ SetListVoucher((prevList) => [addNewLine, ...prevList]);
+ SetActiveVoucher(addNewLine.code);
+ };
useEffect(() => {
- SetFindVoucher(null)
- }, [bottomPopup])
+ SetFindVoucher(null);
+ }, [bottomPopup]);
useEffect(() => {
const loadExpedisi = async () => {
- let dataExpedisi = await ExpedisiList()
+ let dataExpedisi = await ExpedisiList();
dataExpedisi = dataExpedisi.map((expedisi) => ({
value: expedisi.id,
label: expedisi.name,
- carrierId: expedisi.deliveryCarrierId
- }))
- setExpedisi(dataExpedisi)
- }
- loadExpedisi()
+ carrierId: expedisi.deliveryCarrierId,
+ }));
+ setExpedisi(dataExpedisi);
+ };
+ loadExpedisi();
const handlePopState = () => {
- router.push('/shop/cart')
- }
+ router.push('/shop/cart');
+ };
- window.onpopstate = handlePopState
+ window.onpopstate = handlePopState;
return () => {
- window.onpopstate = null
- }
+ window.onpopstate = null;
+ };
// voucher()
- }, [])
+ }, []);
const hitungDiscountVoucher = (code) => {
- let dataVoucherIndex = listVouchers.findIndex((voucher) => voucher.code == code)
- let dataActiveVoucher = listVouchers[dataVoucherIndex]
+ let dataVoucherIndex = listVouchers.findIndex(
+ (voucher) => voucher.code == code
+ );
+ let dataActiveVoucher = listVouchers[dataVoucherIndex];
- let countDiscount = dataActiveVoucher.discountVoucher
+ let countDiscount = dataActiveVoucher.discountVoucher;
/*if (dataActiveVoucher.discountType === 'percentage') {
countDiscount = cartCheckout?.subtotal * (dataActiveVoucher.discountAmount / 100)
@@ -199,215 +209,257 @@ const Checkout = () => {
countDiscount = dataActiveVoucher.discountAmount
}*/
- return countDiscount
- }
+ return countDiscount;
+ };
useEffect(() => {
- if (!listVouchers) return
- if (!activeVoucher) return
+ if (!listVouchers) return;
+ if (!activeVoucher) return;
- const countDiscount = hitungDiscountVoucher(activeVoucher)
+ const countDiscount = hitungDiscountVoucher(activeVoucher);
- SetDiscountVoucher(countDiscount)
- }, [activeVoucher, listVouchers])
+ SetDiscountVoucher(countDiscount);
+ }, [activeVoucher, listVouchers]);
useEffect(() => {
- setProducts(cartCheckout?.products)
- setCheckWeight(cartCheckout?.hasProductWithoutWeight)
- setTotalWeight(cartCheckout?.totalWeight.g)
- }, [cartCheckout])
+ setProducts(cartCheckout?.products);
+ setCheckWeight(cartCheckout?.hasProductWithoutWeight);
+ setTotalWeight(cartCheckout?.totalWeight.g);
+ setHasFlashSale(cartCheckout?.products[0]?.hasFlashsale ? cartCheckout.products[0].hasFlashsale : false);
+ }, [cartCheckout]);
+
useEffect(() => {
- setCheckoutValidation(false)
+ setCheckoutValidation(false);
const loadServiceRajaOngkir = async () => {
- setLoadingRajaOngkir(true)
+ 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)
+ 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)
+ 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')
+ 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.')
+ toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.');
}
- }
+ };
if (selectedCarrier != 0 && selectedCarrier != 1 && totalWeight > 0) {
- loadServiceRajaOngkir()
+ loadServiceRajaOngkir();
} else {
- setListServiceExpedisi()
- setBiayaKirim(0)
- setselectedExpedisiService()
- setEtd()
+ setListServiceExpedisi();
+ setBiayaKirim(0);
+ setselectedExpedisiService();
+ setEtd();
}
- }, [selectedCarrier, selectedAddress, totalWeight])
+ }, [selectedCarrier, selectedAddress, totalWeight]);
useEffect(() => {
if (selectedServiceType) {
- let serviceType = selectedServiceType.split(',')
- setBiayaKirim(serviceType[0])
- setselectedExpedisiService(serviceType[1])
- setEtd(serviceType[2])
+ let serviceType = selectedServiceType.split(',');
+ setBiayaKirim(serviceType[0]);
+ setselectedExpedisiService(serviceType[1]);
+ setEtd(serviceType[2]);
}
- }, [selectedServiceType])
+ }, [selectedServiceType]);
useEffect(() => {
- if (etd) setEtdFix(calculateEstimatedArrival(etd))
- }, [etd])
+ if (etd) setEtdFix(calculateEstimatedArrival(etd));
+ }, [etd]);
useEffect(() => {
if (selectedExpedisi) {
- let serviceType = selectedExpedisi.split(',')
- if (serviceType[0] === 0) return
+ let serviceType = selectedExpedisi.split(',');
+ if (serviceType[0] === 0) return;
- setselectedCarrier(serviceType[0])
- setselectedCarrierId(serviceType[1])
- setListServiceExpedisi([])
+ setselectedCarrier(serviceType[0]);
+ setselectedCarrierId(serviceType[1]);
+ setListServiceExpedisi([]);
}
- }, [selectedExpedisi])
+ }, [selectedExpedisi]);
+
+ const poNumber = useRef(null);
+ const poFile = useRef(null);
- const poNumber = useRef(null)
- const poFile = useRef(null)
+ const [isLoading, setIsLoading] = useState(false);
- 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]
+ const file = poFile.current.files[0];
if (typeof file !== 'undefined' && file.size > 5000000) {
- toast.error('Maksimal ukuran file adalah 5MB', { position: 'bottom-center' })
- return
+ toast.error('Maksimal ukuran file adalah 5MB', {
+ position: 'bottom-center',
+ });
+ return;
}
if (selectedExpedisi === 0) {
- setCheckoutValidation(true)
+ setCheckoutValidation(true);
if (expedisiValidation.current) {
- const position = expedisiValidation.current.getBoundingClientRect()
+ const position = expedisiValidation.current.getBoundingClientRect();
window.scrollTo({
top: position.top - 300 + window.pageYOffset,
- behavior: 'smooth'
- })
+ behavior: 'smooth',
+ });
}
- return
+ return;
}
if (selectedCarrier != 1 && biayaKirim == 0) {
- toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.')
- return
+ toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.');
+ return;
}
- setIsLoading(true)
+ 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: 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'
- }
+ type: 'sale_order',
+ };
if (query) {
- data.source = 'buy'
+ data.source = 'buy';
}
- if (poNumber.current.value) data.po_number = poNumber.current.value
- if (typeof file !== 'undefined') data.po_file = await getFileBase64(file)
+ if (poNumber.current.value) data.po_number = poNumber.current.value;
+ if (typeof file !== 'undefined') data.po_file = await getFileBase64(file);
- const isCheckouted = await checkoutApi({ data })
+ const isCheckouted = await checkoutApi({ data });
if (!isCheckouted?.id) {
- toast.error('Gagal melakukan transaksi, terjadi kesalahan internal')
- return
+ toast.error('Gagal melakukan transaksi, terjadi kesalahan internal');
+ return;
}
-
- gtagPurchase(products, biayaKirim, isCheckouted.name)
+
+ 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
- }
+ for (const product of products) deleteItemCart({ productId: product.id });
+ 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', {
send_to: 'AW-954540379/nDymCL3BhaQYENvClMcD',
- value: cartCheckout?.grandTotal + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000,
+ value:
+ cartCheckout?.grandTotal +
+ Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000,
currency: 'IDR',
transaction_id: isCheckouted.id,
- event_callback: midtrans
- })
- }
+ event_callback: midtrans,
+ });
+ };
const handlingActivateCode = async () => {
- VoucherCode(codeVoucher)
- }
+ VoucherCode(codeVoucher);
+ };
const handleUseVoucher = async (code, isCheck) => {
if (isCheck) {
if (code === activeVoucher) {
- SetActiveVoucher(null)
- SetDiscountVoucher(0)
+ SetActiveVoucher(null);
+ SetDiscountVoucher(0);
} else {
- SetActiveVoucher(code)
- SetFindVoucher(null)
- document.getElementById('uniqCode').value = ''
- SetButtonTerapkan(false)
+ SetActiveVoucher(code);
+ SetFindVoucher(null);
+ document.getElementById('uniqCode').value = '';
+ SetButtonTerapkan(false);
}
} else {
- SetActiveVoucher(code)
- SetFindVoucher(null)
- document.getElementById('uniqCode').value = ''
- SetButtonTerapkan(false)
+ SetActiveVoucher(code);
+ SetFindVoucher(null);
+ document.getElementById('uniqCode').value = '';
+ SetButtonTerapkan(false);
}
- }
+ };
const onChangeCodeVoucher = async (e) => {
- SetCodeVoucher(e.target.value)
- SetButtonTerapkan(false)
- }
+ SetCodeVoucher(e.target.value);
+ SetButtonTerapkan(false);
+ };
- const [isChecked, setIsChecked] = useState(false)
+ const [isChecked, setIsChecked] = useState(false);
const ToggleSwitch = (code) => {
- setIsChecked(!isChecked)
- handleUseVoucher(code, !isChecked)
- }
+ setIsChecked(!isChecked);
+ handleUseVoucher(code, !isChecked);
+ };
const handlingTnC = async (item) => {
- setItemTnC(item)
- SetBottomPopupTnC(true)
- }
+ setItemTnC(item);
+ SetBottomPopupTnC(true);
+ };
// const taxTotal = (totalAmount - totalDiscountAmount - discountVoucher) * 0.11
+ const hasNoPrice = useMemo(() => {
+ if (!products) return false;
+ for (const item of products) {
+ if (item.price.priceDiscount == 0) return true;
+ }
+ return false;
+ }, [products]);
+
return (
<>
<BottomPopup
className='w-full md:!w-[40%] !min-h-[90vh]'
active={bottomPopupTnC}
close={() => {
- SetBottomPopupTnC(false)
- SetBottomPopup(false)
+ SetBottomPopupTnC(false);
+ SetBottomPopup(false);
}}
title={
<div>
- <button className='flex gap-x-2 items-center' onClick={() => SetBottomPopupTnC(false)}>
- <ChevronLeftIcon class='h- w-5 text-black' /> <span className='text-lg'>Voucher</span>
+ <button
+ className='flex gap-x-2 items-center'
+ onClick={() => SetBottomPopupTnC(false)}
+ >
+ <ChevronLeftIcon class='h- w-5 text-black' />{' '}
+ <span className='text-lg'>Voucher</span>
</button>{' '}
</div>
}
@@ -420,13 +472,17 @@ const Checkout = () => {
<span className='text-sm'>
{' '}
Berakhir dalam :{' '}
- <span className='text-sm text-red-500'>{itemTnC?.remainingTime} lagi</span>
+ <span className='text-sm text-red-500'>
+ {itemTnC?.remainingTime} lagi
+ </span>
</span>
</div>
<div className='flex items-center gap-x-1'>
<BanknotesIcon class='h-6 w-6 text-green-500' />
<span className='text-sm'> Kode Voucher : </span>
- <span className='text-red-500 font-semibold'>{itemTnC?.code}</span>
+ <span className='text-red-500 font-semibold'>
+ {itemTnC?.code}
+ </span>
</div>
</div>
<div>
@@ -441,6 +497,7 @@ const Checkout = () => {
</div>
</div>
</BottomPopup>
+
<BottomPopup
className='w-full md:!w-[40%] !min-h-[350px]'
active={bottomPopup}
@@ -448,8 +505,8 @@ const Checkout = () => {
title='Gunakan Promo'
>
<div className='row'>
- <div className='flex justify-between items-center'>
- <div className='flex md:w-[70%]'>
+ <div className='flex justify-between items-center gap-x-4'>
+ <div className='flex flex-1 md:w-[70%]'>
<input
type='text'
id='uniqCode'
@@ -481,15 +538,16 @@ const Checkout = () => {
{findCodeVoucher === 1 && (
<div className='mt-2'>
<span className='text-caption-1 mt-2 text-red-600'>
- Kode voucher salah / sudah tidak berlaku lagi. Coba voucher lainnya, ya.
+ Kode voucher salah / sudah tidak berlaku lagi. Coba voucher
+ lainnya, ya.
</span>
</div>
)}
{findCodeVoucher === 2 && (
<div className='mt-2'>
<span className='text-caption-1 mt-2 text-red-600'>
- Tambah <span className='text-red-600'>{selisihHargaCode}</span> untuk pakai promo
- ini
+ Tambah <span className='text-red-600'>{selisihHargaCode}</span>{' '}
+ untuk pakai promo ini
</span>
</div>
)}
@@ -500,15 +558,21 @@ const Checkout = () => {
<div className='flex items-center justify-center mt-4 mb-4'>
<div className='text-center'>
<h1 className='font-bold mb-4'>Tidak ada voucher tersedia</h1>
- <p className='text-gray-500'>Maaf, saat ini tidak ada voucher yang tersedia.</p>
+ <p className='text-gray-500'>
+ Maaf, saat ini tidak ada voucher yang tersedia.
+ </p>
</div>
</div>
) : (
- <h3 className='font-semibold mb-4'>Promo Khusus Untuk {auth?.name}</h3>
+ <h3 className='font-semibold mb-4'>
+ Promo Khusus Untuk {auth?.name}
+ </h3>
)}
{loadingVoucher && (
<>
- <div className={`border border-solid w-full hover:cursor-pointer p-2`}>
+ <div
+ className={`border border-solid w-full hover:cursor-pointer p-2`}
+ >
<div class='flex items-center space-x-3'>
<div class='flex items-center justify-center h-28 w-48 mb-4 bg-gray-300 rounded dark:bg-gray-700'>
<svg
@@ -529,7 +593,9 @@ const Checkout = () => {
</div>
</div>
</div>
- <div className={`border border-solid w-full hover:cursor-pointer p-2`}>
+ <div
+ className={`border border-solid w-full hover:cursor-pointer p-2`}
+ >
<div class='flex items-center space-x-3'>
<div class='flex items-center justify-center h-28 w-48 mb-4 bg-gray-300 rounded dark:bg-gray-700'>
<svg
@@ -579,7 +645,9 @@ const Checkout = () => {
>
<p>
Voucher tidak bisa di gunakan,{' '}
- <span className='text-red font-bold'>Baca Selengkapnya !</span>
+ <span className='text-red font-bold'>
+ Baca Selengkapnya !
+ </span>
</p>
</div>
)}
@@ -589,14 +657,20 @@ const Checkout = () => {
<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`} />
+ <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>
+ <span className='text-sm line-clamp-3'>
+ {item.description}{' '}
+ </span>
</div>
</div>
<div className='flex justify-end'>
@@ -605,7 +679,9 @@ const Checkout = () => {
type='checkbox'
value=''
class='sr-only peer'
- checked={activeVoucher === item.code ? true : false}
+ checked={
+ activeVoucher === item.code ? true : false
+ }
onChange={() => ToggleSwitch(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>
@@ -616,11 +692,15 @@ const Checkout = () => {
<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>
+ <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>
+ <span className=' text-green-600'>
+ Voucher digunakan{' '}
+ </span>
)}
</p>
</div>
@@ -642,7 +722,10 @@ const Checkout = () => {
<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,{' '}
+ <span className='text-red-600'>
+ {item.remainingTime}
+ </span>{' '}
+ lagi,{' '}
</div>
<div
className='text-sm ml-2 text-red-600'
@@ -670,6 +753,7 @@ const Checkout = () => {
</div>
</div>
</BottomPopup>
+
<MobileView>
<div className='p-4'>
<Alert type='info' className='text-caption-2 flex gap-x-3'>
@@ -677,8 +761,8 @@ const Checkout = () => {
<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>
@@ -701,16 +785,22 @@ const Checkout = () => {
</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.
+ 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 && (
+ <PickupAddress label='Alamat Pickup' />
+ )}
{selectedCarrierId != SELF_PICKUP_ID && (
- <>
+ <Skeleton
+ isLoaded={!!selectedAddress.invoicing && !!selectedAddress.shipping}
+ minHeight={320}
+ >
<SectionAddress
address={selectedAddress.shipping}
label='Alamat Pengiriman'
@@ -722,7 +812,7 @@ const Checkout = () => {
label='Alamat Penagihan'
url='/my/address?select=invoice'
/>
- </>
+ </Skeleton>
)}
<Divider />
<SectionValidation address={selectedAddress.invoicing} />
@@ -742,7 +832,10 @@ const Checkout = () => {
/>
<div className='p-4 flex flex-col gap-y-4'>
- {products && <VariantGroupCard openOnClick={false} variants={products} />}
+ {!!products &&
+ snakecaseKeys(products).map((item, index) => (
+ <CartItem key={index} item={item} editable={false} />
+ ))}
</div>
<Divider />
@@ -750,7 +843,6 @@ const Checkout = () => {
<div className='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>
<hr className='my-4 border-gray_r-6' />
{!cartCheckout ? (
@@ -804,7 +896,9 @@ const Checkout = () => {
{activeVoucher && (
<div className='flex gap-x-2 justify-between'>
<div className='text-gray_r-11'>Diskon Voucher</div>
- <div className='text-danger-500'>- {currencyFormat(discountVoucher)}</div>
+ <div className='text-danger-500'>
+ - {currencyFormat(discountVoucher)}
+ </div>
</div>
)}
<div className='flex gap-x-2 justify-between'>
@@ -819,7 +913,11 @@ const Checkout = () => {
<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>
+ {currencyFormat(
+ Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000
+ )}
+ </div>
</div>
</div>
)}
@@ -839,9 +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>
)}
@@ -852,8 +948,8 @@ const Checkout = () => {
<button
type='button'
onClick={() => {
- SetBottomPopup(true)
- voucher()
+ SetBottomPopup(true);
+ voucher();
}}
className='text-gray-900 p-4 flex items-center justify-between rounded-lg bg-white border font-medium border-gray-300 hover:bg-gray-100 py-2.5 h-[50px] w-[100%]'
>
@@ -886,7 +982,8 @@ const Checkout = () => {
</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 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>{' '}
@@ -911,10 +1008,16 @@ const Checkout = () => {
</div>
<div className='w-6/12'>
<label className='form-label font-normal'>Nomor PO</label>
- <input type='text' className='form-input mt-2 h-12' ref={poNumber} />
+ <input
+ type='text'
+ className='form-input mt-2 h-12'
+ ref={poNumber}
+ />
</div>
</div>
- <p className='text-caption-2 text-gray_r-11 mt-2'>Ukuran dokumen PO Maksimal 5MB</p>
+ <p className='text-caption-2 text-gray_r-11 mt-2'>
+ Ukuran dokumen PO Maksimal 5MB
+ </p>
</div>
<Divider />
@@ -923,7 +1026,13 @@ const Checkout = () => {
<button
className='flex-1 btn-yellow'
onClick={checkout}
- disabled={isLoading || !products || products?.length == 0 || priceCheck}
+ disabled={
+ isLoading ||
+ !products ||
+ products?.length == 0 ||
+ priceCheck ||
+ hasNoPrice
+ }
>
{isLoading ? 'Loading...' : 'Lanjut Pembayaran'}
</button>
@@ -957,8 +1066,9 @@ const Checkout = () => {
</svg>
<span class='sr-only'>Info</span>
<div>
- Fitur Self Pickup, hanya berlaku untuk customer di area jakarta. Apa bila memilih
- fitur ini, anda akan dihubungi setelah barang siap diambil.
+ Fitur Self Pickup, hanya berlaku untuk customer di area jakarta.
+ Apa bila memilih fitur ini, anda akan dihubungi setelah barang
+ siap diambil.
</div>
</div>
)}
@@ -966,9 +1076,16 @@ const Checkout = () => {
<div className='flex'>
{' '}
<div className='w-3/4 border border-gray_r-6 rounded bg-white'>
- {selectedCarrierId == SELF_PICKUP_ID && <PickupAddress label='Alamat Pickup' />}
+ {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'
@@ -980,7 +1097,7 @@ const Checkout = () => {
label='Alamat Penagihan'
url='/my/address?select=invoice'
/>
- </>
+ </Skeleton>
)}
<Divider />
<SectionValidation address={selectedAddress.invoicing} />
@@ -1000,170 +1117,13 @@ const Checkout = () => {
/>
<div className='p-4'>
- <div className='font-medium'>Detail Pesanan</div>
- <CardProdcuctsList isLoading={isLoading} products={products} />
-
- {/* <table className='table-checkout'>
- <thead>
- <tr>
- <th>Nama Produk</th>
- <th>Jumlah</th>
- <th>Harga</th>
- <th>Subtotal</th>
- </tr>
- </thead>
- <tbody>
- {!products ? (
- <tr>
- <td colSpan={4}>
- <div className='container my-4'>
- <div class='h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4'></div>
- <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div>
- <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div>
- <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700'></div>
- </div>
- </td>
- </tr>
- ) : (
- products?.map((product) => (
- <>
- <tr
- key={product.id}
- className={`${product.program ? '!border-t-0 !border-b-0' : ''}`}
- >
- <td className='flex'>
- <div 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-40 w-full rounded-md'
- />
- </div>
- <div className='px-2 text-left'>
- <div className='line-clamp-2 leading-6 !text-gray_r-12 font-normal'>
- {product?.parent?.name}
- </div>
- <div className='text-gray_r-11 mt-2'>
- {product?.code}{' '}
- {product?.attributes.length > 0
- ? `| ${product?.attributes.join(', ')}`
- : ''}
- </div>
- <div className='text-gray_r-11 mt-2'>
- Berat item : {product?.weight} Kg
- </div>
- </div>
- </td>
- <td>
- <input
- className='form-input w-16 py-2 text-center bg-gray_r-1'
- type='number'
- value={product?.quantity}
- disabled
- />
- </td>
- <td>
- {product?.hasFlashsale ? (
- <>
- <div className='flex gap-x-1 items-center justify-center mt-3'>
- <div className='text-gray_r-11 line-through text-caption-1'>
- {currencyFormat(product?.price?.price)}
- </div>
- <div className='badge-solid-red'>
- {product?.price?.discountPercentage}%
- </div>
- </div>
- <div className='font-normal mt-1'>
- {currencyFormat(product?.price?.priceDiscount)}
- </div>
- </>
- ) : (
- <div className='font-normal mt-1'>
- {product.price.priceDiscount > 0
- ? currencyFormat(product?.price?.priceDiscount)
- : 'Call for Inquiry'}
- </div>
- )}
- </td>
- <td>
- <div className='text-danger-500 font-medium'>
- {product.price.priceDiscount > 0 ? (
- currencyFormat(product?.price?.priceDiscount * product?.quantity)
- ) : (
- <a
- href={whatsappUrl('product', {
- name: product.name,
- url: createSlug(
- '/shop/product/',
- product.name,
- product.id,
- true
- )
- })}
- className='underline'
- >
- Call for Inquiry{' '}
- </a>
- )}
- </div>
- </td>
- </tr>
- {product.program &&
- product.program.items &&
- product.program.items.map((item) => (
- <>
- <tr key={product?.program?.id} className='!border-t-0'>
- <td className='flex'>
- <div className='w-[20%] flex-shrink-0'>
- <Image
- src={item.parent.image}
- alt={item.name}
- className='object-contain object-center border border-gray_r-6 h-40 w-full rounded-md'
- />
- </div>
- <div className='px-2 text-left'>
- <div className=''>
- <span className='border border-solid border-red-600 rounded-md p-1 text-red-600'>
- {product.program.type.label}
- </span>
- </div>
- <div className='mt-2 line-clamp-2 leading-6'>{item.name}</div>
- </div>
- </td>
- <td>
- <input
- className='form-input w-16 py-2 text-center bg-gray_r-1'
- type='number'
- value={1}
- disabled
- />
- </td>
- <td>
- {item?.price?.discountPercentage > 0 && (
- <div className='flex gap-x-1 items-center justify-center mt-3'>
- <div className='text-gray_r-11 line-through text-caption-1'>
- {currencyFormat(product?.price?.price)}
- </div>
- </div>
- )}
- <div className='font-normal mt-1'>
- {item?.price.priceDiscount > 0 ? 'Gratis' : ''}
- </div>
- </td>
- <td>
- <div className='text-danger-500 font-medium'>
- {item.price.priceDiscount > 0 ? 'Gratis' : ''}
- </div>
- </td>
- <td></td>
- </tr>
- </>
- ))}
- </>
- ))
- )}
- </tbody>
- </table> */}
+ <div className='font-medium mb-6'>Detail Pesanan</div>
+ <div className='flex flex-col gap-y-8 border-t border-gray-300 pt-8'>
+ {!!products &&
+ snakecaseKeys(products).map((item, index) => (
+ <CartItem key={index} item={item} editable={false} />
+ ))}
+ </div>
</div>
</div>
<div className='w-1/4 pl-4'>
@@ -1171,7 +1131,7 @@ const Checkout = () => {
<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 - {cartCheckout?.totalWeight.kg} Kg
+ {cartCheckout?.totalWeight.kg} Kg
</div>
</div>
@@ -1227,7 +1187,9 @@ const Checkout = () => {
{activeVoucher && (
<div className='flex gap-x-2 justify-between'>
<div className='text-gray_r-11'>Diskon Voucher</div>
- <div className='text-danger-500'>- {currencyFormat(discountVoucher)}</div>
+ <div className='text-danger-500'>
+ - {currencyFormat(discountVoucher)}
+ </div>
</div>
)}
<div className='flex gap-x-2 justify-between'>
@@ -1244,7 +1206,9 @@ const Checkout = () => {
<p className='text-xs mt-3'>{etdFix}</p>
</div>
<div>
- {currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000)}
+ {currencyFormat(
+ Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000
+ )}
</div>
</div>
</div>
@@ -1265,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>
)}
@@ -1279,8 +1240,8 @@ const Checkout = () => {
<button
type='button'
onClick={() => {
- SetBottomPopup(true)
- voucher()
+ SetBottomPopup(true);
+ voucher();
}}
className='text-gray-900 p-3 flex items-center justify-between rounded-lg bg-white border font-medium border-gray-300 hover:bg-gray-100 py-2.5 h-[50px] w-[100%]'
>
@@ -1312,7 +1273,8 @@ const Checkout = () => {
</div>
<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>{' '}
@@ -1322,7 +1284,8 @@ const Checkout = () => {
<hr className='my-4 border-gray_r-6' />
<div className='font-medium mt-4'>
- Purchase Order <span className='font-normal text-gray_r-11'>(Opsional)</span>
+ Purchase Order{' '}
+ <span className='font-normal text-gray_r-11'>(Opsional)</span>
</div>
<div className='mt-4 flex gap-x-3'>
@@ -1337,17 +1300,29 @@ const Checkout = () => {
</div>
<div className='w-6/12'>
<label className='form-label font-normal'>Nomor PO</label>
- <input type='text' className='form-input mt-2 h-12' ref={poNumber} />
+ <input
+ type='text'
+ className='form-input mt-2 h-12'
+ ref={poNumber}
+ />
</div>
</div>
- <p className='text-caption-2 text-gray_r-11 mt-2'>Ukuran dokumen PO Maksimal 5MB</p>
+ <p className='text-caption-2 text-gray_r-11 mt-2'>
+ Ukuran dokumen PO Maksimal 5MB
+ </p>
<hr className='my-4 border-gray_r-6' />
<button
className='w-full btn-yellow mt-4'
onClick={checkout}
- disabled={isLoading || !products || products?.length == 0 || priceCheck}
+ disabled={
+ isLoading ||
+ !products ||
+ products?.length == 0 ||
+ priceCheck ||
+ hasNoPrice
+ }
>
{isLoading ? 'Loading...' : 'Lanjut Pembayaran'}
</button>
@@ -1367,8 +1342,8 @@ const Checkout = () => {
</div>
</DesktopView>
</>
- )
-}
+ );
+};
const SectionAddress = ({ address, label, url }) => (
<div className='p-4'>
@@ -1382,7 +1357,9 @@ const SectionAddress = ({ address, label, url }) => (
{address && (
<div className='mt-4 text-caption-1'>
<div className='badge-red mb-2'>
- {address.type.charAt(0).toUpperCase() + address.type.slice(1) + ' Address'}
+ {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>
@@ -1392,7 +1369,7 @@ const SectionAddress = ({ address, label, url }) => (
</div>
)}
</div>
-)
+);
const SectionValidation = ({ address }) =>
address?.rajaongkirCityId == 0 && (
@@ -1409,7 +1386,7 @@ const SectionValidation = ({ address }) =>
</Link>
</div>
</BottomPopup>
- )
+ );
const SectionExpedisi = ({
address,
@@ -1418,7 +1395,7 @@ const SectionExpedisi = ({
checkWeigth,
checkoutValidation,
expedisiValidation,
- loadingRajaOngkir
+ loadingRajaOngkir,
}) =>
address?.rajaongkirCityId > 0 && (
<div className='p-4' ref={expedisiValidation}>
@@ -1427,7 +1404,9 @@ const SectionExpedisi = ({
<div className='w-[250px]'>
<div className='flex items-center gap-x-4'>
<select
- className={`form-input ${checkoutValidation ? 'border-red-500 shake' : ''}`}
+ className={`form-input ${
+ checkoutValidation ? 'border-red-500 shake' : ''
+ }`}
onChange={(e) => setSelectedExpedisi(e.target.value)}
required
>
@@ -1453,7 +1432,7 @@ const SectionExpedisi = ({
animate={{ opacity: 1, width: '28px' }}
exit={{ opacity: 0, width: 0 }}
transition={{
- duration: 0.25
+ duration: 0.25,
}}
className='overflow-hidden'
>
@@ -1463,7 +1442,9 @@ const SectionExpedisi = ({
</AnimatePresence>
</div>
{checkoutValidation && (
- <span className='text-sm text-red-500'>*silahkan pilih expedisi</span>
+ <span className='text-sm text-red-500'>
+ *silahkan pilih expedisi
+ </span>
)}
</div>
<style jsx>{`
@@ -1474,18 +1455,19 @@ const SectionExpedisi = ({
</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{' '}
+ Mohon maaf, pengiriman hanya tersedia untuk self pickup karena
+ terdapat barang yang belum diatur beratnya. Mohon atur berat barang
+ dengan menghubungi admin melalui{' '}
<a
className='text-danger-500 inline'
- href='https://api.whatsapp.com/send?phone=628128080622'
+ href='https://api.whatsapp.com/send?phone=6281717181922'
>
tautan ini
</a>
</p>
)}
</div>
- )
+ );
const SectionListService = ({ listserviceExpedisi, setSelectedServiceType }) =>
listserviceExpedisi?.length > 0 && (
@@ -1494,7 +1476,10 @@ const SectionListService = ({ listserviceExpedisi, setSelectedServiceType }) =>
<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)}>
+ <select
+ className='form-input'
+ onChange={(e) => setSelectedServiceType(e.target.value)}
+ >
{listserviceExpedisi.map((service) => (
<option
value={
@@ -1511,7 +1496,9 @@ const SectionListService = ({ listserviceExpedisi, setSelectedServiceType }) =>
{' '}
{service.description} - {service.service.toUpperCase()}
{extractDuration(service.cost[0].etd) &&
- ` (Estimasi Tiba ${extractDuration(service.cost[0].etd)} Hari)`}
+ ` (Estimasi Tiba ${extractDuration(
+ service.cost[0].etd
+ )} Hari)`}
</option>
))}
</select>
@@ -1520,73 +1507,73 @@ const SectionListService = ({ listserviceExpedisi, setSelectedServiceType }) =>
</div>
<Divider />
</>
- )
+ );
function addDays(date, days) {
- const result = new Date(date)
- result.setDate(result.getDate() + days)
- return result
+ 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}`
+ const day = date.getDate();
+ const month = date.toLocaleString('default', { month: 'short' });
+ return `${day} ${month}`;
}
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 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)}`
+ let etdText = `*Estimasi tiba ${formatDate(from)}`;
if (estimationDate[1] > estimationDate[0]) {
- etdText += ` - ${formatDate(to)}`
+ etdText += ` - ${formatDate(to)}`;
}
- return etdText
+ return etdText;
}
- return ''
+ return '';
}
function splitDuration(duration) {
if (duration) {
- let estimationDate = null
+ let estimationDate = null;
if (duration.includes('-')) {
- estimationDate = duration.split('-')
- estimationDate = parseInt(estimationDate[1])
+ estimationDate = duration.split('-');
+ estimationDate = parseInt(estimationDate[1]);
} else {
- estimationDate = parseInt(duration)
+ estimationDate = parseInt(duration);
}
- return estimationDate
+ return estimationDate;
}
- return ''
+ return '';
}
const extractDuration = (text) => {
- const matches = text.match(/\d+(?:-\d+)?/g)
+ 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])
+ const parts = matches[0].split('-');
+ const min = parseInt(parts[0]);
+ const max = parseInt(parts[1]);
if (min === max) {
- return min.toString()
+ return min.toString();
}
- return matches[0]
+ return matches[0];
}
- return ''
-}
+ return '';
+};
const PickupAddress = ({ label }) => (
<div className='p-4'>
@@ -1596,13 +1583,14 @@ const PickupAddress = ({ label }) => (
<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
+ 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>
-)
+);
-export default Checkout
+export default Checkout;
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 && (
+ <>
+ &nbsp;
+ <span style={style.productPriceB}>
+ {currencyFormat(product.price.price)}
+ </span>
+ </>
+ )}
+ &nbsp; 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 && (
- <>
- &nbsp;
- <span style={style.productPriceB}>
- {currencyFormat(product.price.price)}
- </span>
- </>
- )}
- &nbsp; 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/form/components/KunjunganSales.jsx b/src/lib/form/components/KunjunganSales.jsx
index 7470395a..ffa8f135 100644
--- a/src/lib/form/components/KunjunganSales.jsx
+++ b/src/lib/form/components/KunjunganSales.jsx
@@ -10,6 +10,9 @@ import * as Yup from 'yup'
import createLeadApi from '../api/createLeadApi'
import PageContent from '@/lib/content/components/PageContent'
+import useAuth from '@/core/hooks/useAuth'
+import { useRouter } from 'next/router'
+
const KunjunganSales = () => {
const {
register,
@@ -23,10 +26,18 @@ const KunjunganSales = () => {
})
const [cities, setCities] = useState([])
const [companyTypes, setCompanyTypes] = useState([])
+ const router = useRouter()
+
+ const auth = useAuth()
+
+
const recaptchaRef = useRef(null)
useEffect(() => {
+ if(auth == false) {
+ router.push('/login')
+ }
const loadCities = async () => {
let dataCities = await cityApi()
dataCities = dataCities.map((obj) => ({ value: obj.name, label: obj.name }))
@@ -39,7 +50,7 @@ const KunjunganSales = () => {
loadCompanyTypes()
loadCities()
- }, [])
+ }, [auth])
const onSubmitHandler = async (values) => {
const recaptchaValue = recaptchaRef.current.getValue()
diff --git a/src/lib/form/components/KunjunganService.jsx b/src/lib/form/components/KunjunganService.jsx
index 1cb0b446..5720d14e 100644
--- a/src/lib/form/components/KunjunganService.jsx
+++ b/src/lib/form/components/KunjunganService.jsx
@@ -8,6 +8,9 @@ import { toast } from 'react-hot-toast'
import * as Yup from 'yup'
import createLeadApi from '../api/createLeadApi'
import PageContent from '@/lib/content/components/PageContent'
+import { useRouter } from 'next/router'
+
+import useAuth from '@/core/hooks/useAuth'
const CreateKunjunganService = () => {
const {
@@ -22,17 +25,24 @@ const CreateKunjunganService = () => {
})
const [cities, setCities] = useState([])
const [company_unit, setCompany_unit] = useState([])
+
+ const router = useRouter()
+
+ const auth = useAuth()
const recaptchaRef = useRef(null)
useEffect(() => {
+ if(auth == false) {
+ router.push('/login')
+ }
const loadCities = async () => {
let dataCities = await cityApi()
dataCities = dataCities.map((city) => ({ value: city.id, label: city.name }))
setCities(dataCities)
}
loadCities()
- }, [])
+ }, [auth])
const onSubmitHandler = async (values) => {
const recaptchaValue = recaptchaRef.current.getValue()
diff --git a/src/lib/form/components/Merchant.jsx b/src/lib/form/components/Merchant.jsx
index 6c1af231..85f72bf8 100644
--- a/src/lib/form/components/Merchant.jsx
+++ b/src/lib/form/components/Merchant.jsx
@@ -8,6 +8,9 @@ import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
import createLeadApi from '../api/createLeadApi';
import PageContent from '@/lib/content/components/PageContent';
+import { useRouter } from 'next/router';
+import useAuth from '@/core/hooks/useAuth'
+
const CreateMerchant = () => {
const {
@@ -50,8 +53,14 @@ const CreateMerchant = () => {
const [company_unit, setCompany_unit] = useState(list_unit);
const recaptchaRef = useRef(null);
+ const router = useRouter()
+
+ const auth = useAuth()
useEffect(() => {
+ if(auth == false) {
+ router.push('/login')
+ }
const loadCities = async () => {
let dataCities = await cityApi();
dataCities = dataCities.map((city) => ({
@@ -61,7 +70,7 @@ const CreateMerchant = () => {
setCities(dataCities);
};
loadCities();
- }, []);
+ }, [auth]);
const onSubmitHandler = async (values) => {
const recaptchaValue = recaptchaRef.current.getValue();
diff --git a/src/lib/form/components/PembayaranTempo.jsx b/src/lib/form/components/PembayaranTempo.jsx
index 8c624fe2..fc4d248a 100644
--- a/src/lib/form/components/PembayaranTempo.jsx
+++ b/src/lib/form/components/PembayaranTempo.jsx
@@ -1,12 +1,15 @@
import getFileBase64 from '@/core/utils/getFileBase64'
import { yupResolver } from '@hookform/resolvers/yup'
-import React, { useRef } from 'react'
+import React, { useEffect, useRef } from 'react'
import ReCAPTCHA from 'react-google-recaptcha'
import { useForm } from 'react-hook-form'
import { toast } from 'react-hot-toast'
import * as Yup from 'yup'
import createLeadApi from '../api/createLeadApi'
import PageContent from '@/lib/content/components/PageContent'
+import { useRouter } from 'next/router'
+
+import useAuth from '@/core/hooks/useAuth'
const PembayaranTempo = () => {
@@ -21,6 +24,15 @@ const PembayaranTempo = () => {
})
const recaptchaRef = useRef(null)
+ const router = useRouter()
+
+ const auth = useAuth()
+
+ useEffect(() => {
+ if(auth == false) {
+ router.push('/login')
+ }
+ },[auth])
const onSubmitHandler = async (values) => {
const recaptchaValue = recaptchaRef.current.getValue()
diff --git a/src/lib/form/components/RequestForQuotation.jsx b/src/lib/form/components/RequestForQuotation.jsx
index fa526d5f..68b7fa17 100644
--- a/src/lib/form/components/RequestForQuotation.jsx
+++ b/src/lib/form/components/RequestForQuotation.jsx
@@ -10,6 +10,9 @@ import * as Yup from 'yup'
import createLeadApi from '../api/createLeadApi'
import getFileBase64 from '@/core/utils/getFileBase64'
import PageContent from '@/lib/content/components/PageContent'
+import { useRouter } from 'next/router'
+
+import useAuth from '@/core/hooks/useAuth'
const RequestForQuotation = () => {
const {
@@ -26,15 +29,22 @@ const RequestForQuotation = () => {
const quotationFileRef = useRef(null)
const recaptchaRef = useRef(null)
+ const router = useRouter()
+
+ const auth = useAuth()
+
useEffect(() => {
+ if(auth == false) {
+ router.push('/login')
+ }
const loadCities = async () => {
let dataCities = await cityApi()
dataCities = dataCities.map((obj) => ({ value: obj.name, label: obj.name }))
setCities(dataCities)
}
loadCities()
- }, [])
+ }, [auth])
const onSubmitHandler = async (values) => {
const recaptchaValue = recaptchaRef.current.getValue()
diff --git a/src/lib/form/components/SuratDukungan.jsx b/src/lib/form/components/SuratDukungan.jsx
index d73c3fab..31e7ee83 100644
--- a/src/lib/form/components/SuratDukungan.jsx
+++ b/src/lib/form/components/SuratDukungan.jsx
@@ -10,6 +10,9 @@ import createLeadsApi from '../api/createLeadApi';
import PageContent from '@/lib/content/components/PageContent';
+import useAuth from '@/core/hooks/useAuth'
+import { useRouter } from 'next/router';
+
const CreateSuratDukungan = () => {
const {
register,
@@ -25,8 +28,14 @@ const CreateSuratDukungan = () => {
const [company_unit, setCompany_unit] = useState([]);
const recaptchaRef = useRef(null);
+ const router = useRouter()
+
+ const auth = useAuth()
useEffect(() => {
+ if(auth == false) {
+ router.push('/login')
+ }
const loadCities = async () => {
let dataCities = await cityApi();
dataCities = dataCities.map((city) => ({
@@ -36,7 +45,7 @@ const CreateSuratDukungan = () => {
setCities(dataCities);
};
loadCities();
- }, []);
+ }, [auth]);
const onSubmitHandler = async (values) => {
const recaptchaValue = recaptchaRef.current.getValue();
diff --git a/src/lib/home/api/categoryHomeApi.js b/src/lib/home/api/categoryHomeApi.js
index 9e7d1402..e5def608 100644
--- a/src/lib/home/api/categoryHomeApi.js
+++ b/src/lib/home/api/categoryHomeApi.js
@@ -1,11 +1,10 @@
-import odooApi from '@/core/api/odooApi'
-import axios from 'axios'
+import axios from 'axios';
const categoryHomeIdApi = async ({ id }) => {
- // const dataCategoryHomeIdO = await odooApi('GET', `/api/v1/product/category-homepage?id=${id}`)
- // console.log('ini adalah odoo', dataCategoryHomeIdO)
- const dataCategoryHomeId = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/product-homepage?id=` + id)
- return dataCategoryHomeId.data
-}
+ const dataCategoryHomeId = await axios(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/product-homepage?id=` + id
+ );
+ return dataCategoryHomeId.data;
+};
-export default categoryHomeIdApi
+export default categoryHomeIdApi;
diff --git a/src/lib/home/components/PreferredBrand.jsx b/src/lib/home/components/PreferredBrand.jsx
index 571c4745..ec09aa4e 100644
--- a/src/lib/home/components/PreferredBrand.jsx
+++ b/src/lib/home/components/PreferredBrand.jsx
@@ -1,13 +1,41 @@
import { Swiper, SwiperSlide } from 'swiper/react'
+import { useCallback, useEffect, useState } from 'react'
import usePreferredBrand from '../hooks/usePreferredBrand'
import PreferredBrandSkeleton from './Skeleton/PreferredBrandSkeleton'
import BrandCard from '@/lib/brand/components/BrandCard'
import useDevice from '@/core/hooks/useDevice'
import Link from '@/core/components/elements/Link/Link'
+import axios from 'axios'
const PreferredBrand = () => {
let query = 'level_s'
let params = 'prioritas'
+ const [isLoading, setIsLoading] = useState(true)
+ const [startWith, setStartWith] = useState(null)
+ const [manufactures, setManufactures] = useState([])
+
+ const loadBrand = useCallback(async () => {
+ setIsLoading(true)
+ const name = startWith ? `${startWith}*` : ''
+ const result = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/brands?params=${name}`)
+
+ setIsLoading(false)
+ setManufactures((manufactures) => [...result.data])
+ }, [startWith])
+
+ const toggleStartWith = (alphabet) => {
+ setManufactures([])
+ if (alphabet == startWith) {
+ setStartWith(null)
+ return
+ }
+ setStartWith(alphabet)
+ }
+
+ useEffect(() => {
+ loadBrand()
+ }, [loadBrand])
+
const { preferredBrands } = usePreferredBrand(query)
const { isMobile, isDesktop } = useDevice()
@@ -21,12 +49,12 @@ const PreferredBrand = () => {
</Link>
)}
</div>
- {preferredBrands.isLoading && <PreferredBrandSkeleton />}
- {!preferredBrands.isLoading && (
+ {manufactures.isLoading && <PreferredBrandSkeleton />}
+ {!manufactures.isLoading && (
<Swiper slidesPerView={isMobile ? 3.5 : 7.5} spaceBetween={isMobile ? 12 : 24} freeMode>
- {preferredBrands.data?.data.map((brand) => (
- <SwiperSlide key={brand.id}>
- <BrandCard brand={brand} />
+ {manufactures.map((manufacture) => (
+ <SwiperSlide key={manufacture.id}>
+ <BrandCard brand={manufacture} />
</SwiperSlide>
))}
</Swiper>
diff --git a/src/lib/home/components/PromotionProgram.jsx b/src/lib/home/components/PromotionProgram.jsx
new file mode 100644
index 00000000..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 5f034c09..444ddd8e 100644
--- a/src/lib/product/components/Product/ProductDesktop.jsx
+++ b/src/lib/product/components/Product/ProductDesktop.jsx
@@ -1,416 +1,442 @@
-import Image from '@/core/components/elements/Image/Image'
-import Link from '@/core/components/elements/Link/Link'
-import DesktopView from '@/core/components/views/DesktopView'
-import currencyFormat from '@/core/utils/currencyFormat'
-import { HeartIcon } from '@heroicons/react/24/outline'
-import { useCallback, useEffect, useRef, useState } from 'react'
-import LazyLoad from 'react-lazy-load'
-import ProductSimilar from '../ProductSimilar'
-import { toast } from 'react-hot-toast'
-import { updateItemCart } from '@/core/utils/cart'
-import { useRouter } from 'next/router'
-import { createSlug } from '@/core/utils/slug'
-import BottomPopup from '@/core/components/elements/Popup/BottomPopup'
-import ProductCard from '../ProductCard'
-import productSimilarApi from '../../api/productSimilarApi'
-import whatsappUrl from '@/core/utils/whatsappUrl'
-import odooApi from '@/core/api/odooApi'
-import PromotionType from '@/lib/promotinProgram/components/PromotionType'
-import useAuth from '@/core/hooks/useAuth'
-import ImageNext from 'next/image'
-import CountDown2 from '@/core/components/elements/CountDown/CountDown2'
-import { LazyLoadComponent } from 'react-lazy-load-image-component'
-import ColumnsSLA from './ColumnsSLA'
-import { useProductCartContext } from '@/contexts/ProductCartContext'
-import { Box, Skeleton, Tooltip } from '@chakra-ui/react'
-import { Info } from 'lucide-react'
-import Breadcrumb from './Breadcrumb'
-import { sellingProductFormat } from '@/core/utils/formatValue'
+import { useEffect, useRef, useState } from 'react';
+import ImageNext from 'next/image';
+import { LazyLoadComponent } from 'react-lazy-load-image-component';
+import { Box, Skeleton, Tooltip } from '@chakra-ui/react';
+import { HeartIcon } from '@heroicons/react/24/outline';
+import { Info } from 'lucide-react';
+import LazyLoad from 'react-lazy-load';
+import { toast } from 'react-hot-toast';
+import { useRouter } from 'next/router';
+
+import Image from '@/core/components/elements/Image/Image';
+import Link from '@/core/components/elements/Link/Link';
+import DesktopView from '@/core/components/views/DesktopView';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import CountDown2 from '@/core/components/elements/CountDown/CountDown2';
+
+import currencyFormat from '@/core/utils/currencyFormat';
+import { updateItemCart } from '@/core/utils/cart';
+import { createSlug } from '@/core/utils/slug';
+import whatsappUrl from '@/core/utils/whatsappUrl';
+import { sellingProductFormat } from '@/core/utils/formatValue';
+
+import odooApi from '@/core/api/odooApi';
+import useAuth from '@/core/hooks/useAuth';
+
+import { useProductCartContext } from '@/contexts/ProductCartContext';
+
+import PromotionType from '@/lib/promotinProgram/components/PromotionType';
+
+import ProductSimilar from '../ProductSimilar';
+import ProductCard from '../ProductCard';
+import productSimilarApi from '../../api/productSimilarApi';
+import ColumnsSLA from './ColumnsSLA';
+import Breadcrumb from './Breadcrumb';
+
+import ProductPromoSection from '~/modules/product-promo/components/Section';
const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
- const router = useRouter()
- const auth = useAuth()
- const { slug } = router.query
+ const router = useRouter();
+ const auth = useAuth();
+ const { slug } = router.query;
- const [quantityActive, setQuantity] = useState(null)
- const [lowestPrice, setLowestPrice] = useState(null)
- const [product, setProducts] = useState(products)
+ const [quantityActive, setQuantity] = useState(null);
+ const [lowestPrice, setLowestPrice] = useState(null);
+ const [product, setProducts] = useState(products);
- const [addCartAlert, setAddCartAlert] = useState(false)
- const [isLoadingSLA, setIsLoadingSLA] = useState(true)
- const [promotionType, setPromotionType] = useState(false)
- const [promotionActiveId, setPromotionActiveId] = useState(null)
- const [selectVariantPromoActive, setSelectVariantPromoActive] = useState(null)
- const [backgorundFlashSale, setBackgorundFlashSale] = useState(null)
+ const [addCartAlert, setAddCartAlert] = useState(false);
+ const [isLoadingSLA, setIsLoadingSLA] = useState(true);
+ const [promotionType, setPromotionType] = useState(false);
+ const [promotionActiveId, setPromotionActiveId] = useState(null);
+ const [selectVariantPromoActive, setSelectVariantPromoActive] =
+ useState(null);
+ const [backgorundFlashSale, setBackgorundFlashSale] = useState(null);
- const { setRefreshCart, refreshCart } = useProductCartContext()
+ const { setRefreshCart, refreshCart } = useProductCartContext();
useEffect(() => {
- setLowestPrice({ price: product?.lowestPrice })
- }, [product])
+ setLowestPrice({ price: product?.lowestPrice });
+ }, [product]);
useEffect(() => {
const getBackgound = async () => {
- const get = await odooApi('GET', '/api/v1/banner?type=flash-sale-background-banner')
- setBackgorundFlashSale(get[0].image)
- }
- getBackgound()
- }, [])
+ const get = await odooApi(
+ 'GET',
+ '/api/v1/banner?type=flash-sale-background-banner'
+ );
+ setBackgorundFlashSale(get[0].image);
+ };
+ getBackgound();
+ }, []);
- const [informationTab, setInformationTab] = useState(informationTabOptions[0].value)
+ const [informationTab, setInformationTab] = useState(
+ informationTabOptions[0].value
+ );
- const variantQuantityRefs = useRef([])
+ const variantQuantityRefs = useRef([]);
const setVariantQuantityRef = (variantId) => (element) => {
if (element) {
- let variantIndex = product.variants.findIndex((varian) => varian.id == variantId)
- product.variants[variantIndex].quantity = element?.value
+ let variantIndex = product.variants.findIndex(
+ (varian) => varian.id == variantId
+ );
+ product.variants[variantIndex].quantity = element?.value;
}
- variantQuantityRefs.current[variantId] = element
- }
+ variantQuantityRefs.current[variantId] = element;
+ };
const validQuantity = (quantity) => {
- let isValid = true
+ let isValid = true;
if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) {
- toast.error('Jumlah barang minimal 1')
- isValid = false
+ toast.error('Jumlah barang minimal 1');
+ isValid = false;
}
- return isValid
- }
+ return isValid;
+ };
const updateCart = (variantId, quantity, source) => {
let dataUpdate = {
productId: variantId,
quantity,
selected: true,
- source: source === 'buy' ? 'buy' : null
- }
+ source: source === 'buy' ? 'buy' : null,
+ };
if (product.variants.length > 1) {
- let variantIndex = product.variants.findIndex((varian) => varian.id == variantId)
- dataUpdate['programLineId'] = product.variants[variantIndex].programActive
+ let variantIndex = product.variants.findIndex(
+ (varian) => varian.id == variantId
+ );
+ dataUpdate['programLineId'] =
+ product.variants[variantIndex].programActive;
} else {
- dataUpdate['programLineId'] = promotionActiveId
+ dataUpdate['programLineId'] = promotionActiveId;
}
- updateItemCart(dataUpdate)
- }
+ updateItemCart(dataUpdate);
+ };
const redirectToLogin = (action, variantId, quantity) => {
- const nextURL = `/shop/product/${slug}?action=${action}&variantId=${variantId}&qty=${quantity}`
- router.push(`/login?next=${encodeURIComponent(nextURL)}`)
- return true
- }
+ const nextURL = `/shop/product/${slug}?action=${action}&variantId=${variantId}&qty=${quantity}`;
+ router.push(`/login?next=${encodeURIComponent(nextURL)}`);
+ return true;
+ };
const handleAddToCart = (variantId) => {
- const quantity = variantQuantityRefs.current[variantId].value
+ const quantity = variantQuantityRefs.current[variantId].value;
- if (!validQuantity(quantity)) return
+ if (!validQuantity(quantity)) return;
if (!auth) {
- return redirectToLogin('add_to_cart', variantId, quantity)
+ return redirectToLogin('add_to_cart', variantId, quantity);
}
- let source = 'cart'
- updateCart(variantId, quantity, source)
- setRefreshCart(true)
- setAddCartAlert(true)
- }
+ let source = 'cart';
+ updateCart(variantId, quantity, source);
+ setRefreshCart(true);
+ setAddCartAlert(true);
+ };
const handleQuantityChange = (variantId) => (event) => {
- const { value } = event.target
- const variantIndex = product.variants.findIndex((variant) => variant.id === variantId)
+ const { value } = event.target;
+ const variantIndex = product.variants.findIndex(
+ (variant) => variant.id === variantId
+ );
if (variantIndex !== -1) {
- product.variants[variantIndex].quantity = parseInt(value, 10) // Pastikan untuk mengubah ke tipe number jika diperlukan
+ product.variants[variantIndex].quantity = parseInt(value, 10); // Pastikan untuk mengubah ke tipe number jika diperlukan
// Lakukan sesuatu jika nilai quantity diubah
}
- }
+ };
const handleBuy = (variant) => {
- const quantity = variantQuantityRefs.current[variant].value
- if (!validQuantity(quantity)) return
+ const quantity = variantQuantityRefs.current[variant].value;
+ if (!validQuantity(quantity)) return;
if (!auth) {
- return redirectToLogin('buy', variant, quantity)
+ return redirectToLogin('buy', variant, quantity);
}
- let source = 'buy'
- updateCart(variant, quantity, source)
- router.push(`/shop/checkout?source=buy`)
- }
+ let source = 'buy';
+ updateCart(variant, quantity, source);
+ router.push(`/shop/checkout?source=buy`);
+ };
- const variantSectionRef = useRef(null)
+ const variantSectionRef = useRef(null);
const goToVariantSection = () => {
if (variantSectionRef.current) {
- const position = variantSectionRef.current.getBoundingClientRect()
+ const position = variantSectionRef.current.getBoundingClientRect();
window.scrollTo({
top: position.top - 120 + window.pageYOffset,
- behavior: 'smooth'
- })
+ behavior: 'smooth',
+ });
}
- }
+ };
const handlePromoClick = (variantId) => {
- setSelectVariantPromoActive(variantId)
- setPromotionType(true)
- }
+ setSelectVariantPromoActive(variantId);
+ setPromotionType(true);
+ };
const productSimilarQuery = [
product?.name,
`fq=-product_id_i:${product.id}`,
- `fq=-manufacture_id_i:${product.manufacture?.id || 0}`
- ].join('&')
+ `fq=-manufacture_id_i:${product.manufacture?.id || 0}`,
+ ].join('&');
- const [productSimilarInBrand, setProductSimilarInBrand] = useState(null)
+ const [productSimilarInBrand, setProductSimilarInBrand] = useState(null);
useEffect(() => {
const loadProductSimilarInBrand = async () => {
- const productSimilarQuery = [product?.name, `fq=-product_id_i:${product.id}`].join('&')
- const source = 'right'
- const dataProductSimilar = await productSimilarApi({ query: productSimilarQuery, source })
- setProductSimilarInBrand(dataProductSimilar.products)
- }
- if (!productSimilarInBrand) loadProductSimilarInBrand()
- }, [product, productSimilarInBrand])
+ const productSimilarQuery = [
+ product?.name,
+ `fq=-product_id_i:${product.id}`,
+ ].join('&');
+ const source = 'right';
+ const dataProductSimilar = await productSimilarApi({
+ query: productSimilarQuery,
+ source,
+ });
+ setProductSimilarInBrand(dataProductSimilar.products);
+ };
+ if (!productSimilarInBrand) loadProductSimilarInBrand();
+ }, [product, productSimilarInBrand]);
useEffect(() => {
const fetchData = async () => {
const promises = product.variants.map(async (variant) => {
- const dataSLA = await odooApi('GET', `/api/v1/product_variant/${variant.id}/stock`)
+ const dataSLA = await odooApi(
+ 'GET',
+ `/api/v1/product_variant/${variant.id}/stock`
+ );
return {
...variant,
- sla: dataSLA
- }
- })
- const variantData = await Promise.all(promises)
- product.variants = variantData
+ sla: dataSLA,
+ };
+ });
+ const variantData = await Promise.all(promises);
+ product.variants = variantData;
- setIsLoadingSLA(false)
- }
- if (product.variantTotal == 1) fetchData()
- }, [product])
+ setIsLoadingSLA(false);
+ };
+ if (product.variantTotal == 1) fetchData();
+ }, [product]);
return (
<DesktopView>
<div className='container mx-auto pt-10'>
<Breadcrumb productId={product.id} productName={product.name} />
- <div className='flex'>
- <div className='w-full flex flex-wrap'>
- <div className='w-5/12'>
- <div className='relative mb-2'>
- {product?.flashSale?.remainingTime > 0 &&
- lowestPrice?.price.discountPercentage > 0 && (
- <div className={`absolute bottom-0 w-full`}>
- <div className='absolute bottom-0 w-full h-full'>
- <ImageNext
- src={backgorundFlashSale || '/images/GAMBAR-BG-FLASH-SALE.jpg'}
- width={1000}
- height={100}
- />
- </div>
- <div className='relative'>
- <div className='flex gap-x-2 items-center p-2'>
- <div className='bg-yellow-400 rounded-full p-1 h-9 w-20 flex items-center justify-center '>
- <span className='text-lg font-bold'>
- {Math.floor(product.lowestPrice.discountPercentage)}%
- </span>
- </div>
- <div
- className={`bg-red-600 border border-solid border-yellow-400 rounded-full h-9 p-2 flex w-[50%] items-center justify-center gap-x-4`}
- >
- <ImageNext
- src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg'
- width={17}
- height={10}
- />
- <span className='text-white text-lg font-semibold'>
- {product?.flashSale?.tag != 'false' || product?.flashSale?.tag
- ? product?.flashSale?.tag
- : 'FLASH SALE'}
- </span>
- </div>
- <div>
- <CountDown2 initialTime={product.flashSale.remainingTime} />
- </div>
+
+ <div className='w-full flex flex-wrap'>
+ <div className='w-3/12'>
+ <div className='relative mb-2'>
+ {product?.flashSale?.remainingTime > 0 &&
+ lowestPrice?.price.discountPercentage > 0 && (
+ <div className={`absolute bottom-0 w-full`}>
+ <div className='absolute bottom-0 w-full h-full'>
+ <ImageNext
+ src={
+ backgorundFlashSale ||
+ '/images/BG-FLASH-SALE.jpg'
+ }
+ width={1000}
+ height={100}
+ className='h-full'
+ />
+ </div>
+ <div className='relative'>
+ <div className='flex gap-x-2 items-center p-2'>
+ <div className='bg-yellow-400 rounded-full p-1 h-9 w-20 flex items-center justify-center '>
+ <span className='text-lg font-bold'>
+ {Math.floor(product.lowestPrice.discountPercentage)}
+ %
+ </span>
+ </div>
+ <div
+ className={`bg-red-600 border border-solid border-yellow-400 rounded-full h-9 p-2 flex w-[50%] items-center justify-center gap-x-1`}
+ >
+ <ImageNext
+ src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg'
+ width={17}
+ height={10}
+ />
+ <span className='text-white text-sm font-semibold'>
+ {product?.flashSale?.tag != 'false' ||
+ product?.flashSale?.tag
+ ? product?.flashSale?.tag
+ : 'FLASH SALE'}
+ </span>
+ </div>
+ <div>
+ <CountDown2
+ initialTime={product.flashSale.remainingTime}
+ />
</div>
</div>
</div>
- )}
- <Image
- src={product.image}
- alt={product.name}
- className='h-[430px] object-contain object-center w-full border border-gray_r-4'
- />
- </div>
- <div>
- <p className='text-justify text-xs leading-5'>
- <span className='font-semibold '>Keterangan : </span>Gambar atau foto berperan
- sebagai ilustrasi produk. Kadang tidak sesuai dengan kondisi terbaru dengan
- berbagai perubahan dan perbaikan. Hubungi tim sales kami untuk informasi yang
- lebih baik perihal gambar di 021-2933 8828.
- </p>
- </div>
- </div>
-
- <div className='w-7/12 px-4'>
- <h1 className='text-title-md leading-10 font-medium'>{product?.name}</h1>
- <div className='mt-10'>
- <div className='flex p-3'>
- <div className='w-4/12 text-gray_r-12/70'>Nomor SKU</div>
- <div className='w-8/12'>SKU-{product.id}</div>
- </div>
- <div className='flex p-3 bg-gray_r-4'>
- <div className='w-4/12 text-gray_r-12/70'>Part Number</div>
- <div className='w-8/12'>{product.code || '-'}</div>
- </div>
- <div className='flex p-3'>
- <div className='w-4/12 text-gray_r-12/70'>Manufacture</div>
- <div className='w-8/12'>
- {product.manufacture?.name ? (
- <Link
- href={createSlug(
- '/shop/brands/',
- product.manufacture?.name,
- product.manufacture?.id
- )}
- >
- {product.manufacture?.name}
- </Link>
- ) : (
- <div>-</div>
- )}
</div>
- </div>
- <div className='flex p-3 items-center bg-gray_r-4'>
- <div className='w-4/12 text-gray_r-12/70'>Persiapan Barang</div>
- <div className='w-8/12'>
- {product.variants.length > 1 && (
- <button
- type='button'
- onClick={goToVariantSection}
- className={`flex gap-x-1 items-center p-2 rounded-lg w-auto btn-light`}
- >
- <span className='text-red-600 text-sm'>Lihat Selengkapnya</span>
- </button>
- )}
+ )}
+ <Image
+ src={product.image}
+ alt={product.name}
+ className='h-[430px] object-contain object-center w-full border border-gray_r-4'
+ />
+ </div>
+ <div>
+ <p className='text-justify text-xs leading-5'>
+ <span className='font-semibold '>Keterangan : </span>Gambar atau
+ foto berperan sebagai ilustrasi produk. Kadang tidak sesuai
+ dengan kondisi terbaru dengan berbagai perubahan dan perbaikan.
+ Hubungi tim sales kami untuk informasi yang lebih baik perihal
+ gambar di 021-2933 8828.
+ </p>
+ </div>
+ </div>
- {product.variants.length === 1 && (
- <>
- {!product.variants[0]?.sla && <Skeleton width='20%' height='16px' />}
- {product.variants[0]?.sla && (
- <Tooltip
- placement='top'
- label={`Masa Persiapan Barang ${product.variants[0]?.sla?.slaDate}`}
- >
- <Box className='w-fit flex items-center gap-x-2'>
- {product.variants[0]?.sla?.slaDate}
- <Info size={16} />
- </Box>
- </Tooltip>
- )}
- </>
- )}
- </div>
+ <div className='w-6/12 px-6'>
+ <h1 className='text-title-md leading-10 font-medium'>
+ {product?.name}
+ </h1>
+ <div className='mt-10'>
+ <div className='flex p-3'>
+ <div className='w-4/12 text-gray_r-12/70'>Nomor SKU</div>
+ <div className='w-8/12'>SKU-{product.id}</div>
+ </div>
+ <div className='flex p-3 bg-gray_r-4'>
+ <div className='w-4/12 text-gray_r-12/70'>Part Number</div>
+ <div className='w-8/12'>{product.code || '-'}</div>
+ </div>
+ <div className='flex p-3'>
+ <div className='w-4/12 text-gray_r-12/70'>Manufacture</div>
+ <div className='w-8/12'>
+ {product.manufacture?.name ? (
+ <Link
+ href={createSlug(
+ '/shop/brands/',
+ product.manufacture?.name,
+ product.manufacture?.id
+ )}
+ >
+ {product.manufacture?.name}
+ </Link>
+ ) : (
+ <div>-</div>
+ )}
</div>
+ </div>
+ <div className='flex p-3 items-center bg-gray_r-4'>
+ <div className='w-4/12 text-gray_r-12/70'>Persiapan Barang</div>
+ <div className='w-8/12'>
+ {product.variants.length > 1 && (
+ <button
+ type='button'
+ onClick={goToVariantSection}
+ className={`flex gap-x-1 items-center p-2 rounded-lg w-auto btn-light`}
+ >
+ <span className='text-red-600 text-sm'>
+ Lihat Selengkapnya
+ </span>
+ </button>
+ )}
- {product.variants.length === 1 && (
- <div className='flex p-3 '>
- <div className='w-4/12 text-gray_r-12/70'>Stock</div>
- <div className='w-8/12'>
- {!product.variants[0]?.sla && <Skeleton width='10%' height='16px' />}
- {product?.variants[0].sla?.qty > 0 && (
- <span>{product?.variants[0].sla?.qty}</span>
+ {product.variants.length === 1 && (
+ <>
+ {!product.variants[0]?.sla && (
+ <Skeleton width='20%' height='16px' />
)}
- {product?.variants[0].sla?.qty == 0 && (
- <a
- href={whatsappUrl('product', {
- name: product.name,
- manufacture: product?.manufacture?.name,
- url: createSlug('/shop/product/', product.name, product.id, true)
- })}
- className='text-danger-500 font-medium'
+ {product.variants[0]?.sla && (
+ <Tooltip
+ placement='top'
+ label={`Masa Persiapan Barang ${product.variants[0]?.sla?.slaDate}`}
>
- Tanya Admin
- </a>
+ <Box className='w-fit flex items-center gap-x-2'>
+ {product.variants[0]?.sla?.slaDate}
+ <Info size={16} />
+ </Box>
+ </Tooltip>
)}
- </div>
- </div>
- )}
+ </>
+ )}
+ </div>
+ </div>
- <div className={`flex p-3 ${product.variants.length > 1 ? '' : 'bg-gray_r-4'} `}>
- <div className='w-4/12 text-gray_r-12/70'>Berat Barang</div>
+ {product.variants.length === 1 && (
+ <div className='flex p-3 '>
+ <div className='w-4/12 text-gray_r-12/70'>Stock</div>
<div className='w-8/12'>
- {product?.weight > 0 && <span>{product?.weight} KG</span>}
- {product?.weight == 0 && (
+ {!product.variants[0]?.sla && (
+ <Skeleton width='10%' height='16px' />
+ )}
+ {product?.variants[0].sla?.qty > 0 && (
+ <span>{product?.variants[0].sla?.qty}</span>
+ )}
+ {product?.variants[0].sla?.qty == 0 && (
<a
- href={whatsappUrl('productWeight', {
+ href={whatsappUrl('product', {
name: product.name,
- url: createSlug('/shop/product/', product.name, product.id, true)
+ manufacture: product?.manufacture?.name,
+ url: createSlug(
+ '/shop/product/',
+ product.name,
+ product.id,
+ true
+ ),
})}
className='text-danger-500 font-medium'
>
- Tanya Berat
+ Tanya Admin
</a>
)}
</div>
</div>
- {product.variants.length <= 1 && (
- <div className='pt-3'>
- <div className='flex mt-1'>
- <PromotionType
- variantId={product.variants[0].id}
- setPromotionActiveId={setPromotionActiveId}
- promotionActiveId={promotionActiveId}
- quantity={quantityActive}
- product={product}
- ></PromotionType>
- </div>
- </div>
- )}
- </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)}
+ <div
+ className={`flex p-3 ${
+ product.variants.length > 1 ? '' : 'bg-gray_r-4'
+ } `}
+ >
+ <div className='w-4/12 text-gray_r-12/70'>Berat Barang</div>
+ <div className='w-8/12'>
+ {product?.weight > 0 && <span>{product?.weight} KG</span>}
+ {product?.weight == 0 && (
+ <a
+ href={whatsappUrl('productWeight', {
+ name: product.name,
+ url: createSlug(
+ '/shop/product/',
+ product.name,
+ product.id,
+ true
+ ),
+ })}
+ className='text-danger-500 font-medium'
>
- {option.label}
- </TabButton>
- ))}
+ Tanya Berat
+ </a>
+ )}
</div>
- <div className='flex'>
- <div className='w-3/4 leading-8 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>
+ {product.variants.length <= 1 && (
+ <div className='pt-3'>
+ <div className='flex mt-1'>
+ <PromotionType
+ variantId={product.variants[0].id}
+ setPromotionActiveId={setPromotionActiveId}
+ promotionActiveId={promotionActiveId}
+ quantity={quantityActive}
+ product={product}
+ ></PromotionType>
+ <ProductPromoSection productId={product.variants[0].id} />
</div>
</div>
- </div>
+ )}
</div>
</div>
- <div className='w-[30%]'>
- {product.variants.length > 1 && product.lowestPrice.priceDiscount > 0 && (
- <div className='text-gray_r-12/80'>Harga mulai dari: </div>
- )}
+ <div className='w-3/12'>
+ {product.variants.length > 1 &&
+ product.lowestPrice.priceDiscount > 0 && (
+ <div className='text-gray_r-12/80'>Harga mulai dari: </div>
+ )}
{/* {lowestPrice?.discountPercentage > 0 && (
<div className='flex gap-x-1 items-center mt-2'>
@@ -441,7 +467,8 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
{sellingProductFormat(product?.qtySold) + ' Terjual'}
</div>
)}
- {product?.flashSale?.id && lowestPrice?.price.discountPercentage > 0 ? (
+ {product?.flashSale?.id &&
+ lowestPrice?.price.discountPercentage > 0 ? (
<>
<div className='flex gap-x-1 items-center mt-2'>
<div className='badge-solid-red text-caption-1'>
@@ -456,7 +483,10 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
</div>
<div className='text-gray_r-9 text-base font-normal mt-1'>
Termasuk PPN:{' '}
- {currencyFormat(lowestPrice?.price.priceDiscount * process.env.NEXT_PUBLIC_PPN)}
+ {currencyFormat(
+ lowestPrice?.price.priceDiscount *
+ process.env.NEXT_PUBLIC_PPN
+ )}
</div>
</>
) : (
@@ -466,7 +496,9 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
{currencyFormat(lowestPrice?.price.price)}
<div className='text-gray_r-9 text-base font-normal mt-1'>
Termasuk PPN:{' '}
- {currencyFormat(lowestPrice?.price.price * process.env.NEXT_PUBLIC_PPN)}
+ {currencyFormat(
+ lowestPrice?.price.price * process.env.NEXT_PUBLIC_PPN
+ )}
</div>
</>
) : (
@@ -476,7 +508,12 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
href={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
+ ),
})}
className='text-danger-500 underline'
rel='noopener noreferrer'
@@ -524,7 +561,10 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
)}
<div className='flex mt-4'>
- <button className='flex items-center gap-x-1' onClick={toggleWishlist}>
+ <button
+ className='flex items-center gap-x-1'
+ onClick={toggleWishlist}
+ >
{wishlist.data?.productTotal > 0 ? (
<HeartIcon className='w-6 fill-danger-500 text-danger-500' />
) : (
@@ -538,7 +578,7 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
<div className='font-medium text-center p-4 bg-gray_r-1 border-b border-gray_r-6 sticky top-0 z-10'>
Produk Serupa
</div>
- <div className='h-full divide-y divide-gray_r-6 max-h-96'>
+ <div className='h-full divide-y divide-gray_r-6 max-h-[550px]'>
{productSimilarInBrand &&
productSimilarInBrand?.map((product) => (
<div className='py-2' key={product.id}>
@@ -550,6 +590,42 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
</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-8 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>
+
{product.variants.length > 1 && (
<div className='mt-12' ref={variantSectionRef}>
<div className='text-h-lg font-semibold mb-6'>Varian Produk</div>
@@ -571,7 +647,9 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
<tr key={variant.id}>
<td className='flex items-center justify-center gap-x-1'>
{variant.isFlashsale && (
- <span className='blink-color-flash-sale'>&#128498;</span>
+ <span className='blink-color-flash-sale'>
+ &#128498;
+ </span>
)}
{variant.code}
</td>
@@ -580,11 +658,13 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
<ColumnsSLA variant={variant} product={product} />
</LazyLoadComponent>
<td>
- {variant.isFlashsale && variant?.price?.discountPercentage > 0 ? (
+ {variant.isFlashsale &&
+ variant?.price?.discountPercentage > 0 ? (
<>
<div className='flex items-center gap-x-1 justify-center'>
<div className='badge-solid-red text-caption-1'>
- {Math.floor(variant?.price?.discountPercentage)}%
+ {Math.floor(variant?.price?.discountPercentage)}
+ %
</div>
<div className='line-through text-caption-1 text-gray_r-11'>
{currencyFormat(variant?.price?.price)}
@@ -596,7 +676,8 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
<div className=' text-caption-1 text-gray_r-11 mb-1'>
Inc. PPN:{' '}
{currencyFormat(
- variant.price.priceDiscount * process.env.NEXT_PUBLIC_PPN
+ variant.price.priceDiscount *
+ process.env.NEXT_PUBLIC_PPN
)}
</div>
</>
@@ -610,7 +691,8 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
<div className=' text-caption-1 text-gray_r-11 mb-1'>
Inc. PPN:{' '}
{currencyFormat(
- variant?.price?.price * process.env.NEXT_PUBLIC_PPN
+ variant?.price?.price *
+ process.env.NEXT_PUBLIC_PPN
)}
</div>
</>
@@ -619,7 +701,12 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
href={whatsappUrl('product', {
name: variant.name,
manufacture: product.manufacture?.name,
- url: createSlug('/shop/product/', product.name, product.id, true)
+ url: createSlug(
+ '/shop/product/',
+ product.name,
+ product.id,
+ true
+ ),
})}
className='text-red_r-11'
rel='noopener noreferrer'
@@ -705,11 +792,14 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
)}
<div className='my-12'>
- <div className='text-h-lg font-semibold mb-6'>Kamu Mungkin Juga Suka</div>
+ <div className='text-h-lg font-semibold mb-6'>
+ Kamu Mungkin Juga Suka
+ </div>
<LazyLoad>
<ProductSimilar query={productSimilarQuery} />
</LazyLoad>
</div>
+
<BottomPopup
className=' !h-[75%]'
title='Pakai Promo'
@@ -728,6 +818,7 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
></PromotionType>
</div>
</BottomPopup>
+
<BottomPopup
className='!container'
title='Berhasil Ditambahkan'
@@ -742,16 +833,23 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
className='h-32 object-contain object-center w-full border border-gray_r-4'
/>
</div>
- <div className='ml-3 flex flex-1 items-center font-normal'>{product.name}</div>
+ <div className='ml-3 flex flex-1 items-center font-normal'>
+ {product.name}
+ </div>
<div className='ml-3 flex items-center font-normal'>
- <Link href='/shop/cart' className='flex-1 py-2 text-gray_r-12 btn-yellow'>
+ <Link
+ href='/shop/cart'
+ className='flex-1 py-2 text-gray_r-12 btn-yellow'
+ >
Lihat Keranjang
</Link>
</div>
</div>
<div className='mt-8 mb-4'>
- <div className='text-h-sm font-semibold mb-6'>Kamu Mungkin Juga Suka</div>
+ <div className='text-h-sm font-semibold mb-6'>
+ Kamu Mungkin Juga Suka
+ </div>
<LazyLoad>
<ProductSimilar query={productSimilarQuery} />
</LazyLoad>
@@ -759,29 +857,33 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
</BottomPopup>
</div>
</DesktopView>
- )
-}
+ );
+};
const informationTabOptions = [
{ value: 'description', label: 'Deskripsi' },
- { value: 'information', label: 'Info Penting' }
-]
+ { value: 'information', label: 'Info Penting' },
+];
const TabButton = ({ children, active, ...props }) => {
const activeClassName = active
? 'text-danger-500 underline underline-offset-4'
- : 'text-gray_r-12/80'
+ : 'text-gray_r-12/80';
return (
- <button {...props} type='button' className={`font-medium ${activeClassName}`}>
+ <button
+ {...props}
+ type='button'
+ className={`font-medium ${activeClassName}`}
+ >
{children}
</button>
- )
-}
+ );
+};
const TabContent = ({ children, active, className = '', ...props }) => (
<div {...props} className={`${active ? 'block' : 'hidden'} ${className}`}>
{children}
</div>
-)
+);
-export default ProductDesktop
+export default ProductDesktop;
diff --git a/src/lib/product/components/Product/ProductDesktopVariant.jsx b/src/lib/product/components/Product/ProductDesktopVariant.jsx
index ef61bafd..09b30a44 100644
--- a/src/lib/product/components/Product/ProductDesktopVariant.jsx
+++ b/src/lib/product/components/Product/ProductDesktopVariant.jsx
@@ -1,137 +1,155 @@
-import Image from '@/core/components/elements/Image/Image'
-import Link from '@/core/components/elements/Link/Link'
-import DesktopView from '@/core/components/views/DesktopView'
-import currencyFormat from '@/core/utils/currencyFormat'
-import { HeartIcon } from '@heroicons/react/24/outline'
-import { useCallback, useEffect, useRef, useState } from 'react'
-import LazyLoad from 'react-lazy-load'
-import ProductSimilar from '../ProductSimilar'
-import { toast } from 'react-hot-toast'
-import { updateItemCart } from '@/core/utils/cart'
-import { useRouter } from 'next/router'
-import { createSlug } from '@/core/utils/slug'
-import BottomPopup from '@/core/components/elements/Popup/BottomPopup'
-import ProductCard from '../ProductCard'
-import productSimilarApi from '../../api/productSimilarApi'
-import whatsappUrl from '@/core/utils/whatsappUrl'
-import useAuth from '@/core/hooks/useAuth'
-import odooApi from '@/core/api/odooApi'
-import { useProductCartContext } from '@/contexts/ProductCartContext'
-import { Box, Skeleton, Tooltip } from '@chakra-ui/react'
-import { Info } from 'lucide-react'
-
-const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant }) => {
- const router = useRouter()
- const auth = useAuth()
- const { slug } = router.query
-
- const [lowestPrice, setLowestPrice] = useState(null)
-
- const [addCartAlert, setAddCartAlert] = useState(false)
- const [isLoadingSLA, setIsLoadingSLA] = useState(true)
-
- const { setRefreshCart } = useProductCartContext()
+
+import { Box, Skeleton, Tooltip } from '@chakra-ui/react';
+import { HeartIcon } from '@heroicons/react/24/outline';
+import { Info } from 'lucide-react';
+import { useRouter } from 'next/router';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { toast } from 'react-hot-toast';
+import LazyLoad from 'react-lazy-load';
+
+import { useProductCartContext } from '@/contexts/ProductCartContext';
+import odooApi from '@/core/api/odooApi';
+import Image from '@/core/components/elements/Image/Image';
+import Link from '@/core/components/elements/Link/Link';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import DesktopView from '@/core/components/views/DesktopView';
+import useAuth from '@/core/hooks/useAuth';
+import { updateItemCart } from '@/core/utils/cart';
+import currencyFormat from '@/core/utils/currencyFormat';
+import { createSlug } from '@/core/utils/slug';
+import whatsappUrl from '@/core/utils/whatsappUrl';
+
+import productSimilarApi from '../../api/productSimilarApi';
+import ProductCard from '../ProductCard';
+import ProductSimilar from '../ProductSimilar';
+
+const ProductDesktopVariant = ({
+ product,
+ wishlist,
+ toggleWishlist,
+ isVariant,
+}) => {
+ const router = useRouter();
+ const auth = useAuth();
+ const { slug } = router.query;
+
+ const [lowestPrice, setLowestPrice] = useState(null);
+
+ const [addCartAlert, setAddCartAlert] = useState(false);
+ const [isLoadingSLA, setIsLoadingSLA] = useState(true);
+
+ const { setRefreshCart } = useProductCartContext();
const getLowestPrice = useCallback(() => {
- const lowest = product.price
+ const lowest = product.price;
/* const lowest = prices.reduce((lowest, price) => {
return price.priceDiscount < lowest.priceDiscount ? price : lowest
}, prices[0])*/
- return lowest
- }, [product])
+ return lowest;
+ }, [product]);
useEffect(() => {
- const lowest = getLowestPrice()
- setLowestPrice(lowest)
- }, [getLowestPrice])
+ const lowest = getLowestPrice();
+ setLowestPrice(lowest);
+ }, [getLowestPrice]);
- const [informationTab, setInformationTab] = useState(informationTabOptions[0].value)
+ const [informationTab, setInformationTab] = useState(
+ informationTabOptions[0].value
+ );
- const variantQuantityRefs = useRef([])
+ const variantQuantityRefs = useRef([]);
const setVariantQuantityRef = (variantId) => (element) => {
- variantQuantityRefs.current[variantId] = element
- }
+ variantQuantityRefs.current[variantId] = element;
+ };
const validQuantity = (quantity) => {
- let isValid = true
+ let isValid = true;
if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) {
- toast.error('Jumlah barang minimal 1')
- isValid = false
+ toast.error('Jumlah barang minimal 1');
+ isValid = false;
}
- return isValid
- }
+ return isValid;
+ };
const handleAddToCart = (variant) => {
if (!auth) {
- router.push(`/login?next=/shop/product/${slug}`)
- return
+ router.push(`/login?next=/shop/product/${slug}`);
+ return;
}
- const quantity = variantQuantityRefs.current[product.id].value
- if (!validQuantity(quantity)) return
+ const quantity = variantQuantityRefs.current[product.id].value;
+ if (!validQuantity(quantity)) return;
updateItemCart({
productId: product.id,
quantity,
programLineId: null,
selected: true,
- source: null
+ source: null,
}).then(() => {
- setRefreshCart(true)
- })
- setAddCartAlert(true)
- }
+ setRefreshCart(true);
+ });
+ setAddCartAlert(true);
+ };
const handleBuy = (variant) => {
- const quantity = variantQuantityRefs.current[product.id].value
- if (!validQuantity(quantity)) return
+ const quantity = variantQuantityRefs.current[product.id].value;
+ if (!validQuantity(quantity)) return;
updateItemCart({
productId: variant,
quantity,
programLineId: null,
selected: true,
- source: 'buy'
- })
- router.push(`/shop/checkout?source=buy`)
- }
+ source: 'buy',
+ });
+ router.push(`/shop/checkout?source=buy`);
+ };
- const variantSectionRef = useRef(null)
+ const variantSectionRef = useRef(null);
const goToVariantSection = () => {
if (variantSectionRef.current) {
- const position = variantSectionRef.current.getBoundingClientRect()
+ const position = variantSectionRef.current.getBoundingClientRect();
window.scrollTo({
top: position.top - 120 + window.pageYOffset,
- behavior: 'smooth'
- })
+ behavior: 'smooth',
+ });
}
- }
+ };
const productSimilarQuery = [
product?.name,
`fq=-product_id_i:${product.id}`,
- `fq=-manufacture_id_i:${product.manufacture?.id || 0}`
- ].join('&')
+ `fq=-manufacture_id_i:${product.manufacture?.id || 0}`,
+ ].join('&');
- const [productSimilarInBrand, setProductSimilarInBrand] = useState(null)
+ const [productSimilarInBrand, setProductSimilarInBrand] = useState(null);
useEffect(() => {
const loadProductSimilarInBrand = async () => {
- const productSimilarQuery = [product?.name, `fq=-product_id_i:${product.id}`].join('&')
- const dataProductSimilar = await productSimilarApi({ query: productSimilarQuery })
- setProductSimilarInBrand(dataProductSimilar.products)
- }
- if (!productSimilarInBrand) loadProductSimilarInBrand()
- }, [product, productSimilarInBrand])
+ const productSimilarQuery = [
+ product?.name,
+ `fq=-product_id_i:${product.id}`,
+ ].join('&');
+ const dataProductSimilar = await productSimilarApi({
+ query: productSimilarQuery,
+ });
+ setProductSimilarInBrand(dataProductSimilar.products);
+ };
+ if (!productSimilarInBrand) loadProductSimilarInBrand();
+ }, [product, productSimilarInBrand]);
useEffect(() => {
const fetchData = async () => {
- const dataSLA = await odooApi('GET', `/api/v1/product_variant/${product.id}/stock`)
- product.sla = dataSLA
+ const dataSLA = await odooApi(
+ 'GET',
+ `/api/v1/product_variant/${product.id}/stock`
+ );
+ product.sla = dataSLA;
- setIsLoadingSLA(false)
- }
- fetchData()
- }, [product])
+ setIsLoadingSLA(false);
+ };
+ fetchData();
+ }, [product]);
return (
<DesktopView>
@@ -140,14 +158,16 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
<div className='w-full flex flex-wrap'>
<div className='w-5/12'>
<Image
- src={product.image}
+ src={product.image + '?variant=True'}
alt={product.name}
className='h-[430px] object-contain object-center w-full border border-gray_r-4'
/>
</div>
<div className='w-7/12 px-4'>
- <h1 className='text-title-md leading-10 font-medium'>{product?.name}</h1>
+ <h1 className='text-title-md leading-10 font-medium'>
+ {product?.name}
+ </h1>
<div className='mt-10'>
<div className='flex p-3'>
<div className='w-4/12 text-gray_r-12/70'>Nomor SKU</div>
@@ -177,7 +197,9 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
</div>
<div className='flex p-3 items-center bg-gray_r-4'>
- <div className='w-4/12 text-gray_r-12/70'>Persiapan Barang</div>
+ <div className='w-4/12 text-gray_r-12/70'>
+ Persiapan Barang
+ </div>
<div className='w-8/12'>
{!product?.sla && <Skeleton width='20%' height='16px' />}
{product?.sla && (
@@ -203,8 +225,13 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
<a
href={whatsappUrl('product', {
name: product.name,
- manufacture: product?.manufacture?.name,
- url: createSlug('/shop/product/', product.name, product.id, true)
+ manufacture: product?.manufacture?.name,
+ url: createSlug(
+ '/shop/product/',
+ product.name,
+ product.id,
+ true
+ ),
})}
className='text-danger-500 font-medium'
>
@@ -221,7 +248,12 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
<a
href={whatsappUrl('productWeight', {
name: product.name,
- url: createSlug('/shop/product/', product.name, product.id, true)
+ url: createSlug(
+ '/shop/product/',
+ product.name,
+ product.id,
+ true
+ ),
})}
className='text-danger-500 font-medium'
>
@@ -233,44 +265,23 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
</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 && product?.price?.discountPercentage > 0? (
+ {product?.isFlashsale > 0 &&
+ product?.price?.discountPercentage > 0 ? (
<>
<div className='flex gap-x-1 items-center mt-2'>
<div className='badge-solid-red text-caption-1'>
@@ -285,7 +296,9 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
</div>
<div className='text-gray_r-9 text-base font-normal mt-1'>
Termasuk PPN:{' '}
- {currencyFormat(product?.price?.priceDiscount * process.env.NEXT_PUBLIC_PPN)}
+ {currencyFormat(
+ product?.price?.priceDiscount * process.env.NEXT_PUBLIC_PPN
+ )}
</div>
</>
) : (
@@ -295,7 +308,9 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
{currencyFormat(product?.price?.price)}
<div className='text-gray_r-9 text-base font-normal mt-1'>
Termasuk PPN:{' '}
- {currencyFormat(product?.price?.price * process.env.NEXT_PUBLIC_PPN)}
+ {currencyFormat(
+ product?.price?.price * process.env.NEXT_PUBLIC_PPN
+ )}
</div>
</>
) : (
@@ -305,7 +320,12 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
href={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
+ ),
})}
className='text-danger-500 underline'
rel='noopener noreferrer'
@@ -340,7 +360,10 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
</button>
</div>
<div className='flex mt-4'>
- <button className='flex items-center gap-x-1' onClick={toggleWishlist}>
+ <button
+ className='flex items-center gap-x-1'
+ onClick={toggleWishlist}
+ >
{wishlist.data?.productTotal > 0 ? (
<HeartIcon className='w-6 fill-danger-500 text-danger-500' />
) : (
@@ -366,7 +389,9 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
</div>
<div className='my-12'>
- <div className='text-h-lg font-semibold mb-6'>Kamu Mungkin Juga Suka</div>
+ <div className='text-h-lg font-semibold mb-6'>
+ Kamu Mungkin Juga Suka
+ </div>
<LazyLoad>
<ProductSimilar query={productSimilarQuery} />
</LazyLoad>
@@ -381,21 +406,28 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
<div className='flex mt-4'>
<div className='w-[10%]'>
<Image
- src={product.image}
+ src={product.image + '?variant=True'}
alt={product.name}
className='h-32 object-contain object-center w-full border border-gray_r-4'
/>
</div>
- <div className='ml-3 flex flex-1 items-center font-normal'>{product.name}</div>
+ <div className='ml-3 flex flex-1 items-center font-normal'>
+ {product.name}
+ </div>
<div className='ml-3 flex items-center font-normal'>
- <Link href='/shop/cart' className='flex-1 py-2 text-gray_r-12 btn-yellow'>
+ <Link
+ href='/shop/cart'
+ className='flex-1 py-2 text-gray_r-12 btn-yellow'
+ >
Lihat Keranjang
</Link>
</div>
</div>
<div className='mt-8 mb-4'>
- <div className='text-h-sm font-semibold mb-6'>Kamu Mungkin Juga Suka</div>
+ <div className='text-h-sm font-semibold mb-6'>
+ Kamu Mungkin Juga Suka
+ </div>
<LazyLoad>
<ProductSimilar query={productSimilarQuery} />
</LazyLoad>
@@ -403,29 +435,33 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
</BottomPopup>
</div>
</DesktopView>
- )
-}
+ );
+};
const informationTabOptions = [
{ value: 'description', label: 'Deskripsi' },
- { value: 'information', label: 'Info Penting' }
-]
+ { value: 'information', label: 'Info Penting' },
+];
const TabButton = ({ children, active, ...props }) => {
const activeClassName = active
? 'text-danger-500 underline underline-offset-4'
- : 'text-gray_r-12/80'
+ : 'text-gray_r-12/80';
return (
- <button {...props} type='button' className={`font-medium ${activeClassName}`}>
+ <button
+ {...props}
+ type='button'
+ className={`font-medium ${activeClassName}`}
+ >
{children}
</button>
- )
-}
+ );
+};
const TabContent = ({ children, active, className = '', ...props }) => (
<div {...props} className={`${active ? 'block' : 'hidden'} ${className}`}>
{children}
</div>
-)
+);
-export default ProductDesktopVariant
+export default ProductDesktopVariant;
diff --git a/src/lib/product/components/Product/ProductMobile.jsx b/src/lib/product/components/Product/ProductMobile.jsx
index e23e2fb9..113a1e42 100644
--- a/src/lib/product/components/Product/ProductMobile.jsx
+++ b/src/lib/product/components/Product/ProductMobile.jsx
@@ -1,60 +1,66 @@
-import Divider from '@/core/components/elements/Divider/Divider'
-import Image from '@/core/components/elements/Image/Image'
-import Link from '@/core/components/elements/Link/Link'
-import currencyFormat from '@/core/utils/currencyFormat'
-import { useEffect, useState } from 'react'
-import Select from 'react-select'
-import ProductSimilar from '../ProductSimilar'
-import LazyLoad from 'react-lazy-load'
-import { updateItemCart } from '@/core/utils/cart'
-import { HeartIcon } from '@heroicons/react/24/outline'
-import { useRouter } from 'next/router'
-import MobileView from '@/core/components/views/MobileView'
-import { toast } from 'react-hot-toast'
-import { createSlug } from '@/core/utils/slug'
-import BottomPopup from '@/core/components/elements/Popup/BottomPopup'
-import whatsappUrl from '@/core/utils/whatsappUrl'
-import PromotionType from '@/lib/promotinProgram/components/PromotionType'
-import { gtagAddToCart } from '@/core/utils/googleTag'
-import odooApi from '@/core/api/odooApi'
-import ImageNext from 'next/image'
-import CountDown2 from '@/core/components/elements/CountDown/CountDown2'
-import Breadcrumb from './Breadcrumb'
-import useAuth from '@/core/hooks/useAuth'
-import { sellingProductFormat } from '@/core/utils/formatValue'
+import Divider from '@/core/components/elements/Divider/Divider';
+import Image from '@/core/components/elements/Image/Image';
+import Link from '@/core/components/elements/Link/Link';
+import currencyFormat from '@/core/utils/currencyFormat';
+import { useEffect, useState } from 'react';
+import Select from 'react-select';
+import ProductSimilar from '../ProductSimilar';
+import LazyLoad from 'react-lazy-load';
+import { updateItemCart } from '@/core/utils/cart';
+import { HeartIcon } from '@heroicons/react/24/outline';
+import { useRouter } from 'next/router';
+import MobileView from '@/core/components/views/MobileView';
+import { toast } from 'react-hot-toast';
+import { createSlug } from '@/core/utils/slug';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import whatsappUrl from '@/core/utils/whatsappUrl';
+import PromotionType from '@/lib/promotinProgram/components/PromotionType';
+import { gtagAddToCart } from '@/core/utils/googleTag';
+import odooApi from '@/core/api/odooApi';
+import ImageNext from 'next/image';
+import CountDown2 from '@/core/components/elements/CountDown/CountDown2';
+import Breadcrumb from './Breadcrumb';
+import useAuth from '@/core/hooks/useAuth';
+import { sellingProductFormat } from '@/core/utils/formatValue';
+import ProductPromoSection from '~/modules/product-promo/components/Section';
const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
- const router = useRouter()
- const auth = useAuth()
- const { slug } = router.query
-
- const [quantity, setQuantity] = useState('1')
- const [selectedVariant, setSelectedVariant] = useState(null)
- const [informationTab, setInformationTab] = useState(informationTabOptions[0].value)
- const [addCartAlert, setAddCartAlert] = useState(false)
-
- const [isLoadingSLA, setIsLoadingSLA] = useState(true)
- const [promotionType, setPromotionType] = useState(false)
- const [promotionActiveId, setPromotionActiveId] = useState(null)
- const [backgorundFlashSale, setBackgorundFlashSale] = useState(null)
+ const router = useRouter();
+ const auth = useAuth();
+ const { slug } = router.query;
+
+ const [quantity, setQuantity] = useState('1');
+ const [selectedVariant, setSelectedVariant] = useState(null);
+ const [informationTab, setInformationTab] = useState(
+ informationTabOptions[0].value
+ );
+ const [addCartAlert, setAddCartAlert] = useState(false);
+
+ const [isLoadingSLA, setIsLoadingSLA] = useState(true);
+ const [promotionType, setPromotionType] = useState(false);
+ const [promotionActiveId, setPromotionActiveId] = useState(null);
+ const [backgorundFlashSale, setBackgorundFlashSale] = useState(null);
const getLowestPrice = () => {
- const prices = product.variants.map((variant) => variant.price)
+ const prices = product.variants.map((variant) => variant.price);
const lowest = prices.reduce((lowest, price) => {
- return price.priceDiscount < lowest.priceDiscount ? price : lowest
- }, prices[0])
- return lowest
- }
+ return price.priceDiscount < lowest.priceDiscount ? price : lowest;
+ }, prices[0]);
+ return lowest;
+ };
useEffect(() => {
const getBackgound = async () => {
- const get = await odooApi('GET', '/api/v1/banner?type=flash-sale-background-banner')
+ const get = await odooApi(
+ 'GET',
+ '/api/v1/banner?type=flash-sale-background-banner'
+ );
if (get.length > 0) {
- setBackgorundFlashSale(get[0].image)
+ setBackgorundFlashSale(get[0].image);
}
- }
- getBackgound()
- }, [])
+ };
+ getBackgound();
+ }, []);
const [activeVariant, setActiveVariant] = useState({
id: null,
@@ -64,40 +70,44 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
stock: product.stockTotal,
weight: product.weight,
hasProgram: false,
- qtySold: product.qtySold
- })
+ qtySold: product.qtySold,
+ });
const variantOptions = product.variants?.map((variant) => {
- let label = []
+ let label = [];
if (variant.isFlashsale) {
- label.push("<span class='blink-color-flash-sale'>&#128498;</span>")
+ label.push("<span class='blink-color-flash-sale'>&#128498;</span>");
}
if (variant.code) {
- label.push(`[${variant.code}]`)
+ label.push(`[${variant.code}]`);
}
if (variant.attributes.length > 0) {
- label.push(variant.attributes.join(', '))
+ label.push(variant.attributes.join(', '));
} else {
- label.push(product.name)
+ label.push(product.name);
}
return {
value: variant.id,
- label: label.join(' ')
- }
- })
+ label: label.join(' '),
+ };
+ });
useEffect(() => {
if (!selectedVariant && variantOptions.length == 1) {
- setSelectedVariant(variantOptions[0])
+ setSelectedVariant(variantOptions[0]);
}
- }, [selectedVariant, variantOptions])
+ }, [selectedVariant, variantOptions]);
useEffect(() => {
if (selectedVariant) {
- const variant = product.variants.find((variant) => variant.id == selectedVariant.value)
+ const variant = product.variants.find(
+ (variant) => variant.id == selectedVariant.value
+ );
const variantAttributes =
- variant.attributes.length > 0 ? ' - ' + variant.attributes.join(', ') : ''
+ variant.attributes.length > 0
+ ? ' - ' + variant.attributes.join(', ')
+ : '';
const newActiveVariant = {
id: variant.id,
@@ -108,60 +118,63 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
weight: variant.weight,
hasProgram: variant.hasProgram,
isFlashsale: variant.isFlashsale,
- qtySold: variant.qtySold
- }
+ qtySold: variant.qtySold,
+ };
- setActiveVariant(newActiveVariant)
+ setActiveVariant(newActiveVariant);
const fetchSLA = async () => {
- const dataSLA = await odooApi('GET', `/api/v1/product_variant/${variant.id}/stock`)
- setActiveVariant({ ...newActiveVariant, sla: dataSLA })
- }
- fetchSLA()
+ const dataSLA = await odooApi(
+ 'GET',
+ `/api/v1/product_variant/${variant.id}/stock`
+ );
+ setActiveVariant({ ...newActiveVariant, sla: dataSLA });
+ };
+ fetchSLA();
}
- }, [selectedVariant, product])
+ }, [selectedVariant, product]);
const validAction = () => {
- let isValid = true
+ let isValid = true;
if (!selectedVariant) {
- toast.error('Pilih varian terlebih dahulu')
- isValid = false
+ toast.error('Pilih varian terlebih dahulu');
+ isValid = false;
}
if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) {
- toast.error('Jumlah barang minimal 1')
- isValid = false
+ toast.error('Jumlah barang minimal 1');
+ isValid = false;
}
- return isValid
- }
+ return isValid;
+ };
const redirectToLogin = (action) => {
- const nextURL = `/shop/product/${slug}?action=${action}&variantId=${activeVariant.id}&qty=${quantity}`
- router.push(`/login?next=${encodeURIComponent(nextURL)}`)
- return true
- }
+ const nextURL = `/shop/product/${slug}?action=${action}&variantId=${activeVariant.id}&qty=${quantity}`;
+ router.push(`/login?next=${encodeURIComponent(nextURL)}`);
+ return true;
+ };
const handleClickCart = () => {
- if (!validAction()) return
- gtagAddToCart(activeVariant, quantity)
+ if (!validAction()) return;
+ gtagAddToCart(activeVariant, quantity);
if (!auth) {
- return redirectToLogin('add_to_cart')
+ return redirectToLogin('add_to_cart');
}
updateItemCart({
productId: activeVariant.id,
quantity,
programLineId: promotionActiveId,
- selected: true
- })
- setAddCartAlert(true)
- }
+ selected: true,
+ });
+ setAddCartAlert(true);
+ };
const handleClickBuy = () => {
- if (!validAction()) return
+ if (!validAction()) return;
if (!auth) {
- return redirectToLogin('buy')
+ return redirectToLogin('buy');
}
updateItemCart({
@@ -169,58 +182,60 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
quantity,
programLineId: promotionActiveId,
selected: true,
- source: 'buy'
- })
- router.push(`/shop/checkout?source=buy`)
- }
+ source: 'buy',
+ });
+ router.push(`/shop/checkout?source=buy`);
+ };
const productSimilarQuery = [
product?.name,
`fq=-product_id_i:${product.id}`,
- `fq=-manufacture_id_i:${product.manufacture?.id || 0}`
- ].join('&')
+ `fq=-manufacture_id_i:${product.manufacture?.id || 0}`,
+ ].join('&');
return (
<MobileView>
<Breadcrumb productId={product.id} productName={product.name} />
<div className='relative'>
- {product?.flashSale?.remainingTime > 0 && activeVariant?.price.discountPercentage > 0 && (
- <div className={`absolute bottom-0 w-full`}>
- <div className='absolute bottom-0 w-full'>
- <ImageNext
- src={backgorundFlashSale || '/images/GAMBAR-BG-FLASH-SALE.jpg'}
- width={1000}
- height={100}
- />
- </div>
- <div className='relative'>
- <div className='flex gap-x-2 items-center p-2'>
- <div className='bg-yellow-400 rounded-full p-1 h-9 w-20 flex items-center justify-center '>
- <span className='text-lg font-bold'>
- {Math.floor(product.lowestPrice.discountPercentage)}%
- </span>
- </div>
- <div
- className={`bg-red-600 border border-solid border-yellow-400 rounded-full h-9 p-2 flex w-[50%] items-center justify-center gap-x-4`}
- >
- <ImageNext
- src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg'
- width={17}
- height={10}
- />
- <span className='text-white text-lg font-semibold'>
- {product?.flashSale?.tag != 'false' || product?.flashSale?.tag
- ? product?.flashSale?.tag
- : 'FLASH SALE'}
- </span>
- </div>
- <div>
- <CountDown2 initialTime={product.flashSale.remainingTime} />
+ {product?.flashSale?.remainingTime > 0 &&
+ activeVariant?.price.discountPercentage > 0 && (
+ <div className={`absolute bottom-0 w-full`}>
+ <div className='absolute bottom-0 w-full'>
+ <ImageNext
+ src={backgorundFlashSale || '/images/BG-FLASH-SALE.jpg'}
+ width={1000}
+ height={100}
+ />
+ </div>
+ <div className='relative'>
+ <div className='flex gap-x-2 items-center p-2'>
+ <div className='bg-yellow-400 rounded-full p-1 h-9 w-20 flex items-center justify-center '>
+ <span className='text-lg font-bold'>
+ {Math.floor(product.lowestPrice.discountPercentage)}%
+ </span>
+ </div>
+ <div
+ className={`bg-red-600 border border-solid border-yellow-400 rounded-full h-9 p-2 flex w-[50%] items-center justify-center gap-x-4`}
+ >
+ <ImageNext
+ src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg'
+ width={17}
+ height={10}
+ />
+ <span className='text-white text-lg font-semibold'>
+ {product?.flashSale?.tag != 'false' ||
+ product?.flashSale?.tag
+ ? product?.flashSale?.tag
+ : 'FLASH SALE'}
+ </span>
+ </div>
+ <div>
+ <CountDown2 initialTime={product.flashSale.remainingTime} />
+ </div>
</div>
</div>
</div>
- </div>
- )}
+ )}
<Image
src={product.image}
alt={product.name}
@@ -232,7 +247,11 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
<div className='flex items-end mb-2'>
{product.manufacture?.name ? (
<Link
- href={createSlug('/shop/brands/', product.manufacture?.name, product.manufacture?.id)}
+ href={createSlug(
+ '/shop/brands/',
+ product.manufacture?.name,
+ product.manufacture?.id
+ )}
>
{product.manufacture?.name}
</Link>
@@ -249,18 +268,25 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
</div>
<h1 className='leading-6 font-medium mb-3'>{activeVariant?.name}</h1>
{product?.qtySold > 0 && (
- <div className='text-gray_r-9'>{sellingProductFormat(activeVariant?.qtySold) + ' Terjual'}</div>
+ <div className='text-gray_r-9'>
+ {sellingProductFormat(activeVariant?.qtySold) + ' Terjual'}
+ </div>
)}
{product.variants.length > 1 &&
activeVariant.price.priceDiscount > 0 &&
!selectedVariant && (
- <div className='text-gray_r-12/80 text-caption-2 mt-2 mb-1'>Harga mulai dari: </div>
+ <div className='text-gray_r-12/80 text-caption-2 mt-2 mb-1'>
+ Harga mulai dari:{' '}
+ </div>
)}
- {activeVariant.isFlashsale && activeVariant?.price?.discountPercentage > 0 ? (
+ {activeVariant.isFlashsale &&
+ activeVariant?.price?.discountPercentage > 0 ? (
<>
<div className='flex gap-x-1 items-center'>
- <div className='badge-solid-red'>{Math.floor(activeVariant?.price?.discountPercentage)}%</div>
+ <div className='badge-solid-red'>
+ {Math.floor(activeVariant?.price?.discountPercentage)}%
+ </div>
<div className='text-gray_r-11 line-through text-caption-1'>
{currencyFormat(activeVariant?.price?.price)}
</div>
@@ -270,7 +296,9 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
</div>
<div className='text-gray_r-9 text-base font-normal mt-1'>
Termasuk PPN:{' '}
- {currencyFormat(activeVariant?.price.priceDiscount * process.env.NEXT_PUBLIC_PPN)}
+ {currencyFormat(
+ activeVariant?.price.priceDiscount * process.env.NEXT_PUBLIC_PPN
+ )}
</div>
</>
) : (
@@ -280,7 +308,9 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
{currencyFormat(activeVariant?.price?.price)}
<div className='text-gray_r-9 text-base font-normal mt-1'>
Termasuk PPN:{' '}
- {currencyFormat(activeVariant?.price.price * process.env.NEXT_PUBLIC_PPN)}
+ {currencyFormat(
+ activeVariant?.price.price * process.env.NEXT_PUBLIC_PPN
+ )}
</div>
</>
) : (
@@ -289,7 +319,12 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
<a
href={whatsappUrl('product', {
name: product.name,
- url: createSlug('/shop/product/', product.name, product.id, true)
+ url: createSlug(
+ '/shop/product/',
+ product.name,
+ product.id,
+ true
+ ),
})}
className='text-danger-500 underline'
>
@@ -307,13 +342,17 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
<div>
<label className='flex justify-between'>
Pilih Varian:
- <span className='text-gray_r-11'>{product?.variantTotal} Varian</span>
+ <span className='text-gray_r-11'>
+ {product?.variantTotal} Varian
+ </span>
</label>
<Select
name='variant'
classNamePrefix='form-select'
options={variantOptions}
- formatOptionLabel={({ label }) => <div dangerouslySetInnerHTML={{ __html: label }} />}
+ formatOptionLabel={({ label }) => (
+ <div dangerouslySetInnerHTML={{ __html: label }} />
+ )}
className='mt-2'
value={selectedVariant}
onChange={(option) => setSelectedVariant(option)}
@@ -342,15 +381,27 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
onChange={(e) => setQuantity(e.target.value)}
/>
</div>
- <button type='button' className='btn-yellow flex-1' onClick={handleClickCart}>
+ <button
+ type='button'
+ className='btn-yellow flex-1'
+ onClick={handleClickCart}
+ >
Keranjang
</button>
- <button type='button' className='btn-solid-red flex-1' onClick={handleClickBuy}>
+ <button
+ type='button'
+ className='btn-solid-red flex-1'
+ onClick={handleClickBuy}
+ >
Beli
</button>
</div>
+
+ <div className='h-4' />
</div>
+ <ProductPromoSection productId={activeVariant.id} />
+
<Divider />
<div className='p-4'>
@@ -380,12 +431,16 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
type='button'
title={`Masa Persiapan Barang ${activeVariant?.sla?.slaDate}`}
className={`flex gap-x-1 items-center p-2 h-8 rounded-lg w-full ${
- activeVariant?.sla?.slaDate === 'indent' ? 'bg-indigo-900' : 'btn-light'
+ activeVariant?.sla?.slaDate === 'indent'
+ ? 'bg-indigo-900'
+ : 'btn-light'
}`}
>
<div
className={`flex-1 text-sm ${
- activeVariant?.sla?.slaDate === 'indent' ? 'text-white' : ''
+ activeVariant?.sla?.slaDate === 'indent'
+ ? 'text-white'
+ : ''
}`}
>
{activeVariant?.sla?.slaDate}
@@ -397,7 +452,9 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
stroke='currentColor'
stroke-width='1.5'
className={`w-7 h-7 text-sm ${
- activeVariant?.sla?.slaDate === 'indent' ? 'text-white' : ''
+ activeVariant?.sla?.slaDate === 'indent'
+ ? 'text-white'
+ : ''
}`}
>
<path
@@ -436,7 +493,12 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
<a
href={whatsappUrl('product', {
name: product.name,
- url: createSlug('/shop/product/', product.name, product.id, true)
+ url: createSlug(
+ '/shop/product/',
+ product.name,
+ product.id,
+ true
+ ),
})}
className='text-danger-500 font-medium'
>
@@ -445,12 +507,19 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
)}
</SpecificationContent>
<SpecificationContent label='Berat Barang'>
- {activeVariant?.weight > 0 && <span>{activeVariant?.weight} KG</span>}
+ {activeVariant?.weight > 0 && (
+ <span>{activeVariant?.weight} KG</span>
+ )}
{activeVariant?.weight == 0 && (
<a
href={whatsappUrl('productWeight', {
name: product.name,
- url: createSlug('/shop/product/', product.name, product.id, true)
+ url: createSlug(
+ '/shop/product/',
+ product.name,
+ product.id,
+ true
+ ),
})}
className='text-danger-500 font-medium'
>
@@ -464,7 +533,10 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
active={informationTab == 'description'}
className='leading-6 text-gray_r-11'
dangerouslySetInnerHTML={{
- __html: product.description != '' ? product.description : 'Belum ada deskripsi produk.'
+ __html:
+ product.description != ''
+ ? product.description
+ : 'Belum ada deskripsi produk.',
}}
/>
</div>
@@ -491,50 +563,63 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
className='h-20 object-contain object-center w-full border border-gray_r-4'
/>
</div>
- <div className='ml-3 flex flex-1 items-center text-sm font-normal'>{product.name}</div>
+ <div className='ml-3 flex flex-1 items-center text-sm font-normal'>
+ {product.name}
+ </div>
<div className='ml-3 flex items-center text-sm font-normal'>
- <Link href='/shop/cart' className='flex-1 py-2 text-gray_r-12 btn-yellow'>
+ <Link
+ href='/shop/cart'
+ className='flex-1 py-2 text-gray_r-12 btn-yellow'
+ >
Lihat Keranjang
</Link>
</div>
</div>
<div className='mt-8 mb-4'>
- <div className='text-h-sm font-semibold mb-6'>Kamu Mungkin Juga Suka</div>
+ <div className='text-h-sm font-semibold mb-6'>
+ Kamu Mungkin Juga Suka
+ </div>
<LazyLoad>
<ProductSimilar query={productSimilarQuery} />
</LazyLoad>
</div>
</BottomPopup>
</MobileView>
- )
-}
+ );
+};
const informationTabOptions = [
{ value: 'specification', label: 'Spesifikasi' },
{ value: 'description', label: 'Deskripsi' },
- { value: 'information', label: 'Info Penting' }
-]
+ { value: 'information', label: 'Info Penting' },
+];
const TabButton = ({ children, active, ...props }) => {
- const activeClassName = active ? 'text-danger-500 underline underline-offset-4' : 'text-gray_r-11'
+ const activeClassName = active
+ ? 'text-danger-500 underline underline-offset-4'
+ : 'text-gray_r-11';
return (
- <button {...props} type='button' className={`font-medium pb-1 ${activeClassName}`}>
+ <button
+ {...props}
+ type='button'
+ className={`font-medium pb-1 ${activeClassName}`}
+ >
{children}
</button>
- )
-}
+ );
+};
const TabContent = ({ children, active, className, ...props }) => (
<div {...props} className={`${active ? 'block' : 'hidden'} ${className}`}>
{children}
</div>
-)
+);
const SpecificationContent = ({ children, label }) => (
<div className='flex justify-between p-3 items-center'>
<span className='text-gray_r-11'>{label}</span>
{children}
</div>
-)
+);
-export default ProductMobile
+export default ProductMobile;
diff --git a/src/lib/product/components/Product/ProductMobileVariant.jsx b/src/lib/product/components/Product/ProductMobileVariant.jsx
index 9888e482..af9e52bb 100644
--- a/src/lib/product/components/Product/ProductMobileVariant.jsx
+++ b/src/lib/product/components/Product/ProductMobileVariant.jsx
@@ -1,37 +1,40 @@
-import Divider from '@/core/components/elements/Divider/Divider'
-import Image from '@/core/components/elements/Image/Image'
-import Link from '@/core/components/elements/Link/Link'
-import currencyFormat from '@/core/utils/currencyFormat'
-import { useEffect, useState } from 'react'
-import Select from 'react-select'
-import ProductSimilar from '../ProductSimilar'
-import LazyLoad from 'react-lazy-load'
-import { updateItemCart } from '@/core/utils/cart'
-import { HeartIcon } from '@heroicons/react/24/outline'
-import { useRouter } from 'next/router'
-import MobileView from '@/core/components/views/MobileView'
-import { toast } from 'react-hot-toast'
-import { createSlug } from '@/core/utils/slug'
-import BottomPopup from '@/core/components/elements/Popup/BottomPopup'
-import whatsappUrl from '@/core/utils/whatsappUrl'
-import { gtagAddToCart } from '@/core/utils/googleTag'
-import odooApi from '@/core/api/odooApi'
-import { Skeleton } from '@chakra-ui/react'
+import { Skeleton } from '@chakra-ui/react';
+import { HeartIcon } from '@heroicons/react/24/outline';
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-hot-toast';
+import LazyLoad from 'react-lazy-load';
+
+import odooApi from '@/core/api/odooApi';
+import Divider from '@/core/components/elements/Divider/Divider';
+import Image from '@/core/components/elements/Image/Image';
+import Link from '@/core/components/elements/Link/Link';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import MobileView from '@/core/components/views/MobileView';
+import { updateItemCart } from '@/core/utils/cart';
+import currencyFormat from '@/core/utils/currencyFormat';
+import { gtagAddToCart } from '@/core/utils/googleTag';
+import { createSlug } from '@/core/utils/slug';
+import whatsappUrl from '@/core/utils/whatsappUrl';
+
+import ProductSimilar from '../ProductSimilar';
const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
- const router = useRouter()
+ const router = useRouter();
- const [quantity, setQuantity] = useState('1')
- const [selectedVariant, setSelectedVariant] = useState(product.id)
- const [informationTab, setInformationTab] = useState(informationTabOptions[0].value)
- const [addCartAlert, setAddCartAlert] = useState(false)
+ const [quantity, setQuantity] = useState('1');
+ const [selectedVariant, setSelectedVariant] = useState(product.id);
+ const [informationTab, setInformationTab] = useState(
+ informationTabOptions[0].value
+ );
+ const [addCartAlert, setAddCartAlert] = useState(false);
- const [isLoadingSLA, setIsLoadingSLA] = useState(true)
+ const [isLoadingSLA, setIsLoadingSLA] = useState(true);
const getLowestPrice = () => {
- const lowest = product.lowestPrice
- return lowest
- }
+ const lowest = product.lowestPrice;
+ return lowest;
+ };
const [activeVariant, setActiveVariant] = useState({
id: null,
@@ -40,8 +43,8 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
price: getLowestPrice(),
stock: product.stockTotal,
weight: product.weight,
- isFlashSale: product.isFlashSale
- })
+ isFlashSale: product.isFlashSale,
+ });
useEffect(() => {
if (selectedVariant) {
@@ -52,70 +55,73 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
price: product.price,
stock: product.stockTotal,
weight: product.weight,
- isFlashSale: product.isFlashSale
- })
+ isFlashSale: product.isFlashSale,
+ });
}
- }, [selectedVariant, product])
+ }, [selectedVariant, product]);
const validAction = () => {
- let isValid = true
+ let isValid = true;
if (!selectedVariant) {
- toast.error('Pilih varian terlebih dahulu')
- isValid = false
+ toast.error('Pilih varian terlebih dahulu');
+ isValid = false;
}
if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) {
- toast.error('Jumlah barang minimal 1')
- isValid = false
+ toast.error('Jumlah barang minimal 1');
+ isValid = false;
}
- return isValid
- }
+ return isValid;
+ };
const handleClickCart = () => {
- if (!validAction()) return
- gtagAddToCart(activeVariant, quantity)
+ if (!validAction()) return;
+ gtagAddToCart(activeVariant, quantity);
updateItemCart({
productId: variant,
quantity,
programLineId: null,
selected: true,
- source: null
- })
- setAddCartAlert(true)
- }
+ source: null,
+ });
+ setAddCartAlert(true);
+ };
const handleClickBuy = () => {
- if (!validAction()) return
+ if (!validAction()) return;
updateItemCart({
productId: product.id,
quantity,
programLineId: null,
selected: true,
- source: 'buy'
- })
- router.push(`/shop/checkout?source=buy`)
- }
+ source: 'buy',
+ });
+ router.push(`/shop/checkout?source=buy`);
+ };
const productSimilarQuery = [
product?.name,
`fq=-product_id_i:${product.id}`,
- `fq=-manufacture_id_i:${product.manufacture?.id || 0}`
- ].join('&')
+ `fq=-manufacture_id_i:${product.manufacture?.id || 0}`,
+ ].join('&');
useEffect(() => {
const fetchData = async () => {
- const dataSLA = await odooApi('GET', `/api/v1/product_variant/${product.id}/stock`)
- product.sla = dataSLA
+ const dataSLA = await odooApi(
+ 'GET',
+ `/api/v1/product_variant/${product.id}/stock`
+ );
+ product.sla = dataSLA;
- setIsLoadingSLA(false)
- }
- fetchData()
- }, [product])
+ setIsLoadingSLA(false);
+ };
+ fetchData();
+ }, [product]);
return (
<MobileView>
<Image
- src={product.image}
+ src={product.image + '?variant=True'}
alt={product.name}
className='h-72 object-contain object-center w-full border-b border-gray_r-4'
/>
@@ -124,7 +130,11 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
<div className='flex items-end mb-2'>
{product.manufacture?.name ? (
<Link
- href={createSlug('/shop/brands/', product.manufacture?.name, product.manufacture?.id)}
+ href={createSlug(
+ '/shop/brands/',
+ product.manufacture?.name,
+ product.manufacture?.id
+ )}
>
{product.manufacture?.name}
</Link>
@@ -141,10 +151,13 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
</div>
<h1 className='leading-6 font-medium mb-3'>{activeVariant?.name}</h1>
- {activeVariant.isFlashSale && activeVariant?.price?.discountPercentage > 0 ? (
+ {activeVariant.isFlashSale &&
+ activeVariant?.price?.discountPercentage > 0 ? (
<>
<div className='flex gap-x-1 items-center'>
- <div className='badge-solid-red'>{activeVariant?.price?.discountPercentage}%</div>
+ <div className='badge-solid-red'>
+ {activeVariant?.price?.discountPercentage}%
+ </div>
<div className='text-gray_r-11 line-through text-caption-1'>
{currencyFormat(activeVariant?.price?.price)}
</div>
@@ -154,7 +167,9 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
</div>
<div className='text-gray_r-9 text-base font-normal mt-1'>
Termasuk PPN:{' '}
- {currencyFormat(activeVariant?.price.priceDiscount * process.env.NEXT_PUBLIC_PPN)}
+ {currencyFormat(
+ activeVariant?.price.priceDiscount * process.env.NEXT_PUBLIC_PPN
+ )}
</div>
</>
) : (
@@ -164,7 +179,9 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
{currencyFormat(activeVariant?.price?.price)}
<div className='text-gray_r-9 text-base font-normal mt-1'>
Termasuk PPN:{' '}
- {currencyFormat(activeVariant?.price.price * process.env.NEXT_PUBLIC_PPN)}
+ {currencyFormat(
+ activeVariant?.price.price * process.env.NEXT_PUBLIC_PPN
+ )}
</div>
</>
) : (
@@ -173,7 +190,12 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
<a
href={whatsappUrl('product', {
name: product.name,
- url: createSlug('/shop/product/', product.name, product.id, true)
+ url: createSlug(
+ '/shop/product/',
+ product.name,
+ product.id,
+ true
+ ),
})}
className='text-danger-500 underline'
>
@@ -199,10 +221,18 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
onChange={(e) => setQuantity(e.target.value)}
/>
</div>
- <button type='button' className='btn-yellow flex-1' onClick={handleClickCart}>
+ <button
+ type='button'
+ className='btn-yellow flex-1'
+ onClick={handleClickCart}
+ >
Keranjang
</button>
- <button type='button' className='btn-solid-red flex-1' onClick={handleClickBuy}>
+ <button
+ type='button'
+ className='btn-solid-red flex-1'
+ onClick={handleClickBuy}
+ >
Beli
</button>
</div>
@@ -238,7 +268,9 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
type='button'
title={`Masa Persiapan Barang ${product?.sla?.slaDate}`}
className={`flex gap-x-1 items-center p-2 h-8 rounded-lg w-full ${
- product?.sla?.slaDate === 'indent' ? 'bg-indigo-900' : 'btn-light'
+ product?.sla?.slaDate === 'indent'
+ ? 'bg-indigo-900'
+ : 'btn-light'
}`}
>
<div
@@ -281,14 +313,21 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
{activeVariant?.stock > 0 && (
<span className='flex gap-x-1.5'>
<div className='badge-solid-red'>Ready Stock</div>
- <div className='badge-gray'>{activeVariant?.stock > 5 ? '> 5' : '< 5'}</div>
+ <div className='badge-gray'>
+ {activeVariant?.stock > 5 ? '> 5' : '< 5'}
+ </div>
</span>
)}
{activeVariant?.stock == 0 && (
<a
href={whatsappUrl('product', {
name: product.name,
- url: createSlug('/shop/product/', product.name, product.id, true)
+ url: createSlug(
+ '/shop/product/',
+ product.name,
+ product.id,
+ true
+ ),
})}
className='text-danger-500 font-medium'
>
@@ -297,12 +336,19 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
)}
</SpecificationContent>
<SpecificationContent label='Berat Barang'>
- {activeVariant?.weight > 0 && <span>{activeVariant?.weight} KG</span>}
+ {activeVariant?.weight > 0 && (
+ <span>{activeVariant?.weight} KG</span>
+ )}
{activeVariant?.weight == 0 && (
<a
href={whatsappUrl('productWeight', {
name: product.name,
- url: createSlug('/shop/product/', product.name, product.id, true)
+ url: createSlug(
+ '/shop/product/',
+ product.name,
+ product.id,
+ true
+ ),
})}
className='text-danger-500 font-medium'
>
@@ -316,7 +362,10 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
active={informationTab == 'description'}
className='leading-6 text-gray_r-11'
dangerouslySetInnerHTML={{
- __html: product.description != '' ? product.description : 'Belum ada deskripsi produk.'
+ __html:
+ product.description != ''
+ ? product.description
+ : 'Belum ada deskripsi produk.',
}}
/>
</div>
@@ -338,55 +387,68 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
<div className='flex mt-4'>
<div className='w-[15%]'>
<Image
- src={product.image}
+ src={product.image + '?variant=True'}
alt={product.name}
className='h-20 object-contain object-center w-full border border-gray_r-4'
/>
</div>
- <div className='ml-3 flex flex-1 items-center text-sm font-normal'>{product.name}</div>
+ <div className='ml-3 flex flex-1 items-center text-sm font-normal'>
+ {product.name}
+ </div>
<div className='ml-3 flex items-center text-sm font-normal'>
- <Link href='/shop/cart' className='flex-1 py-2 text-gray_r-12 btn-yellow'>
+ <Link
+ href='/shop/cart'
+ className='flex-1 py-2 text-gray_r-12 btn-yellow'
+ >
Lihat Keranjang
</Link>
</div>
</div>
<div className='mt-8 mb-4'>
- <div className='text-h-sm font-semibold mb-6'>Kamu Mungkin Juga Suka</div>
+ <div className='text-h-sm font-semibold mb-6'>
+ Kamu Mungkin Juga Suka
+ </div>
<LazyLoad>
<ProductSimilar query={productSimilarQuery} />
</LazyLoad>
</div>
</BottomPopup>
</MobileView>
- )
-}
+ );
+};
const informationTabOptions = [
- { value: 'specification', label: 'Spesifikasi' }
+ { value: 'specification', label: 'Spesifikasi' },
// { value: 'description', label: 'Deskripsi' },
// { value: 'information', label: 'Info Penting' }
-]
+];
const TabButton = ({ children, active, ...props }) => {
- const activeClassName = active ? 'text-danger-500 underline underline-offset-4' : 'text-gray_r-11'
+ const activeClassName = active
+ ? 'text-danger-500 underline underline-offset-4'
+ : 'text-gray_r-11';
return (
- <button {...props} type='button' className={`font-medium pb-1 ${activeClassName}`}>
+ <button
+ {...props}
+ type='button'
+ className={`font-medium pb-1 ${activeClassName}`}
+ >
{children}
</button>
- )
-}
+ );
+};
const TabContent = ({ children, active, className, ...props }) => (
<div {...props} className={`${active ? 'block' : 'hidden'} ${className}`}>
{children}
</div>
-)
+);
const SpecificationContent = ({ children, label }) => (
<div className='flex justify-between p-3'>
<span className='text-gray_r-11'>{label}</span>
{children}
</div>
-)
+);
-export default ProductMobileVariant
+export default ProductMobileVariant;
diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx
index fa555bcf..98732407 100644
--- a/src/lib/product/components/ProductCard.jsx
+++ b/src/lib/product/components/ProductCard.jsx
@@ -1,38 +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 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]);
+
+ 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={product?.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}
@@ -52,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>
@@ -69,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'>
@@ -103,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>
)}
@@ -116,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>
)}
@@ -128,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]'>
@@ -138,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={product?.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
@@ -178,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'>
@@ -214,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>
)}
@@ -227,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>
)}
@@ -239,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]'>
@@ -249,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 29bb987e..b1a5d409 100644
--- a/src/lib/product/components/ProductSearch.jsx
+++ b/src/lib/product/components/ProductSearch.jsx
@@ -1,171 +1,204 @@
-import { useEffect, useMemo, useState } from 'react'
-import useProductSearch from '../hooks/useProductSearch'
-import ProductCard from './ProductCard'
-import Pagination from '@/core/components/elements/Pagination/Pagination'
-import { toQuery } from 'lodash-contrib'
-import _ from 'lodash'
-import ProductSearchSkeleton from './Skeleton/ProductSearchSkeleton'
-import ProductFilter from './ProductFilter'
-import useActive from '@/core/hooks/useActive'
-import MobileView from '@/core/components/views/MobileView'
-import DesktopView from '@/core/components/views/DesktopView'
-import NextImage from 'next/image'
-import ProductFilterDesktop from './ProductFilterDesktop'
-import { useRouter } from 'next/router'
-import searchSpellApi from '@/core/api/searchSpellApi'
-import Link from '@/core/components/elements/Link/Link'
-import whatsappUrl from '@/core/utils/whatsappUrl'
-import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react'
-import odooApi from '@/core/api/odooApi'
-import { formatCurrency } from '@/core/utils/formatValue'
-import axios from 'axios'
-import Skeleton from 'react-loading-skeleton'
-import { createSlug } from '@/core/utils/slug'
-
-const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) => {
- const router = useRouter()
- const { page = 1 } = query
- const [q, setQ] = useState(query?.q || '*')
- const [search, setSearch] = useState(query?.q || '*')
- const [limit, setLimit] = useState(query?.limit || 30)
- const [orderBy, setOrderBy] = useState(router.query?.orderBy || 'popular')
- if (defaultBrand) query.brand = defaultBrand.toLowerCase()
+import NextImage from 'next/image';
+import { useRouter } from 'next/router';
+import { useEffect, useMemo, useState } from 'react';
+
+import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react';
+import axios from 'axios';
+import _ from 'lodash';
+import { toQuery } from 'lodash-contrib';
+
+import odooApi from '@/core/api/odooApi';
+import searchSpellApi from '@/core/api/searchSpellApi';
+import Link from '@/core/components/elements/Link/Link';
+import Pagination from '@/core/components/elements/Pagination/Pagination';
+import DesktopView from '@/core/components/views/DesktopView';
+import MobileView from '@/core/components/views/MobileView';
+import useActive from '@/core/hooks/useActive';
+import { formatCurrency } from '@/core/utils/formatValue';
+import { createSlug } from '@/core/utils/slug';
+import whatsappUrl from '@/core/utils/whatsappUrl';
+
+import useProductSearch from '../hooks/useProductSearch';
+import ProductCard from './ProductCard';
+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,
+ defaultBrand = null,
+ brand = null,
+}) => {
+ const router = useRouter();
+ const { page = 1 } = query;
+ const [q, setQ] = useState(query?.q || '*');
+ const [search, setSearch] = useState(query?.q || '*');
+ const [limit, setLimit] = useState(query?.limit || 30);
+ const [orderBy, setOrderBy] = useState(router.query?.orderBy || 'popular');
+ if (defaultBrand) query.brand = defaultBrand.toLowerCase();
const { productSearch } = useProductSearch({
query: { ...query, q, limit, orderBy },
- operation: 'AND'
- })
- const [products, setProducts] = useState(null)
- const [spellings, setSpellings] = useState(null)
- const [bannerPromotionHeader, setBannerPromotionHeader] = useState(null)
- const [bannerPromotionFooter, setBannerPromotionFooter] = useState(null)
- const [isBrand, setIsBrand] = useState(null)
- const popup = useActive()
- const numRows = [30, 50, 80, 100]
+ operation: 'AND',
+ });
+ const [products, setProducts] = useState(null);
+ const [spellings, setSpellings] = useState(null);
+ const [bannerPromotionHeader, setBannerPromotionHeader] = useState(null);
+ const [bannerPromotionFooter, setBannerPromotionFooter] = useState(null);
+ const [isBrand, setIsBrand] = useState(null);
+ const popup = useActive();
+ const numRows = [30, 50, 80, 100];
const [brandValues, setBrand] = useState(
- !router.pathname.includes('brands') ? (query.brand ? query.brand.split(',') : []) : []
- )
- const [categoryValues, setCategory] = useState(query?.category?.split(',') || [])
- const [priceFrom, setPriceFrom] = useState(query?.priceFrom || null)
- const [priceTo, setPriceTo] = useState(query?.priceTo || null)
-
- const pageCount = Math.ceil(productSearch.data?.response.numFound / limit)
- const productStart = productSearch.data?.responseHeader.params.start
- const productRows = limit
- const productFound = productSearch.data?.response.numFound
+ !router.pathname.includes('brands')
+ ? query.brand
+ ? query.brand.split(',')
+ : []
+ : []
+ );
+ const [categoryValues, setCategory] = useState(
+ query?.category?.split(',') || []
+ );
+ const [priceFrom, setPriceFrom] = useState(query?.priceFrom || null);
+ const [priceTo, setPriceTo] = useState(query?.priceTo || null);
+
+ const pageCount = Math.ceil(productSearch.data?.response.numFound / limit);
+ const productStart = productSearch.data?.responseHeader.params.start;
+ const productRows = limit;
+ const productFound = productSearch.data?.response.numFound;
useEffect(() => {
if (productFound == 0 && query.q && !spellings) {
searchSpellApi({ query: query.q }).then((response) => {
const oddIndexSuggestions = response.data.spellcheck.suggestions.filter(
(_, index) => index % 2 === 1
- )
+ );
const oddIndexCollations = response.data.spellcheck.collations.filter(
(_, index) => index % 2 === 1
- )
+ );
const dataSpellings = oddIndexSuggestions.reduce((acc, curr) => {
oddIndexCollations.forEach((collation) => {
- acc.push(collation.collationQuery)
- })
+ acc.push(collation.collationQuery);
+ });
curr.suggestion.forEach((s) => {
- if (!acc.includes(s.word)) acc.push(s.word)
- })
- return acc
- }, [])
+ if (!acc.includes(s.word)) acc.push(s.word);
+ });
+ return acc;
+ }, []);
if (dataSpellings.length > 0) {
- setQ(dataSpellings[0])
+ setQ(dataSpellings[0]);
}
- setSpellings(dataSpellings)
- })
+ setSpellings(dataSpellings);
+ });
}
- }, [productFound, query, spellings])
+ }, [productFound, query, spellings]);
useEffect(() => {
const checkIfBrand = async () => {
const brand = await axios(
`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/brands?params=search&q=${search}`
- )
- console.log('ini brand', brand)
+ );
+
if (brand.data.length > 0) {
- setIsBrand(brand?.data[0])
+ setIsBrand(brand?.data[0]);
} else {
- setIsBrand(null)
+ setIsBrand(null);
}
+ };
+ if (router.pathname.includes('search') && q !== '*') {
+ checkIfBrand();
}
- if (router.pathname.includes('search')) {
- checkIfBrand()
- }
- }, [q])
+ }, [q]);
- const brands = []
+ const brands = [];
for (
let i = 0;
i < productSearch.data?.facetCounts?.facetFields?.manufactureNameS.length;
i += 2
) {
- const brand = productSearch.data?.facetCounts?.facetFields?.manufactureNameS[i]
- const qty = productSearch.data?.facetCounts?.facetFields?.manufactureNameS[i + 1]
+ const brand =
+ productSearch.data?.facetCounts?.facetFields?.manufactureNameS[i];
+ const qty =
+ productSearch.data?.facetCounts?.facetFields?.manufactureNameS[i + 1];
if (qty > 0) {
- brands.push({ brand, qty })
+ brands.push({ brand, qty });
}
}
+
- const categories = []
- for (let i = 0; i < productSearch.data?.facetCounts?.facetFields?.categoryName.length; i += 2) {
- const name = productSearch.data?.facetCounts?.facetFields?.categoryName[i]
- const qty = productSearch.data?.facetCounts?.facetFields?.categoryName[i + 1]
+ const categories = [];
+ for (
+ let i = 0;
+ i < productSearch.data?.facetCounts?.facetFields?.categoryName.length;
+ i += 2
+ ) {
+ const name = productSearch.data?.facetCounts?.facetFields?.categoryName[i];
+ const qty =
+ productSearch.data?.facetCounts?.facetFields?.categoryName[i + 1];
if (qty > 0) {
- categories.push({ name, qty })
+ categories.push({ name, qty });
}
}
+
const orderOptions = [
{ value: 'price-asc', label: 'Harga Terendah' },
{ value: 'price-desc', label: 'Harga Tertinggi' },
{ value: 'popular', label: 'Populer' },
- { value: 'stock', label: 'Ready Stock' }
- ]
+ { value: 'stock', label: 'Ready Stock' },
+ ];
const handleOrderBy = (e) => {
let params = {
...router.query,
- orderBy: e.target.value
- }
- params = _.pickBy(params, _.identity)
- params = toQuery(params)
- router.push(`${prefixUrl}?${params}`)
- }
+ orderBy: e.target.value,
+ };
+ params = _.pickBy(params, _.identity);
+ params = toQuery(params);
+ router.push(`${prefixUrl}?${params}`);
+ };
const handleLimit = (e) => {
let params = {
...router.query,
- limit: e.target.value
- }
- params = _.pickBy(params, _.identity)
- params = toQuery(params)
- router.push(`${prefixUrl}?${params}`)
- }
+ limit: e.target.value,
+ };
+ params = _.pickBy(params, _.identity);
+ params = toQuery(params);
+ router.push(`${prefixUrl}?${params}`);
+ };
const getBanner = async () => {
if (router.pathname.includes('search')) {
- const getBannerHeader = await odooApi('GET', '/api/v1/banner?type=promotion-header')
- const getBannerFooter = await odooApi('GET', '/api/v1/banner?type=promotion-footer')
- var randomIndex = Math.floor(Math.random() * getBannerHeader.length)
- var randomIndexFooter = Math.floor(Math.random() * getBannerFooter.length)
- setBannerPromotionHeader(getBannerHeader[randomIndex])
- setBannerPromotionFooter(getBannerFooter[randomIndexFooter])
+ const getBannerHeader = await odooApi(
+ 'GET',
+ '/api/v1/banner?type=promotion-header'
+ );
+ const getBannerFooter = await odooApi(
+ 'GET',
+ '/api/v1/banner?type=promotion-footer'
+ );
+ var randomIndex = Math.floor(Math.random() * getBannerHeader.length);
+ var randomIndexFooter = Math.floor(
+ Math.random() * getBannerFooter.length
+ );
+ setBannerPromotionHeader(getBannerHeader[randomIndex]);
+ setBannerPromotionFooter(getBannerFooter[randomIndexFooter]);
}
- }
+ };
useEffect(() => {
- getBanner()
- }, [])
+ getBanner();
+ }, []);
useEffect(() => {
- setProducts(productSearch.data?.response?.products)
- }, [productSearch])
+ setProducts(productSearch.data?.response?.products);
+ }, [productSearch]);
const SpellingComponent = useMemo(() => {
return (
@@ -182,8 +215,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
</Link>
))}
</>
- )
- }, [spellings])
+ );
+ }, [spellings]);
const handleDeleteFilter = async (source, value) => {
let params = {
@@ -192,51 +225,64 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
brand: brandValues.join(','),
category: categoryValues.join(','),
priceFrom,
- priceTo
- }
+ priceTo,
+ };
- let brands = brandValues
- let catagories = categoryValues
+ let brands = brandValues;
+ let catagories = categoryValues;
switch (source) {
case 'brands':
- brands = brandValues.filter((item) => item !== value)
- params.brand = brands.join(',')
- await setBrand(brands)
- break
+ brands = brandValues.filter((item) => item !== value);
+ params.brand = brands.join(',');
+ await setBrand(brands);
+ break;
case 'category':
- catagories = categoryValues.filter((item) => item !== value)
- params.category = catagories.join(',')
- await setCategory(catagories)
- break
+ catagories = categoryValues.filter((item) => item !== value);
+ params.category = catagories.join(',');
+ await setCategory(catagories);
+ break;
case 'price':
- params.priceFrom = null
- params.priceTo = null
- break
+ params.priceFrom = null;
+ params.priceTo = null;
+ break;
case 'delete':
params = {
q: router.query.q,
- orderBy: orderBy
- }
- break
+ orderBy: orderBy,
+ };
+ break;
}
- handleSubmitFilter(params)
- }
+ handleSubmitFilter(params);
+ };
const handleSubmitFilter = (params) => {
- params = _.pickBy(params, _.identity)
- params = toQuery(params)
- router.push(`${prefixUrl}?${params}`)
- }
+ params = _.pickBy(params, _.identity);
+ params = toQuery(params);
+ router.push(`${prefixUrl}?${params}`);
+ };
+
+ const isNotReadyStockPage = router.asPath !== '/shop/search?orderBy=stock';
return (
<>
<MobileView>
{productSearch.isLoading && <ProductSearchSkeleton />}
<div className='p-4 pt-0'>
- {isBrand && isBrand.logo && (
+ {isNotReadyStockPage && isBrand && isBrand.logo && (
<div className='mb-3'>
- <h1 className='mb-2 font-semibold text-h-sm'>Brand Pencarian {q}</h1>
- <Image src={isBrand?.logo} alt='' className='object-cover object-center h-[60px]' />
+ <h1 className='mb-2 font-semibold text-h-sm'>
+ Brand Pencarian {q}
+ </h1>
+ <Link
+ href={createSlug('/shop/brands/', isBrand.name, isBrand.id)}
+ className='inline'
+ >
+ <Image
+ src={isBrand?.logo}
+ alt=''
+ className='object-cover object-center h-[60px]'
+ />
+ </Link>
</div>
)}
<h1 className='mb-2 font-semibold text-h-sm'>Produk</h1>
@@ -255,7 +301,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
{pageCount > 1 ? (
<>
{productStart + 1}-
- {parseInt(productStart) + parseInt(productRows) > productFound
+ {parseInt(productStart) + parseInt(productRows) >
+ productFound
? productFound
: parseInt(productStart) + parseInt(productRows)}
&nbsp;dari&nbsp;
@@ -267,7 +314,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
&nbsp;produk{' '}
{query.q && (
<>
- untuk pencarian <span className='font-semibold'>{query.q}</span>
+ untuk pencarian{' '}
+ <span className='font-semibold'>{query.q}</span>
</>
)}
</>
@@ -279,7 +327,10 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
{productFound > 0 && (
<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}>
+ <button
+ className='btn-light py-2 px-5 h-[40px]'
+ onClick={popup.activate}
+ >
Filter
</button>
</div>
@@ -303,7 +354,9 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
<div className='grid grid-cols-2 gap-3'>
{products &&
- products.map((product) => <ProductCard product={product} key={product.id} />)}
+ products.map((product) => (
+ <ProductCard product={product} key={product.id} />
+ ))}
</div>
<Pagination
@@ -329,7 +382,9 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
<div className='w-3/12'>
{brand && (
<div className='p-4'>
- <div className='text-caption-1 text-gray_r-11 mb-2'>Produk dari brand:</div>
+ <div className='text-caption-1 text-gray_r-11 mb-2'>
+ Produk dari brand:
+ </div>
{brand?.data?.logo && (
<Image
src={brand?.data?.logo}
@@ -351,6 +406,10 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
prefixUrl={prefixUrl}
defaultBrand={defaultBrand}
/>
+
+ <div className='h-6' />
+
+ <SideBanner />
</div>
<div className='w-9/12 pl-6'>
{bannerPromotionHeader && bannerPromotionHeader?.image && (
@@ -363,14 +422,20 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
</div>
)}
- {isBrand && isBrand.logo && (
+ {isNotReadyStockPage && isBrand && isBrand.logo && (
<div className='mb-3'>
- <h1 className='text-2xl mb-2 font-semibold'>Brand Pencarian {q}</h1>
+ <h1 className='text-2xl mb-2 font-semibold'>
+ Brand Pencarian {q}
+ </h1>
<Link
href={createSlug('/shop/brands/', isBrand.name, isBrand.id)}
className='inline'
>
- <Image src={isBrand?.logo} alt='' className='object-cover object-center h-24' />
+ <Image
+ src={isBrand?.logo}
+ alt=''
+ className='object-cover object-center h-24'
+ />
</Link>
</div>
)}
@@ -391,7 +456,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
{pageCount > 1 ? (
<>
{productStart + 1}-
- {parseInt(productStart) + parseInt(productRows) > productFound
+ {parseInt(productStart) + parseInt(productRows) >
+ productFound
? productFound
: parseInt(productStart) + parseInt(productRows)}
&nbsp;dari&nbsp;
@@ -403,7 +469,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
&nbsp;produk{' '}
{query.q && (
<>
- untuk pencarian <span className='font-semibold'>{query.q}</span>
+ untuk pencarian{' '}
+ <span className='font-semibold'>{query.q}</span>
</>
)}
</>
@@ -447,7 +514,9 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
{productSearch.isLoading && <ProductSearchSkeleton />}
<div className='grid grid-cols-5 gap-x-3 gap-y-6'>
{products &&
- products.map((product) => <ProductCard product={product} key={product.id} />)}
+ products.map((product) => (
+ <ProductCard product={product} key={product.id} />
+ ))}
</div>
<div className='flex justify-between items-center mt-6 mb-2'>
<div className='pt-2 pb-6 flex items-center gap-x-3'>
@@ -464,7 +533,7 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
href={
query?.q
? whatsappUrl('productSearch', {
- name: query.q
+ name: query.q,
})
: whatsappUrl()
}
@@ -492,44 +561,66 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
/>
</div>
)}
+ <FooterBanner />
</div>
</div>
</DesktopView>
</>
- )
-}
+ );
+};
-export default ProductSearch
+export default ProductSearch;
const FilterChoicesComponent = ({
brandValues,
categoryValues,
priceFrom,
priceTo,
- handleDeleteFilter
+ handleDeleteFilter,
}) => (
<div className='flex items-center'>
<HStack spacing={2} className='flex-wrap'>
{brandValues?.map((value, index) => (
- <Tag size='lg' key={index} borderRadius='lg' variant='outline' colorScheme='gray'>
+ <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'>
+ <Tag
+ size='lg'
+ key={index}
+ borderRadius='lg'
+ variant='outline'
+ colorScheme='gray'
+ >
<TagLabel>{value}</TagLabel>
- <TagCloseButton onClick={() => handleDeleteFilter('category', value)} />
+ <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)} />
+ <TagLabel>
+ {formatCurrency(priceFrom) + '-' + formatCurrency(priceTo)}
+ </TagLabel>
+ <TagCloseButton
+ onClick={() => handleDeleteFilter('price', priceFrom)}
+ />
</Tag>
)}
- {brandValues?.length > 0 || categoryValues?.length > 0 || priceFrom || priceTo ? (
+ {brandValues?.length > 0 ||
+ categoryValues?.length > 0 ||
+ priceFrom ||
+ priceTo ? (
<span>
<button
className='btn-transparent py-2 px-5 h-[40px] text-red-700'
@@ -543,4 +634,4 @@ const FilterChoicesComponent = ({
)}
</HStack>
</div>
-)
+);
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..8855c6c4 100644
--- a/src/lib/quotation/components/Quotation.jsx
+++ b/src/lib/quotation/components/Quotation.jsx
@@ -1,102 +1,295 @@
-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 [isApproval, setIsApproval] = useState(false);
+
+ 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();
+ setIsApproval(auth?.feature?.soApproval);
+ }, [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]);
+
+ useEffect(() => {
+ if (isApproval) {
+ setselectedCarrierId(1);
+ setselectedExpedisiService('indoteknik');
+ }
+ }, [isApproval]);
+
+ // 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 && !isApproval) {
+ 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 && !isApproval) {
+ 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,
+ };
+ 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
+ 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 +300,85 @@ 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} />
+ {!isApproval && (
+ <>
+ <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 +386,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 +398,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 +410,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 +447,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 +460,65 @@ 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} />
+ {!isApproval && (
+ <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 +541,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 +558,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 +590,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/listSiteApi.js b/src/lib/transaction/api/listSiteApi.js
new file mode 100644
index 00000000..8b7740c5
--- /dev/null
+++ b/src/lib/transaction/api/listSiteApi.js
@@ -0,0 +1,10 @@
+import odooApi from '@/core/api/odooApi'
+import { getAuth } from '@/core/utils/auth'
+
+const getSite = async () => {
+ const auth = getAuth()
+ const dataSite = await odooApi('GET', `/api/v1/partner/${auth?.partnerId}/list/site`)
+ return dataSite
+}
+
+export default getSite \ No newline at end of file
diff --git a/src/lib/transaction/api/rejectApi.js b/src/lib/transaction/api/rejectApi.js
new file mode 100644
index 00000000..127c0d38
--- /dev/null
+++ b/src/lib/transaction/api/rejectApi.js
@@ -0,0 +1,13 @@
+import odooApi from '@/core/api/odooApi'
+import { getAuth } from '@/core/utils/auth'
+
+const rejectApi = async ({ id }) => {
+ const auth = getAuth()
+ const dataCheckout = await odooApi(
+ 'POST',
+ `/api/v1/partner/${auth?.partnerId}/sale_order/${id}/reject`
+ )
+ return dataCheckout
+}
+
+export default rejectApi
diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx
index 82eb1775..c6152ca9 100644
--- a/src/lib/transaction/components/Transaction.jsx
+++ b/src/lib/transaction/components/Transaction.jsx
@@ -1,83 +1,120 @@
-import Spinner from '@/core/components/elements/Spinner/Spinner'
-import useTransaction from '../hooks/useTransaction'
-import TransactionStatusBadge from './TransactionStatusBadge'
-import Divider from '@/core/components/elements/Divider/Divider'
-import { useMemo, useRef, useState } from 'react'
-import { downloadPurchaseOrder, downloadQuotation } from '../utils/transactions'
-import BottomPopup from '@/core/components/elements/Popup/BottomPopup'
-import uploadPoApi from '../api/uploadPoApi'
-import { toast } from 'react-hot-toast'
-import getFileBase64 from '@/core/utils/getFileBase64'
-import currencyFormat from '@/core/utils/currencyFormat'
-import VariantGroupCard from '@/lib/variant/components/VariantGroupCard'
-import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon } from '@heroicons/react/24/outline'
-import Link from '@/core/components/elements/Link/Link'
-import checkoutPoApi from '../api/checkoutPoApi'
-import cancelTransactionApi from '../api/cancelTransactionApi'
-import MobileView from '@/core/components/views/MobileView'
-import DesktopView from '@/core/components/views/DesktopView'
-import Menu from '@/lib/auth/components/Menu'
-import Image from '@/core/components/elements/Image/Image'
-import { createSlug } from '@/core/utils/slug'
-import toTitleCase from '@/core/utils/toTitleCase'
-import useAirwayBill from '../hooks/useAirwayBill'
-import Manifest from '@/lib/treckingAwb/component/Manifest'
+import Spinner from '@/core/components/elements/Spinner/Spinner';
+import useTransaction from '../hooks/useTransaction';
+import TransactionStatusBadge from './TransactionStatusBadge';
+import Divider from '@/core/components/elements/Divider/Divider';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import ImageNext from 'next/image';
+import {
+ downloadPurchaseOrder,
+ downloadQuotation,
+} from '../utils/transactions';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import uploadPoApi from '../api/uploadPoApi';
+import { toast } from 'react-hot-toast';
+import getFileBase64 from '@/core/utils/getFileBase64';
+import currencyFormat from '@/core/utils/currencyFormat';
+import VariantGroupCard from '@/lib/variant/components/VariantGroupCard';
+import {
+ ChevronDownIcon,
+ ChevronRightIcon,
+ ChevronUpIcon,
+} from '@heroicons/react/24/outline';
+import Link from '@/core/components/elements/Link/Link';
+import checkoutPoApi from '../api/checkoutPoApi';
+import cancelTransactionApi from '../api/cancelTransactionApi';
+import MobileView from '@/core/components/views/MobileView';
+import DesktopView from '@/core/components/views/DesktopView';
+import Menu from '@/lib/auth/components/Menu';
+import Image from '@/core/components/elements/Image/Image';
+import { createSlug } from '@/core/utils/slug';
+import toTitleCase from '@/core/utils/toTitleCase';
+import useAirwayBill from '../hooks/useAirwayBill';
+import Manifest from '@/lib/treckingAwb/component/Manifest';
+import useAuth from '@/core/hooks/useAuth';
+import StepApproval from './stepper';
+import aprpoveApi from '../api/approveApi';
+import rejectApi from '../api/rejectApi';
const Transaction = ({ id }) => {
- const { transaction } = useTransaction({ id })
- const { queryAirwayBill } = useAirwayBill({ orderId: id })
+ const auth = useAuth();
+ const { transaction } = useTransaction({ id });
- const [airwayBillPopup, setAirwayBillPopup] = useState(null)
+ const statusApprovalWeb = transaction.data?.approvalStep;
+
+ const { queryAirwayBill } = useAirwayBill({ orderId: id });
+ const [airwayBillPopup, setAirwayBillPopup] = useState(null);
+
+ const poNumber = useRef(null);
+ const poFile = useRef(null);
+ const [uploadPo, setUploadPo] = useState(false);
+ const [idAWB, setIdAWB] = useState(null);
+ const openUploadPo = () => setUploadPo(true);
+ const closeUploadPo = () => setUploadPo(false);
- const poNumber = useRef(null)
- const poFile = useRef(null)
- const [uploadPo, setUploadPo] = useState(false)
- const [idAWB, setIdAWB] = useState(null)
- const openUploadPo = () => setUploadPo(true)
- const closeUploadPo = () => setUploadPo(false)
const submitUploadPo = async () => {
- const file = poFile.current.files[0]
- const name = poNumber.current.value
+ const file = poFile.current.files[0];
+ const name = poNumber.current.value;
if (typeof file === 'undefined' || !name) {
- toast.error('Nomor dan Dokumen PO harus diisi')
- return
+ toast.error('Nomor dan Dokumen PO harus diisi');
+ return;
}
if (file.size > 5000000) {
- toast.error('Maksimal ukuran file adalah 5MB')
- return
+ toast.error('Maksimal ukuran file adalah 5MB');
+ return;
}
- const data = { name, file: await getFileBase64(file) }
- const isUploaded = await uploadPoApi({ id, data })
+ const data = { name, file: await getFileBase64(file) };
+ const isUploaded = await uploadPoApi({ id, data });
if (isUploaded) {
- toast.success('Berhasil upload PO')
- transaction.refetch()
- closeUploadPo()
- return
+ toast.success('Berhasil upload PO');
+ transaction.refetch();
+ closeUploadPo();
+ return;
}
- toast.error('Terjadi kesalahan internal, coba lagi nanti atau hubungi kami')
- }
+ toast.error(
+ 'Terjadi kesalahan internal, coba lagi nanti atau hubungi kami'
+ );
+ };
+
+ const [cancelTransaction, setCancelTransaction] = useState(false);
+ const openCancelTransaction = () => setCancelTransaction(true);
+ const closeCancelTransaction = () => setCancelTransaction(false);
- const [cancelTransaction, setCancelTransaction] = useState(false)
- const openCancelTransaction = () => setCancelTransaction(true)
- const closeCancelTransaction = () => setCancelTransaction(false)
+ const [rejectTransaction, setRejectTransaction] = useState(false);
+
+ const openRejectTransaction = () => setRejectTransaction(true);
+ const closeRejectTransaction = () => setRejectTransaction(false);
const submitCancelTransaction = async () => {
- const isCancelled = await cancelTransactionApi({ transaction: transaction.data })
+ const isCancelled = await cancelTransactionApi({
+ transaction: transaction.data,
+ });
if (isCancelled) {
- toast.success('Berhasil batalkan transaksi')
- transaction.refetch()
+ toast.success('Berhasil batalkan transaksi');
+ transaction.refetch();
}
- closeCancelTransaction()
- }
+ closeCancelTransaction();
+ };
const checkout = async () => {
if (!transaction.data?.purchaseOrderFile) {
- toast.error('Mohon upload dokumen PO anda sebelum melanjutkan pesanan')
- return
+ toast.error('Mohon upload dokumen PO anda sebelum melanjutkan pesanan');
+ return;
}
- await checkoutPoApi({ id })
- toast.success('Berhasil melanjutkan pesanan')
- transaction.refetch()
- }
+ await checkoutPoApi({ id });
+ toast.success('Berhasil melanjutkan pesanan');
+ transaction.refetch();
+ };
+
+ const handleApproval = async () => {
+ await aprpoveApi({ id });
+ toast.success('Berhasil melanjutkan approval');
+ transaction.refetch();
+ };
+
+ const handleReject = async () => {
+ await rejectApi({ id });
+ closeRejectTransaction();
+ transaction.refetch();
+ };
const memoizeVariantGroupCard = useMemo(
() => (
@@ -102,19 +139,19 @@ const Transaction = ({ id }) => {
</div>
),
[transaction.data]
- )
+ );
if (transaction.isLoading) {
return (
<div className='flex justify-center my-6'>
<Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' />
</div>
- )
+ );
}
const closePopup = () => {
- setIdAWB(null)
- }
+ setIdAWB(null);
+ };
return (
transaction.data?.name && (
@@ -146,6 +183,33 @@ const Transaction = ({ id }) => {
</div>
</BottomPopup>
+ <BottomPopup
+ active={rejectTransaction}
+ close={closeRejectTransaction}
+ title='Batalkan Transaksi'
+ >
+ <div className='leading-7 text-gray_r-12/80'>
+ Apakah anda yakin Membatalkan transaksi{' '}
+ <span className='underline'>{transaction.data?.name}</span>?
+ </div>
+ <div className='flex justify-end mt-6 gap-x-4'>
+ <button
+ className='btn-solid-red w-full md:w-fit'
+ type='button'
+ onClick={handleReject}
+ >
+ Ya, Batalkan
+ </button>
+ <button
+ className='btn-light w-full md:w-fit'
+ type='button'
+ onClick={closeRejectTransaction}
+ >
+ Batal
+ </button>
+ </div>
+ </BottomPopup>
+
<BottomPopup title='Upload PO' close={closeUploadPo} active={uploadPo}>
<div>
<label>Nomor PO</label>
@@ -156,10 +220,18 @@ const Transaction = ({ id }) => {
<input type='file' className='form-input mt-3 py-2' ref={poFile} />
</div>
<div className='grid grid-cols-2 gap-x-3 mt-6'>
- <button type='button' className='btn-light w-full' onClick={closeUploadPo}>
+ <button
+ type='button'
+ className='btn-light w-full'
+ onClick={closeUploadPo}
+ >
Batal
</button>
- <button type='button' className='btn-solid-red w-full' onClick={submitUploadPo}>
+ <button
+ type='button'
+ className='btn-solid-red w-full'
+ onClick={submitUploadPo}
+ >
Upload
</button>
</div>
@@ -167,18 +239,33 @@ const Transaction = ({ id }) => {
<Manifest idAWB={idAWB} closePopup={closePopup}></Manifest>
<MobileView>
+ <div className='p-4'>
+ {auth?.feature?.soApproval && (
+ <StepApproval
+ layer={statusApprovalWeb}
+ status={transaction?.data?.status}
+ className='ml-auto'
+ />
+ )}
+ </div>
<div className='flex flex-col gap-y-4 p-4'>
<DescriptionRow label='Status Transaksi'>
<div className='flex justify-end'>
<TransactionStatusBadge status={transaction.data?.status} />
</div>
</DescriptionRow>
- <DescriptionRow label='No Transaksi'>{transaction.data?.name}</DescriptionRow>
+ <DescriptionRow label='No Transaksi'>
+ {transaction.data?.name}
+ </DescriptionRow>
<DescriptionRow label='Ketentuan Pembayaran'>
{transaction.data?.paymentTerm}
</DescriptionRow>
- <DescriptionRow label='Nama Sales'>{transaction.data?.sales}</DescriptionRow>
- <DescriptionRow label='Waktu Transaksi'>{transaction.data?.dateOrder}</DescriptionRow>
+ <DescriptionRow label='Nama Sales'>
+ {transaction.data?.sales}
+ </DescriptionRow>
+ <DescriptionRow label='Waktu Transaksi'>
+ {transaction.data?.dateOrder}
+ </DescriptionRow>
</div>
<Divider />
@@ -214,25 +301,27 @@ const Transaction = ({ id }) => {
<Divider />
- <div className='p-4 flex flex-col gap-y-4'>
- <DescriptionRow label='Purchase Order'>
- {transaction.data?.purchaseOrderName || '-'}
- </DescriptionRow>
- <div className='flex items-center'>
- <p className='text-gray_r-11 leading-none'>Dokumen PO</p>
- <button
- type='button'
- className='btn-light py-1.5 px-3 ml-auto'
- onClick={
- transaction.data?.purchaseOrderFile
- ? () => downloadPurchaseOrder(transaction.data)
- : openUploadPo
- }
- >
- {transaction.data?.purchaseOrderFile ? 'Download' : 'Upload'}
- </button>
+ {!auth?.feature.soApproval && (
+ <div className='p-4 flex flex-col gap-y-4'>
+ <DescriptionRow label='Purchase Order'>
+ {transaction.data?.purchaseOrderName || '-'}
+ </DescriptionRow>
+ <div className='flex items-center'>
+ <p className='text-gray_r-11 leading-none'>Dokumen PO</p>
+ <button
+ type='button'
+ className='btn-light py-1.5 px-3 ml-auto'
+ onClick={
+ transaction.data?.purchaseOrderFile
+ ? () => downloadPurchaseOrder(transaction.data)
+ : openUploadPo
+ }
+ >
+ {transaction.data?.purchaseOrderFile ? 'Download' : 'Upload'}
+ </button>
+ </div>
</div>
- </div>
+ )}
<Divider />
@@ -278,11 +367,43 @@ const Transaction = ({ id }) => {
<Divider />
<div className='p-4 pt-0'>
- {transaction.data?.status == 'draft' && (
- <button className='btn-yellow w-full mt-4' onClick={checkout}>
- Lanjutkan Transaksi
- </button>
- )}
+ {transaction.data?.status == 'draft' &&
+ auth?.feature.soApproval && (
+ <div className='flex gap-x-2'>
+ <button
+ className='btn-yellow w-full'
+ onClick={checkout}
+ disabled={
+ transaction.data?.status === 'cancel'
+ ? true
+ : false || auth?.webRole === statusApprovalWeb
+ ? true
+ : false
+ }
+ >
+ Approve
+ </button>
+ <button
+ className='btn-solid-red px-7 w-full'
+ onClick={checkout}
+ disabled={
+ transaction.data?.status === 'cancel'
+ ? true
+ : false || auth?.webRole === statusApprovalWeb
+ ? true
+ : false
+ }
+ >
+ Reject
+ </button>
+ </div>
+ )}
+ {transaction.data?.status == 'draft' &&
+ !auth?.feature?.soApproval && (
+ <button className='btn-yellow w-full mt-4' onClick={checkout}>
+ Lanjutkan Transaksi
+ </button>
+ )}
<button
className='btn-light w-full mt-4'
disabled={transaction.data?.status != 'draft'}
@@ -308,10 +429,23 @@ const Transaction = ({ id }) => {
<Menu />
</div>
<div className='w-9/12 p-4 py-6 bg-white border border-gray_r-6 rounded'>
- <h1 className='text-title-sm font-semibold mb-6'>Detail Transaksi</h1>
+ <div className='flex justify-between'>
+ <h1 className='text-title-sm font-semibold mb-6'>
+ Detail Transaksi
+ </h1>
+ {auth?.feature?.soApproval && (
+ <StepApproval
+ layer={statusApprovalWeb}
+ status={transaction?.data?.status}
+ className='ml-auto'
+ />
+ )}
+ </div>
<div className='flex items-center gap-x-2 mb-3'>
- <span className='text-h-sm font-medium'>{transaction?.data?.name}</span>
+ <span className='text-h-sm font-medium'>
+ {transaction?.data?.name}
+ </span>
<TransactionStatusBadge status={transaction?.data?.status} />
</div>
<div className='flex gap-x-4'>
@@ -322,20 +456,58 @@ const Transaction = ({ id }) => {
>
Download
</button>
- {transaction.data?.status == 'draft' && (
- <button className='btn-yellow' onClick={checkout}>
- Lanjutkan Transaksi
- </button>
- )}
- {transaction.data?.status != 'draft' && (
- <button
- className='btn-light'
- disabled={transaction.data?.status != 'waiting'}
- onClick={openCancelTransaction}
- >
- Batalkan Transaksi
- </button>
- )}
+ {transaction.data?.status == 'draft' &&
+ auth?.feature?.soApproval &&
+ auth?.webRole && (
+ <div className='flex gap-x-2'>
+ <button
+ className='btn-yellow'
+ onClick={handleApproval}
+ disabled={
+ transaction.data?.status === 'cancel'
+ ? true
+ : false || auth?.webRole === statusApprovalWeb
+ ? true
+ : false || statusApprovalWeb < 1
+ ? true
+ : false
+ }
+ >
+ Approve
+ </button>
+ <button
+ className='btn-solid-red px-7'
+ onClick={openRejectTransaction}
+ disabled={
+ transaction.data?.status === 'cancel'
+ ? true
+ : false || auth?.webRole === statusApprovalWeb
+ ? true
+ : false || statusApprovalWeb < 1
+ ? true
+ : false
+ }
+ >
+ Reject
+ </button>
+ </div>
+ )}
+ {transaction.data?.status == 'draft' &&
+ !auth?.feature.soApproval && (
+ <button className='btn-yellow' onClick={checkout}>
+ Lanjutkan Transaksi
+ </button>
+ )}
+ {transaction.data?.status != 'draft' &&
+ !auth?.feature.soApproval && (
+ <button
+ className='btn-light'
+ disabled={transaction.data?.status != 'waiting'}
+ onClick={openCancelTransaction}
+ >
+ Batalkan Transaksi
+ </button>
+ )}
</div>
<div className='grid grid-cols-2 gap-x-6 mt-6'>
@@ -350,33 +522,50 @@ const Transaction = ({ id }) => {
<div>Ketentuan Pembayaran</div>
<div>: {transaction?.data?.paymentTerm}</div>
- <div>Purchase Order</div>
- <div>
- : {transaction?.data?.purchaseOrderName}{' '}
- <button
- type='button'
- className='inline-block text-danger-500'
- onClick={
- transaction.data?.purchaseOrderFile
- ? () => downloadPurchaseOrder(transaction.data)
- : openUploadPo
- }
- >
- {transaction?.data?.purchaseOrderFile ? 'Download' : 'Upload'}
- </button>
- </div>
+ {!auth?.feature?.soApproval ? (
+ <>
+ <div>Purchase Order</div>
+ <div>
+ : {transaction?.data?.purchaseOrderName}{' '}
+ <button
+ type='button'
+ className='inline-block text-danger-500'
+ onClick={
+ transaction.data?.purchaseOrderFile
+ ? () => downloadPurchaseOrder(transaction.data)
+ : openUploadPo
+ }
+ >
+ {transaction?.data?.purchaseOrderFile
+ ? 'Download'
+ : 'Upload'}
+ </button>
+ </div>
+ </>
+ ) : (
+ <>
+ <div>Site</div>
+ <div>: {transaction?.data?.sitePartner}</div>
+ </>
+ )}
</div>
</div>
- <div className='text-h-sm font-semibold mt-10 mb-4'>Informasi Pelanggan</div>
+ <div className='text-h-sm font-semibold mt-10 mb-4'>
+ Informasi Pelanggan
+ </div>
<div className='grid grid-cols-2 gap-x-4'>
<div className='border border-gray_r-6 rounded p-3'>
<div className='font-medium mb-4'>Detail Pelanggan</div>
- <SectionContent address={transaction?.data?.address?.customer} />
+ <SectionContent
+ address={transaction?.data?.address?.customer}
+ />
</div>
</div>
- <div className='text-h-sm font-semibold mt-10 mb-4'>Pengiriman</div>
+ <div className='text-h-sm font-semibold mt-10 mb-4'>
+ Pengiriman
+ </div>
<div className='grid grid-cols-3 gap-1'>
{transaction?.data?.pickings?.map((airway) => (
<button
@@ -403,12 +592,14 @@ const Transaction = ({ id }) => {
<div className='badge-red text-sm'>Belum ada pengiriman</div>
)}
- <div className='text-h-sm font-semibold mt-10 mb-4'>Rincian Pembelian</div>
+ <div className='text-h-sm font-semibold mt-10 mb-4'>
+ Rincian Pembelian
+ </div>
<table className='table-data'>
<thead>
<tr>
<th>Nama Produk</th>
- <th>Diskon</th>
+ {/* <th>Diskon</th> */}
<th>Jumlah</th>
<th>Harga</th>
<th>Subtotal</th>
@@ -426,11 +617,37 @@ const Transaction = ({ id }) => {
)}
className='w-[20%] flex-shrink-0'
>
- <Image
- src={product?.parent?.image}
- alt={product?.name}
- className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md'
- />
+ <div className='relative'>
+ <Image
+ src={product?.parent?.image}
+ alt={product?.name}
+ className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md'
+ />
+ <div className='absolute top-0 right-4 flex mt-3'>
+ <div className='gambarB '>
+ {product.isSni && (
+ <ImageNext
+ src='/images/sni-logo.png'
+ alt='SNI Logo'
+ className='w-2 h-4 object-contain object-top sm:h-4'
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ <div className='gambarC '>
+ {product.isTkdn && (
+ <ImageNext
+ src='/images/TKDN.png'
+ alt='TKDN'
+ className='w-5 h-4 object-contain object-top ml-1 sm:h-4'
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ </div>
+ </div>
</Link>
<div className='px-2 text-left'>
<Link
@@ -451,18 +668,18 @@ const Transaction = ({ id }) => {
</div>
</div>
</td>
- <td>
+ {/* <td>
{product.price.discountPercentage > 0
? `${product.price.discountPercentage}%`
: ''}
- </td>
+ </td> */}
<td>{product.quantity}</td>
<td>
- {product.price.discountPercentage > 0 && (
+ {/* {product.price.discountPercentage > 0 && (
<div className='line-through mb-1 text-caption-1 text-gray_r-12/70'>
{currencyFormat(product.price.price)}
</div>
- )}
+ )} */}
<div>{currencyFormat(product.price.priceDiscount)}</div>
</td>
<td>{currencyFormat(product.price.subtotal)}</td>
@@ -483,7 +700,9 @@ const Transaction = ({ id }) => {
{currencyFormat(transaction.data?.amountTax)}
</div>
- <div className='text-right whitespace-nowrap'>Biaya Pengiriman</div>
+ <div className='text-right whitespace-nowrap'>
+ Biaya Pengiriman
+ </div>
<div className='text-right font-medium'>
{currencyFormat(transaction.data?.deliveryAmount)}
</div>
@@ -578,18 +797,18 @@ const Transaction = ({ id }) => {
))} */}
</>
)
- )
-}
+ );
+};
const SectionAddress = ({ address }) => {
const [section, setSection] = useState({
customer: false,
invoice: false,
- shipping: false
- })
+ shipping: false,
+ });
const toggleSection = (name) => {
- setSection({ ...section, [name]: !section[name] })
- }
+ setSection({ ...section, [name]: !section[name] });
+ };
return (
<>
@@ -620,39 +839,50 @@ const SectionAddress = ({ address }) => {
/>
{section.invoice && <SectionContent address={address?.invoice} />} */}
</>
- )
-}
+ );
+};
const SectionButton = ({ label, active, toggle }) => (
- <button className='p-4 font-medium flex justify-between w-full' onClick={toggle}>
+ <button
+ className='p-4 font-medium flex justify-between w-full'
+ onClick={toggle}
+ >
<span>{label}</span>
- {active ? <ChevronUpIcon className='w-5' /> : <ChevronDownIcon className='w-5' />}
+ {active ? (
+ <ChevronUpIcon className='w-5' />
+ ) : (
+ <ChevronDownIcon className='w-5' />
+ )}
</button>
-)
+);
const SectionContent = ({ address }) => {
- let fullAddress = []
- if (address?.street) fullAddress.push(address.street)
- if (address?.subDistrict?.name) fullAddress.push(toTitleCase(address.subDistrict.name))
- if (address?.district?.name) fullAddress.push(toTitleCase(address.district.name))
- if (address?.city?.name) fullAddress.push(toTitleCase(address.city.name))
- fullAddress = fullAddress.join(', ')
+ let fullAddress = [];
+ if (address?.street) fullAddress.push(address.street);
+ if (address?.subDistrict?.name)
+ fullAddress.push(toTitleCase(address.subDistrict.name));
+ if (address?.district?.name)
+ fullAddress.push(toTitleCase(address.district.name));
+ if (address?.city?.name) fullAddress.push(toTitleCase(address.city.name));
+ fullAddress = fullAddress.join(', ');
return (
<div className='flex flex-col gap-y-4 p-4 md:p-0 border-t border-gray_r-6 md:border-0'>
<DescriptionRow label='Nama'>{address.name}</DescriptionRow>
<DescriptionRow label='Email'>{address.email || '-'}</DescriptionRow>
- <DescriptionRow label='No Telepon'>{address.mobile || '-'}</DescriptionRow>
+ <DescriptionRow label='No Telepon'>
+ {address.mobile || '-'}
+ </DescriptionRow>
<DescriptionRow label='Alamat'>{fullAddress}</DescriptionRow>
</div>
- )
-}
+ );
+};
const DescriptionRow = ({ children, label }) => (
<div className='grid grid-cols-2'>
<span className='text-gray_r-11'>{label}</span>
<span className='text-right leading-6'>{children}</span>
</div>
-)
+);
-export default Transaction
+export default Transaction;
diff --git a/src/lib/transaction/components/Transactions.jsx b/src/lib/transaction/components/Transactions.jsx
index be63effd..92bdd276 100644
--- a/src/lib/transaction/components/Transactions.jsx
+++ b/src/lib/transaction/components/Transactions.jsx
@@ -1,63 +1,163 @@
-import { useRouter } from 'next/router'
-import { useState } from 'react'
-import { toast } from 'react-hot-toast'
-import { EllipsisVerticalIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-hot-toast';
+import {
+ EllipsisVerticalIcon,
+ MagnifyingGlassIcon,
+} from '@heroicons/react/24/outline';
+import useAuth from '@/core/hooks/useAuth';
-import { downloadPurchaseOrder, downloadQuotation } from '../utils/transactions'
-import useTransactions from '../hooks/useTransactions'
-import currencyFormat from '@/core/utils/currencyFormat'
-import cancelTransactionApi from '../api/cancelTransactionApi'
-import TransactionStatusBadge from './TransactionStatusBadge'
-import Spinner from '@/core/components/elements/Spinner/Spinner'
-import Link from '@/core/components/elements/Link/Link'
-import BottomPopup from '@/core/components/elements/Popup/BottomPopup'
-import Pagination from '@/core/components/elements/Pagination/Pagination'
-import { toQuery } from 'lodash-contrib'
-import _ from 'lodash'
-import Alert from '@/core/components/elements/Alert/Alert'
-import MobileView from '@/core/components/views/MobileView'
-import DesktopView from '@/core/components/views/DesktopView'
-import Menu from '@/lib/auth/components/Menu'
+import {
+ downloadPurchaseOrder,
+ downloadQuotation,
+} from '../utils/transactions';
+import useTransactions from '../hooks/useTransactions';
+import currencyFormat from '@/core/utils/currencyFormat';
+import cancelTransactionApi from '../api/cancelTransactionApi';
+import TransactionStatusBadge from './TransactionStatusBadge';
+import Spinner from '@/core/components/elements/Spinner/Spinner';
+import Link from '@/core/components/elements/Link/Link';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import Pagination from '@/core/components/elements/Pagination/Pagination';
+import { toQuery } from 'lodash-contrib';
+import _ from 'lodash';
+import Alert from '@/core/components/elements/Alert/Alert';
+import MobileView from '@/core/components/views/MobileView';
+import DesktopView from '@/core/components/views/DesktopView';
+import Menu from '@/lib/auth/components/Menu';
+import * as XLSX from 'xlsx';
+import getSite from '../api/listSiteApi';
+import transactionsApi from '../api/transactionsApi';
const Transactions = ({ context = '' }) => {
- const router = useRouter()
- const { q = '', page = 1 } = router.query
+ const auth = useAuth();
+ const router = useRouter();
+ const { q = '', page = 1, site = null } = router.query;
- const limit = 15
+ const limit = 15;
+
+ const [inputQuery, setInputQuery] = useState(q);
+ const [toOthers, setToOthers] = useState(null);
+ const [toCancel, setToCancel] = useState(null);
+ const [listSites, setListSites] = useState([]);
+
+ const [siteFilter, setSiteFilter] = useState(site);
const query = {
name: q,
offset: (page - 1) * limit,
context,
- limit
- }
- const { transactions } = useTransactions({ query })
+ limit,
+ site:
+ siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null),
+ };
+
+ const { transactions } = useTransactions({ query });
- const [inputQuery, setInputQuery] = useState(q)
- const [toOthers, setToOthers] = useState(null)
- const [toCancel, setToCancel] = useState(null)
+ const fetchSite = async () => {
+ const site = await getSite();
+ setListSites(site.sites);
+ };
const submitCancelTransaction = async () => {
const isCancelled = await cancelTransactionApi({
- transaction: toCancel
- })
+ transaction: toCancel,
+ });
if (isCancelled) {
- toast.success('Berhasil batalkan transaksi')
- transactions.refetch()
+ toast.success('Berhasil batalkan transaksi');
+ transactions.refetch();
}
- setToCancel(null)
- }
+ setToCancel(null);
+ };
- const pageCount = Math.ceil(transactions?.data?.saleOrderTotal / limit)
- let pageQuery = _.omit(query, ['limit', 'offset', 'context'])
- pageQuery = _.pickBy(pageQuery, _.identity)
- pageQuery = toQuery(pageQuery)
+ const pageCount = Math.ceil(transactions?.data?.saleOrderTotal / limit);
+ let pageQuery = _.omit(query, ['limit', 'offset', 'context']);
+ pageQuery = _.pickBy(
+ pageQuery,
+ (value, key) => value !== '' && !(key === 'page' && value === '1')
+ );
+ pageQuery = toQuery(pageQuery);
const handleSubmit = (e) => {
- e.preventDefault()
- router.push(`${router.pathname}?q=${inputQuery}`)
- }
+ e.preventDefault();
+ const queryParams = {};
+ if (inputQuery) queryParams.q = inputQuery;
+ if (siteFilter) queryParams.site = siteFilter;
+ router.push({
+ pathname: router.pathname,
+ query: queryParams,
+ });
+ };
+
+ const handleSiteFilterChange = (e) => {
+ setSiteFilter(e.target.value);
+ const queryParams = {};
+ if (inputQuery) queryParams.q = inputQuery;
+ if (e.target.value) queryParams.site = e.target.value;
+ router.push({
+ pathname: router.pathname,
+ query: queryParams,
+ });
+ };
+
+ const exportToExcel = (data, siteFilter) => {
+ const fieldsToExport = [
+ 'No. Transaksi',
+ 'No. PO',
+ 'Tanggal',
+ 'Created By',
+ 'Salesperson',
+ 'Total',
+ 'Status',
+ ];
+ const rowsToExport = [];
+
+ data.forEach((saleOrder) => {
+ const row = {
+ 'No. Transaksi': saleOrder.name,
+ 'No. PO': saleOrder.purchaseOrderName || '-',
+ Tanggal: saleOrder.dateOrder || '-',
+ 'Created By': saleOrder.address.customer?.name || '-',
+ Salesperson: saleOrder.sales,
+ Total: currencyFormat(saleOrder.amountTotal),
+ Status: saleOrder.status,
+ };
+ if (siteFilter) {
+ row['Site'] = siteFilter;
+ }
+ rowsToExport.push(row);
+ });
+ const worksheet = XLSX.utils.json_to_sheet(rowsToExport, {
+ header: fieldsToExport,
+ });
+
+ const workbook = XLSX.utils.book_new();
+ XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
+ XLSX.writeFile(workbook, 'transactions.xlsx');
+ };
+
+ const getAllData = async () => {
+ const query = {
+ name: q,
+ context,
+ site:
+ siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null),
+ };
+ const queryString = toQuery(query)
+ const data = await transactionsApi({ query: queryString });
+ return data;
+ };
+
+ const handleExportExcel = async () => {
+ const dataToExport = await getAllData();
+
+ exportToExcel(dataToExport?.saleOrders, siteFilter);
+ };
+
+ useEffect(() => {
+ fetchSite();
+ }, []);
return (
<>
<MobileView>
@@ -81,17 +181,23 @@ const Transactions = ({ context = '' }) => {
</div>
)}
- {!transactions.isLoading && transactions.data?.saleOrders?.length === 0 && (
- <Alert type='info' className='text-center'>
- Tidak ada transaksi
- </Alert>
- )}
+ {!transactions.isLoading &&
+ transactions.data?.saleOrders?.length === 0 && (
+ <Alert type='info' className='text-center'>
+ Tidak ada transaksi
+ </Alert>
+ )}
{transactions.data?.saleOrders?.map((saleOrder, index) => (
- <div className='p-4 shadow border border-gray_r-3 rounded-md' key={index}>
+ <div
+ className='p-4 shadow border border-gray_r-3 rounded-md'
+ key={index}
+ >
<div className='grid grid-cols-2'>
<Link href={`${router.pathname}/${saleOrder.id}`}>
- <span className='text-caption-2 text-gray_r-11'>No. Transaksi</span>
+ <span className='text-caption-2 text-gray_r-11'>
+ No. Transaksi
+ </span>
<h2 className='text-danger-500 mt-1'>{saleOrder.name}</h2>
</Link>
<div className='flex gap-x-1 justify-end'>
@@ -105,13 +211,17 @@ const Transactions = ({ context = '' }) => {
<Link href={`${router.pathname}/${saleOrder.id}`}>
<div className='grid grid-cols-2 mt-3'>
<div>
- <span className='text-caption-2 text-gray_r-11'>No. Purchase Order</span>
+ <span className='text-caption-2 text-gray_r-11'>
+ No. Purchase Order
+ </span>
<p className='mt-1 font-medium text-gray_r-12'>
{saleOrder.purchaseOrderName || '-'}
</p>
</div>
<div className='text-right'>
- <span className='text-caption-2 text-gray_r-11'>Total Invoice</span>
+ <span className='text-caption-2 text-gray_r-11'>
+ Total Invoice
+ </span>
<p className='mt-1 font-medium text-gray_r-12'>
{saleOrder.invoiceCount} Invoice
</p>
@@ -120,10 +230,14 @@ const Transactions = ({ context = '' }) => {
<div className='grid grid-cols-2 mt-3'>
<div>
<span className='text-caption-2 text-gray_r-11'>Sales</span>
- <p className='mt-1 font-medium text-gray_r-12'>{saleOrder.sales}</p>
+ <p className='mt-1 font-medium text-gray_r-12'>
+ {saleOrder.sales}
+ </p>
</div>
<div className='text-right'>
- <span className='text-caption-2 text-gray_r-11'>Total Harga</span>
+ <span className='text-caption-2 text-gray_r-11'>
+ Total Harga
+ </span>
<p className='mt-1 font-medium text-gray_r-12'>
{currencyFormat(saleOrder.amountTotal)}
</p>
@@ -140,14 +254,18 @@ const Transactions = ({ context = '' }) => {
className='mt-2 mb-2'
/>
- <BottomPopup title='Lainnya' active={toOthers} close={() => setToOthers(null)}>
+ <BottomPopup
+ title='Lainnya'
+ active={toOthers}
+ close={() => setToOthers(null)}
+ >
<div className='flex flex-col gap-y-4 mt-2'>
<button
className='text-left disabled:opacity-60'
disabled={!toOthers?.purchaseOrderFile}
onClick={() => {
- downloadPurchaseOrder(toOthers)
- setToOthers(null)
+ downloadPurchaseOrder(toOthers);
+ setToOthers(null);
}}
>
Download PO
@@ -156,8 +274,8 @@ const Transactions = ({ context = '' }) => {
className='text-left disabled:opacity-60'
disabled={toOthers?.status != 'draft'}
onClick={() => {
- downloadQuotation(toOthers)
- setToOthers(null)
+ downloadQuotation(toOthers);
+ setToOthers(null);
}}
>
Download Quotation
@@ -166,8 +284,8 @@ const Transactions = ({ context = '' }) => {
className='text-left disabled:opacity-60'
disabled={toOthers?.status != 'waiting'}
onClick={() => {
- setToCancel(toOthers)
- setToOthers(null)
+ setToCancel(toOthers);
+ setToOthers(null);
}}
>
Batalkan Transaksi
@@ -175,7 +293,11 @@ const Transactions = ({ context = '' }) => {
</div>
</BottomPopup>
- <BottomPopup active={toCancel} close={() => setToCancel(null)} title='Batalkan Transaksi'>
+ <BottomPopup
+ active={toCancel}
+ close={() => setToCancel(null)}
+ title='Batalkan Transaksi'
+ >
<div className='leading-7 text-gray_r-12/80'>
Apakah anda yakin membatalkan transaksi{' '}
<span className='underline'>{toCancel?.name}</span>?
@@ -188,7 +310,11 @@ const Transactions = ({ context = '' }) => {
>
Ya, Batalkan
</button>
- <button className='btn-light flex-1' type='button' onClick={() => setToCancel(null)}>
+ <button
+ className='btn-light flex-1'
+ type='button'
+ onClick={() => setToCancel(null)}
+ >
Batal
</button>
</div>
@@ -205,21 +331,50 @@ const Transactions = ({ context = '' }) => {
<div className='flex mb-6 items-center justify-between'>
<h1 className='text-title-sm font-semibold'>
Daftar Transaksi{' '}
- {transactions?.data?.saleOrders ? `(${transactions?.data?.saleOrders.length})` : ''}
+ {transactions?.data?.saleOrders
+ ? `(${transactions?.data?.saleOrders.length})`
+ : ''}
</h1>
- <form className='flex gap-x-2' onSubmit={handleSubmit}>
- <input
- type='text'
- className='form-input'
- placeholder='Cari Transaksi...'
- value={inputQuery}
- onChange={(e) => setInputQuery(e.target.value)}
- />
- <button className='btn-light bg-transparent px-3' type='submit'>
- <MagnifyingGlassIcon className='w-6' />
- </button>
- </form>
+ <div className='grid grid-cols-2 gap-2'>
+ {listSites?.length > 0 ? (
+ <select
+ value={siteFilter}
+ onChange={handleSiteFilterChange}
+ className='form-input'
+ >
+ <option value=''>Pilih Site</option>
+ {listSites.map((site) => (
+ <option value={site} key={site}>
+ {site}
+ </option>
+ ))}
+ </select>
+ ) : (<div></div>)}
+
+ <form className='flex gap-x-1' onSubmit={handleSubmit}>
+ <input
+ type='text'
+ className='form-input'
+ placeholder='Cari Transaksi...'
+ value={inputQuery}
+ onChange={(e) => setInputQuery(e.target.value)}
+ />
+ <button
+ className='btn-light bg-transparent px-3'
+ type='submit'
+ >
+ <MagnifyingGlassIcon className='w-6' />
+ </button>
+ </form>
+ </div>
</div>
+ <button
+ onClick={handleExportExcel}
+ type='button'
+ className='btn-solid-red px-3 py-2 mr-auto mb-2'
+ >
+ <span>Download</span>
+ </button>
<table className='table-data'>
<thead>
<tr>
@@ -227,6 +382,9 @@ const Transactions = ({ context = '' }) => {
<th>No. PO</th>
<th>Tanggal</th>
<th>Created By</th>
+ {auth?.feature?.soApproval && (
+ <th>Site</th>
+ )}
<th className='!text-left'>Salesperson</th>
<th className='!text-left'>Total</th>
<th>Status</th>
@@ -252,13 +410,23 @@ const Transactions = ({ context = '' }) => {
{transactions.data?.saleOrders?.map((saleOrder) => (
<tr key={saleOrder.id}>
<td>
- <Link className='whitespace-nowrap' href={`${router.pathname}/${saleOrder.id}`}>{saleOrder.name}</Link>
+ <Link
+ className='whitespace-nowrap'
+ href={`${router.pathname}/${saleOrder.id}`}
+ >
+ {saleOrder.name}
+ </Link>
</td>
<td>{saleOrder.purchaseOrderName || '-'}</td>
<td>{saleOrder.dateOrder || '-'}</td>
<td>{saleOrder.address.customer?.name || '-'}</td>
+ {auth?.feature?.soApproval && (
+ <td>{saleOrder.sitePartner || '-'}</td>
+ )}
<td className='!text-left'>{saleOrder.sales}</td>
- <td className='!text-left'>{currencyFormat(saleOrder.amountTotal)}</td>
+ <td className='!text-left'>
+ {currencyFormat(saleOrder.amountTotal)}
+ </td>
<td>
<div className='flex justify-center'>
<TransactionStatusBadge status={saleOrder.status} />
@@ -272,14 +440,14 @@ const Transactions = ({ context = '' }) => {
<Pagination
pageCount={pageCount}
currentPage={parseInt(page)}
- url={router.pathname + pageQuery}
+ url={router.pathname + (pageQuery ? `?${pageQuery}` : '')}
className='mt-2 mb-2'
/>
</div>
</div>
</DesktopView>
</>
- )
-}
+ );
+};
-export default Transactions
+export default Transactions;
diff --git a/src/lib/transaction/components/stepper.jsx b/src/lib/transaction/components/stepper.jsx
new file mode 100644
index 00000000..9b0da0d9
--- /dev/null
+++ b/src/lib/transaction/components/stepper.jsx
@@ -0,0 +1,83 @@
+import {
+ Box,
+ Step,
+ StepDescription,
+ StepIcon,
+ StepIndicator,
+ StepNumber,
+ StepSeparator,
+ StepStatus,
+ StepTitle,
+ Stepper,
+ useSteps,
+} from '@chakra-ui/react';
+import Image from 'next/image';
+
+const StepApproval = ({ layer, status }) => {
+ const steps = [
+ { title: 'Indoteknik', layer_approval: 1 },
+ { title: 'Manager', layer_approval: 2 },
+ { title: 'Director', layer_approval: 3 },
+ ];
+ const { activeStep } = useSteps({
+ index: layer,
+ count: steps.length,
+ });
+ return (
+ <Stepper size='md' index={layer} colorScheme='green'>
+ {steps.map((step, index) => (
+ <Step key={index}>
+ <StepIndicator>
+ {layer === step.layer_approval && status === 'cancel' ? (
+ <StepStatus
+ complete={
+ <Image
+ src='/images/remove.png'
+ width={20}
+ height={20}
+ alt=''
+ className='w-full'
+ />
+ }
+ incomplete={<StepNumber />}
+ active={<StepNumber />}
+ />
+ ) : (
+ <StepStatus
+ complete={<StepIcon />}
+ incomplete={<StepNumber />}
+ active={<StepNumber />}
+ />
+ )}
+ </StepIndicator>
+
+ <Box flexShrink='0'>
+ <StepTitle className='md:text-xs'>{step.title}</StepTitle>
+ {status === 'cancel' ? (
+ layer > step.layer_approval ? (
+ <StepDescription className='md:text-[8px]'>
+ Approved
+ </StepDescription>
+ ) : (
+ <StepDescription className='md:text-[8px]'>
+ Rejected
+ </StepDescription>
+ )
+ ) : layer >= step.layer_approval ? (
+ <StepDescription className='md:text-[8px]'>
+ Approved
+ </StepDescription>
+ ) : (
+ <StepDescription className='md:text-[8px]'>
+ Pending
+ </StepDescription>
+ )}
+ </Box>
+ <StepSeparator _horizontal={{ ml: '0' }} />
+ </Step>
+ ))}
+ </Stepper>
+ );
+};
+
+export default StepApproval;
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>