summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authortrisusilo48 <tri.susilo@altama.co.id>2024-07-02 14:33:25 +0700
committertrisusilo48 <tri.susilo@altama.co.id>2024-07-02 14:33:25 +0700
commite8ad23dbad5e96dddcd6b10bdc46400c6721e80b (patch)
treefafea81669ea00f824260ecb4a0acc9e1096499f /src
parentc6eec1fcd70c878f9fa4911ae4ebf1a1c97a18b7 (diff)
parent66d787499d0751365c1cda9d79b31e9f3c3c28bc (diff)
Merge branch 'release' into feature/generate_recomendation
Diffstat (limited to 'src')
-rw-r--r--src/api/bannerApi.js3
-rw-r--r--src/api/promoApi.js70
-rw-r--r--src/core/components/elements/Footer/BasicFooter.jsx127
-rw-r--r--src/core/components/elements/Footer/SimpleFooter.jsx2
-rw-r--r--src/core/components/elements/Navbar/NavbarDesktop.jsx117
-rw-r--r--src/core/components/layouts/BasicLayout.jsx26
-rw-r--r--src/core/utils/auth.js2
-rw-r--r--src/core/utils/googleTag.js24
-rw-r--r--src/core/utils/whatsappUrl.js2
-rw-r--r--src/images/logo-idul-fitri.pngbin0 -> 10642 bytes
-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/checkout/components/Checkout.jsx52
-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/home/components/PromotionProgram.jsx66
-rw-r--r--src/lib/product/components/Product/ProductDesktop.jsx2
-rw-r--r--src/lib/product/components/Product/ProductDesktopVariant.jsx49
-rw-r--r--src/lib/product/components/Product/ProductMobile.jsx4
-rw-r--r--src/lib/product/components/ProductCard.jsx195
-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.jsx10
-rw-r--r--src/lib/promo/components/Promocrumb.jsx40
-rw-r--r--src/lib/quotation/components/Quotation.jsx494
-rw-r--r--src/lib/transaction/api/approveApi.js13
-rw-r--r--src/lib/transaction/api/rejectApi.js13
-rw-r--r--src/lib/transaction/components/Transaction.jsx534
-rw-r--r--src/lib/transaction/components/stepper.jsx83
-rw-r--r--src/lib/variant/components/VariantCard.jsx35
-rw-r--r--src/pages/_app.jsx4
-rw-r--r--src/pages/api/shop/search.js118
-rw-r--r--src/pages/api/shop/variant-detail.js5
-rw-r--r--src/pages/index.jsx36
-rw-r--r--src/pages/shop/promo/[slug].tsx523
-rw-r--r--src/pages/shop/promo/index.tsx186
-rw-r--r--src/pages/video.jsx2
-rw-r--r--src/utils/solrMapping.js9
42 files changed, 2971 insertions, 724 deletions
diff --git a/src/api/bannerApi.js b/src/api/bannerApi.js
index 8bae131d..431225a5 100644
--- a/src/api/bannerApi.js
+++ b/src/api/bannerApi.js
@@ -3,3 +3,6 @@ import odooApi from '@/core/api/odooApi'
export const bannerApi = ({ type }) => {
return async () => await odooApi('GET', `/api/v1/banner?type=${type}`)
}
+
+// ubah ke SOLR
+
diff --git a/src/api/promoApi.js b/src/api/promoApi.js
new file mode 100644
index 00000000..4c386fba
--- /dev/null
+++ b/src/api/promoApi.js
@@ -0,0 +1,70 @@
+// src/api/promoApi.js
+import odooApi from '@/core/api/odooApi';
+import { type } from 'os';
+// import { SolrResponse } from "../../../../src-migrate/types/solr.ts";
+
+export const fetchPromoItems = async (type) => {
+ try {
+ const response = await odooApi('GET', `/api/v1/program-line?type=${type}&limit=3`);
+ return response.map((item) => ({ value: item.id, label: item.name, product: item.products ,price:item.price}));
+ } catch (error) {
+ console.error('Error fetching promo items:', error);
+ return [];
+ }
+};
+
+export const fetchPromoItemsSolr = async (type) => {
+ // let query = type ? `type_value_s:${type}` : '*:*';
+ let start = 0
+ let rows = 100
+ try {
+ const queryParams = new URLSearchParams({ q: type });
+ const response = await fetch(`/solr/promotion_program_lines/select?${queryParams.toString()}&rows=${rows}&start=${start}`);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const data = await response.json();
+ const promotions = await map(data.response.docs);
+ return promotions;
+ } catch (error) {
+ console.error("Error fetching promotion data:", error);
+ return [];
+ }
+};
+
+export const fetchVariantSolr = async(data)=>{
+ try {
+ const queryParams = new URLSearchParams({ q: data });
+ const response = await fetch(`/solr/variants/select?${queryParams.toString()}`);
+ const responseData = await response.json();
+ return responseData;
+ } catch (error) {
+ console.error("Error fetching promotion data:", error);
+ return [];
+ }
+};
+
+const map = async (promotions) => {
+ const result = [];
+ for (const promotion of promotions) {
+ const data = {
+ id: promotion.id,
+ program_id: promotion.program_id_i,
+ name: promotion.name_s,
+ type: {
+ value: promotion.type_value_s,
+ label: promotion.type_label_s,
+ },
+ limit: promotion.package_limit_i,
+ limit_user: promotion.package_limit_user_i,
+ limit_trx: promotion.package_limit_trx_i,
+ price: promotion.price_f,
+ total_qty: promotion.total_qty_i,
+ products: JSON.parse(promotion.products_s),
+ product_id: promotion.product_ids[0],
+ free_products: JSON.parse(promotion.free_products_s),
+ };
+ result.push(data);
+ }
+ return result;
+}; \ No newline at end of file
diff --git a/src/core/components/elements/Footer/BasicFooter.jsx b/src/core/components/elements/Footer/BasicFooter.jsx
index 28a3764c..6129143d 100644
--- a/src/core/components/elements/Footer/BasicFooter.jsx
+++ b/src/core/components/elements/Footer/BasicFooter.jsx
@@ -41,6 +41,8 @@ const BasicFooter = () => {
<Form />
<CustomerGuide />
<Payments />
+ <Shippings />
+ <Secures />
</div>
<div className='w-full mt-8 leading-5 text-caption-2 text-gray_r-12/80'>
@@ -53,7 +55,7 @@ const BasicFooter = () => {
<DesktopView>
<footer className='bg-gray_r-3 py-6'>
<div className='container mx-auto flex flex-wrap justify-between'>
- <div className='w-3/12'>
+ <div className='w-4/12'>
<NextImage
src={IndoteknikLogo}
alt='Logo Indoteknik'
@@ -64,27 +66,40 @@ const BasicFooter = () => {
PT. Indoteknik Dotcom Gemilang
</div>
<InformationCenter />
+ <div className='h-4' />
+ <OfficeLocation />
+ <div className='h-4' />
+ <OpenHours />
</div>
- <CustomerGuide />
- <Form />
- <AboutUs />
+
+ <div className='w-2/12'>
+ <CustomerGuide />
+ <div className='h-6' />
+ <Form />
+ </div>
+
+ <div className='w-2/12'>
+ <AboutUs />
+ </div>
+
<div className='w-3/12'>
- <div className='grid grid-cols-1 gap-y-4'>
- <OfficeLocation />
- {/* <WarehouseLocation /> */}
- <OpenHours />
+ <div className='grid grid-cols-1 gap-y-6'>
<Payments />
+ <Shippings />
+ <Secures />
</div>
</div>
+
<hr className='w-full my-4 border-gray_r-7' />
+
<div className='w-full flex justify-between items-center'>
<div className='text-caption-1'>
Copyright © 2007 - {new Date().getFullYear()}, PT. Indoteknik
Dotcom Gemilang
</div>
- <div>
+ {/* <div>
<SocialMedias />
- </div>
+ </div> */}
</div>
</div>
</footer>
@@ -256,7 +271,7 @@ const InformationCenter = () => (
<li className='text-gray_r-12/80 flex items-center'>
<DevicePhoneMobileIcon className='w-[18px] mr-2' />
<a href={whatsappUrl()} target='_blank' rel='noreferrer'>
- 0812-8080-622
+ 0817-1718-1922
</a>
</li>
</ul>
@@ -286,7 +301,7 @@ const SocialMedias = () => (
<a
target='_blank'
rel='noreferrer'
- href='https://www.youtube.com/@indoteknikb2bindustriale-c778'
+ href='https://www.youtube.com/@indoteknikcom'
>
<NextImage
src='/images/socials/youtube.webp'
@@ -369,57 +384,43 @@ const SocialMedias = () => (
const Payments = () => (
<div>
- <div className={headerClassName}>Pembayaran</div>
- <div className='flex flex-wrap gap-2'>
- <NextImage
- src='/images/payments/bca.png'
- alt='Bank BCA Logo'
- width={48}
- height={24}
- />
- <NextImage
- src='/images/payments/bni.png'
- alt='Bank BNI Logo'
- width={48}
- height={24}
- />
- <NextImage
- src='/images/payments/bri.png'
- alt='Bank BRI Logo'
- width={48}
- height={24}
- />
- <NextImage
- src='/images/payments/gopay.png'
- alt='Gopay Logo'
- width={48}
- height={24}
- />
- <NextImage
- src='/images/payments/mandiri.png'
- alt='Bank Mandiri Logo'
- width={48}
- height={24}
- />
- <NextImage
- src='/images/payments/mastercard.png'
- alt='Mastercard Logo'
- width={48}
- height={24}
- />
- <NextImage
- src='/images/payments/permata.png'
- alt='Bank Permata Logo'
- width={48}
- height={24}
- />
- <NextImage
- src='/images/payments/visa.png'
- alt='Visa Logo'
- width={48}
- height={24}
- />
- </div>
+ <div className={headerClassName}>Metode Pembayaran</div>
+ <NextImage
+ src='/images/footer/payment-method-new.png'
+ alt='Metode Pembayaran - Indoteknik'
+ width={512}
+ height={512}
+ quality={100}
+ className='w-full'
+ />
+ </div>
+);
+
+const Shippings = () => (
+ <div>
+ <div className={headerClassName}>Jasa Pengiriman</div>
+ <NextImage
+ src='/images/footer/shippings.png'
+ alt='Jasa Pengiriman - Indoteknik'
+ width={512}
+ height={512}
+ quality={100}
+ className='w-full'
+ />
+ </div>
+);
+
+const Secures = () => (
+ <div>
+ <div className={headerClassName}>Keamanan Belanja</div>
+ <NextImage
+ src='/images/footer/secures.png'
+ alt='Keamanan Belanja - Indoteknik'
+ width={512}
+ height={512}
+ quality={100}
+ className='w-full'
+ />
</div>
);
diff --git a/src/core/components/elements/Footer/SimpleFooter.jsx b/src/core/components/elements/Footer/SimpleFooter.jsx
index 26f7f786..371b1652 100644
--- a/src/core/components/elements/Footer/SimpleFooter.jsx
+++ b/src/core/components/elements/Footer/SimpleFooter.jsx
@@ -22,7 +22,7 @@ const SimpleFooter = () => (
<li className='text-gray_r-12/80 flex items-center'>
<DevicePhoneMobileIcon className='w-[18px] mr-2' />
<a href={whatsappUrl()} target='_blank' rel='noreferrer'>
- 0812-8080-622
+ 081717181922
</a>
</li>
</ul>
diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx
index e11ad214..308f2623 100644
--- a/src/core/components/elements/Navbar/NavbarDesktop.jsx
+++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx
@@ -18,6 +18,7 @@ import { useEffect, useState } from 'react';
import DesktopView from '../../views/DesktopView';
import Link from '../Link/Link';
import NavbarUserDropdown from './NavbarUserDropdown';
+import NextImage from 'next/image';
const Search = dynamic(() => import('./Search'), { ssr: false });
const TopBanner = dynamic(() => import('./TopBanner'), { ssr: false });
@@ -69,12 +70,17 @@ const NavbarDesktop = () => {
return (
<DesktopView>
<TopBanner />
- <div className='py-3 bg-warning-400' id='desktop-nav-top'>
+ <div className='py-2 bg-warning-400' id='desktop-nav-top'>
<div className='container mx-auto flex justify-between'>
- <Link href='/tentang-kami' className='!text-gray_r-12'>
- Tentang Indoteknik.com
- </Link>
+ <div className='flex items-start gap-5'>
+ <div>
+ <SocialMedias />
+ </div>
+ </div>
<div className='flex gap-x-6'>
+ <Link href='/tentang-kami' className='!text-gray_r-12'>
+ Tentang Indoteknik.com
+ </Link>
<Link href='/my/pembayaran-tempo' className='!text-gray_r-12'>
Pembayaran Tempo
</Link>
@@ -136,7 +142,7 @@ const NavbarDesktop = () => {
/>
<div>
<div className='font-semibold'>Whatsapp</div>
- 0812 8080 622 (Chat)
+ 0817 1718 1922 (Chat)
</div>
</a>
</div>
@@ -166,7 +172,9 @@ const NavbarDesktop = () => {
<div className='w-6/12 flex px-1 divide-x divide-gray_r-6'>
<Link
href='/shop/brands'
- className={`${router.asPath === '/shop/brands' && 'bg-gray_r-3'} p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`}
+ className={`${
+ router.asPath === '/shop/brands' && 'bg-gray_r-3'
+ } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`}
target='_blank'
rel='noreferrer'
>
@@ -174,7 +182,10 @@ const NavbarDesktop = () => {
</Link>
<Link
href='/shop/search?orderBy=stock'
- className={`${router.asPath === '/shop/search?orderBy=stock' && 'bg-gray_r-3'} p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`}
+ className={`${
+ router.asPath === '/shop/search?orderBy=stock' &&
+ 'bg-gray_r-3'
+ } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`}
target='_blank'
rel='noreferrer'
>
@@ -190,7 +201,9 @@ const NavbarDesktop = () => {
</Link>
<Link
href='/video'
- className={`${router.asPath === '/video' && 'bg-gray_r-3'} p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`}
+ className={`${
+ router.asPath === '/video' && 'bg-gray_r-3'
+ } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`}
target='_blank'
rel='noreferrer'
>
@@ -234,4 +247,92 @@ const NavbarDesktop = () => {
);
};
+const SocialMedias = () => (
+ <div>
+ {/* <div className={headerClassName + 'block md:hidden'}>Temukan Kami</div> */}
+ <div className='flex flex-wrap gap-3 items-start'>
+ <a
+ target='_blank'
+ rel='noreferrer'
+ href='https://www.youtube.com/@indoteknikcom'
+ >
+ <NextImage
+ src='/images/socials/youtube.webp'
+ alt='Youtube - Indoteknik.com'
+ width={24}
+ height={24}
+ />
+ </a>
+ <a
+ target='_blank'
+ rel='noreferrer'
+ href='https://www.tiktok.com/@indoteknikcom'
+ >
+ <NextImage
+ src='/images/socials/tiktok.png'
+ alt='TikTok - Indoteknik.com'
+ width={24}
+ height={24}
+ />
+ </a>
+ {/* <a target='_blank' rel='noreferrer' href={whatsappUrl(null)}>
+ <NextImage
+ src='/images/socials/Whatsapp.png'
+ alt='Whatsapp - Indoteknik.com'
+ width={24}
+ height={24}
+ />
+ </a> */}
+ <a
+ target='_blank'
+ rel='noreferrer'
+ href='https://www.facebook.com/indoteknikcom'
+ >
+ <NextImage
+ src='/images/socials/Facebook.png'
+ alt='Facebook - Indoteknik.com'
+ width={24}
+ height={24}
+ />
+ </a>
+ <a
+ target='_blank'
+ rel='noreferrer'
+ href='https://www.instagram.com/indoteknikcom/'
+ >
+ <NextImage
+ src='/images/socials/Instagram.png'
+ alt='Instagram - Indoteknik.com'
+ width={24}
+ height={24}
+ />
+ </a>
+ <a
+ target='_blank'
+ rel='noreferrer'
+ href='https://www.linkedin.com/company/pt-indoteknik-dotcom-gemilang/'
+ >
+ <NextImage
+ src='/images/socials/Linkedin.png'
+ alt='Linkedin - Indoteknik.com'
+ width={24}
+ height={24}
+ />
+ </a>
+ <a
+ target='_blank'
+ rel='noreferrer'
+ href='https://goo.gl/maps/GF8EmDjpQTHZPsJ1A'
+ >
+ <NextImage
+ src='/images/socials/g_maps.png'
+ alt='Maps - Indoteknik.com'
+ width={24}
+ height={24}
+ />
+ </a>
+ </div>
+ </div>
+);
+
export default NavbarDesktop;
diff --git a/src/core/components/layouts/BasicLayout.jsx b/src/core/components/layouts/BasicLayout.jsx
index fa41a8ed..a4f3a856 100644
--- a/src/core/components/layouts/BasicLayout.jsx
+++ b/src/core/components/layouts/BasicLayout.jsx
@@ -1,16 +1,13 @@
import dynamic from 'next/dynamic';
import Image from 'next/image';
-import { useCallback, useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
import { useProductContext } from '@/contexts/ProductContext';
import odooApi from '@/core/api/odooApi';
import whatsappUrl from '@/core/utils/whatsappUrl';
-import { useRouter } from 'next/router';
+import Navbar from '../elements/Navbar/Navbar';
-const Navbar = dynamic(() => import('../elements/Navbar/Navbar'), {
- ssr: false,
- loading: () => <div className='h-[156px]' />,
-});
const AnimationLayout = dynamic(() => import('./AnimationLayout'), {
ssr: false,
});
@@ -42,13 +39,10 @@ const BasicLayout = ({ children }) => {
}
}, [product, router]);
- const recordActivity = useCallback(async () => {
- const recordedPath = [
- '/shop/product/[slug]',
- '/shop/product/variant/[slug]',
- ];
-
- if (!recordedPath.includes(router.pathname)) return;
+ const recordActivity = async (pathname) => {
+ const ONLY_ON_PATH = false;
+ const recordedPath = [];
+ if (ONLY_ON_PATH && !recordedPath.includes(pathname)) return;
const ip = await odooApi('GET', '/api/ip-address');
const data = new URLSearchParams({
@@ -58,11 +52,11 @@ const BasicLayout = ({ children }) => {
});
fetch(`/api/user-activity?${data.toString()}`);
- }, [router.pathname]);
+ };
useEffect(() => {
- recordActivity();
- }, [recordActivity]);
+ recordActivity(router.pathname);
+ }, [router.pathname]);
return (
<>
diff --git a/src/core/utils/auth.js b/src/core/utils/auth.js
index a7244747..03b20ae2 100644
--- a/src/core/utils/auth.js
+++ b/src/core/utils/auth.js
@@ -29,7 +29,7 @@ const setAuth = (user) => {
* @returns {boolean} - Returns `true`.
*/
const deleteAuth = async() => {
- // await signOut()
+ await signOut()
deleteCookie('auth')
return true
}
diff --git a/src/core/utils/googleTag.js b/src/core/utils/googleTag.js
index cc6d1283..96a6bd2e 100644
--- a/src/core/utils/googleTag.js
+++ b/src/core/utils/googleTag.js
@@ -33,6 +33,20 @@ const sumTotal = (variants) => {
}
}
+const mapProducts = (product) => {
+ const res = {
+ item_id: product.id,
+ item_name: product.name,
+ discount: product.lowest_price.price_discount || 0,
+ // index: 0,
+ item_brand: product.manufacture.name,
+ item_category: product.categories,
+ item_variant: product.variants,
+ price: product.lowest_price.price,
+ quantity: product.stock_total
+ }
+ return res
+}
export const gtagAddToCart = (variant, quantity) => {
const param = {
currency: 'IDR',
@@ -77,3 +91,13 @@ export const gtagPurchase = (variants, shipping, transactionId) => {
}
gtag('event', 'purchase', param)
}
+
+export const gtagProductDetail = (product) => {
+ const items = mapProducts(product)
+ const param = {
+ currency: 'IDR',
+ value: product.id,
+ items
+ }
+ gtag('event', 'view_item', param)
+} \ No newline at end of file
diff --git a/src/core/utils/whatsappUrl.js b/src/core/utils/whatsappUrl.js
index 9a92f424..7a129aa6 100644
--- a/src/core/utils/whatsappUrl.js
+++ b/src/core/utils/whatsappUrl.js
@@ -7,7 +7,7 @@ const whatsappUrl = (template = 'default', payload, urlPath = null) => {
if(!urlPath) return '/login'
}
let parentName = user.parentName || '-'
- let url = 'https://wa.me/628128080622'
+ let url = 'https://wa.me/6281717181922'
let text = 'Hallo Indoteknik.com,'
switch (template) {
case 'product':
diff --git a/src/images/logo-idul-fitri.png b/src/images/logo-idul-fitri.png
new file mode 100644
index 00000000..db04faa5
--- /dev/null
+++ b/src/images/logo-idul-fitri.png
Binary files differ
diff --git a/src/lib/auth/components/LoginDesktop.jsx b/src/lib/auth/components/LoginDesktop.jsx
index 1333db14..9a68dc53 100644
--- a/src/lib/auth/components/LoginDesktop.jsx
+++ b/src/lib/auth/components/LoginDesktop.jsx
@@ -8,6 +8,7 @@ import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner';
+import Image from 'next/image';
const LoginDesktop = () => {
const {
@@ -108,7 +109,7 @@ const LoginDesktop = () => {
{!isLoading ? 'Masuk' : 'Loading...'}
</button>
</form>
- {/* <div className='flex items-center mt-3 mb-3'>
+ <div className='flex items-center mt-3 mb-3'>
<hr className='flex-1' />
<p className='text-gray-400'>ATAU</p>
<hr className='flex-1' />
@@ -127,7 +128,7 @@ const LoginDesktop = () => {
height={10}
/>
<p>Masuk dengan Google</p>
- </button> */}
+ </button>
<div className='text-gray_r-11 mt-10'>
Belum punya akun Indoteknik?{' '}
diff --git a/src/lib/auth/components/LoginMobile.jsx b/src/lib/auth/components/LoginMobile.jsx
index 40924fbe..d2bf704f 100644
--- a/src/lib/auth/components/LoginMobile.jsx
+++ b/src/lib/auth/components/LoginMobile.jsx
@@ -117,7 +117,7 @@ const LoginMobile = () => {
{!isLoading ? 'Masuk' : 'Loading...'}
</button>
</form>
- {/* <div className='flex items-center mt-3 mb-3'>
+ <div className='flex items-center mt-3 mb-3'>
<hr className='flex-1' />
<p className='text-gray-400'>ATAU</p>
<hr className='flex-1' />
@@ -136,7 +136,7 @@ const LoginMobile = () => {
height={10}
/>
<p>Masuk dengan Google</p>
- </button> */}
+ </button>
<div className='text-gray_r-11 mt-4'>
Belum punya akun Indoteknik?{' '}
diff --git a/src/lib/auth/hooks/useLogin.js b/src/lib/auth/hooks/useLogin.js
index dc9580ea..dd5a4b03 100644
--- a/src/lib/auth/hooks/useLogin.js
+++ b/src/lib/auth/hooks/useLogin.js
@@ -74,7 +74,7 @@ const useLogin = () => {
if (data.isAuth) {
session.odooUser = data.user;
setCookie('auth', JSON.stringify(session?.odooUser));
- router.push(decodeURIComponent(router?.query?.next) ?? '/');
+ router.push(router?.query?.next || '/');
return;
}
};
diff --git a/src/lib/brand/components/BrandCard.jsx b/src/lib/brand/components/BrandCard.jsx
index bb1a17f7..731214ff 100644
--- a/src/lib/brand/components/BrandCard.jsx
+++ b/src/lib/brand/components/BrandCard.jsx
@@ -1,4 +1,4 @@
-import Image from '@/core/components/elements/Image/Image'
+import Image from '~/components/ui/image'
import Link from '@/core/components/elements/Link/Link'
import useDevice from '@/core/hooks/useDevice'
import { createSlug } from '@/core/utils/slug'
@@ -16,6 +16,8 @@ const BrandCard = ({ brand }) => {
<Image
src={brand.logo}
alt={brand.name}
+ width={128}
+ height={128}
className='h-full w-full object-contain object-center'
/>
)}
diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx
index 52edbd05..4aafdece 100644
--- a/src/lib/checkout/components/Checkout.jsx
+++ b/src/lib/checkout/components/Checkout.jsx
@@ -111,6 +111,8 @@ const Checkout = () => {
const [checkoutValidation, setCheckoutValidation] = useState(false);
const [loadingVoucher, setLoadingVoucher] = useState(true);
const [loadingRajaOngkir, setLoadingRajaOngkir] = useState(false);
+ const [grandTotal, setGrandTotal] = useState(0);
+ const [hasFlashSale, setHasFlashSale] = useState(false);
const expedisiValidation = useRef(null);
@@ -223,7 +225,9 @@ const Checkout = () => {
setProducts(cartCheckout?.products);
setCheckWeight(cartCheckout?.hasProductWithoutWeight);
setTotalWeight(cartCheckout?.totalWeight.g);
+ setHasFlashSale(cartCheckout?.products[0]?.hasFlashsale ? cartCheckout.products[0].hasFlashsale : false);
}, [cartCheckout]);
+
useEffect(() => {
setCheckoutValidation(false);
@@ -295,6 +299,14 @@ const Checkout = () => {
const [isLoading, setIsLoading] = useState(false);
+ useEffect(() => {
+ const GT =
+ cartCheckout?.grandTotal +
+ Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000;
+ const finalGT = GT < 0 ? 0 : GT;
+ setGrandTotal(finalGT);
+ }, [biayaKirim, cartCheckout?.grandTotal, activeVoucher]);
+
const checkout = async () => {
const file = poFile.current.files[0];
if (typeof file !== 'undefined' && file.size > 5000000) {
@@ -324,14 +336,17 @@ const Checkout = () => {
quantity: product.quantity,
}));
let data = {
- partner_shipping_id: auth.partnerId,
- partner_invoice_id: auth.partnerId,
+ // partner_shipping_id: auth.partnerId,
+ // partner_invoice_id: auth.partnerId,
+ partner_shipping_id: selectedAddress?.shipping?.id || auth.partnerId,
+ partner_invoice_id: selectedAddress?.invoicing?.id || auth.partnerId,
user_id: auth.id,
order_line: JSON.stringify(productOrder),
delivery_amount: biayaKirim,
carrier_id: selectedCarrierId,
estimated_arrival_days: splitDuration(etd),
delivery_service_type: selectedExpedisiService,
+ flash_sale : hasFlashSale, // dibuat negasi untuk ngetest kebalikan nilai false
voucher: activeVoucher,
type: 'sale_order',
};
@@ -347,16 +362,25 @@ const Checkout = () => {
toast.error('Gagal melakukan transaksi, terjadi kesalahan internal');
return;
}
-
+
gtagPurchase(products, biayaKirim, isCheckouted.name);
const midtrans = async () => {
for (const product of products) deleteItemCart({ productId: product.id });
- const payment = await axios.post(
- `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/midtrans-payment?transactionId=${isCheckouted.id}`
- );
- setIsLoading(false);
- window.location.href = payment.data.redirectUrl;
+ if (grandTotal > 0) {
+ const payment = await axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/midtrans-payment?transactionId=${isCheckouted.id}`
+ );
+ setIsLoading(false);
+ window.location.href = payment.data.redirectUrl;
+ } else {
+ window.location.href = `${
+ process.env.NEXT_PUBLIC_SELF_HOST
+ }/shop/checkout/success?order_id=${isCheckouted.name.replace(
+ /\//g,
+ '-'
+ )}`;
+ }
};
gtag('event', 'conversion', {
@@ -913,10 +937,7 @@ const Checkout = () => {
<div className='flex gap-x-2 justify-between mb-4'>
<div>Grand Total</div>
<div className='font-semibold text-gray_r-12'>
- {currencyFormat(
- cartCheckout?.grandTotal +
- Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000
- )}
+ {currencyFormat(grandTotal)}
</div>
</div>
)}
@@ -1208,10 +1229,7 @@ const Checkout = () => {
<div className='flex gap-x-2 justify-between mb-4'>
<div>Grand Total</div>
<div className='font-semibold text-gray_r-12'>
- {currencyFormat(
- cartCheckout?.grandTotal +
- Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000
- )}
+ {currencyFormat(grandTotal)}
</div>
</div>
)}
@@ -1442,7 +1460,7 @@ const SectionExpedisi = ({
dengan menghubungi admin melalui{' '}
<a
className='text-danger-500 inline'
- href='https://api.whatsapp.com/send?phone=628128080622'
+ href='https://api.whatsapp.com/send?phone=6281717181922'
>
tautan ini
</a>
diff --git a/src/lib/checkout/components/CheckoutOld.jsx b/src/lib/checkout/components/CheckoutOld.jsx
index d57fbd66..e2c45ce6 100644
--- a/src/lib/checkout/components/CheckoutOld.jsx
+++ b/src/lib/checkout/components/CheckoutOld.jsx
@@ -696,7 +696,7 @@ const SectionExpedisi = ({ address, listExpedisi, setSelectedExpedisi, checkWeig
diatur beratnya. Mohon atur berat barang dengan menghubungi admin melalui{' '}
<a
className='text-danger-500 inline'
- href='https://api.whatsapp.com/send?phone=628128080622'
+ href='https://api.whatsapp.com/send?phone=6281717181922'
>
tautan ini
</a>
diff --git a/src/lib/checkout/components/CheckoutSection.jsx b/src/lib/checkout/components/CheckoutSection.jsx
new file mode 100644
index 00000000..affe6138
--- /dev/null
+++ b/src/lib/checkout/components/CheckoutSection.jsx
@@ -0,0 +1,257 @@
+import Link from 'next/link';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import { AnimatePresence, motion } from 'framer-motion';
+import { Divider, Spinner } from '@chakra-ui/react';
+
+export const SectionAddress = ({ address, label, url }) => {
+ return (
+ <div className='p-4'>
+ <div className='flex justify-between items-center'>
+ <div className='font-medium'>{label}</div>
+ <Link className='text-caption-1' href={url}>
+ Pilih Alamat Lain
+ </Link>
+ </div>
+
+ {address && (
+ <div className='mt-4 text-caption-1'>
+ <div className='badge-red mb-2'>
+ {address.type.charAt(0).toUpperCase() +
+ address.type.slice(1) +
+ ' Address'}
+ </div>
+ <p className='font-medium'>{address.name}</p>
+ <p className='mt-2 text-gray_r-11'>{address.mobile}</p>
+ <p className='mt-1 text-gray_r-11'>
+ {address.street}, {address?.city?.name}
+ </p>
+ </div>
+ )}
+ </div>
+ );
+};
+
+export const SectionValidation = ({ address }) =>
+ address?.rajaongkirCityId == 0 && (
+ <BottomPopup active={true} title='Update Alamat'>
+ <div className='leading-7 text-gray_r-12/80'>
+ Mohon untuk memperbarui alamat Anda dengan mengklik tombol di bawah ini.{' '}
+ </div>
+ <div className='flex justify-center mt-6 gap-x-4'>
+ <Link
+ className='btn-solid-red w-full md:w-fit text-white'
+ href={`/my/address/${address?.id}/edit`}
+ >
+ Update Alamat
+ </Link>
+ </div>
+ </BottomPopup>
+ );
+
+export const SectionExpedisi = ({
+ address,
+ listExpedisi,
+ setSelectedExpedisi,
+ checkWeigth,
+ checkoutValidation,
+ expedisiValidation,
+ loadingRajaOngkir,
+}) =>
+ address?.rajaongkirCityId > 0 && (
+ <div className='p-4' ref={expedisiValidation}>
+ <div className='flex justify-between items-center'>
+ <div className='font-medium'>Pilih Ekspedisi: </div>
+ <div className='w-[250px]'>
+ <div className='flex items-center gap-x-4'>
+ <select
+ className={`form-input ${
+ checkoutValidation ? 'border-red-500 shake' : ''
+ }`}
+ onChange={(e) => setSelectedExpedisi(e.target.value)}
+ required
+ >
+ <option value='0,0'>Pilih Pengiriman</option>
+ <option value='1,32'>SELF PICKUP</option>
+ {checkWeigth != true &&
+ listExpedisi.map((expedisi) => (
+ <option
+ disabled={checkWeigth}
+ value={expedisi.label + ',' + expedisi.carrierId}
+ key={expedisi.value}
+ >
+ {' '}
+ {expedisi.label.toUpperCase()}{' '}
+ </option>
+ ))}
+ </select>
+
+ <AnimatePresence>
+ {loadingRajaOngkir && (
+ <motion.div
+ initial={{ opacity: 0, width: 0 }}
+ animate={{ opacity: 1, width: '28px' }}
+ exit={{ opacity: 0, width: 0 }}
+ transition={{
+ duration: 0.25,
+ }}
+ className='overflow-hidden'
+ >
+ <Spinner thickness='3px' speed='0.5s' color='red.500' />
+ </motion.div>
+ )}
+ </AnimatePresence>
+ </div>
+ {checkoutValidation && (
+ <span className='text-sm text-red-500'>
+ *silahkan pilih expedisi
+ </span>
+ )}
+ </div>
+ <style jsx>{`
+ .shake {
+ animation: shake 0.4s ease-in-out;
+ }
+ `}</style>
+ </div>
+ {checkWeigth == true && (
+ <p className='mt-4 text-gray_r-11 leading-6'>
+ Mohon maaf, pengiriman hanya tersedia untuk self pickup karena
+ terdapat barang yang belum diatur beratnya. Mohon atur berat barang
+ dengan menghubungi admin melalui{' '}
+ <a
+ className='text-danger-500 inline'
+ href='https://api.whatsapp.com/send?phone=6281717181922'
+ >
+ tautan ini
+ </a>
+ </p>
+ )}
+ </div>
+ );
+
+export const SectionListService = ({
+ listserviceExpedisi,
+ setSelectedServiceType,
+}) =>
+ listserviceExpedisi?.length > 0 && (
+ <>
+ <div className='p-4'>
+ <div className='flex justify-between items-center'>
+ <div className='font-medium'>Tipe Layanan Ekspedisi: </div>
+ <div>
+ <select
+ className='form-input'
+ onChange={(e) => setSelectedServiceType(e.target.value)}
+ >
+ {listserviceExpedisi.map((service) => (
+ <option
+ value={
+ service.cost[0].value +
+ ',' +
+ service.description +
+ '-' +
+ service.service +
+ ',' +
+ extractDuration(service.cost[0].etd)
+ }
+ key={service.service}
+ >
+ {' '}
+ {service.description} - {service.service.toUpperCase()}
+ {extractDuration(service.cost[0].etd) &&
+ ` (Estimasi Tiba ${extractDuration(
+ service.cost[0].etd
+ )} Hari)`}
+ </option>
+ ))}
+ </select>
+ </div>
+ </div>
+ </div>
+ <Divider />
+ </>
+ );
+
+export const PickupAddress = ({ label }) => (
+ <div className='p-4'>
+ <div className='flex justify-between items-center'>
+ <div className='font-medium'>{label}</div>
+ </div>
+ <div className='mt-4 text-caption-1'>
+ <p className='font-medium'>Indoteknik</p>
+ <p className='mt-2 mb-2 text-gray_r-11 leading-6'>
+ Jl. Bandengan Utara Raya No.85, RT.3/RW.16, Penjaringan, Kec.
+ Penjaringan, Kota Jkt Utara, Daerah Khusus Ibukota Jakarta, Indonesia
+ Kodepos : 14440
+ </p>
+ <p className='mt-1 text-gray_r-11'>Telp : 021-2933 8828/29</p>
+ <p className='mt-1 text-gray_r-11'>Mobile : 0813 9000 7430</p>
+ </div>
+ </div>
+);
+
+const extractDuration = (text) => {
+ const matches = text.match(/\d+(?:-\d+)?/g);
+
+ if (matches && matches.length === 1) {
+ const parts = matches[0].split('-');
+ const min = parseInt(parts[0]);
+ const max = parseInt(parts[1]);
+
+ if (min === max) {
+ return min.toString();
+ }
+
+ return matches[0];
+ }
+
+ return '';
+};
+
+export function calculateEstimatedArrival(duration) {
+ if (duration) {
+ let estimationDate = duration.split('-');
+ estimationDate[0] = parseInt(estimationDate[0]);
+ estimationDate[1] = parseInt(estimationDate[1]);
+ const from = addDays(new Date(), estimationDate[0] + 3);
+ const to = addDays(new Date(), estimationDate[1] + 3);
+
+ let etdText = `*Estimasi tiba ${formatDate(from)}`;
+
+ if (estimationDate[1] > estimationDate[0]) {
+ etdText += ` - ${formatDate(to)}`;
+ }
+
+ return etdText;
+ }
+
+ return '';
+}
+
+function addDays(date, days) {
+ const result = new Date(date);
+ result.setDate(result.getDate() + days);
+ return result;
+}
+
+function formatDate(date) {
+ const day = date.getDate();
+ const month = date.toLocaleString('default', { month: 'short' });
+ return `${day} ${month}`;
+}
+
+export function splitDuration(duration) {
+ if (duration) {
+ let estimationDate = null;
+ if (duration.includes('-')) {
+ estimationDate = duration.split('-');
+ estimationDate = parseInt(estimationDate[1]);
+ } else {
+ estimationDate = parseInt(duration);
+ }
+
+ return estimationDate;
+ }
+
+ return '';
+} \ No newline at end of file
diff --git a/src/lib/checkout/email/FinishCheckoutEmail.jsx b/src/lib/checkout/email/FinishCheckoutEmail.jsx
index d40ce7d4..d19ba1ca 100644
--- a/src/lib/checkout/email/FinishCheckoutEmail.jsx
+++ b/src/lib/checkout/email/FinishCheckoutEmail.jsx
@@ -14,8 +14,10 @@ import {
Section,
Text
} from '@react-email/components'
+import FinishCheckout from '../components/FinishCheckout'
const FinishCheckoutEmail = ({ transaction, payment, statusPayment }) => {
+
return (
<Html>
<Head />
@@ -38,7 +40,10 @@ const FinishCheckoutEmail = ({ transaction, payment, statusPayment }) => {
</Heading>
<Text style={style.text}>Hai {transaction.address.customer.name},</Text>
- <Text style={style.text}>
+
+ {transaction.amountTotal > 0 ?
+ <div>
+ <Text style={style.text}>
{statusPayment == 'success' && (
<>
Terima kasih atas kepercayaan anda berbelanja di Indoteknik. Dengan ini kami
@@ -71,202 +76,204 @@ const FinishCheckoutEmail = ({ transaction, payment, statusPayment }) => {
& Solution
</>
)}
- </Text>
- <Text style={style.text}>
- {['pending', 'failed'].includes(statusPayment) && (
- <>
- Jika anda mengalami kesulitan, dapat menghubungi Customer Service kami untuk
- menanyakan transaksi anda lakukan melalui Whatsapp kami.
- </>
- )}
- {statusPayment == 'success' && (
- <>
- Anda dapat menghubungi Customer Service kami untuk menanyakan status pesanan yang
- sudah berhasil anda lakukan melalui Whatsapp kami.
- </>
- )}
- {statusPayment == 'manual' && (
+ </Text>
+ <Text style={style.text}>
+ {['pending', 'failed'].includes(statusPayment) && (
+ <>
+ Jika anda mengalami kesulitan, dapat menghubungi Customer Service kami untuk
+ menanyakan transaksi anda lakukan melalui Whatsapp kami.
+ </>
+ )}
+ {statusPayment == 'success' && (
+ <>
+ Anda dapat menghubungi Customer Service kami untuk menanyakan status pesanan yang
+ sudah berhasil anda lakukan melalui Whatsapp kami.
+ </>
+ )}
+ {statusPayment == 'manual' && (
+ <>
+ Kami mohon kepada {transaction.address.customer.name} untuk dapat segera
+ menyelesaikan transaksi dengan detail dibawah ini:
+ <ul>
+ <li>Nomor Pembelian: {transaction.name}</li>
+ <li>Nominal: {currencyFormat(transaction.amountTotal)}</li>
+ <li>Tanggal: {transaction.dateOrder}</li>
+ </ul>
+ </>
+ )}
+ </Text>
+
+ {['pending', 'failed', 'success'].includes(statusPayment) && (
<>
- Kami mohon kepada {transaction.address.customer.name} untuk dapat segera
- menyelesaikan transaksi dengan detail dibawah ini:
- <ul>
- <li>Nomor Pembelian: {transaction.name}</li>
- <li>Nominal: {currencyFormat(transaction.amountTotal)}</li>
- <li>Tanggal: {transaction.dateOrder}</li>
- </ul>
- </>
- )}
- </Text>
+ <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}>
+ <strong>Detail Transaksi</strong>
+ </Text>
+
+ <Hr style={style.hr} />
- {['pending', 'failed', 'success'].includes(statusPayment) && (
- <>
- <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}>
- <strong>Detail Transaksi</strong>
- </Text>
+ <Section style={style.alert}>
+ {statusPayment == 'success' &&
+ 'Struk ini dapat anda simpan sebagai bukti tambahan dalam transaksi yang telah dilakukan.'}
+ {statusPayment == 'pending' &&
+ 'Kami akan menginformasikan melalui email setelah anda berhasil melakukan pembayaran.'}
+ {statusPayment == 'failed' &&
+ 'Dimohon untuk tidak melakukan pembayaran. Karena transaksi anda tidak berhasil dibuat.'}
+ </Section>
- <Hr style={style.hr} />
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>No Transaksi (SO)</Column>
+ <Column style={style.descriptionRCol}>{transaction.name}</Column>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Tanggal Transaksi</Column>
+ <Column style={style.descriptionRCol}>{payment.transactionTime}</Column>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Status Pembayaran</Column>
+ <Column style={{ ...style.descriptionRCol }}>
+ {statusPayment == 'success' && (
+ <div style={{ ...style.badge, ...style.badgeGreen }}>Berhasil</div>
+ )}
+ {statusPayment == 'pending' && (
+ <div style={{ ...style.badge, ...style.badgeRed }}>Pending</div>
+ )}
+ {statusPayment == 'failed' && (
+ <div style={{ ...style.badge, ...style.badgeRed }}>Tidak Berhasil</div>
+ )}
+ </Column>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Metode Pembayaran</Column>
+ <Column style={style.descriptionRCol}>
+ {toTitleCase(payment.paymentType.replaceAll('_', ' '))}
+ </Column>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Batas Akhir Pembayaran</Column>
+ <Column style={style.descriptionRCol}>{payment.expiryTime}</Column>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Nominal Transfer</Column>
+ <Column style={style.descriptionRCol}>
+ <span style={{ fontWeight: '600' }}>{currencyFormat(payment.grossAmount)}</span>
+ </Column>
+ </Row>
- <Section style={style.alert}>
- {statusPayment == 'success' &&
- 'Struk ini dapat anda simpan sebagai bukti tambahan dalam transaksi yang telah dilakukan.'}
- {statusPayment == 'pending' &&
- 'Kami akan menginformasikan melalui email setelah anda berhasil melakukan pembayaran.'}
- {statusPayment == 'failed' &&
- 'Dimohon untuk tidak melakukan pembayaran. Karena transaksi anda tidak berhasil dibuat.'}
- </Section>
+ <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}>
+ <strong>Detail Produk</strong>
+ </Text>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>No Transaksi (SO)</Column>
- <Column style={style.descriptionRCol}>{transaction.name}</Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Tanggal Transaksi</Column>
- <Column style={style.descriptionRCol}>{payment.transactionTime}</Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Status Pembayaran</Column>
- <Column style={{ ...style.descriptionRCol }}>
- {statusPayment == 'success' && (
- <div style={{ ...style.badge, ...style.badgeGreen }}>Berhasil</div>
- )}
- {statusPayment == 'pending' && (
- <div style={{ ...style.badge, ...style.badgeRed }}>Pending</div>
- )}
- {statusPayment == 'failed' && (
- <div style={{ ...style.badge, ...style.badgeRed }}>Tidak Berhasil</div>
- )}
- </Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Metode Pembayaran</Column>
- <Column style={style.descriptionRCol}>
- {toTitleCase(payment.paymentType.replaceAll('_', ' '))}
- </Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Batas Akhir Pembayaran</Column>
- <Column style={style.descriptionRCol}>{payment.expiryTime}</Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Nominal Transfer</Column>
- <Column style={style.descriptionRCol}>
- <span style={{ fontWeight: '600' }}>{currencyFormat(payment.grossAmount)}</span>
- </Column>
- </Row>
+ <Hr style={style.hr} />
- <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}>
- <strong>Detail Produk</strong>
- </Text>
+ {transaction.products.map((product) => (
+ <Row style={style.productRow} key={product.id}>
+ <Column style={style.productLCol}>
+ <Img src={product.parent.image} width='100%' />
+ </Column>
+ <Column style={style.productRCol}>
+ <Text style={style.productName}>{product.name}</Text>
+ <Text style={style.productCode}>{product.code}</Text>
+ <div style={{ dislay: 'flex' }}>
+ <span style={style.productPriceA}>
+ {currencyFormat(product.price.priceDiscount)}
+ </span>
+ {product.price.discountPercentage > 0 && (
+ <>
+ &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/home/components/PromotionProgram.jsx b/src/lib/home/components/PromotionProgram.jsx
new file mode 100644
index 00000000..b204df8e
--- /dev/null
+++ b/src/lib/home/components/PromotionProgram.jsx
@@ -0,0 +1,66 @@
+import Link from '@/core/components/elements/Link/Link'
+import Image from 'next/image'
+import { bannerApi } from '@/api/bannerApi';
+import useDevice from '@/core/hooks/useDevice'
+import { Swiper, SwiperSlide } from 'swiper/react';
+const { useQuery } = require('react-query')
+const BannerSection = () => {
+ const promotionProgram = useQuery('promotionProgram', bannerApi({ type: 'banner-promotion' }));
+ const { isMobile, isDesktop } = useDevice()
+
+ return (
+ <div className='px-4 sm:px-0'>
+ <div className='flex justify-between items-center mb-4 '>
+ <div className='font-semibold sm:text-h-lg'>Promo Tersedia</div>
+ {isDesktop && (
+ <div></div>
+ // <Link href='/shop/promo' className='!text-red-500 font-semibold'>
+ // Lihat Semua
+ // </Link>
+ )}
+ </div>
+ {isDesktop && (promotionProgram.data &&
+ promotionProgram.data?.length > 0 && (
+ <div className='grid grid-cols-3 sm:grid-cols-3 gap-4 rounded-md'>
+ {promotionProgram.data?.map((banner) => (
+ <Link key={banner.id} href={banner.url}>
+ <Image
+ width={439}
+ height={150}
+ quality={100}
+ src={banner.image}
+ alt={banner.name}
+ className='h-auto w-full rounded hover:scale-105 transition duration-500 ease-in-out'
+ />
+ </Link>
+ ))}
+ </div>
+
+ ))}
+
+{isMobile && (
+
+ <Swiper slidesPerView={1.1} spaceBetween={8} freeMode>
+ {promotionProgram.data?.map((banner) => (
+ <SwiperSlide key={banner.id}>
+ <Link key={banner.id} href={banner.url}>
+ <Image
+ width={439}
+ height={150}
+ quality={100}
+ src={banner.image}
+ alt={banner.name}
+ className='h-auto w-full rounded '
+ />
+ </Link>
+ </SwiperSlide>
+ ))}
+ </Swiper>
+
+ )}
+ </div>
+
+ )
+}
+
+export default BannerSection
diff --git a/src/lib/product/components/Product/ProductDesktop.jsx b/src/lib/product/components/Product/ProductDesktop.jsx
index a12b8609..444ddd8e 100644
--- a/src/lib/product/components/Product/ProductDesktop.jsx
+++ b/src/lib/product/components/Product/ProductDesktop.jsx
@@ -235,7 +235,7 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
<ImageNext
src={
backgorundFlashSale ||
- '/images/GAMBAR-BG-FLASH-SALE.jpg'
+ '/images/BG-FLASH-SALE.jpg'
}
width={1000}
height={100}
diff --git a/src/lib/product/components/Product/ProductDesktopVariant.jsx b/src/lib/product/components/Product/ProductDesktopVariant.jsx
index bae00b87..09b30a44 100644
--- a/src/lib/product/components/Product/ProductDesktopVariant.jsx
+++ b/src/lib/product/components/Product/ProductDesktopVariant.jsx
@@ -1,3 +1,4 @@
+
import { Box, Skeleton, Tooltip } from '@chakra-ui/react';
import { HeartIcon } from '@heroicons/react/24/outline';
import { Info } from 'lucide-react';
@@ -264,41 +265,19 @@ const ProductDesktopVariant = ({
</div>
</div>
- {/* <div className='w-full'>
- <div className='mt-12'>
- <div className='text-h-lg font-semibold'>Informasi Produk</div>
- <div className='flex gap-x-4 mt-6 mb-4'>
- {informationTabOptions.map((option) => (
- <TabButton
- value={option.value}
- key={option.value}
- active={informationTab == option.value}
- onClick={() => setInformationTab(option.value)}
- >
- {option.label}
- </TabButton>
- ))}
- </div>
- <div className='flex'>
- <div className='w-3/4 leading-7 product__description'>
- <TabContent active={informationTab == 'description'}>
- <span
- dangerouslySetInnerHTML={{
- __html:
- product.description != ''
- ? product.description
- : 'Belum ada deskripsi produk.'
- }}
- />
- </TabContent>
-
- <TabContent active={informationTab == 'information'}>
- Belum ada informasi.
- </TabContent>
- </div>
- </div>
- </div>
- </div> */}
+ <div className='p-4 md:p-6 md:bg-gray-50 rounded-xl'>
+ <h2 className='text-h-md md:text-h-lg font-medium'>Informasi Produk</h2>
+ <div className='h-4' />
+ <div
+ className='leading-relaxed text-gray-700'
+ dangerouslySetInnerHTML={{
+ __html:
+ !product.parent.description || product.parent.description == '<p><br></p>'
+ ? 'Belum ada deskripsi'
+ : product.parent.description,
+ }}
+ />
+ </div>
</div>
<div className='w-[25%]'>
{product?.isFlashsale > 0 &&
diff --git a/src/lib/product/components/Product/ProductMobile.jsx b/src/lib/product/components/Product/ProductMobile.jsx
index e9e64469..113a1e42 100644
--- a/src/lib/product/components/Product/ProductMobile.jsx
+++ b/src/lib/product/components/Product/ProductMobile.jsx
@@ -202,9 +202,7 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
<div className={`absolute bottom-0 w-full`}>
<div className='absolute bottom-0 w-full'>
<ImageNext
- src={
- backgorundFlashSale || '/images/GAMBAR-BG-FLASH-SALE.jpg'
- }
+ src={backgorundFlashSale || '/images/BG-FLASH-SALE.jpg'}
width={1000}
height={100}
/>
diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx
index 1cec0804..98732407 100644
--- a/src/lib/product/components/ProductCard.jsx
+++ b/src/lib/product/components/ProductCard.jsx
@@ -1,44 +1,85 @@
-import Image from '@/core/components/elements/Image/Image'
-import Link from '@/core/components/elements/Link/Link'
-import currencyFormat from '@/core/utils/currencyFormat'
-import { sellingProductFormat } from '@/core/utils/formatValue'
-import { createSlug } from '@/core/utils/slug'
-import whatsappUrl from '@/core/utils/whatsappUrl'
-import ImageNext from 'next/image'
-import { useRouter } from 'next/router'
-import { useMemo } from 'react'
+import clsx from 'clsx';
+import ImageNext from 'next/image';
+import { useRouter } from 'next/router';
+import { useMemo, useEffect, useState } from 'react';
+
+import Image from '@/core/components/elements/Image/Image';
+import Link from '@/core/components/elements/Link/Link';
+import currencyFormat from '@/core/utils/currencyFormat';
+import { sellingProductFormat } from '@/core/utils/formatValue';
+import { createSlug } from '@/core/utils/slug';
+import whatsappUrl from '@/core/utils/whatsappUrl';
+import useUtmSource from '~/hooks/useUtmSource';
const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
- const router = useRouter()
+ const router = useRouter();
+ const utmSource = useUtmSource();
+
const callForPriceWhatsapp = whatsappUrl('product', {
name: product.name,
manufacture: product.manufacture?.name,
- url: createSlug('/shop/product/', product.name, product.id, true)
- })
+ url: createSlug('/shop/product/', product.name, product.id, true),
+ });
const image = useMemo(() => {
- if (product.image) return product.image + '?ratio=square'
- return '/images/noimage.jpeg'
- }, [product.image])
+ if (product.image) return product.image + '?ratio=square';
+ return '/images/noimage.jpeg';
+ }, [product.image]);
+
+ const URL = {
+ product:
+ createSlug('/shop/product/', product?.name, product?.id) +
+ `?utm_source=${utmSource}`,
+ manufacture: createSlug(
+ '/shop/brands/',
+ product?.manufacture?.name,
+ product?.manufacture.id
+ ),
+ };
if (variant == 'vertical') {
return (
<div className='rounded shadow-sm border border-gray_r-4 bg-white h-[300px] md:h-[350px]'>
- <Link
- href={createSlug('/shop/product/', product?.name, product?.id)}
- className='border-b border-gray_r-4 relative'
- >
+ <Link href={URL.product} className='border-b border-gray_r-4 relative'>
+ <div className="relative">
<Image
src={image}
alt={product?.name}
- className='w-full object-contain object-center h-36 sm:h-48'
+ className="gambarA w-full object-contain object-center h-36 sm:h-48"
/>
+ <div className="absolute top-0 right-0 flex mt-3">
+ <div className="gambarB ">
+ {product?.isSni && (
+ <ImageNext
+ src="/images/sni-logo.png"
+ alt="SNI Logo"
+ className="w-4 h-5 object-contain object-top sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ <div className="gambarC ">
+ {product?.isTkdn && (
+ <ImageNext
+ src="/images/TKDN.png"
+ alt="TKDN"
+ className="w-11 h-6 object-contain object-top ml-1 mr-1 sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ </div>
+ </div>
+
+
{router.pathname != '/' && product?.flashSale?.id > 0 && (
<div className='absolute bottom-0 w-full grid'>
<div className='absolute bottom-0 w-full h-full'>
<ImageNext
- src='/images/GAMBAR-BG-FLASH-SALE.jpg'
+ src='/images/BG-FLASH-SALE.jpg'
className='h-full'
width={1000}
height={100}
@@ -58,7 +99,8 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
height={5}
/>
<span className='text-white text-[9px] md:text-[10px] font-semibold'>
- {product?.flashSale?.tag != 'false' || product?.flashSale?.tag
+ {product?.flashSale?.tag != 'false' ||
+ product?.flashSale?.tag
? product?.flashSale?.tag
: 'FLASH SALE'}
</span>
@@ -75,27 +117,21 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
</Link>
<div className='p-2 sm:p-3 pb-3 text-caption-2 sm:text-body-2 leading-5'>
{product?.manufacture?.name ? (
- <Link
- href={createSlug(
- '/shop/brands/',
- product?.manufacture?.name,
- product?.manufacture.id
- )}
- className='mb-1'
- >
+ <Link href={URL.manufacture} className='mb-1'>
{product.manufacture.name}
</Link>
) : (
<div>-</div>
)}
<Link
- href={createSlug('/shop/product/', product?.name, product?.id)}
+ href={URL.product}
className={`mb-2 !text-gray_r-12 leading-6 block line-clamp-3 h-[64px]`}
title={product?.name}
>
{product?.name}
</Link>
- {product?.flashSale?.id > 0 && product?.lowestPrice.discountPercentage > 0 ? (
+ {product?.flashSale?.id > 0 &&
+ product?.lowestPrice.discountPercentage > 0 ? (
<>
<div className='flex gap-x-1 mb-1 items-center'>
<div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'>
@@ -109,7 +145,11 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
{product?.lowestPrice.priceDiscount > 0 ? (
currencyFormat(product?.lowestPrice.priceDiscount)
) : (
- <a rel='noopener noreferrer' target='_blank' href={callForPriceWhatsapp}>
+ <a
+ rel='noopener noreferrer'
+ target='_blank'
+ href={callForPriceWhatsapp}
+ >
Call for Inquiry
</a>
)}
@@ -122,11 +162,17 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
{currencyFormat(product?.lowestPrice.price)}
<div className='text-gray_r-9 text-[10px] font-normal mt-2'>
Inc. PPN:{' '}
- {currencyFormat(product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN)}
+ {currencyFormat(
+ product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN
+ )}
</div>
</>
) : (
- <a rel='noopener noreferrer' target='_blank' href={callForPriceWhatsapp}>
+ <a
+ rel='noopener noreferrer'
+ target='_blank'
+ href={callForPriceWhatsapp}
+ >
Call for Inquiry
</a>
)}
@@ -134,7 +180,9 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
)}
<div className='flex w-full items-center gap-x-1 '>
- {product?.stockTotal > 0 && <div className='badge-solid-red'>Ready Stock</div>}
+ {product?.stockTotal > 0 && (
+ <div className='badge-solid-red'>Ready Stock</div>
+ )}
{/* <div className='badge-gray'>{product?.stockTotal > 5 ? '> 5' : '< 5'}</div> */}
{product?.qtySold > 0 && (
<div className='text-gray_r-9 text-[11px]'>
@@ -144,22 +192,45 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
</div>
</div>
</div>
- )
+ );
}
if (variant == 'horizontal') {
return (
<div className='flex bg-white'>
<div className='w-4/12'>
- <Link
- href={createSlug('/shop/product/', product?.name, product?.id)}
- className='relative'
- >
+ <Link href={URL.product} className='relative'>
+ <div className="relative">
<Image
src={image}
alt={product?.name}
- className='w-full object-contain object-center h-36'
+ className="gambarA w-full object-contain object-center h-36 sm:h-48"
/>
+ <div className="absolute top-0 right-0 flex mt-3">
+ <div className="gambarB ">
+ {product?.isSni && (
+ <ImageNext
+ src="/images/sni-logo.png"
+ alt="SNI Logo"
+ className="w-4 h-5 object-contain object-top sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ <div className="gambarC ">
+ {product?.isTkdn && (
+ <ImageNext
+ src="/images/TKDN.png"
+ alt="TKDN"
+ className="w-11 h-6 object-contain object-top ml-1 sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ </div>
+ </div>
{product.variantTotal > 1 && (
<div className='absolute badge-gray bottom-1.5 left-1.5'>
{product.variantTotal} Varian
@@ -184,26 +255,20 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
</div>
)}
{product?.manufacture?.name ? (
- <Link
- href={createSlug(
- '/shop/brands/',
- product?.manufacture?.name,
- product?.manufacture.id
- )}
- className='mb-1'
- >
+ <Link href={URL.manufacture} className='mb-1'>
{product.manufacture.name}
</Link>
) : (
<div>-</div>
)}
<Link
- href={createSlug('/shop/product/', product?.name, product?.id)}
+ href={URL.product}
className={`mb-3 !text-gray_r-12 leading-6 line-clamp-3`}
>
{product?.name}
</Link>
- {product?.flashSale?.id > 0 && product?.lowestPrice?.discountPercentage > 0 ? (
+ {product?.flashSale?.id > 0 &&
+ product?.lowestPrice?.discountPercentage > 0 ? (
<>
{product?.lowestPrice.discountPercentage > 0 && (
<div className='flex gap-x-1 mb-1 items-center'>
@@ -220,7 +285,11 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
{product?.lowestPrice?.priceDiscount > 0 ? (
currencyFormat(product?.lowestPrice?.priceDiscount)
) : (
- <a rel='noopener noreferrer' target='_blank' href={callForPriceWhatsapp}>
+ <a
+ rel='noopener noreferrer'
+ target='_blank'
+ href={callForPriceWhatsapp}
+ >
Call for Inquiry
</a>
)}
@@ -233,11 +302,17 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
{currencyFormat(product?.lowestPrice.price)}
<div className='text-gray_r-9 text-[11px] sm:text-caption-2 font-normal mt-2'>
Inc. PPN:{' '}
- {currencyFormat(product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN)}
+ {currencyFormat(
+ product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN
+ )}
</div>
</>
) : (
- <a rel='noopener noreferrer' target='_blank' href={callForPriceWhatsapp}>
+ <a
+ rel='noopener noreferrer'
+ target='_blank'
+ href={callForPriceWhatsapp}
+ >
Call for Inquiry
</a>
)}
@@ -245,7 +320,9 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
)}
<div className='flex w-full items-center gap-x-1 '>
- {product?.stockTotal > 0 && <div className='badge-solid-red'>Ready Stock</div>}
+ {product?.stockTotal > 0 && (
+ <div className='badge-solid-red'>Ready Stock</div>
+ )}
{/* <div className='badge-gray'>{product?.stockTotal > 5 ? '> 5' : '< 5'}</div> */}
{product?.qtySold > 0 && (
<div className='text-gray_r-9 text-[11px]'>
@@ -255,8 +332,8 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
</div>
</div>
</div>
- )
+ );
}
-}
+};
-export default ProductCard
+export default ProductCard;
diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx
index e4a62abb..a8073036 100644
--- a/src/lib/product/components/ProductFilterDesktop.jsx
+++ b/src/lib/product/components/ProductFilterDesktop.jsx
@@ -21,6 +21,7 @@ import Image from '@/core/components/elements/Image/Image'
import { formatCurrency } from '@/core/utils/formatValue'
const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = null }) => {
+
const router = useRouter()
const { query } = router
const [order, setOrder] = useState(query?.orderBy)
@@ -102,7 +103,14 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu
}
params = _.pickBy(params, _.identity)
params = toQuery(params)
- router.push(`${prefixUrl}?${params}`)
+
+ const slug = Array.isArray(router.query.slug) ? router.query.slug[0] : router.query.slug;
+
+ if (slug) {
+ router.push(`${prefixUrl}/${slug}?${params}`)
+ } else {
+ router.push(`${prefixUrl}?${params}`)
+ }
}
diff --git a/src/lib/product/components/ProductFilterDesktopPromotion.jsx b/src/lib/product/components/ProductFilterDesktopPromotion.jsx
new file mode 100644
index 00000000..0815b881
--- /dev/null
+++ b/src/lib/product/components/ProductFilterDesktopPromotion.jsx
@@ -0,0 +1,132 @@
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+import _ from 'lodash';
+import { toQuery } from 'lodash-contrib';
+import { Button } from '@chakra-ui/react';
+import { MultiSelect } from 'react-multi-select-component';
+
+const ProductFilterDesktop = ({ brands, categories, prefixUrl }) => {
+ const router = useRouter();
+ const { query } = router;
+ const [order, setOrder] = useState(query?.orderBy);
+ const [brandValues, setBrand] = useState([]);
+ const [categoryValues, setCategory] = useState([]);
+ const [priceFrom, setPriceFrom] = useState(query?.priceFrom);
+ const [priceTo, setPriceTo] = useState(query?.priceTo);
+ const [stock, setStock] = useState(query?.stock);
+ const [activeRange, setActiveRange] = useState(null);
+ const [isBrandDropdownClicked, setIsBrandDropdownClicked] = useState(false);
+ const [isCategoryDropdownClicked, setIsCategoryDropdownClicked] = useState(false);
+
+ // Effect to set brandValues from query parameter 'brand'
+ useEffect(() => {
+ const brandParam = query?.brand;
+ if (brandParam) {
+ const brandsArray = brandParam.split(',').map((b) => ({
+ label: b,
+ value: b,
+ }));
+ setBrand(brandsArray);
+ }
+
+ }, [query.brand]); // Trigger effect whenever query.brand changes
+
+ useEffect(() => {
+ const categoryParam = query?.category;
+ if (categoryParam) {
+ const categoriesArray = categoryParam.split(',').map((c) => ({
+ label: c,
+ value: c,
+ }));
+ setCategory(categoriesArray);
+ }
+ }, [query.category]); // Trigger effect whenever query.category changes
+
+ const handleSubmit = () => {
+ let params = {
+ q: router.query.q,
+ orderBy: order,
+ brand: brandValues.map((b) => b.value).join(','),
+ category: categoryValues.map((c) => c.value).join(','),
+ priceFrom,
+ priceTo,
+ stock: stock,
+ };
+ params = _.pickBy(params, _.identity);
+ params = toQuery(params);
+
+ const slug = Array.isArray(router.query.slug)
+ ? router.query.slug[0]
+ : router.query.slug;
+
+ if (slug) {
+ router.push(`${prefixUrl}/${slug}?${params}`);
+ } else {
+ router.push(`${prefixUrl}?${params}`);
+ }
+ };
+
+
+ const brandOptions = brands.map((brand) => ({
+ label: `${brand.brand} (${brand.qty})`,
+ value: brand.brand,
+ }));
+
+ const categoryOptions = categories.map((category) => ({
+ label: `${category.name} (${category.qty})`,
+ value: category.name,
+ }));
+
+ return (
+ <>
+ <div className='flex h-full w-[100%] justify-end '>
+ {/* Brand MultiSelect */}
+ <div className='mb-[20px] mr-4 w-64 h-full flex justify-start '>
+ <div className='relative'>
+ <label>Brand</label>
+ <div className='h-auto z-50 w-64 '>
+ <MultiSelect
+ options={brandOptions}
+ value={brandValues}
+ onChange={setBrand}
+ labelledBy='Select Brand'
+ onMenuToggle={(isOpen) => setIsBrandDropdownClicked(isOpen)}
+ hasSelectAll={false}
+ />
+ </div>
+ </div>
+ </div>
+
+ {/* Category MultiSelect */}
+ <div className='mb-[20px] mr-4 w-64 h-full flex justify-start '>
+ <div className='relative'>
+ <label>Kategori</label>
+ <div className=' h-auto w-64'>
+ <MultiSelect
+ options={categoryOptions}
+ value={categoryValues}
+ onChange={setCategory}
+ labelledBy='Select Kategori'
+ onMenuToggle={() =>
+ setIsCategoryDropdownClicked(!isCategoryDropdownClicked)
+ }
+ hasSelectAll={false}
+ />
+ </div>
+ </div>
+ </div>
+
+ {/* Apply Button */}
+ <div className='TOMBOL mb-1 h-24 flex justify-center items-center w-24'>
+ <div className=' bottom-1 pb-1 left-0 right-0 flex justify-center rounded' >
+ <Button colorScheme='red' width={"full"} onClick={handleSubmit}>
+ Terapkan
+ </Button>
+ </div>
+ </div>
+ </div>
+ </>
+ );
+};
+
+export default ProductFilterDesktop;
diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx
index ee4ec2de..b1a5d409 100644
--- a/src/lib/product/components/ProductSearch.jsx
+++ b/src/lib/product/components/ProductSearch.jsx
@@ -24,6 +24,9 @@ import ProductFilter from './ProductFilter';
import ProductFilterDesktop from './ProductFilterDesktop';
import ProductSearchSkeleton from './Skeleton/ProductSearchSkeleton';
+import SideBanner from '~/modules/side-banner';
+import FooterBanner from '~/modules/footer-banner';
+
const ProductSearch = ({
query,
prefixUrl,
@@ -127,6 +130,7 @@ const ProductSearch = ({
brands.push({ brand, qty });
}
}
+
const categories = [];
for (
@@ -141,6 +145,7 @@ const ProductSearch = ({
categories.push({ name, qty });
}
}
+
const orderOptions = [
{ value: 'price-asc', label: 'Harga Terendah' },
@@ -401,6 +406,10 @@ const ProductSearch = ({
prefixUrl={prefixUrl}
defaultBrand={defaultBrand}
/>
+
+ <div className='h-6' />
+
+ <SideBanner />
</div>
<div className='w-9/12 pl-6'>
{bannerPromotionHeader && bannerPromotionHeader?.image && (
@@ -552,6 +561,7 @@ const ProductSearch = ({
/>
</div>
)}
+ <FooterBanner />
</div>
</div>
</DesktopView>
diff --git a/src/lib/promo/components/Promocrumb.jsx b/src/lib/promo/components/Promocrumb.jsx
new file mode 100644
index 00000000..4f5cf346
--- /dev/null
+++ b/src/lib/promo/components/Promocrumb.jsx
@@ -0,0 +1,40 @@
+import { Breadcrumb as ChakraBreadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'
+import Link from 'next/link'
+import React from 'react'
+
+/**
+ * Renders a breadcrumb component with links to navigate through different pages.
+ *
+ * @param {Object} props - The props object containing the brand name.
+ * @param {string} props.brandName - The name of the brand to display in the breadcrumb.
+ * @return {JSX.Element} The rendered breadcrumb component.
+ */
+const Breadcrumb = ({ brandName }) => {
+ return (
+ <div className='container mx-auto py-4 md:py-6'>
+ <ChakraBreadcrumb>
+ <BreadcrumbItem>
+ <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'>
+ Shop
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+
+ {/* <BreadcrumbItem>
+ <BreadcrumbLink
+ as={Link}
+ href='/shop/promo'
+ className='!text-danger-500 whitespace-nowrap'
+ >
+ Promo
+ </BreadcrumbLink>
+ </BreadcrumbItem> */}
+
+ <BreadcrumbItem isCurrentPage>
+ <BreadcrumbLink className='whitespace-nowrap'>{brandName}</BreadcrumbLink>
+ </BreadcrumbItem>
+ </ChakraBreadcrumb>
+ </div>
+ )
+}
+
+export default Breadcrumb
diff --git a/src/lib/quotation/components/Quotation.jsx b/src/lib/quotation/components/Quotation.jsx
index 8c379ead..09d55e92 100644
--- a/src/lib/quotation/components/Quotation.jsx
+++ b/src/lib/quotation/components/Quotation.jsx
@@ -1,102 +1,283 @@
-import Alert from '@/core/components/elements/Alert/Alert'
-import Divider from '@/core/components/elements/Divider/Divider'
-import Link from '@/core/components/elements/Link/Link'
-import useAuth from '@/core/hooks/useAuth'
-import CartApi from '@/lib/cart/api/CartApi'
-import { ExclamationCircleIcon } from '@heroicons/react/24/outline'
-import { useEffect, useState } from 'react'
-import _ from 'lodash'
-import { deleteItemCart, getCart, getItemCart } from '@/core/utils/cart'
-import currencyFormat from '@/core/utils/currencyFormat'
-import { toast } from 'react-hot-toast'
+import Alert from '@/core/components/elements/Alert/Alert';
+import Divider from '@/core/components/elements/Divider/Divider';
+import Link from '@/core/components/elements/Link/Link';
+import useAuth from '@/core/hooks/useAuth';
+import CartApi from '@/lib/cart/api/CartApi';
+import { ExclamationCircleIcon } from '@heroicons/react/24/outline';
+import { useEffect, useRef, useState } from 'react';
+import _ from 'lodash';
+import { deleteItemCart, getCart, getItemCart } from '@/core/utils/cart';
+import currencyFormat from '@/core/utils/currencyFormat';
+import { toast } from 'react-hot-toast';
// import checkoutApi from '@/lib/checkout/api/checkoutApi'
-import { useRouter } from 'next/router'
-import VariantGroupCard from '@/lib/variant/components/VariantGroupCard'
-import MobileView from '@/core/components/views/MobileView'
-import DesktopView from '@/core/components/views/DesktopView'
-import Image from '@/core/components/elements/Image/Image'
-import { useQuery } from 'react-query'
-import CardProdcuctsList from '@/core/components/elements/Product/cartProductsList'
+import { useRouter } from 'next/router';
+import VariantGroupCard from '@/lib/variant/components/VariantGroupCard';
+import MobileView from '@/core/components/views/MobileView';
+import DesktopView from '@/core/components/views/DesktopView';
+import Image from '@/core/components/elements/Image/Image';
+import { useQuery } from 'react-query';
+import CardProdcuctsList from '@/core/components/elements/Product/cartProductsList';
+import { Skeleton } from '@chakra-ui/react';
+import {
+ PickupAddress,
+ SectionAddress,
+ SectionExpedisi,
+ SectionListService,
+ SectionValidation,
+ calculateEstimatedArrival,
+ splitDuration,
+} from '../../checkout/components/CheckoutSection';
+import addressesApi from '@/lib/address/api/addressesApi';
+import { getItemAddress } from '@/core/utils/address';
+import ExpedisiList from '../../checkout/api/ExpedisiList';
+import axios from 'axios';
-const { checkoutApi } = require('@/lib/checkout/api/checkoutApi')
-const { getProductsCheckout } = require('@/lib/checkout/api/checkoutApi')
+const { checkoutApi } = require('@/lib/checkout/api/checkoutApi');
+const { getProductsCheckout } = require('@/lib/checkout/api/checkoutApi');
const Quotation = () => {
- const router = useRouter()
- const auth = useAuth()
+ const router = useRouter();
+ const auth = useAuth();
- const { data: cartCheckout } = useQuery('cartCheckout', () => getProductsCheckout())
+ const { data: cartCheckout } = useQuery('cartCheckout', () =>
+ getProductsCheckout()
+ );
- const [products, setProducts] = useState(null)
- const [totalAmount, setTotalAmount] = useState(0)
- const [totalDiscountAmount, setTotalDiscountAmount] = useState(0)
+ const SELF_PICKUP_ID = 32;
+
+ const [products, setProducts] = useState(null);
+ const [totalAmount, setTotalAmount] = useState(0);
+ const [totalDiscountAmount, setTotalDiscountAmount] = useState(0);
+
+ //start set up address and carrier
+ const [selectedCarrierId, setselectedCarrierId] = useState(0);
+ const [listExpedisi, setExpedisi] = useState([]);
+ const [selectedExpedisi, setSelectedExpedisi] = useState(0);
+ const [checkWeigth, setCheckWeight] = useState(false);
+ const [checkoutValidation, setCheckoutValidation] = useState(false);
+ const [loadingRajaOngkir, setLoadingRajaOngkir] = useState(false);
+
+ const [listserviceExpedisi, setListServiceExpedisi] = useState([]);
+ const [selectedServiceType, setSelectedServiceType] = useState(null);
+
+ const [selectedCarrier, setselectedCarrier] = useState(0);
+ const [totalWeight, setTotalWeight] = useState(0);
+
+ const [biayaKirim, setBiayaKirim] = useState(0);
+ const [selectedExpedisiService, setselectedExpedisiService] = useState(null);
+ const [etd, setEtd] = useState(null);
+ const [etdFix, setEtdFix] = useState(null);
+
+ const expedisiValidation = useRef(null);
+
+ const [selectedAddress, setSelectedAddress] = useState({
+ shipping: null,
+ invoicing: null,
+ });
+
+ const [addresses, setAddresses] = useState(null);
+
+ useEffect(() => {
+ if (!auth) return;
+
+ const getAddresses = async () => {
+ const dataAddresses = await addressesApi();
+ setAddresses(dataAddresses);
+ };
+
+ getAddresses();
+ }, [auth]);
+
+ useEffect(() => {
+ if (!addresses) return;
+
+ const matchAddress = (key) => {
+ const addressToMatch = getItemAddress(key);
+ const foundAddress = addresses.filter(
+ (address) => address.id == addressToMatch
+ );
+ if (foundAddress.length > 0) {
+ return foundAddress[0];
+ }
+ return addresses[0];
+ };
+
+ setSelectedAddress({
+ shipping: matchAddress('shipping'),
+ invoicing: matchAddress('invoicing'),
+ });
+ }, [addresses]);
+
+ const loadExpedisi = async () => {
+ let dataExpedisi = await ExpedisiList();
+ dataExpedisi = dataExpedisi.map((expedisi) => ({
+ value: expedisi.id,
+ label: expedisi.name,
+ carrierId: expedisi.deliveryCarrierId,
+ }));
+ setExpedisi(dataExpedisi);
+ };
+
+ const loadServiceRajaOngkir = async () => {
+ setLoadingRajaOngkir(true);
+ const body = {
+ origin: 2127,
+ destination: selectedAddress.shipping.rajaongkirCityId,
+ weight: totalWeight,
+ courier: selectedCarrier,
+ originType: 'subdistrict',
+ destinationType: 'subdistrict',
+ };
+ setBiayaKirim(0);
+ const dataService = await axios(
+ '/api/rajaongkir-service?body=' + JSON.stringify(body)
+ );
+ setLoadingRajaOngkir(false);
+ setListServiceExpedisi(dataService.data[0].costs);
+ if (dataService.data[0].costs[0]) {
+ setBiayaKirim(dataService.data[0].costs[0]?.cost[0].value);
+ setselectedExpedisiService(
+ dataService.data[0].costs[0]?.description +
+ '-' +
+ dataService.data[0].costs[0]?.service
+ );
+ setEtd(dataService.data[0].costs[0]?.cost[0].etd);
+ toast.success('Harap pilih tipe layanan pengiriman');
+ } else {
+ toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.');
+ }
+ };
+
+ useEffect(() => {
+ setCheckoutValidation(false);
+
+ if (selectedCarrier != 0 && selectedCarrier != 1 && totalWeight > 0) {
+ loadServiceRajaOngkir();
+ } else {
+ setListServiceExpedisi();
+ setBiayaKirim(0);
+ setselectedExpedisiService();
+ setEtd();
+ }
+ }, [selectedCarrier, selectedAddress, totalWeight]);
+
+ useEffect(() => {
+ if (selectedExpedisi) {
+ let serviceType = selectedExpedisi.split(',');
+ if (serviceType[0] === 0) return;
+
+ setselectedCarrier(serviceType[0]);
+ setselectedCarrierId(serviceType[1]);
+ setListServiceExpedisi([]);
+ }
+ }, [selectedExpedisi]);
+
+ useEffect(() => {
+ if (selectedServiceType) {
+ let serviceType = selectedServiceType.split(',');
+ setBiayaKirim(serviceType[0]);
+ setselectedExpedisiService(serviceType[1]);
+ setEtd(serviceType[2]);
+ }
+ }, [selectedServiceType]);
+
+ useEffect(() => {
+ if (etd) setEtdFix(calculateEstimatedArrival(etd));
+ }, [etd]);
+
+ // end set up address and carrier
useEffect(() => {
const loadProducts = async () => {
- const cart = getCart()
+ const cart = getCart();
const variantIds = _.filter(cart, (o) => o.selected == true)
.map((o) => o.productId)
- .join(',')
- const dataProducts = await CartApi({ variantIds })
+ .join(',');
+ const dataProducts = await CartApi({ variantIds });
const productsWithQuantity = dataProducts?.map((product) => {
return {
...product,
- quantity: getItemCart({ productId: product.id }).quantity
- }
- })
+ quantity: getItemCart({ productId: product.id }).quantity,
+ };
+ });
if (productsWithQuantity) {
Promise.all(productsWithQuantity).then((resolvedProducts) => {
- setProducts(resolvedProducts)
- })
+ setProducts(resolvedProducts);
+ });
}
- }
+ };
+ loadExpedisi();
// loadProducts()
- }, [])
+ }, []);
useEffect(() => {
- setProducts(cartCheckout?.products)
- }, [cartCheckout])
+ setProducts(cartCheckout?.products);
+ setCheckWeight(cartCheckout?.hasProductWithoutWeight);
+ setTotalWeight(cartCheckout?.totalWeight.g);
+ }, [cartCheckout]);
useEffect(() => {
if (products) {
- let calculateTotalAmount = 0
- let calculateTotalDiscountAmount = 0
+ let calculateTotalAmount = 0;
+ let calculateTotalDiscountAmount = 0;
products.forEach((product) => {
- calculateTotalAmount += product.price.price * product.quantity
+ calculateTotalAmount += product.price.price * product.quantity;
calculateTotalDiscountAmount +=
- (product.price.price - product.price.priceDiscount) * product.quantity
- })
- setTotalAmount(calculateTotalAmount)
- setTotalDiscountAmount(calculateTotalDiscountAmount)
+ (product.price.price - product.price.priceDiscount) *
+ product.quantity;
+ });
+ setTotalAmount(calculateTotalAmount);
+ setTotalDiscountAmount(calculateTotalDiscountAmount);
}
- }, [products])
+ }, [products]);
- const [isLoading, setIsLoading] = useState(false)
+ const [isLoading, setIsLoading] = useState(false);
const checkout = async () => {
- if (!products || products.length == 0) return
- setIsLoading(true)
+ // validation checkout
+ if (selectedExpedisi === 0) {
+ setCheckoutValidation(true);
+ if (expedisiValidation.current) {
+ const position = expedisiValidation.current.getBoundingClientRect();
+ window.scrollTo({
+ top: position.top - 300 + window.pageYOffset,
+ behavior: 'smooth',
+ });
+ }
+ return;
+ }
+ if (selectedCarrier != 1 && biayaKirim == 0) {
+ toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.');
+ return;
+ }
+
+ if (!products || products.length == 0) return;
+ setIsLoading(true);
const productOrder = products.map((product) => ({
product_id: product.id,
- quantity: product.quantity
- }))
+ quantity: product.quantity,
+ }));
let data = {
- partner_shipping_id: auth.partnerId,
- partner_invoice_id: auth.partnerId,
+ partner_shipping_id: selectedAddress.shipping.id,
+ partner_invoice_id: selectedAddress.invoicing.id,
user_id: auth.id,
- order_line: JSON.stringify(productOrder)
- }
- const isSuccess = await checkoutApi({ data })
- setIsLoading(false)
+ order_line: JSON.stringify(productOrder),
+ delivery_amount: biayaKirim,
+ carrier_id: selectedCarrierId,
+ estimated_arrival_days: splitDuration(etd),
+ delivery_service_type: selectedExpedisiService,
+ };
+ const isSuccess = await checkoutApi({ data });
+ setIsLoading(false);
if (isSuccess?.id) {
- for (const product of products) deleteItemCart({ productId: product.id })
- router.push(`/shop/quotation/finish?id=${isSuccess.id}`)
- return
+ for (const product of products) deleteItemCart({ productId: product.id });
+ router.push(`/shop/quotation/finish?id=${isSuccess.id}`);
+ return;
}
- toast.error('Gagal melakukan transaksi, terjadi kesalahan internal')
- }
+ toast.error('Gagal melakukan transaksi, terjadi kesalahan internal');
+ };
- const taxTotal = (totalAmount - totalDiscountAmount) * 0.11
+ const taxTotal = (totalAmount - totalDiscountAmount) * 0.11;
return (
<>
@@ -107,16 +288,80 @@ const Quotation = () => {
<ExclamationCircleIcon className='w-7 text-blue-700' />
</div>
<span className='leading-5'>
- Jika mengalami kesulitan dalam melakukan pembelian di website Indoteknik. Hubungi kami
- disini
+ Jika mengalami kesulitan dalam melakukan pembelian di website
+ Indoteknik. Hubungi kami disini
</span>
</Alert>
</div>
<Divider />
+ {selectedCarrierId == SELF_PICKUP_ID && (
+ <div className='p-4'>
+ <div
+ class='flex items-center p-4 mb-4 text-sm border border-yellow-500 text-yellow-800 rounded-lg bg-yellow-50'
+ role='alert'
+ >
+ <svg
+ class='flex-shrink-0 inline w-4 h-4 mr-3'
+ aria-hidden='true'
+ fill='currentColor'
+ viewBox='0 0 20 20'
+ >
+ <path d='M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z' />
+ </svg>
+ <span class='sr-only'>Info</span>
+ <div className='text-justify'>
+ Fitur Self Pickup, hanya berlaku untuk customer di area jakarta.
+ Apa bila memilih fitur ini, anda akan dihubungi setelah barang
+ siap diambil.
+ </div>
+ </div>
+ </div>
+ )}
+
+ {selectedCarrierId == SELF_PICKUP_ID && (
+ <PickupAddress label='Alamat Pickup' />
+ )}
+ {selectedCarrierId != SELF_PICKUP_ID && (
+ <Skeleton
+ isLoaded={!!selectedAddress.invoicing && !!selectedAddress.shipping}
+ minHeight={320}
+ >
+ <SectionAddress
+ address={selectedAddress.shipping}
+ label='Alamat Pengiriman'
+ url='/my/address?select=shipping'
+ />
+ <Divider />
+ <SectionAddress
+ address={selectedAddress.invoicing}
+ label='Alamat Penagihan'
+ url='/my/address?select=invoice'
+ />
+ </Skeleton>
+ )}
+ <Divider />
+ <SectionValidation address={selectedAddress.invoicing} />
+ <SectionExpedisi
+ address={selectedAddress.shipping}
+ listExpedisi={listExpedisi}
+ setSelectedExpedisi={setSelectedExpedisi}
+ checkWeigth={checkWeigth}
+ checkoutValidation={checkoutValidation}
+ expedisiValidation={expedisiValidation}
+ loadingRajaOngkir={loadingRajaOngkir}
+ />
+ <Divider />
+ <SectionListService
+ listserviceExpedisi={listserviceExpedisi}
+ setSelectedServiceType={setSelectedServiceType}
+ />
+
<div className='p-4 flex flex-col gap-y-4'>
- {products && <VariantGroupCard openOnClick={false} variants={products} />}
+ {products && (
+ <VariantGroupCard openOnClick={false} variants={products} />
+ )}
</div>
<Divider />
@@ -124,7 +369,9 @@ const Quotation = () => {
<div className='p-4'>
<div className='flex justify-between items-center'>
<div className='font-medium'>Ringkasan Penawaran</div>
- <div className='text-gray_r-11 text-caption-1'>{products?.length} Barang</div>
+ <div className='text-gray_r-11 text-caption-1'>
+ {products?.length} Barang
+ </div>
</div>
<hr className='my-4 border-gray_r-6' />
<div className='flex flex-col gap-y-4'>
@@ -134,7 +381,9 @@ const Quotation = () => {
</div>
<div className='flex gap-x-2 justify-between'>
<div className='text-gray_r-11'>Diskon Produk</div>
- <div className='text-danger-500'>- {currencyFormat(cartCheckout?.totalDiscount)}</div>
+ <div className='text-danger-500'>
+ - {currencyFormat(cartCheckout?.totalDiscount)}
+ </div>
</div>
<div className='flex gap-x-2 justify-between'>
<div className='text-gray_r-11'>Subtotal</div>
@@ -144,17 +393,33 @@ const Quotation = () => {
<div className='text-gray_r-11'>PPN 11%</div>
<div>{currencyFormat(cartCheckout?.tax)}</div>
</div>
+ <div className='flex gap-x-2 justify-between'>
+ <div className='text-gray_r-11'>
+ Biaya Kirim <p className='text-xs mt-3'>{etdFix}</p>
+ </div>
+ <div>
+ {currencyFormat(
+ Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000
+ )}
+ </div>
+ </div>
</div>
<hr className='my-4 border-gray_r-6' />
<div className='flex gap-x-2 justify-between mb-4'>
<div>Grand Total</div>
<div className='font-semibold text-gray_r-12'>
- {currencyFormat(cartCheckout?.grandTotal)}
+ {currencyFormat(
+ cartCheckout?.grandTotal +
+ Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000
+ )}
</div>
</div>
- <p className='text-caption-2 text-gray_r-10 mb-2'>*) Belum termasuk biaya pengiriman</p>
+ <p className='text-caption-2 text-gray_r-10 mb-2'>
+ *) Belum termasuk biaya pengiriman
+ </p>
<p className='text-caption-2 text-gray_r-10 leading-5'>
- Dengan melakukan pembelian melalui website Indoteknik, saya menyetujui{' '}
+ Dengan melakukan pembelian melalui website Indoteknik, saya
+ menyetujui{' '}
<Link href='/syarat-ketentuan' className='inline font-normal'>
Syarat & Ketentuan
</Link>{' '}
@@ -165,7 +430,11 @@ const Quotation = () => {
<Divider />
<div className='flex gap-x-3 p-4'>
- <button className='flex-1 btn-yellow' onClick={checkout} disabled={isLoading}>
+ <button
+ className='flex-1 btn-yellow'
+ onClick={checkout}
+ disabled={isLoading}
+ >
{isLoading ? 'Loading...' : 'Quotation'}
</button>
</div>
@@ -174,15 +443,62 @@ const Quotation = () => {
<DesktopView>
<div className='container mx-auto py-10 flex'>
<div className='w-3/4 border border-gray_r-6 rounded bg-white p-4'>
- <div className='font-medium'>Detail Barang</div>
- <CardProdcuctsList isLoading={isLoading} products={products} source='checkout' />
+ {selectedCarrierId == SELF_PICKUP_ID && (
+ <PickupAddress label='Alamat Pickup' />
+ )}
+ {selectedCarrierId != SELF_PICKUP_ID && (
+ <Skeleton
+ isLoaded={
+ !!selectedAddress.invoicing && !!selectedAddress.shipping
+ }
+ minHeight={290}
+ >
+ <SectionAddress
+ address={selectedAddress.shipping}
+ label='Alamat Pengiriman'
+ url='/my/address?select=shipping'
+ />
+ <Divider />
+ <SectionAddress
+ address={selectedAddress.invoicing}
+ label='Alamat Penagihan'
+ url='/my/address?select=invoice'
+ />
+ </Skeleton>
+ )}
+ <Divider />
+ <SectionValidation address={selectedAddress.invoicing} />
+ <SectionExpedisi
+ address={selectedAddress.shipping}
+ listExpedisi={listExpedisi}
+ setSelectedExpedisi={setSelectedExpedisi}
+ checkWeigth={checkWeigth}
+ checkoutValidation={checkoutValidation}
+ expedisiValidation={expedisiValidation}
+ loadingRajaOngkir={loadingRajaOngkir}
+ />
+ <Divider />
+ <SectionListService
+ listserviceExpedisi={listserviceExpedisi}
+ setSelectedServiceType={setSelectedServiceType}
+ />
+ {/* <div className='p-4'> */}
+ <div className='font-medium mb-6'>Detail Barang</div>
+ <CardProdcuctsList
+ isLoading={isLoading}
+ products={products}
+ source='checkout'
+ />
+ {/* </div> */}
</div>
<div className='w-1/4 pl-4'>
<div className='sticky top-48 border border-gray_r-6 bg-white rounded p-4'>
<div className='flex justify-between items-center'>
<div className='font-medium'>Ringkasan Pesanan</div>
- <div className='text-gray_r-11 text-caption-1'>{products?.length} Barang</div>
+ <div className='text-gray_r-11 text-caption-1'>
+ {products?.length} Barang
+ </div>
</div>
<hr className='my-4 border-gray_r-6' />
@@ -205,6 +521,16 @@ const Quotation = () => {
<div className='text-gray_r-11'>PPN 11%</div>
<div>{currencyFormat(cartCheckout?.tax)}</div>
</div>
+ <div className='flex gap-x-2 justify-between'>
+ <div className='text-gray_r-11'>
+ Biaya Kirim <p className='text-xs mt-3'>{etdFix}</p>
+ </div>
+ <div>
+ {currencyFormat(
+ Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000
+ )}
+ </div>
+ </div>
</div>
<hr className='my-4 border-gray_r-6' />
@@ -212,14 +538,18 @@ const Quotation = () => {
<div className='flex gap-x-2 justify-between mb-4'>
<div>Grand Total</div>
<div className='font-semibold text-gray_r-12'>
- {currencyFormat(cartCheckout?.grandTotal)}
+ {currencyFormat(
+ cartCheckout?.grandTotal +
+ Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000
+ )}
</div>
</div>
- <p className='text-caption-2 text-gray_r-11 mb-2'>
+ {/* <p className='text-caption-2 text-gray_r-11 mb-2'>
*) Belum termasuk biaya pengiriman
- </p>
+ </p> */}
<p className='text-caption-2 text-gray_r-11 leading-5'>
- Dengan melakukan pembelian melalui website Indoteknik, saya menyetujui{' '}
+ Dengan melakukan pembelian melalui website Indoteknik, saya
+ menyetujui{' '}
<Link href='/syarat-ketentuan' className='inline font-normal'>
Syarat & Ketentuan
</Link>{' '}
@@ -240,7 +570,7 @@ const Quotation = () => {
</div>
</DesktopView>
</>
- )
-}
+ );
+};
-export default Quotation
+export default Quotation;
diff --git a/src/lib/transaction/api/approveApi.js b/src/lib/transaction/api/approveApi.js
new file mode 100644
index 00000000..891f0235
--- /dev/null
+++ b/src/lib/transaction/api/approveApi.js
@@ -0,0 +1,13 @@
+import odooApi from '@/core/api/odooApi'
+import { getAuth } from '@/core/utils/auth'
+
+const aprpoveApi = async ({ id }) => {
+ const auth = getAuth()
+ const dataCheckout = await odooApi(
+ 'POST',
+ `/api/v1/partner/${auth?.partnerId}/sale_order/${id}/approve`
+ )
+ return dataCheckout
+}
+
+export default aprpoveApi
diff --git a/src/lib/transaction/api/rejectApi.js b/src/lib/transaction/api/rejectApi.js
new file mode 100644
index 00000000..127c0d38
--- /dev/null
+++ b/src/lib/transaction/api/rejectApi.js
@@ -0,0 +1,13 @@
+import odooApi from '@/core/api/odooApi'
+import { getAuth } from '@/core/utils/auth'
+
+const rejectApi = async ({ id }) => {
+ const auth = getAuth()
+ const dataCheckout = await odooApi(
+ 'POST',
+ `/api/v1/partner/${auth?.partnerId}/sale_order/${id}/reject`
+ )
+ return dataCheckout
+}
+
+export default rejectApi
diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx
index 82eb1775..9962b46e 100644
--- a/src/lib/transaction/components/Transaction.jsx
+++ b/src/lib/transaction/components/Transaction.jsx
@@ -1,83 +1,123 @@
-import Spinner from '@/core/components/elements/Spinner/Spinner'
-import useTransaction from '../hooks/useTransaction'
-import TransactionStatusBadge from './TransactionStatusBadge'
-import Divider from '@/core/components/elements/Divider/Divider'
-import { useMemo, useRef, useState } from 'react'
-import { downloadPurchaseOrder, downloadQuotation } from '../utils/transactions'
-import BottomPopup from '@/core/components/elements/Popup/BottomPopup'
-import uploadPoApi from '../api/uploadPoApi'
-import { toast } from 'react-hot-toast'
-import getFileBase64 from '@/core/utils/getFileBase64'
-import currencyFormat from '@/core/utils/currencyFormat'
-import VariantGroupCard from '@/lib/variant/components/VariantGroupCard'
-import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon } from '@heroicons/react/24/outline'
-import Link from '@/core/components/elements/Link/Link'
-import checkoutPoApi from '../api/checkoutPoApi'
-import cancelTransactionApi from '../api/cancelTransactionApi'
-import MobileView from '@/core/components/views/MobileView'
-import DesktopView from '@/core/components/views/DesktopView'
-import Menu from '@/lib/auth/components/Menu'
-import Image from '@/core/components/elements/Image/Image'
-import { createSlug } from '@/core/utils/slug'
-import toTitleCase from '@/core/utils/toTitleCase'
-import useAirwayBill from '../hooks/useAirwayBill'
-import Manifest from '@/lib/treckingAwb/component/Manifest'
+import Spinner from '@/core/components/elements/Spinner/Spinner';
+import useTransaction from '../hooks/useTransaction';
+import TransactionStatusBadge from './TransactionStatusBadge';
+import Divider from '@/core/components/elements/Divider/Divider';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import ImageNext from 'next/image';
+import {
+ downloadPurchaseOrder,
+ downloadQuotation,
+} from '../utils/transactions';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import uploadPoApi from '../api/uploadPoApi';
+import { toast } from 'react-hot-toast';
+import getFileBase64 from '@/core/utils/getFileBase64';
+import currencyFormat from '@/core/utils/currencyFormat';
+import VariantGroupCard from '@/lib/variant/components/VariantGroupCard';
+import {
+ ChevronDownIcon,
+ ChevronRightIcon,
+ ChevronUpIcon,
+} from '@heroicons/react/24/outline';
+import Link from '@/core/components/elements/Link/Link';
+import checkoutPoApi from '../api/checkoutPoApi';
+import cancelTransactionApi from '../api/cancelTransactionApi';
+import MobileView from '@/core/components/views/MobileView';
+import DesktopView from '@/core/components/views/DesktopView';
+import Menu from '@/lib/auth/components/Menu';
+import Image from '@/core/components/elements/Image/Image';
+import { createSlug } from '@/core/utils/slug';
+import toTitleCase from '@/core/utils/toTitleCase';
+import useAirwayBill from '../hooks/useAirwayBill';
+import Manifest from '@/lib/treckingAwb/component/Manifest';
+import useAuth from '@/core/hooks/useAuth';
+import StepApproval from './stepper';
+import aprpoveApi from '../api/approveApi';
+import rejectApi from '../api/rejectApi';
const Transaction = ({ id }) => {
- const { transaction } = useTransaction({ id })
- const { queryAirwayBill } = useAirwayBill({ orderId: id })
+ const auth = useAuth();
+ const { transaction } = useTransaction({ id });
- const [airwayBillPopup, setAirwayBillPopup] = useState(null)
+ const statusApprovalWeb = transaction.data?.approvalStep;
+
+ const { queryAirwayBill } = useAirwayBill({ orderId: id });
+ const [airwayBillPopup, setAirwayBillPopup] = useState(null);
+
+ const poNumber = useRef(null);
+ const poFile = useRef(null);
+ const [uploadPo, setUploadPo] = useState(false);
+ const [idAWB, setIdAWB] = useState(null);
+ const openUploadPo = () => setUploadPo(true);
+ const closeUploadPo = () => setUploadPo(false);
+
+
+
- const poNumber = useRef(null)
- const poFile = useRef(null)
- const [uploadPo, setUploadPo] = useState(false)
- const [idAWB, setIdAWB] = useState(null)
- const openUploadPo = () => setUploadPo(true)
- const closeUploadPo = () => setUploadPo(false)
const submitUploadPo = async () => {
- const file = poFile.current.files[0]
- const name = poNumber.current.value
+ const file = poFile.current.files[0];
+ const name = poNumber.current.value;
if (typeof file === 'undefined' || !name) {
- toast.error('Nomor dan Dokumen PO harus diisi')
- return
+ toast.error('Nomor dan Dokumen PO harus diisi');
+ return;
}
if (file.size > 5000000) {
- toast.error('Maksimal ukuran file adalah 5MB')
- return
+ toast.error('Maksimal ukuran file adalah 5MB');
+ return;
}
- const data = { name, file: await getFileBase64(file) }
- const isUploaded = await uploadPoApi({ id, data })
+ const data = { name, file: await getFileBase64(file) };
+ const isUploaded = await uploadPoApi({ id, data });
if (isUploaded) {
- toast.success('Berhasil upload PO')
- transaction.refetch()
- closeUploadPo()
- return
+ toast.success('Berhasil upload PO');
+ transaction.refetch();
+ closeUploadPo();
+ return;
}
- toast.error('Terjadi kesalahan internal, coba lagi nanti atau hubungi kami')
- }
+ toast.error(
+ 'Terjadi kesalahan internal, coba lagi nanti atau hubungi kami'
+ );
+ };
- const [cancelTransaction, setCancelTransaction] = useState(false)
- const openCancelTransaction = () => setCancelTransaction(true)
- const closeCancelTransaction = () => setCancelTransaction(false)
+ const [cancelTransaction, setCancelTransaction] = useState(false);
+ const openCancelTransaction = () => setCancelTransaction(true);
+ const closeCancelTransaction = () => setCancelTransaction(false);
+
+ const [rejectTransaction, setRejectTransaction] = useState(false);
+
+ const openRejectTransaction = () => setRejectTransaction(true);
+ const closeRejectTransaction = () => setRejectTransaction(false);
const submitCancelTransaction = async () => {
- const isCancelled = await cancelTransactionApi({ transaction: transaction.data })
+ const isCancelled = await cancelTransactionApi({
+ transaction: transaction.data,
+ });
if (isCancelled) {
- toast.success('Berhasil batalkan transaksi')
- transaction.refetch()
+ toast.success('Berhasil batalkan transaksi');
+ transaction.refetch();
}
- closeCancelTransaction()
- }
+ closeCancelTransaction();
+ };
const checkout = async () => {
if (!transaction.data?.purchaseOrderFile) {
- toast.error('Mohon upload dokumen PO anda sebelum melanjutkan pesanan')
- return
+ toast.error('Mohon upload dokumen PO anda sebelum melanjutkan pesanan');
+ return;
}
- await checkoutPoApi({ id })
- toast.success('Berhasil melanjutkan pesanan')
- transaction.refetch()
- }
+ await checkoutPoApi({ id });
+ toast.success('Berhasil melanjutkan pesanan');
+ transaction.refetch();
+ };
+
+ const handleApproval = async () => {
+ await aprpoveApi({ id });
+ toast.success('Berhasil melanjutkan approval');
+ transaction.refetch();
+ };
+
+ const handleReject = async () => {
+ await rejectApi({ id });
+ closeRejectTransaction();
+ transaction.refetch();
+ };
const memoizeVariantGroupCard = useMemo(
() => (
@@ -102,19 +142,19 @@ const Transaction = ({ id }) => {
</div>
),
[transaction.data]
- )
+ );
if (transaction.isLoading) {
return (
<div className='flex justify-center my-6'>
<Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' />
</div>
- )
+ );
}
const closePopup = () => {
- setIdAWB(null)
- }
+ setIdAWB(null);
+ };
return (
transaction.data?.name && (
@@ -146,6 +186,33 @@ const Transaction = ({ id }) => {
</div>
</BottomPopup>
+ <BottomPopup
+ active={rejectTransaction}
+ close={closeRejectTransaction}
+ title='Batalkan Transaksi'
+ >
+ <div className='leading-7 text-gray_r-12/80'>
+ Apakah anda yakin Membatalkan transaksi{' '}
+ <span className='underline'>{transaction.data?.name}</span>?
+ </div>
+ <div className='flex justify-end mt-6 gap-x-4'>
+ <button
+ className='btn-solid-red w-full md:w-fit'
+ type='button'
+ onClick={handleReject}
+ >
+ Ya, Batalkan
+ </button>
+ <button
+ className='btn-light w-full md:w-fit'
+ type='button'
+ onClick={closeRejectTransaction}
+ >
+ Batal
+ </button>
+ </div>
+ </BottomPopup>
+
<BottomPopup title='Upload PO' close={closeUploadPo} active={uploadPo}>
<div>
<label>Nomor PO</label>
@@ -156,10 +223,18 @@ const Transaction = ({ id }) => {
<input type='file' className='form-input mt-3 py-2' ref={poFile} />
</div>
<div className='grid grid-cols-2 gap-x-3 mt-6'>
- <button type='button' className='btn-light w-full' onClick={closeUploadPo}>
+ <button
+ type='button'
+ className='btn-light w-full'
+ onClick={closeUploadPo}
+ >
Batal
</button>
- <button type='button' className='btn-solid-red w-full' onClick={submitUploadPo}>
+ <button
+ type='button'
+ className='btn-solid-red w-full'
+ onClick={submitUploadPo}
+ >
Upload
</button>
</div>
@@ -167,18 +242,33 @@ const Transaction = ({ id }) => {
<Manifest idAWB={idAWB} closePopup={closePopup}></Manifest>
<MobileView>
+ <div className='p-4'>
+ {auth?.feature?.soApproval && (
+ <StepApproval
+ layer={statusApprovalWeb}
+ status={transaction?.data?.status}
+ className='ml-auto'
+ />
+ )}
+ </div>
<div className='flex flex-col gap-y-4 p-4'>
<DescriptionRow label='Status Transaksi'>
<div className='flex justify-end'>
<TransactionStatusBadge status={transaction.data?.status} />
</div>
</DescriptionRow>
- <DescriptionRow label='No Transaksi'>{transaction.data?.name}</DescriptionRow>
+ <DescriptionRow label='No Transaksi'>
+ {transaction.data?.name}
+ </DescriptionRow>
<DescriptionRow label='Ketentuan Pembayaran'>
{transaction.data?.paymentTerm}
</DescriptionRow>
- <DescriptionRow label='Nama Sales'>{transaction.data?.sales}</DescriptionRow>
- <DescriptionRow label='Waktu Transaksi'>{transaction.data?.dateOrder}</DescriptionRow>
+ <DescriptionRow label='Nama Sales'>
+ {transaction.data?.sales}
+ </DescriptionRow>
+ <DescriptionRow label='Waktu Transaksi'>
+ {transaction.data?.dateOrder}
+ </DescriptionRow>
</div>
<Divider />
@@ -214,25 +304,27 @@ const Transaction = ({ id }) => {
<Divider />
- <div className='p-4 flex flex-col gap-y-4'>
- <DescriptionRow label='Purchase Order'>
- {transaction.data?.purchaseOrderName || '-'}
- </DescriptionRow>
- <div className='flex items-center'>
- <p className='text-gray_r-11 leading-none'>Dokumen PO</p>
- <button
- type='button'
- className='btn-light py-1.5 px-3 ml-auto'
- onClick={
- transaction.data?.purchaseOrderFile
- ? () => downloadPurchaseOrder(transaction.data)
- : openUploadPo
- }
- >
- {transaction.data?.purchaseOrderFile ? 'Download' : 'Upload'}
- </button>
+ {!auth?.feature.soApproval && (
+ <div className='p-4 flex flex-col gap-y-4'>
+ <DescriptionRow label='Purchase Order'>
+ {transaction.data?.purchaseOrderName || '-'}
+ </DescriptionRow>
+ <div className='flex items-center'>
+ <p className='text-gray_r-11 leading-none'>Dokumen PO</p>
+ <button
+ type='button'
+ className='btn-light py-1.5 px-3 ml-auto'
+ onClick={
+ transaction.data?.purchaseOrderFile
+ ? () => downloadPurchaseOrder(transaction.data)
+ : openUploadPo
+ }
+ >
+ {transaction.data?.purchaseOrderFile ? 'Download' : 'Upload'}
+ </button>
+ </div>
</div>
- </div>
+ )}
<Divider />
@@ -278,11 +370,43 @@ const Transaction = ({ id }) => {
<Divider />
<div className='p-4 pt-0'>
- {transaction.data?.status == 'draft' && (
- <button className='btn-yellow w-full mt-4' onClick={checkout}>
- Lanjutkan Transaksi
- </button>
- )}
+ {transaction.data?.status == 'draft' &&
+ auth?.feature.soApproval && (
+ <div className='flex gap-x-2'>
+ <button
+ className='btn-yellow w-full'
+ onClick={checkout}
+ disabled={
+ transaction.data?.status === 'cancel'
+ ? true
+ : false || auth?.webRole === statusApprovalWeb
+ ? true
+ : false
+ }
+ >
+ Approve
+ </button>
+ <button
+ className='btn-solid-red px-7 w-full'
+ onClick={checkout}
+ disabled={
+ transaction.data?.status === 'cancel'
+ ? true
+ : false || auth?.webRole === statusApprovalWeb
+ ? true
+ : false
+ }
+ >
+ Reject
+ </button>
+ </div>
+ )}
+ {transaction.data?.status == 'draft' &&
+ !auth?.feature?.soApproval && (
+ <button className='btn-yellow w-full mt-4' onClick={checkout}>
+ Lanjutkan Transaksi
+ </button>
+ )}
<button
className='btn-light w-full mt-4'
disabled={transaction.data?.status != 'draft'}
@@ -308,10 +432,23 @@ const Transaction = ({ id }) => {
<Menu />
</div>
<div className='w-9/12 p-4 py-6 bg-white border border-gray_r-6 rounded'>
- <h1 className='text-title-sm font-semibold mb-6'>Detail Transaksi</h1>
+ <div className='flex justify-between'>
+ <h1 className='text-title-sm font-semibold mb-6'>
+ Detail Transaksi
+ </h1>
+ {auth?.feature?.soApproval && (
+ <StepApproval
+ layer={statusApprovalWeb}
+ status={transaction?.data?.status}
+ className='ml-auto'
+ />
+ )}
+ </div>
<div className='flex items-center gap-x-2 mb-3'>
- <span className='text-h-sm font-medium'>{transaction?.data?.name}</span>
+ <span className='text-h-sm font-medium'>
+ {transaction?.data?.name}
+ </span>
<TransactionStatusBadge status={transaction?.data?.status} />
</div>
<div className='flex gap-x-4'>
@@ -322,20 +459,58 @@ const Transaction = ({ id }) => {
>
Download
</button>
- {transaction.data?.status == 'draft' && (
- <button className='btn-yellow' onClick={checkout}>
- Lanjutkan Transaksi
- </button>
- )}
- {transaction.data?.status != 'draft' && (
- <button
- className='btn-light'
- disabled={transaction.data?.status != 'waiting'}
- onClick={openCancelTransaction}
- >
- Batalkan Transaksi
- </button>
- )}
+ {transaction.data?.status == 'draft' &&
+ auth?.feature?.soApproval &&
+ auth?.webRole && (
+ <div className='flex gap-x-2'>
+ <button
+ className='btn-yellow'
+ onClick={handleApproval}
+ disabled={
+ transaction.data?.status === 'cancel'
+ ? true
+ : false || auth?.webRole === statusApprovalWeb
+ ? true
+ : false || statusApprovalWeb < 1
+ ? true
+ : false
+ }
+ >
+ Approve
+ </button>
+ <button
+ className='btn-solid-red px-7'
+ onClick={openRejectTransaction}
+ disabled={
+ transaction.data?.status === 'cancel'
+ ? true
+ : false || auth?.webRole === statusApprovalWeb
+ ? true
+ : false || statusApprovalWeb < 1
+ ? true
+ : false
+ }
+ >
+ Reject
+ </button>
+ </div>
+ )}
+ {transaction.data?.status == 'draft' &&
+ !auth?.feature.soApproval && (
+ <button className='btn-yellow' onClick={checkout}>
+ Lanjutkan Transaksi
+ </button>
+ )}
+ {transaction.data?.status != 'draft' &&
+ !auth?.feature.soApproval && (
+ <button
+ className='btn-light'
+ disabled={transaction.data?.status != 'waiting'}
+ onClick={openCancelTransaction}
+ >
+ Batalkan Transaksi
+ </button>
+ )}
</div>
<div className='grid grid-cols-2 gap-x-6 mt-6'>
@@ -350,33 +525,45 @@ const Transaction = ({ id }) => {
<div>Ketentuan Pembayaran</div>
<div>: {transaction?.data?.paymentTerm}</div>
- <div>Purchase Order</div>
- <div>
- : {transaction?.data?.purchaseOrderName}{' '}
- <button
- type='button'
- className='inline-block text-danger-500'
- onClick={
- transaction.data?.purchaseOrderFile
- ? () => downloadPurchaseOrder(transaction.data)
- : openUploadPo
- }
- >
- {transaction?.data?.purchaseOrderFile ? 'Download' : 'Upload'}
- </button>
- </div>
+ {!auth?.feature?.soApproval && (
+ <>
+ <div>Purchase Order</div>
+ <div>
+ : {transaction?.data?.purchaseOrderName}{' '}
+ <button
+ type='button'
+ className='inline-block text-danger-500'
+ onClick={
+ transaction.data?.purchaseOrderFile
+ ? () => downloadPurchaseOrder(transaction.data)
+ : openUploadPo
+ }
+ >
+ {transaction?.data?.purchaseOrderFile
+ ? 'Download'
+ : 'Upload'}
+ </button>
+ </div>
+ </>
+ )}
</div>
</div>
- <div className='text-h-sm font-semibold mt-10 mb-4'>Informasi Pelanggan</div>
+ <div className='text-h-sm font-semibold mt-10 mb-4'>
+ Informasi Pelanggan
+ </div>
<div className='grid grid-cols-2 gap-x-4'>
<div className='border border-gray_r-6 rounded p-3'>
<div className='font-medium mb-4'>Detail Pelanggan</div>
- <SectionContent address={transaction?.data?.address?.customer} />
+ <SectionContent
+ address={transaction?.data?.address?.customer}
+ />
</div>
</div>
- <div className='text-h-sm font-semibold mt-10 mb-4'>Pengiriman</div>
+ <div className='text-h-sm font-semibold mt-10 mb-4'>
+ Pengiriman
+ </div>
<div className='grid grid-cols-3 gap-1'>
{transaction?.data?.pickings?.map((airway) => (
<button
@@ -403,7 +590,9 @@ const Transaction = ({ id }) => {
<div className='badge-red text-sm'>Belum ada pengiriman</div>
)}
- <div className='text-h-sm font-semibold mt-10 mb-4'>Rincian Pembelian</div>
+ <div className='text-h-sm font-semibold mt-10 mb-4'>
+ Rincian Pembelian
+ </div>
<table className='table-data'>
<thead>
<tr>
@@ -426,11 +615,39 @@ const Transaction = ({ id }) => {
)}
className='w-[20%] flex-shrink-0'
>
- <Image
+
+ <div className="relative">
+ <Image
src={product?.parent?.image}
alt={product?.name}
className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md'
/>
+ <div className="absolute top-0 right-4 flex mt-3">
+ <div className="gambarB ">
+ {product.isSni && (
+ <ImageNext
+ src="/images/sni-logo.png"
+ alt="SNI Logo"
+ className="w-2 h-4 object-contain object-top sm:h-4"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ <div className="gambarC ">
+ {product.isTkdn && (
+ <ImageNext
+ src="/images/TKDN.png"
+ alt="TKDN"
+ className="w-5 h-4 object-contain object-top ml-1 sm:h-4"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ </div>
+ </div>
+
</Link>
<div className='px-2 text-left'>
<Link
@@ -483,7 +700,9 @@ const Transaction = ({ id }) => {
{currencyFormat(transaction.data?.amountTax)}
</div>
- <div className='text-right whitespace-nowrap'>Biaya Pengiriman</div>
+ <div className='text-right whitespace-nowrap'>
+ Biaya Pengiriman
+ </div>
<div className='text-right font-medium'>
{currencyFormat(transaction.data?.deliveryAmount)}
</div>
@@ -578,18 +797,18 @@ const Transaction = ({ id }) => {
))} */}
</>
)
- )
-}
+ );
+};
const SectionAddress = ({ address }) => {
const [section, setSection] = useState({
customer: false,
invoice: false,
- shipping: false
- })
+ shipping: false,
+ });
const toggleSection = (name) => {
- setSection({ ...section, [name]: !section[name] })
- }
+ setSection({ ...section, [name]: !section[name] });
+ };
return (
<>
@@ -620,39 +839,50 @@ const SectionAddress = ({ address }) => {
/>
{section.invoice && <SectionContent address={address?.invoice} />} */}
</>
- )
-}
+ );
+};
const SectionButton = ({ label, active, toggle }) => (
- <button className='p-4 font-medium flex justify-between w-full' onClick={toggle}>
+ <button
+ className='p-4 font-medium flex justify-between w-full'
+ onClick={toggle}
+ >
<span>{label}</span>
- {active ? <ChevronUpIcon className='w-5' /> : <ChevronDownIcon className='w-5' />}
+ {active ? (
+ <ChevronUpIcon className='w-5' />
+ ) : (
+ <ChevronDownIcon className='w-5' />
+ )}
</button>
-)
+);
const SectionContent = ({ address }) => {
- let fullAddress = []
- if (address?.street) fullAddress.push(address.street)
- if (address?.subDistrict?.name) fullAddress.push(toTitleCase(address.subDistrict.name))
- if (address?.district?.name) fullAddress.push(toTitleCase(address.district.name))
- if (address?.city?.name) fullAddress.push(toTitleCase(address.city.name))
- fullAddress = fullAddress.join(', ')
+ let fullAddress = [];
+ if (address?.street) fullAddress.push(address.street);
+ if (address?.subDistrict?.name)
+ fullAddress.push(toTitleCase(address.subDistrict.name));
+ if (address?.district?.name)
+ fullAddress.push(toTitleCase(address.district.name));
+ if (address?.city?.name) fullAddress.push(toTitleCase(address.city.name));
+ fullAddress = fullAddress.join(', ');
return (
<div className='flex flex-col gap-y-4 p-4 md:p-0 border-t border-gray_r-6 md:border-0'>
<DescriptionRow label='Nama'>{address.name}</DescriptionRow>
<DescriptionRow label='Email'>{address.email || '-'}</DescriptionRow>
- <DescriptionRow label='No Telepon'>{address.mobile || '-'}</DescriptionRow>
+ <DescriptionRow label='No Telepon'>
+ {address.mobile || '-'}
+ </DescriptionRow>
<DescriptionRow label='Alamat'>{fullAddress}</DescriptionRow>
</div>
- )
-}
+ );
+};
const DescriptionRow = ({ children, label }) => (
<div className='grid grid-cols-2'>
<span className='text-gray_r-11'>{label}</span>
<span className='text-right leading-6'>{children}</span>
</div>
-)
+);
-export default Transaction
+export default Transaction;
diff --git a/src/lib/transaction/components/stepper.jsx b/src/lib/transaction/components/stepper.jsx
new file mode 100644
index 00000000..9b0da0d9
--- /dev/null
+++ b/src/lib/transaction/components/stepper.jsx
@@ -0,0 +1,83 @@
+import {
+ Box,
+ Step,
+ StepDescription,
+ StepIcon,
+ StepIndicator,
+ StepNumber,
+ StepSeparator,
+ StepStatus,
+ StepTitle,
+ Stepper,
+ useSteps,
+} from '@chakra-ui/react';
+import Image from 'next/image';
+
+const StepApproval = ({ layer, status }) => {
+ const steps = [
+ { title: 'Indoteknik', layer_approval: 1 },
+ { title: 'Manager', layer_approval: 2 },
+ { title: 'Director', layer_approval: 3 },
+ ];
+ const { activeStep } = useSteps({
+ index: layer,
+ count: steps.length,
+ });
+ return (
+ <Stepper size='md' index={layer} colorScheme='green'>
+ {steps.map((step, index) => (
+ <Step key={index}>
+ <StepIndicator>
+ {layer === step.layer_approval && status === 'cancel' ? (
+ <StepStatus
+ complete={
+ <Image
+ src='/images/remove.png'
+ width={20}
+ height={20}
+ alt=''
+ className='w-full'
+ />
+ }
+ incomplete={<StepNumber />}
+ active={<StepNumber />}
+ />
+ ) : (
+ <StepStatus
+ complete={<StepIcon />}
+ incomplete={<StepNumber />}
+ active={<StepNumber />}
+ />
+ )}
+ </StepIndicator>
+
+ <Box flexShrink='0'>
+ <StepTitle className='md:text-xs'>{step.title}</StepTitle>
+ {status === 'cancel' ? (
+ layer > step.layer_approval ? (
+ <StepDescription className='md:text-[8px]'>
+ Approved
+ </StepDescription>
+ ) : (
+ <StepDescription className='md:text-[8px]'>
+ Rejected
+ </StepDescription>
+ )
+ ) : layer >= step.layer_approval ? (
+ <StepDescription className='md:text-[8px]'>
+ Approved
+ </StepDescription>
+ ) : (
+ <StepDescription className='md:text-[8px]'>
+ Pending
+ </StepDescription>
+ )}
+ </Box>
+ <StepSeparator _horizontal={{ ml: '0' }} />
+ </Step>
+ ))}
+ </Stepper>
+ );
+};
+
+export default StepApproval;
diff --git a/src/lib/variant/components/VariantCard.jsx b/src/lib/variant/components/VariantCard.jsx
index 9f1b5733..9f65fc3c 100644
--- a/src/lib/variant/components/VariantCard.jsx
+++ b/src/lib/variant/components/VariantCard.jsx
@@ -7,9 +7,14 @@ import { createSlug } from '@/core/utils/slug'
import currencyFormat from '@/core/utils/currencyFormat'
import { updateItemCart } from '@/core/utils/cart'
import whatsappUrl from '@/core/utils/whatsappUrl'
+import ImageNext from 'next/image';
+import { useMemo, useEffect, useState } from 'react';
const VariantCard = ({ product, openOnClick = true, buyMore = false }) => {
const router = useRouter()
+
+
+
const addItemToCart = () => {
toast.success('Berhasil menambahkan ke keranjang', { duration: 1500 })
@@ -27,11 +32,39 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => {
const Card = () => (
<div className='flex gap-x-3'>
<div className='w-4/12 flex items-center gap-x-2'>
- <Image
+
+ <div className="relative">
+ <Image
src={product.parent.image}
alt={product.parent.name}
className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md'
/>
+ <div className="absolute top-0 right-4 flex mt-3">
+ <div className="gambarB ">
+ {product.isSni && (
+ <ImageNext
+ src="/images/sni-logo.png"
+ alt="SNI Logo"
+ className="w-2 h-5 object-contain object-top sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ <div className="gambarC ">
+ {product.isTkdn && (
+ <ImageNext
+ src="/images/TKDN.png"
+ alt="TKDN"
+ className="w-5 h-6 object-contain object-top ml-1 mr-1 sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ </div>
+ </div>
+
</div>
<div className='w-8/12 flex flex-col'>
<p className='product-card__title wrap-line-ellipsis-2'>{product.parent.name}</p>
diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx
index 01dec611..bcb41dd6 100644
--- a/src/pages/_app.jsx
+++ b/src/pages/_app.jsx
@@ -89,9 +89,9 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }) {
<AnimatePresence>
{animateLoader && (
<motion.div
- initial={{ opacity: 0.4 }}
+ initial={{ opacity: 0.25 }}
animate={{ opacity: 1 }}
- exit={{ opacity: 0.4 }}
+ exit={{ opacity: 0.25 }}
transition={{
duration: 0.1,
}}
diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js
index adb23511..b6b8c795 100644
--- a/src/pages/api/shop/search.js
+++ b/src/pages/api/shop/search.js
@@ -1,6 +1,6 @@
-import { productMappingSolr } from '@/utils/solrMapping'
-import axios from 'axios'
-import camelcaseObjectDeep from 'camelcase-object-deep'
+import { productMappingSolr } from '@/utils/solrMapping';
+import axios from 'axios';
+import camelcaseObjectDeep from 'camelcase-object-deep';
export default async function handler(req, res) {
const {
@@ -14,35 +14,36 @@ export default async function handler(req, res) {
operation = 'AND',
fq = '',
limit = 30,
- stock = ''
- } = req.query
+ } = req.query;
- let paramOrderBy = ''
+ let { stock = '' } = req.query;
+
+ let paramOrderBy = '';
switch (orderBy) {
case 'price-asc':
- paramOrderBy += 'price_tier1_v2_f ASC'
- break
+ paramOrderBy += 'price_tier1_v2_f ASC';
+ break;
case 'price-desc':
- paramOrderBy += 'price_tier1_v2_f DESC'
- break
+ paramOrderBy += 'price_tier1_v2_f DESC';
+ break;
case 'popular':
- paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,'
- break
+ paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,';
+ break;
case 'popular-weekly':
- paramOrderBy += 'search_rank_weekly_i DESC'
- break
+ paramOrderBy += 'search_rank_weekly_i DESC';
+ break;
case 'stock':
- paramOrderBy += 'product_rating_f DESC, stock_total_f DESC'
- break
+ paramOrderBy += 'product_rating_f DESC, stock_total_f DESC';
+ break;
case 'flashsale-price-asc':
- paramOrderBy += 'flashsale_price_f ASC'
- break
+ paramOrderBy += 'flashsale_price_f ASC';
+ break;
default:
- paramOrderBy += 'product_rating_f DESC, price_discount_f DESC'
- break
+ paramOrderBy += 'product_rating_f DESC, price_discount_f DESC';
+ break;
}
- let offset = (page - 1) * limit
+ let offset = (page - 1) * limit;
let parameter = [
'facet.field=manufacture_name_s',
'facet.field=category_name',
@@ -55,59 +56,82 @@ export default async function handler(req, res) {
`start=${parseInt(offset)}`,
`rows=${limit}`,
`sort=${paramOrderBy}`,
- `fq=-publish_b:false`
- ]
+ `fq=-publish_b:false`,
+ ];
if (priceFrom > 0 || priceTo > 0) {
parameter.push(
`fq=price_tier1_v2_f:[${priceFrom == '' ? '*' : priceFrom} TO ${
priceTo == '' ? '*' : priceTo
}]`
- )
+ );
+ }
+
+ let { auth } = req.cookies;
+ if (auth) {
+ auth = JSON.parse(auth);
+ if (auth.feature.onlyReadyStock) stock = true;
}
- if (brand) parameter.push(`fq=${brand.split(',').map(manufacturer => `manufacture_name:"${manufacturer}"`).join(" OR ")}`)
- if (category) parameter.push(`fq=${category.split(',').map(cat => `category_name:"${cat}"`).join(' OR ')}`)
+ if (brand)
+ parameter.push(
+ `fq=${brand
+ .split(',')
+ .map((manufacturer) => `manufacture_name:"${encodeURIComponent(manufacturer)}"`)
+ .join(' OR ')}`
+ );
+ if (category)
+ parameter.push(
+ `fq=${category
+ .split(',')
+ .map((cat) => `category_name:"${encodeURIComponent(cat)}"`)
+ .join(' OR ')}`
+ );
// if (category) parameter.push(`fq=category_name:${capitalizeFirstLetter(category.replace(/,/g, ' OR '))}`)
- if (stock) parameter.push(`fq=stock_total_f:{1 TO *}`)
+ if (stock) parameter.push(`fq=stock_total_f:{1 TO *}`);
// Single fq in url params
- if (typeof fq === 'string') parameter.push(`fq=${fq}`)
+ if (typeof fq === 'string') parameter.push(`fq=${fq}`);
// Multi fq in url params
- if (Array.isArray(fq)) parameter = parameter.concat(fq.map((val) => `fq=${val}`))
+ if (Array.isArray(fq))
+ parameter = parameter.concat(fq.map((val) => `fq=${val}`));
- let result = await axios(process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&'))
+ let result = await axios(
+ process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&')
+ );
try {
- let { auth } = req.cookies
- if (auth) auth = JSON.parse(auth)
result.data.response.products = productMappingSolr(
result.data.response.docs,
auth?.pricelist || false
- )
- result.data.responseHeader.params.start = parseInt(result.data.responseHeader.params.start)
- result.data.responseHeader.params.rows = parseInt(result.data.responseHeader.params.rows)
- delete result.data.response.docs
- result.data = camelcaseObjectDeep(result.data)
- res.status(200).json(result.data)
+ );
+ result.data.responseHeader.params.start = parseInt(
+ result.data.responseHeader.params.start
+ );
+ result.data.responseHeader.params.rows = parseInt(
+ result.data.responseHeader.params.rows
+ );
+ delete result.data.response.docs;
+ result.data = camelcaseObjectDeep(result.data);
+ res.status(200).json(result.data);
} catch (error) {
- res.status(400).json({ error: error.message })
+ res.status(400).json({ error: error.message });
}
}
const escapeSolrQuery = (query) => {
- if (query == '*') return query
+ if (query == '*') return query;
- const specialChars = /([\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g
- const words = query.split(/\s+/)
+ const specialChars = /([\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g;
+ const words = query.split(/\s+/);
const escapedWords = words.map((word) => {
if (specialChars.test(word)) {
- return `"${word.replace(specialChars, '\\$1')}"`
+ return `"${word.replace(specialChars, '\\$1')}"`;
}
- return word
- })
+ return word;
+ });
- return escapedWords.join(' ')
-}
+ return escapedWords.join(' ');
+};
/*const productResponseMap = (products, pricelist) => {
return products.map((product) => {
diff --git a/src/pages/api/shop/variant-detail.js b/src/pages/api/shop/variant-detail.js
index fadbe000..08ce75b8 100644
--- a/src/pages/api/shop/variant-detail.js
+++ b/src/pages/api/shop/variant-detail.js
@@ -8,7 +8,10 @@ export default async function handler(req, res) {
`/solr/variants/select?q=id:${req.query.id}&q.op=OR&indent=true`
)
let auth = req.query.auth === 'false' ? JSON.parse(req.query.auth) : req.query.auth
- let result = variantsMappingSolr('',productVariants.data.response.docs, auth || false)
+ let productTemplate = await axios(
+ process.env.SOLR_HOST + `/solr/product/select?q=id:${req.query.id}&q.op=OR&indent=true`
+ )
+ let result = variantsMappingSolr(productTemplate.data.response.docs, productVariants.data.response.docs, auth || false)
res.status(200).json(result)
} catch (error) {
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
index 65d953d2..3da381b6 100644
--- a/src/pages/index.jsx
+++ b/src/pages/index.jsx
@@ -1,14 +1,15 @@
import dynamic from 'next/dynamic';
-import MobileView from '@/core/components/views/MobileView';
-import DesktopView from '@/core/components/views/DesktopView';
import { useRef } from 'react';
-import Seo from '@/core/components/Seo';
-import DelayRender from '@/core/components/elements/DelayRender/DelayRender';
+
import { HeroBannerSkeleton } from '@/components/skeleton/BannerSkeleton';
import { PopularProductSkeleton } from '@/components/skeleton/PopularProductSkeleton';
-import PromotinProgram from '@/lib/promotinProgram/components/HomePage';
-import PreferredBrandSkeleton from '@/lib/home/components/Skeleton/PreferredBrandSkeleton';
+import Seo from '@/core/components/Seo';
+import DelayRender from '@/core/components/elements/DelayRender/DelayRender';
+import DesktopView from '@/core/components/views/DesktopView';
+import MobileView from '@/core/components/views/MobileView';
import { FlashSaleSkeleton } from '@/lib/flashSale/skeleton/FlashSaleSkeleton';
+import PreferredBrandSkeleton from '@/lib/home/components/Skeleton/PreferredBrandSkeleton';
+import PromotinProgram from '@/lib/promotinProgram/components/HomePage';
import PagePopupIformation from '~/modules/popup-information';
const BasicLayout = dynamic(() =>
@@ -40,6 +41,11 @@ const FlashSale = dynamic(
loading: () => <FlashSaleSkeleton />,
}
);
+
+const ProgramPromotion = dynamic(() =>
+ import('@/lib/home/components/PromotionProgram')
+);
+
const BannerSection = dynamic(() =>
import('@/lib/home/components/BannerSection')
);
@@ -74,8 +80,9 @@ export default function Home() {
]}
/>
+ <PagePopupIformation />
+
<DesktopView>
- <PagePopupIformation />
<div className='container mx-auto'>
<div
className='flex min-h-[400px] h-[460px]'
@@ -95,9 +102,12 @@ export default function Home() {
</div>
</div>
- <div className='my-16 flex flex-col gap-y-16'>
+ <div className='my-16 flex flex-col gap-y-8'>
<ServiceList />
- <PreferredBrand />
+ <div id='flashsale'>
+ <PreferredBrand />
+ </div>
+ <ProgramPromotion/>
<FlashSale />
<PromotinProgram />
<CategoryHomeId />
@@ -108,7 +118,6 @@ export default function Home() {
</DesktopView>
<MobileView>
- <PagePopupIformation />
<DelayRender renderAfter={200}>
<HeroBanner />
</DelayRender>
@@ -117,7 +126,12 @@ export default function Home() {
<ServiceList />
</DelayRender>
<DelayRender renderAfter={400}>
- <PreferredBrand />
+ <div id='flashsale'>
+ <PreferredBrand />
+ </div>
+ </DelayRender>
+ <DelayRender renderAfter={400}>
+ <ProgramPromotion/>
</DelayRender>
<DelayRender renderAfter={600}>
<FlashSale />
diff --git a/src/pages/shop/promo/[slug].tsx b/src/pages/shop/promo/[slug].tsx
new file mode 100644
index 00000000..bd69c071
--- /dev/null
+++ b/src/pages/shop/promo/[slug].tsx
@@ -0,0 +1,523 @@
+import dynamic from 'next/dynamic'
+import NextImage from 'next/image';
+import { useEffect, useState } from 'react'
+import { useRouter } from 'next/router'
+import Seo from '../../../core/components/Seo'
+import Promocrumb from '../../../lib/promo/components/Promocrumb'
+import { fetchPromoItemsSolr, fetchVariantSolr } from '../../../api/promoApi'
+import LogoSpinner from '../../../core/components/elements/Spinner/LogoSpinner.jsx'
+import ProductPromoCard from '../../../../src-migrate/modules/product-promo/components/Card'
+import { IPromotion } from '../../../../src-migrate/types/promotion'
+import React from 'react'
+import { SolrResponse } from "../../../../src-migrate/types/solr.ts";
+import DesktopView from '../../../core/components/views/DesktopView';
+import MobileView from '../../../core/components/views/MobileView';
+import 'swiper/swiper-bundle.css';
+import useDevice from '../../../core/hooks/useDevice'
+import ProductFilterDesktop from '../../../lib/product/components/ProductFilterDesktopPromotion';
+import ProductFilter from '../../../lib/product/components/ProductFilter';
+import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react';
+import { formatCurrency } from '../../../core/utils/formatValue';
+import Pagination from '../../../core/components/elements/Pagination/Pagination';
+import SideBanner from '../../../../src-migrate/modules/side-banner';
+import whatsappUrl from '../../../core/utils/whatsappUrl';
+import { cons, toQuery } from 'lodash-contrib';
+import _ from 'lodash';
+import useActive from '../../../core/hooks/useActive';
+
+const BasicLayout = dynamic(() => import('../../../core/components/layouts/BasicLayout'))
+
+export default function PromoDetail() {
+ const router = useRouter()
+ const { slug = '', brand ='', category='', priceFrom = '', priceTo = '', page = '1' } = router.query
+ const [promoItems, setPromoItems] = useState<any[]>([])
+ const [promoData, setPromoData] = useState<IPromotion[] | null>(null)
+ const [currentPage, setCurrentPage] = useState(parseInt(page as string, 10) || 1);
+ const itemsPerPage = 12; // Jumlah item yang ingin ditampilkan per halaman
+ const [loading, setLoading] = useState(true);
+ const { isMobile, isDesktop } = useDevice()
+ const [brands, setBrands] = useState<Brand[]>([]);
+ const [categories, setCategories] = useState<Category[]>([]);
+ const [brandValues, setBrandValues] = useState<string[]>([]);
+ const [categoryValues, setCategoryValues] = useState<string[]>([]);
+ const [orderBy, setOrderBy] = useState(router.query?.orderBy || 'popular');
+ const popup = useActive();
+ const prefixUrl = `/shop/promo/${slug}`
+
+ useEffect(() => {
+ if (router.query.brand) {
+ let brandsArray: string[] = [];
+ if (Array.isArray(router.query.brand)) {
+ brandsArray = router.query.brand;
+ } else if (typeof router.query.brand === 'string') {
+ brandsArray = router.query.brand.split(',').map((brand) => brand.trim());
+ }
+ setBrandValues(brandsArray);
+ } else {
+ setBrandValues([]);
+ }
+
+ if (router.query.category) {
+ let categoriesArray: string[] = [];
+
+ if (Array.isArray(router.query.category)) {
+ categoriesArray = router.query.category;
+ } else if (typeof router.query.category === 'string') {
+ categoriesArray = router.query.category.split(',').map((category) => category.trim());
+ }
+ setCategoryValues(categoriesArray);
+ } else {
+ setCategoryValues([]);
+ }
+ }, [router.query.brand, router.query.category]);
+
+ interface Brand {
+ brand: string;
+ qty: number;
+ }
+
+ interface Category {
+ name: string;
+ qty: number;
+ }
+
+ useEffect(() => {
+ const loadPromo = async () => {
+ setLoading(true);
+ const brandsData: Brand[] = [];
+ const categoriesData: Category[] = [];
+
+ const pageNumber = Array.isArray(page) ? parseInt(page[0], 10) : parseInt(page, 10);
+ setCurrentPage(pageNumber)
+
+ try {
+ const items = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug}`);
+ setPromoItems(items);
+
+ if (items.length === 0) {
+ setPromoData([])
+ setLoading(false);
+ return;
+ }
+
+ const brandArray = Array.isArray(brand) ? brand : brand.split(',');
+ const categoryArray = Array.isArray(category) ? category : category.split(',');
+
+ const promoDataPromises = items.map(async (item) => {
+
+ try {
+ let brandQuery = '';
+ if (brand) {
+ brandQuery = brandArray.map(b => `manufacture_name_s:${b}`).join(' OR ');
+ brandQuery = `(${brandQuery})`;
+ }
+
+ let categoryQuery = '';
+ if (category) {
+ categoryQuery = categoryArray.map(c => `category_name:${c}`).join(' OR ');
+ categoryQuery = `(${categoryQuery})`;
+ }
+
+ let priceQuery = '';
+ if (priceFrom && priceTo) {
+ priceQuery = `price_f:[${priceFrom} TO ${priceTo}]`;
+ } else if (priceFrom) {
+ priceQuery = `price_f:[${priceFrom} TO *]`;
+ } else if (priceTo) {
+ priceQuery = `price_f:[* TO ${priceTo}]`;
+ }
+
+ let combinedQuery = '';
+ let combinedQueryPrice = `${priceQuery}`;
+ if (brand && category && priceFrom || priceTo) {
+ combinedQuery = `${brandQuery} AND ${categoryQuery} `;
+ } else if (brand && category) {
+ combinedQuery = `${brandQuery} AND ${categoryQuery}`;
+ } else if (brand && priceFrom || priceTo) {
+ combinedQuery = `${brandQuery}`;
+ } else if (category && priceFrom || priceTo) {
+ combinedQuery = `${categoryQuery}`;
+ } else if (brand) {
+ combinedQuery = brandQuery;
+ } else if (category) {
+ combinedQuery = categoryQuery;
+ }
+
+ if (combinedQuery && priceFrom || priceTo) {
+ const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`);
+ const product = response.response.docs[0];
+ const product_id = product.id;
+ const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} AND ${combinedQueryPrice}`);
+ return response2;
+ }else if(combinedQuery){
+ const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`);
+ const product = response.response.docs[0];
+ const product_id = product.id;
+ const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} `);
+ return response2;
+ } else {
+ const response = await fetchPromoItemsSolr(`id:${item.id}`);
+ return response;
+ }
+ } catch (fetchError) {
+ return [];
+ }
+ });
+
+ const promoDataArray = await Promise.all(promoDataPromises);
+ const mergedPromoData = promoDataArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
+ setPromoData(mergedPromoData);
+
+ const dataBrandCategoryPromises = promoDataArray.map(async (promoData) => {
+ if (promoData) {
+ const dataBrandCategory = promoData.map(async (item) => {
+ let response;
+ if(category){
+ const categoryQuery = categoryArray.map(c => `category_name:${c}`).join(' OR ');
+ response = await fetchVariantSolr(`id:${item.products[0].product_id} AND (${categoryQuery})`);
+ }else{
+ response = await fetchVariantSolr(`id:${item.products[0].product_id}`)
+ }
+
+
+ if (response.response?.docs?.length > 0) {
+ const product = response.response.docs[0];
+ const manufactureNameS = product.manufacture_name;
+ if (Array.isArray(manufactureNameS)) {
+ for (let i = 0; i < manufactureNameS.length; i += 2) {
+ const brand = manufactureNameS[i];
+ const qty = 1;
+ const existingBrandIndex = brandsData.findIndex(b => b.brand === brand);
+ if (existingBrandIndex !== -1) {
+ brandsData[existingBrandIndex].qty += qty;
+ } else {
+ brandsData.push({ brand, qty });
+ }
+ }
+ }
+
+ const categoryNameS = product.category_name;
+ if (Array.isArray(categoryNameS)) {
+ for (let i = 0; i < categoryNameS.length; i += 2) {
+ const name = categoryNameS[i];
+ const qty = 1;
+ const existingCategoryIndex = categoriesData.findIndex(c => c.name === name);
+ if (existingCategoryIndex !== -1) {
+ categoriesData[existingCategoryIndex].qty += qty;
+ } else {
+ categoriesData.push({ name, qty });
+ }
+ }
+ }
+ }
+ });
+
+ return Promise.all(dataBrandCategory);
+ }
+ });
+
+ await Promise.all(dataBrandCategoryPromises);
+ setBrands(brandsData);
+ setCategories(categoriesData);
+ setLoading(false);
+
+ } catch (loadError) {
+ // console.error("Error loading promo items:", loadError)
+ setLoading(false);
+ }
+ }
+
+ if (slug) {
+ loadPromo()
+ }
+ },[slug, brand, category, priceFrom, priceTo, currentPage]);
+
+
+ function capitalizeFirstLetter(string) {
+ string = string.replace(/_/g, ' ');
+ return string.replace(/(^\w|\s\w)/g, function(match) {
+ return match.toUpperCase();
+ });
+ }
+
+ const handleDeleteFilter = async (source, value) => {
+ let params = {
+ q: router.query.q,
+ orderBy: '',
+ brand: brandValues.join(','),
+ category: categoryValues.join(','),
+ priceFrom: priceFrom || '',
+ priceTo: priceTo || '',
+ };
+
+ let brands = brandValues;
+ let catagories = categoryValues;
+ switch (source) {
+ case 'brands':
+ brands = brandValues.filter((item) => item !== value);
+ params.brand = brands.join(',');
+ await setBrandValues(brands);
+ break;
+ case 'category':
+ catagories = categoryValues.filter((item) => item !== value);
+ params.category = catagories.join(',');
+ await setCategoryValues(catagories);
+ break;
+ case 'price':
+ params.priceFrom = '';
+ params.priceTo = '';
+ break;
+ case 'delete':
+ params = {
+ q: router.query.q,
+ orderBy: '',
+ brand: '',
+ category: '',
+ priceFrom: '',
+ priceTo: '',
+ };
+ break;
+ }
+
+ handleSubmitFilter(params);
+ };
+ const handleSubmitFilter = (params) => {
+ params = _.pickBy(params, _.identity);
+ params = toQuery(params);
+ router.push(`${slug}?${params}`);
+ };
+
+ const visiblePromotions = promoData?.slice( (currentPage-1) * itemsPerPage, currentPage * 12)
+
+ const toQuery = (obj) => {
+ const str = Object.keys(obj)
+ .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`)
+ .join('&')
+ return str
+ }
+
+ const whatPromo = capitalizeFirstLetter(slug)
+ const queryWithoutSlug = _.omit(router.query, ['slug'])
+ const queryString = toQuery(queryWithoutSlug)
+
+ return (
+ <BasicLayout>
+ <Seo
+ title={`Promo ${Array.isArray(slug) ? slug[0] : slug} Terkini`}
+ description='B2B Marketplace MRO & Industri dengan Layanan Pembayaran Tempo, Faktur Pajak, Online Quotation, Garansi Resmi & Harga Kompetitif'
+ />
+ <Promocrumb brandName={whatPromo} />
+ <MobileView>
+ <div className='p-4 pt-0'>
+ <h1 className='mb-2 font-semibold text-h-sm'>Promo {whatPromo}</h1>
+
+ <FilterChoicesComponent
+ brandValues={brandValues}
+ categoryValues={categoryValues}
+ priceFrom={priceFrom}
+ priceTo={priceTo}
+ handleDeleteFilter={handleDeleteFilter}
+ />
+ {promoItems.length >= 1 && (
+ <div className='flex items-center gap-x-2 mb-5 justify-between'>
+ <div>
+ <button
+ className='btn-light py-2 px-5 h-[40px]'
+ onClick={popup.activate}
+ >
+ Filter
+ </button>
+ </div>
+ </div>
+ )}
+
+ {loading ? (
+ <div className='container flex justify-center my-4'>
+ <LogoSpinner width={48} height={48} />
+ </div>
+ ) : promoData && promoItems.length >= 1 ? (
+ <>
+ <div className='grid grid-cols-1 gap-x-1 gap-y-1'>
+ {visiblePromotions?.map((promotion) => (
+ <div key={promotion.id} className="min-w-36 max-w-[400px] mb-[20px] sm:w-full md:w-1/2 lg:w-1/3 xl:w-1/4 ">
+ <ProductPromoCard promotion={promotion}/>
+ </div>
+ ))}
+ </div>
+ </>
+ ) : (
+ <div className="text-center my-8">
+ <p>Belum ada promo pada kategori ini</p>
+ </div>
+ )}
+
+ <Pagination
+ pageCount={Math.ceil((promoData?.length ?? 0) / itemsPerPage)}
+ currentPage={currentPage}
+ url={`${prefixUrl}?${toQuery(_.omit(queryWithoutSlug, ['page']))}`}
+ className='mt-6 mb-2'
+ />
+ <ProductFilter
+ active={popup.active}
+ close={popup.deactivate}
+ brands={brands || []}
+ categories={categories || []}
+ prefixUrl={router.asPath.includes('?') ? `${router.asPath}` : `${router.asPath}?`}
+ defaultBrand={null}
+ />
+ </div>
+
+ </MobileView>
+ <DesktopView>
+ <div className='container mx-auto flex mb-3 flex-col'>
+ <div className='w-full pl-6'>
+ <h1 className='text-2xl mb-2 font-semibold'>Promo {whatPromo}</h1>
+ <div className=' w-full h-full flex flex-row items-center '>
+
+ <div className='detail-filter w-1/2 flex justify-start items-center mt-4'>
+
+ <FilterChoicesComponent
+ brandValues={brandValues}
+ categoryValues={categoryValues}
+ priceFrom={priceFrom}
+ priceTo={priceTo}
+ handleDeleteFilter={handleDeleteFilter}
+ />
+ </div>
+ <div className='Filter w-1/2 flex flex-col'>
+
+ <ProductFilterDesktop
+ brands={brands || []}
+ categories={categories || []}
+ prefixUrl={'/shop/promo'}
+ // defaultBrand={null}
+ />
+ </div>
+ </div>
+ {loading ? (
+ <div className='container flex justify-center my-4'>
+ <LogoSpinner width={48} height={48} />
+ </div>
+ ) : promoData && promoItems.length >= 1 ? (
+ <>
+ <div className='grid grid-cols-1 gap-x-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3'>
+ {visiblePromotions?.map((promotion) => (
+ <div key={promotion.id} className="min-w-[400px] max-w-[400px] mb-[20px] sm:min-w-[350px] md:min-w-[380px] lg:min-w-[400px] xl:min-w-[400px] ">
+ <ProductPromoCard promotion={promotion}/>
+ </div>
+ ))}
+ </div>
+ </>
+ ) : (
+ <div className="text-center my-8">
+ <p>Belum ada promo pada kategori ini</p>
+ </div>
+ )}
+ <div className='flex justify-between items-center mt-6 mb-2'>
+ <div className='pt-2 pb-6 flex items-center gap-x-3'>
+ <NextImage
+ src='/images/logo-question.png'
+ alt='Logo Question Indoteknik'
+ width={60}
+ height={60}
+ />
+ <div className='text-gray_r-12/90'>
+ <span>
+ Barang yang anda cari tidak ada?{' '}
+ <a
+ href={
+ router.query?.q
+ ? whatsappUrl('productSearch', {
+ name: router.query.q,
+ })
+ : whatsappUrl()
+ }
+ className='text-danger-500'
+ >
+ Hubungi Kami
+ </a>
+ </span>
+ </div>
+ </div>
+
+
+
+ <Pagination
+ pageCount={Math.ceil((promoData?.length ?? 0) / itemsPerPage)}
+ currentPage={currentPage}
+ url={`${prefixUrl}?${toQuery(_.omit(queryWithoutSlug, ['page']))}`}
+ className='mt-6 mb-2'
+ />
+ </div>
+
+ </div>
+ </div>
+ </DesktopView>
+ </BasicLayout>
+ )
+ }
+
+const FilterChoicesComponent = ({
+ brandValues,
+ categoryValues,
+ priceFrom,
+ priceTo,
+ handleDeleteFilter,
+ }) => (
+ <div className='flex items-center mb-4'>
+ <HStack spacing={2} className='flex-wrap'>
+ {brandValues?.map((value, index) => (
+ <Tag
+ size='lg'
+ key={index}
+ borderRadius='lg'
+ variant='outline'
+ colorScheme='gray'
+ >
+ <TagLabel>{value}</TagLabel>
+ <TagCloseButton onClick={() => handleDeleteFilter('brands', value)} />
+ </Tag>
+ ))}
+
+ {categoryValues?.map((value, index) => (
+ <Tag
+ size='lg'
+ key={index}
+ borderRadius='lg'
+ variant='outline'
+ colorScheme='gray'
+ >
+ <TagLabel>{value}</TagLabel>
+ <TagCloseButton
+ onClick={() => handleDeleteFilter('category', value)}
+ />
+ </Tag>
+ ))}
+ {priceFrom && priceTo && (
+ <Tag size='lg' borderRadius='lg' variant='outline' colorScheme='gray'>
+ <TagLabel>
+ {formatCurrency(priceFrom) + '-' + formatCurrency(priceTo)}
+ </TagLabel>
+ <TagCloseButton
+ onClick={() => handleDeleteFilter('price', priceFrom)}
+ />
+ </Tag>
+ )}
+ {brandValues?.length > 0 ||
+ categoryValues?.length > 0 ||
+ priceFrom ||
+ priceTo ? (
+ <span>
+ <button
+ className='btn-transparent py-2 px-5 h-[40px] text-red-700'
+ onClick={() => handleDeleteFilter('delete')}
+ >
+ Hapus Semua
+ </button>
+ </span>
+ ) : (
+ ''
+ )}
+ </HStack>
+ </div>
+);
diff --git a/src/pages/shop/promo/index.tsx b/src/pages/shop/promo/index.tsx
new file mode 100644
index 00000000..7ec4f6b0
--- /dev/null
+++ b/src/pages/shop/promo/index.tsx
@@ -0,0 +1,186 @@
+import dynamic from 'next/dynamic'
+import { useEffect, useState } from 'react'
+import { useRouter } from 'next/router'
+import Seo from '../../../core/components/Seo.jsx'
+import Promocrumb from '../../../lib/promo/components/Promocrumb.jsx'
+import { fetchPromoItemsSolr } from '../../../api/promoApi.js'
+import LogoSpinner from '../../../core/components/elements/Spinner/LogoSpinner.jsx'
+import ProductPromoCard from '../../../../src-migrate/modules/product-promo/components/Card.tsx'
+import { IPromotion } from '../../../../src-migrate/types/promotion.ts'
+import React from 'react'
+import { SolrResponse } from "../../../../src-migrate/types/solr.ts";
+
+const BasicLayout = dynamic(() => import('../../../core/components/layouts/BasicLayout.jsx'))
+
+export default function Promo() {
+ const router = useRouter()
+ const { slug = '' } = router.query
+ const [promoItems, setPromoItems] = useState<any[]>([])
+ const [promoData, setPromoData] = useState<IPromotion[] | null>(null)
+ const [loading, setLoading] = useState(true)
+ const [currentPage, setCurrentPage] = useState(1)
+ const [fetchingData, setFetchingData] = useState(false)
+
+ useEffect(() => {
+ const loadPromo = async () => {
+ try {
+ const items = await fetchPromoItemsSolr(`*:*`)
+
+
+ setPromoItems(items)
+
+
+ if (items.length === 0) {
+ setPromoData([])
+ setLoading(false);
+ return;
+ }
+
+ const promoDataPromises = items.map(async (item) => {
+ const queryParams = new URLSearchParams({ q: `id:${item.id}` })
+
+
+ try {
+ const response = await fetch(`/solr/promotion_program_lines/select?${queryParams.toString()}`)
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`)
+ }
+
+ const data: SolrResponse<any[]> = await response.json()
+
+
+ const promotions = await map(data.response.docs)
+ return promotions;
+ } catch (fetchError) {
+ console.error("Error fetching promotion data:", fetchError)
+ return [];
+ }
+ });
+
+ const promoDataArray = await Promise.all(promoDataPromises);
+ const mergedPromoData = promoDataArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
+ setPromoData(mergedPromoData);
+ setTimeout(() => setLoading(false), 120); // Menambahkan delay 200ms sebelum mengubah status loading
+ } catch (loadError) {
+ console.error("Error loading promo items:", loadError)
+ setLoading(false);
+ }
+ }
+
+ if (slug) {
+ loadPromo()
+ }
+ }, [slug])
+
+ const map = async (promotions: any[]): Promise<IPromotion[]> => {
+ const result: IPromotion[] = []
+
+ for (const promotion of promotions) {
+ const data: IPromotion = {
+ id: promotion.id,
+ program_id: promotion.program_id_i,
+ name: promotion.name_s,
+ type: {
+ value: promotion.type_value_s,
+ label: promotion.type_label_s,
+ },
+ limit: promotion.package_limit_i,
+ limit_user: promotion.package_limit_user_i,
+ limit_trx: promotion.package_limit_trx_i,
+ price: promotion.price_f,
+ total_qty: promotion.total_qty_i,
+ products: JSON.parse(promotion.products_s),
+ free_products: JSON.parse(promotion.free_products_s),
+ }
+
+ result.push(data)
+ }
+
+ return result
+ }
+
+
+
+
+ useEffect(() => {
+ const handleScroll = () => {
+ if (
+ !fetchingData &&
+ window.innerHeight + document.documentElement.scrollTop >= 0.95 * document.documentElement.offsetHeight
+ ) {
+ // User has scrolled to 95% of page height
+
+ setTimeout(() => setFetchingData(true), 120);
+ setCurrentPage((prevPage) => prevPage + 1)
+ }
+ }
+
+ window.addEventListener('scroll', handleScroll)
+ return () => window.removeEventListener('scroll', handleScroll)
+ }, [fetchingData])
+
+ useEffect(() => {
+ if (fetchingData) {
+ // Fetch more data
+ // You may need to adjust this logic according to your API
+ fetchMoreData()
+ }
+ }, [fetchingData])
+
+ const fetchMoreData = async () => {
+ try {
+ // Add a delay of approximately 150ms
+ setTimeout(async () => {
+ // Fetch more data
+ // Update promoData state with the new data
+ }, 150)
+ } catch (error) {
+ console.error('Error fetching more data:', error)
+ } finally {
+ setTimeout(() => setFetchingData(false), 120);
+
+ }
+ }
+
+ const visiblePromotions = promoData?.slice(0, currentPage * 12)
+
+ return (
+ <BasicLayout>
+ <Seo
+ title={`Promo ${Array.isArray(slug) ? slug[0] : slug} Terkini`}
+ description='B2B Marketplace MRO & Industri dengan Layanan Pembayaran Tempo, Faktur Pajak, Online Quotation, Garansi Resmi & Harga Kompetitif'
+ />
+ {/* <Promocrumb brandName={capitalizeFirstLetter(Array.isArray(slug) ? slug[0] : slug)} /> */}
+ <div className='container mx-auto mt-1 flex mb-1'>
+ <div className=''>
+ <h1 className='font-semibold'>Semua Promo di Indoteknik</h1>
+ </div>
+ </div>
+ {loading ? (
+ <div className='container flex justify-center my-4'>
+ <LogoSpinner width={48} height={48} />
+ </div>
+ ) : promoData && promoItems.length >= 1 ? (
+ <>
+ <div className='flex flex-wrap justify-center'>
+ {visiblePromotions?.map((promotion) => (
+ <div key={promotion.id} className="min-w-[40px] max-w-[400px] mr-[20px] mb-[20px] sm:w-full md:w-1/2 lg:w-1/3 xl:w-1/4">
+ <ProductPromoCard promotion={promotion} />
+ </div>
+ ))}
+ </div>
+ {fetchingData && (
+ <div className='container flex justify-center my-4'>
+ <LogoSpinner width={48} height={48} />
+ </div>
+ )}
+ </>
+ ) : (
+ <div className="text-center my-8">
+ <p>Belum ada promo pada kategori ini</p>
+ </div>
+ )}
+ </BasicLayout>
+ )
+}
diff --git a/src/pages/video.jsx b/src/pages/video.jsx
index 61790dbb..7d1f8372 100644
--- a/src/pages/video.jsx
+++ b/src/pages/video.jsx
@@ -44,7 +44,7 @@ export default function Video() {
</LazyLoadComponent>
<div className='p-3'>
<a
- href='https://www.youtube.com/@indoteknikb2bindustriale-c778'
+ href='https://www.youtube.com/@indoteknikcom'
className='text-danger-500 mb-2 block'
>
{video.channelName}
diff --git a/src/utils/solrMapping.js b/src/utils/solrMapping.js
index 7e887253..dd90ac7d 100644
--- a/src/utils/solrMapping.js
+++ b/src/utils/solrMapping.js
@@ -36,6 +36,8 @@ export const productMappingSolr = (products, pricelist) => {
tag: product?.flashsale_tag_s || 'FLASH SALE',
},
qtySold: product?.qty_sold_f || 0,
+ isTkdn:product?.tkdn_b || false,
+ isSni:product?.sni_b || false,
};
if (product.manufacture_id_i && product.manufacture_name_s) {
@@ -98,9 +100,10 @@ export const variantsMappingSolr = (parent, products, pricelist) => {
};
}
productMapped.parent = {
- id: parent.product_id_i || '',
- image: parent.image_s || '',
- name: parent.name_s || '',
+ id: parent[0]?.product_id_i || '',
+ image: parent[0]?.image_s || '',
+ name: parent[0]?.name_s || '',
+ description: parent[0]?.description_t || '',
};
return productMapped;
});