summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/bannerApi.js3
-rw-r--r--src/api/promoApi.js75
-rw-r--r--src/core/components/elements/Footer/BasicFooter.jsx4
-rw-r--r--src/core/components/elements/Footer/PromoOffer.tsx112
-rw-r--r--src/core/components/elements/Footer/SimpleFooter.jsx2
-rw-r--r--src/core/components/elements/Footer/style/promoOffer.module.css39
-rw-r--r--src/core/components/elements/Navbar/NavbarDesktop.jsx17
-rw-r--r--src/core/components/elements/Navbar/NavbarUserDropdown.jsx5
-rw-r--r--src/core/components/elements/Sidebar/Sidebar.jsx3
-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-default.pngbin47325 -> 0 bytes
-rw-r--r--src/images/logo-idul-fitri.pngbin0 -> 10642 bytes
-rw-r--r--src/images/logo.pngbin10642 -> 47325 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.jsx48
-rw-r--r--src/lib/checkout/components/CheckoutOld.jsx2
-rw-r--r--src/lib/checkout/components/CheckoutSection.jsx2
-rw-r--r--src/lib/checkout/email/FinishCheckoutEmail.jsx371
-rw-r--r--src/lib/home/components/PreferredBrand.jsx38
-rw-r--r--src/lib/home/components/PromotionProgram.jsx65
-rw-r--r--src/lib/product/components/ProductCard.jsx61
-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.jsx62
-rw-r--r--src/lib/transaction/api/listSiteApi.js10
-rw-r--r--src/lib/transaction/api/rejectProductApi.js9
-rw-r--r--src/lib/transaction/components/Transaction.jsx555
-rw-r--r--src/lib/transaction/components/Transactions.jsx328
-rw-r--r--src/lib/variant/components/VariantCard.jsx130
-rw-r--r--src/pages/api/shop/generate-recomendation.js64
-rw-r--r--src/pages/api/shop/search.js4
-rw-r--r--src/pages/index.jsx30
-rw-r--r--src/pages/my/recomendation/api/recomendation.js17
-rw-r--r--src/pages/my/recomendation/components/products-recomendatison.jsx477
-rw-r--r--src/pages/my/recomendation/index.jsx26
-rw-r--r--src/pages/shop/promo/[slug].tsx523
-rw-r--r--src/pages/shop/promo/index.jsx40
-rw-r--r--src/utils/solrMapping.js2
45 files changed, 2869 insertions, 490 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..f262db8d
--- /dev/null
+++ b/src/api/promoApi.js
@@ -0,0 +1,75 @@
+// 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 sort ='sort=if(exists(sequence_i),0,1) asc, sequence_i asc, if(exists(total_qty_sold_f), total_qty_sold_f, -1) desc';
+ let start = 0
+ let rows = 100
+ // let start = 0
+ // let rows = 10
+ try {
+ const queryParams = new URLSearchParams({ q: type });
+ const response = await fetch(`/solr/promotion_program_lines/select?${queryParams.toString()}&rows=${rows}&start=${start}&${sort}`);
+ 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,
+ sequence: promotion.sequence_i,
+ total_qty: promotion.total_qty_i,
+ products: JSON.parse(promotion.products_s),
+ product_id: promotion.product_ids[0],
+ qty_sold_f:promotion.total_qty_sold_f,
+ 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 314d70ea..6129143d 100644
--- a/src/core/components/elements/Footer/BasicFooter.jsx
+++ b/src/core/components/elements/Footer/BasicFooter.jsx
@@ -271,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>
@@ -386,7 +386,7 @@ const Payments = () => (
<div>
<div className={headerClassName}>Metode Pembayaran</div>
<NextImage
- src='/images/footer/payment-method.png'
+ src='/images/footer/payment-method-new.png'
alt='Metode Pembayaran - Indoteknik'
width={512}
height={512}
diff --git a/src/core/components/elements/Footer/PromoOffer.tsx b/src/core/components/elements/Footer/PromoOffer.tsx
new file mode 100644
index 00000000..a5432b6a
--- /dev/null
+++ b/src/core/components/elements/Footer/PromoOffer.tsx
@@ -0,0 +1,112 @@
+
+import React from "react";
+// import { useGeneralSetting } from "@/common/state-management/general-setting";
+import { FormEvent, useEffect, useState } from "react";
+import toast from "react-hot-toast";
+import style from "../Footer/style/promoOffer.module.css"
+const PromoOffer = ()=>{
+ // const { data, isLoading, fetchData } = useGeneralSetting();
+ const [formData, setFormData] = useState<FormData>({
+ email: "",
+ name: "",
+ telephone: "",
+ message: "",
+ });
+
+ useEffect(() => {
+ // fetchData();
+ }, []);
+
+ type FormData = {
+ email: string;
+ name: string;
+ telephone: string;
+ message: string;
+ };
+
+ const [errors, setErrors] = useState({
+ email: false,
+ name: false,
+ message: false,
+ });
+
+
+const handleGetOffer = async (e: FormEvent) => {
+ e.preventDefault();
+ let loadingToast;
+ try {
+ loadingToast = toast.loading("Mengirimkan formulir...");
+ const response = await fetch("/api/contactus", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ ...formData, from: "newsletter" }),
+ });
+
+ if (response.ok) {
+ toast.dismiss(loadingToast);
+ toast.success("Terima kasih telah menghubungi kami");
+ setFormData({
+ email: "",
+ name: "",
+ telephone: "",
+ message: "",
+ });
+ } else {
+ toast.dismiss(loadingToast);
+ toast.error("Gagal mengirimkan formulir. Silakan coba lagi nanti.");
+ }
+ } catch (error) {
+ toast.dismiss(loadingToast);
+ console.error("Gagal mengirimkan formulir", error);
+ toast.error("Terjadi kesalahan. Silakan coba lagi nanti.");
+ }
+ };
+
+
+
+
+ return(
+ <div className=" py-3 mb-3">
+ <div className="md:flex container mx-auto md:justify-between md:items-center md:gap-x-20">
+ <div className="">
+ <span className="text-black font-semibold text-sm md:text-lg">
+ Dapatkan Promo Menarik Setiap Bulan{" "}
+ </span>
+ <p>
+ Promo produk dengan penawaran terbatas setiap bulannya!
+ </p>
+ </div>
+ <div className=" flex-1 flex items-center h-full justify-end text-sm text-slate-950">
+ <form onSubmit={handleGetOffer} className={style['form-input']}>
+ <div className="flex justify-start w-full">
+ <div className="flex justify-end">
+ <input
+ type="email"
+ value={formData.email}
+ onChange={(e) =>
+ setFormData({ ...formData, email: e.target.value })
+ }
+ className={style['input']}
+ placeholder="Masukkan email anda disini"
+ />
+ <button
+ type="submit"
+ className={style['button']}
+ >
+ Dapatkan
+ </button>
+ </div>
+
+
+ </div>
+ </form>
+
+ </div>
+ </div>
+ </div>
+ )
+ };
+
+ export default PromoOffer;
diff --git a/src/core/components/elements/Footer/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/Footer/style/promoOffer.module.css b/src/core/components/elements/Footer/style/promoOffer.module.css
new file mode 100644
index 00000000..3184182d
--- /dev/null
+++ b/src/core/components/elements/Footer/style/promoOffer.module.css
@@ -0,0 +1,39 @@
+.form-input {
+ @apply
+ h-full
+ w-[514px]
+ text-slate-950
+ flex
+ justify-center;
+}
+
+.input{
+ @apply w-[320px]
+ sm:w-[320px]
+ md:w-[500px]
+ xl:w-[514px]
+ lg:w-[514px]
+ 2xl:w-[514px]
+ text-black
+ py-2
+ h-11
+ md:py-3
+ px-4
+ bg-[#FDF1C7]
+ rounded-3xl
+ focus:outline-none
+ ;
+}
+
+.button{
+ @apply bg-[#FAD147]
+ absolute
+ py-1.5
+ rounded-3xl
+ text-black
+ md:py-2.5
+ px-4
+ h-11
+ z-0
+ ;
+} \ No newline at end of file
diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx
index d2f73d2d..e73a35ee 100644
--- a/src/core/components/elements/Navbar/NavbarDesktop.jsx
+++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx
@@ -142,7 +142,7 @@ const NavbarDesktop = () => {
/>
<div>
<div className='font-semibold'>Whatsapp</div>
- 0812 8080 622 (Chat)
+ 0817 1718 1922 (Chat)
</div>
</a>
</div>
@@ -171,6 +171,17 @@ const NavbarDesktop = () => {
</button>
<div className='w-6/12 flex px-1 divide-x divide-gray_r-6'>
<Link
+ href='/shop/promo'
+ className={`${
+ router.asPath === '/shop/promo' && 'bg-gray_r-3'
+ } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`}
+ // className='p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition'
+ target='_blank'
+ rel='noreferrer'
+ >
+ Semua Promo
+ </Link>
+ <Link
href='/shop/brands'
className={`${
router.asPath === '/shop/brands' && 'bg-gray_r-3'
@@ -201,9 +212,7 @@ 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='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'
>
diff --git a/src/core/components/elements/Navbar/NavbarUserDropdown.jsx b/src/core/components/elements/Navbar/NavbarUserDropdown.jsx
index 1851ce84..42bdc12a 100644
--- a/src/core/components/elements/Navbar/NavbarUserDropdown.jsx
+++ b/src/core/components/elements/Navbar/NavbarUserDropdown.jsx
@@ -2,9 +2,11 @@ import { deleteAuth } from '@/core/utils/auth'
import Link from '../Link/Link'
import { useRouter } from 'next/router'
import { signOut, useSession } from 'next-auth/react'
+import useAuth from '@/core/hooks/useAuth'
const NavbarUserDropdown = () => {
const router = useRouter()
+ const atuh = useAuth()
const logout = async () => {
deleteAuth().then(() => {
@@ -21,6 +23,9 @@ const NavbarUserDropdown = () => {
<Link href='/my/invoices'>Invoice & Faktur Pajak</Link>
<Link href='/my/wishlist'>Wishlist</Link>
<Link href='/my/address'>Daftar Alamat</Link>
+ {!atuh?.external &&
+ <Link href='/my/recomendation'>Dashboard Recomendation</Link>
+ }
<button type='button' onClick={logout}>
Keluar Akun
</button>
diff --git a/src/core/components/elements/Sidebar/Sidebar.jsx b/src/core/components/elements/Sidebar/Sidebar.jsx
index 38fcdef8..cde68515 100644
--- a/src/core/components/elements/Sidebar/Sidebar.jsx
+++ b/src/core/components/elements/Sidebar/Sidebar.jsx
@@ -117,6 +117,9 @@ const Sidebar = ({ active, close }) => {
</>
)}
</div>
+ <SidebarLink className={itemClassName} href='/shop/promo'>
+ Semua Promo
+ </SidebarLink>
<SidebarLink className={itemClassName} href='/shop/brands'>
Semua Brand
</SidebarLink>
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-default.png b/src/images/logo-default.png
deleted file mode 100644
index dccc0cd6..00000000
--- a/src/images/logo-default.png
+++ /dev/null
Binary files differ
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/images/logo.png b/src/images/logo.png
index db04faa5..dccc0cd6 100644
--- a/src/images/logo.png
+++ b/src/images/logo.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 042fc258..4355c7bd 100644
--- a/src/lib/checkout/components/Checkout.jsx
+++ b/src/lib/checkout/components/Checkout.jsx
@@ -119,6 +119,8 @@ const Checkout = () => {
const [checkoutValidation, setCheckoutValidation] = useState(false);
const [loadingVoucher, setLoadingVoucher] = useState(true);
const [loadingRajaOngkir, setLoadingRajaOngkir] = useState(false);
+ const [grandTotal, setGrandTotal] = useState(0);
+ const [hasFlashSale, setHasFlashSale] = useState(false);
const expedisiValidation = useRef(null);
@@ -241,7 +243,9 @@ const Checkout = () => {
setProducts(cartCheckout?.products);
setCheckWeight(cartCheckout?.hasProductWithoutWeight);
setTotalWeight(cartCheckout?.totalWeight.g);
+ setHasFlashSale(cartCheckout?.products[0]?.hasFlashsale ? cartCheckout.products[0].hasFlashsale : false);
}, [cartCheckout]);
+
useEffect(() => {
setCheckoutValidation(false);
@@ -313,6 +317,14 @@ const Checkout = () => {
const [isLoading, setIsLoading] = useState(false);
+ useEffect(() => {
+ const GT =
+ cartCheckout?.grandTotal +
+ Math.round(parseInt(finalShippingAmt * 1.1) / 1000) * 1000;
+ const finalGT = GT < 0 ? 0 : GT;
+ setGrandTotal(finalGT);
+ }, [biayaKirim, cartCheckout?.grandTotal, activeVoucher]);
+
const checkout = async () => {
const file = poFile.current.files[0];
if (typeof file !== 'undefined' && file.size > 5000000) {
@@ -342,14 +354,17 @@ const Checkout = () => {
quantity: product.quantity,
}));
let data = {
- partner_shipping_id: auth.partnerId,
- partner_invoice_id: auth.partnerId,
+ // partner_shipping_id: auth.partnerId,
+ // partner_invoice_id: auth.partnerId,
+ partner_shipping_id: selectedAddress?.shipping?.id || auth.partnerId,
+ partner_invoice_id: selectedAddress?.invoicing?.id || auth.partnerId,
user_id: auth.id,
order_line: JSON.stringify(productOrder),
delivery_amount: biayaKirim,
carrier_id: selectedCarrierId,
estimated_arrival_days: splitDuration(etd),
delivery_service_type: selectedExpedisiService,
+ flash_sale : hasFlashSale, // dibuat negasi untuk ngetest kebalikan nilai false
voucher: activeVoucher,
voucher_shipping: activeVoucherShipping,
type: 'sale_order',
@@ -366,16 +381,25 @@ const Checkout = () => {
toast.error('Gagal melakukan transaksi, terjadi kesalahan internal');
return;
}
-
+
gtagPurchase(products, biayaKirim, isCheckouted.name);
const midtrans = async () => {
for (const product of products) deleteItemCart({ productId: product.id });
- const payment = await axios.post(
- `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/midtrans-payment?transactionId=${isCheckouted.id}`
- );
- setIsLoading(false);
- window.location.href = payment.data.redirectUrl;
+ if (grandTotal > 0) {
+ const payment = await axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/midtrans-payment?transactionId=${isCheckouted.id}`
+ );
+ setIsLoading(false);
+ window.location.href = payment.data.redirectUrl;
+ } else {
+ window.location.href = `${
+ process.env.NEXT_PUBLIC_SELF_HOST
+ }/shop/checkout/success?order_id=${isCheckouted.name.replace(
+ /\//g,
+ '-'
+ )}`;
+ }
};
gtag('event', 'conversion', {
@@ -1071,7 +1095,7 @@ const Checkout = () => {
<div className='flex gap-x-2 justify-between mb-4'>
<div>Grand Total</div>
<div className='font-semibold text-gray_r-12'>
- {currencyFormat(cartCheckout?.grandTotal + finalShippingAmt)}
+ {currencyFormat(grandTotal)}
</div>
</div>
)}
@@ -1367,9 +1391,13 @@ const Checkout = () => {
<div className='flex gap-x-2 justify-between mb-4'>
<div>Grand Total</div>
<div className='font-semibold text-gray_r-12'>
+<<<<<<< HEAD
+ {currencyFormat(grandTotal)}
+=======
{currencyFormat(
cartCheckout?.grandTotal + finalShippingAmt
)}
+>>>>>>> dev/voucher-shipment
</div>
</div>
)}
@@ -1600,7 +1628,7 @@ const SectionExpedisi = ({
dengan menghubungi admin melalui{' '}
<a
className='text-danger-500 inline'
- href='https://api.whatsapp.com/send?phone=628128080622'
+ href='https://api.whatsapp.com/send?phone=6281717181922'
>
tautan ini
</a>
diff --git a/src/lib/checkout/components/CheckoutOld.jsx b/src/lib/checkout/components/CheckoutOld.jsx
index d57fbd66..e2c45ce6 100644
--- a/src/lib/checkout/components/CheckoutOld.jsx
+++ b/src/lib/checkout/components/CheckoutOld.jsx
@@ -696,7 +696,7 @@ const SectionExpedisi = ({ address, listExpedisi, setSelectedExpedisi, checkWeig
diatur beratnya. Mohon atur berat barang dengan menghubungi admin melalui{' '}
<a
className='text-danger-500 inline'
- href='https://api.whatsapp.com/send?phone=628128080622'
+ href='https://api.whatsapp.com/send?phone=6281717181922'
>
tautan ini
</a>
diff --git a/src/lib/checkout/components/CheckoutSection.jsx b/src/lib/checkout/components/CheckoutSection.jsx
index 7f9ea08a..affe6138 100644
--- a/src/lib/checkout/components/CheckoutSection.jsx
+++ b/src/lib/checkout/components/CheckoutSection.jsx
@@ -120,7 +120,7 @@ export const SectionExpedisi = ({
dengan menghubungi admin melalui{' '}
<a
className='text-danger-500 inline'
- href='https://api.whatsapp.com/send?phone=628128080622'
+ href='https://api.whatsapp.com/send?phone=6281717181922'
>
tautan ini
</a>
diff --git a/src/lib/checkout/email/FinishCheckoutEmail.jsx b/src/lib/checkout/email/FinishCheckoutEmail.jsx
index d40ce7d4..d19ba1ca 100644
--- a/src/lib/checkout/email/FinishCheckoutEmail.jsx
+++ b/src/lib/checkout/email/FinishCheckoutEmail.jsx
@@ -14,8 +14,10 @@ import {
Section,
Text
} from '@react-email/components'
+import FinishCheckout from '../components/FinishCheckout'
const FinishCheckoutEmail = ({ transaction, payment, statusPayment }) => {
+
return (
<Html>
<Head />
@@ -38,7 +40,10 @@ const FinishCheckoutEmail = ({ transaction, payment, statusPayment }) => {
</Heading>
<Text style={style.text}>Hai {transaction.address.customer.name},</Text>
- <Text style={style.text}>
+
+ {transaction.amountTotal > 0 ?
+ <div>
+ <Text style={style.text}>
{statusPayment == 'success' && (
<>
Terima kasih atas kepercayaan anda berbelanja di Indoteknik. Dengan ini kami
@@ -71,202 +76,204 @@ const FinishCheckoutEmail = ({ transaction, payment, statusPayment }) => {
& Solution
</>
)}
- </Text>
- <Text style={style.text}>
- {['pending', 'failed'].includes(statusPayment) && (
- <>
- Jika anda mengalami kesulitan, dapat menghubungi Customer Service kami untuk
- menanyakan transaksi anda lakukan melalui Whatsapp kami.
- </>
- )}
- {statusPayment == 'success' && (
- <>
- Anda dapat menghubungi Customer Service kami untuk menanyakan status pesanan yang
- sudah berhasil anda lakukan melalui Whatsapp kami.
- </>
- )}
- {statusPayment == 'manual' && (
+ </Text>
+ <Text style={style.text}>
+ {['pending', 'failed'].includes(statusPayment) && (
+ <>
+ Jika anda mengalami kesulitan, dapat menghubungi Customer Service kami untuk
+ menanyakan transaksi anda lakukan melalui Whatsapp kami.
+ </>
+ )}
+ {statusPayment == 'success' && (
+ <>
+ Anda dapat menghubungi Customer Service kami untuk menanyakan status pesanan yang
+ sudah berhasil anda lakukan melalui Whatsapp kami.
+ </>
+ )}
+ {statusPayment == 'manual' && (
+ <>
+ Kami mohon kepada {transaction.address.customer.name} untuk dapat segera
+ menyelesaikan transaksi dengan detail dibawah ini:
+ <ul>
+ <li>Nomor Pembelian: {transaction.name}</li>
+ <li>Nominal: {currencyFormat(transaction.amountTotal)}</li>
+ <li>Tanggal: {transaction.dateOrder}</li>
+ </ul>
+ </>
+ )}
+ </Text>
+
+ {['pending', 'failed', 'success'].includes(statusPayment) && (
<>
- Kami mohon kepada {transaction.address.customer.name} untuk dapat segera
- menyelesaikan transaksi dengan detail dibawah ini:
- <ul>
- <li>Nomor Pembelian: {transaction.name}</li>
- <li>Nominal: {currencyFormat(transaction.amountTotal)}</li>
- <li>Tanggal: {transaction.dateOrder}</li>
- </ul>
- </>
- )}
- </Text>
+ <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}>
+ <strong>Detail Transaksi</strong>
+ </Text>
+
+ <Hr style={style.hr} />
- {['pending', 'failed', 'success'].includes(statusPayment) && (
- <>
- <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}>
- <strong>Detail Transaksi</strong>
- </Text>
+ <Section style={style.alert}>
+ {statusPayment == 'success' &&
+ 'Struk ini dapat anda simpan sebagai bukti tambahan dalam transaksi yang telah dilakukan.'}
+ {statusPayment == 'pending' &&
+ 'Kami akan menginformasikan melalui email setelah anda berhasil melakukan pembayaran.'}
+ {statusPayment == 'failed' &&
+ 'Dimohon untuk tidak melakukan pembayaran. Karena transaksi anda tidak berhasil dibuat.'}
+ </Section>
- <Hr style={style.hr} />
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>No Transaksi (SO)</Column>
+ <Column style={style.descriptionRCol}>{transaction.name}</Column>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Tanggal Transaksi</Column>
+ <Column style={style.descriptionRCol}>{payment.transactionTime}</Column>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Status Pembayaran</Column>
+ <Column style={{ ...style.descriptionRCol }}>
+ {statusPayment == 'success' && (
+ <div style={{ ...style.badge, ...style.badgeGreen }}>Berhasil</div>
+ )}
+ {statusPayment == 'pending' && (
+ <div style={{ ...style.badge, ...style.badgeRed }}>Pending</div>
+ )}
+ {statusPayment == 'failed' && (
+ <div style={{ ...style.badge, ...style.badgeRed }}>Tidak Berhasil</div>
+ )}
+ </Column>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Metode Pembayaran</Column>
+ <Column style={style.descriptionRCol}>
+ {toTitleCase(payment.paymentType.replaceAll('_', ' '))}
+ </Column>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Batas Akhir Pembayaran</Column>
+ <Column style={style.descriptionRCol}>{payment.expiryTime}</Column>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Nominal Transfer</Column>
+ <Column style={style.descriptionRCol}>
+ <span style={{ fontWeight: '600' }}>{currencyFormat(payment.grossAmount)}</span>
+ </Column>
+ </Row>
- <Section style={style.alert}>
- {statusPayment == 'success' &&
- 'Struk ini dapat anda simpan sebagai bukti tambahan dalam transaksi yang telah dilakukan.'}
- {statusPayment == 'pending' &&
- 'Kami akan menginformasikan melalui email setelah anda berhasil melakukan pembayaran.'}
- {statusPayment == 'failed' &&
- 'Dimohon untuk tidak melakukan pembayaran. Karena transaksi anda tidak berhasil dibuat.'}
- </Section>
+ <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}>
+ <strong>Detail Produk</strong>
+ </Text>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>No Transaksi (SO)</Column>
- <Column style={style.descriptionRCol}>{transaction.name}</Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Tanggal Transaksi</Column>
- <Column style={style.descriptionRCol}>{payment.transactionTime}</Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Status Pembayaran</Column>
- <Column style={{ ...style.descriptionRCol }}>
- {statusPayment == 'success' && (
- <div style={{ ...style.badge, ...style.badgeGreen }}>Berhasil</div>
- )}
- {statusPayment == 'pending' && (
- <div style={{ ...style.badge, ...style.badgeRed }}>Pending</div>
- )}
- {statusPayment == 'failed' && (
- <div style={{ ...style.badge, ...style.badgeRed }}>Tidak Berhasil</div>
- )}
- </Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Metode Pembayaran</Column>
- <Column style={style.descriptionRCol}>
- {toTitleCase(payment.paymentType.replaceAll('_', ' '))}
- </Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Batas Akhir Pembayaran</Column>
- <Column style={style.descriptionRCol}>{payment.expiryTime}</Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Nominal Transfer</Column>
- <Column style={style.descriptionRCol}>
- <span style={{ fontWeight: '600' }}>{currencyFormat(payment.grossAmount)}</span>
- </Column>
- </Row>
+ <Hr style={style.hr} />
- <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}>
- <strong>Detail Produk</strong>
- </Text>
+ {transaction.products.map((product) => (
+ <Row style={style.productRow} key={product.id}>
+ <Column style={style.productLCol}>
+ <Img src={product.parent.image} width='100%' />
+ </Column>
+ <Column style={style.productRCol}>
+ <Text style={style.productName}>{product.name}</Text>
+ <Text style={style.productCode}>{product.code}</Text>
+ <div style={{ dislay: 'flex' }}>
+ <span style={style.productPriceA}>
+ {currencyFormat(product.price.priceDiscount)}
+ </span>
+ {product.price.discountPercentage > 0 && (
+ <>
+ &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/home/components/PreferredBrand.jsx b/src/lib/home/components/PreferredBrand.jsx
index 571c4745..ec09aa4e 100644
--- a/src/lib/home/components/PreferredBrand.jsx
+++ b/src/lib/home/components/PreferredBrand.jsx
@@ -1,13 +1,41 @@
import { Swiper, SwiperSlide } from 'swiper/react'
+import { useCallback, useEffect, useState } from 'react'
import usePreferredBrand from '../hooks/usePreferredBrand'
import PreferredBrandSkeleton from './Skeleton/PreferredBrandSkeleton'
import BrandCard from '@/lib/brand/components/BrandCard'
import useDevice from '@/core/hooks/useDevice'
import Link from '@/core/components/elements/Link/Link'
+import axios from 'axios'
const PreferredBrand = () => {
let query = 'level_s'
let params = 'prioritas'
+ const [isLoading, setIsLoading] = useState(true)
+ const [startWith, setStartWith] = useState(null)
+ const [manufactures, setManufactures] = useState([])
+
+ const loadBrand = useCallback(async () => {
+ setIsLoading(true)
+ const name = startWith ? `${startWith}*` : ''
+ const result = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/brands?params=${name}`)
+
+ setIsLoading(false)
+ setManufactures((manufactures) => [...result.data])
+ }, [startWith])
+
+ const toggleStartWith = (alphabet) => {
+ setManufactures([])
+ if (alphabet == startWith) {
+ setStartWith(null)
+ return
+ }
+ setStartWith(alphabet)
+ }
+
+ useEffect(() => {
+ loadBrand()
+ }, [loadBrand])
+
const { preferredBrands } = usePreferredBrand(query)
const { isMobile, isDesktop } = useDevice()
@@ -21,12 +49,12 @@ const PreferredBrand = () => {
</Link>
)}
</div>
- {preferredBrands.isLoading && <PreferredBrandSkeleton />}
- {!preferredBrands.isLoading && (
+ {manufactures.isLoading && <PreferredBrandSkeleton />}
+ {!manufactures.isLoading && (
<Swiper slidesPerView={isMobile ? 3.5 : 7.5} spaceBetween={isMobile ? 12 : 24} freeMode>
- {preferredBrands.data?.data.map((brand) => (
- <SwiperSlide key={brand.id}>
- <BrandCard brand={brand} />
+ {manufactures.map((manufacture) => (
+ <SwiperSlide key={manufacture.id}>
+ <BrandCard brand={manufacture} />
</SwiperSlide>
))}
</Swiper>
diff --git a/src/lib/home/components/PromotionProgram.jsx b/src/lib/home/components/PromotionProgram.jsx
new file mode 100644
index 00000000..5034fd28
--- /dev/null
+++ b/src/lib/home/components/PromotionProgram.jsx
@@ -0,0 +1,65 @@
+import Link from '@/core/components/elements/Link/Link'
+import Image from 'next/image'
+import { bannerApi } from '@/api/bannerApi';
+import useDevice from '@/core/hooks/useDevice'
+import { Swiper, SwiperSlide } from 'swiper/react';
+const { useQuery } = require('react-query')
+const BannerSection = () => {
+ const promotionProgram = useQuery('promotionProgram', bannerApi({ type: 'banner-promotion' }));
+ const { isMobile, isDesktop } = useDevice()
+
+ return (
+ <div className='px-4 sm:px-0'>
+ <div className='flex justify-between items-center mb-4 '>
+ <div className='font-semibold sm:text-h-lg'>Promo Tersedia</div>
+ {isDesktop && (
+ <Link href='/shop/promo' className='!text-red-500 font-semibold'>
+ Lihat Semua
+ </Link>
+ )}
+ </div>
+ {isDesktop && (promotionProgram.data &&
+ promotionProgram.data?.length > 0 && (
+ <div className='grid grid-cols-3 sm:grid-cols-3 gap-4 rounded-md'>
+ {promotionProgram.data?.map((banner) => (
+ <Link key={banner.id} href={banner.url}>
+ <Image
+ width={439}
+ height={150}
+ quality={100}
+ src={banner.image}
+ alt={banner.name}
+ className='h-auto w-full rounded hover:scale-105 transition duration-500 ease-in-out'
+ />
+ </Link>
+ ))}
+ </div>
+
+ ))}
+
+{isMobile && (
+
+ <Swiper slidesPerView={1.1} spaceBetween={8} freeMode>
+ {promotionProgram.data?.map((banner) => (
+ <SwiperSlide key={banner.id}>
+ <Link key={banner.id} href={banner.url}>
+ <Image
+ width={439}
+ height={150}
+ quality={100}
+ src={banner.image}
+ alt={banner.name}
+ className='h-auto w-full rounded '
+ />
+ </Link>
+ </SwiperSlide>
+ ))}
+ </Swiper>
+
+ )}
+ </div>
+
+ )
+}
+
+export default BannerSection
diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx
index 38ed35f8..98732407 100644
--- a/src/lib/product/components/ProductCard.jsx
+++ b/src/lib/product/components/ProductCard.jsx
@@ -1,7 +1,7 @@
import clsx from 'clsx';
import ImageNext from 'next/image';
import { useRouter } from 'next/router';
-import { useMemo } from 'react';
+import { useMemo, useEffect, useState } from 'react';
import Image from '@/core/components/elements/Image/Image';
import Link from '@/core/components/elements/Link/Link';
@@ -15,6 +15,7 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
const router = useRouter();
const utmSource = useUtmSource();
+
const callForPriceWhatsapp = whatsappUrl('product', {
name: product.name,
manufacture: product.manufacture?.name,
@@ -41,11 +42,39 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
return (
<div className='rounded shadow-sm border border-gray_r-4 bg-white h-[300px] md:h-[350px]'>
<Link href={URL.product} className='border-b border-gray_r-4 relative'>
+ <div className="relative">
<Image
src={image}
alt={product?.name}
- className='w-full object-contain object-center h-36 sm:h-48'
+ className="gambarA w-full object-contain object-center h-36 sm:h-48"
/>
+ <div className="absolute top-0 right-0 flex mt-3">
+ <div className="gambarB ">
+ {product?.isSni && (
+ <ImageNext
+ src="/images/sni-logo.png"
+ alt="SNI Logo"
+ className="w-4 h-5 object-contain object-top sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ <div className="gambarC ">
+ {product?.isTkdn && (
+ <ImageNext
+ src="/images/TKDN.png"
+ alt="TKDN"
+ className="w-11 h-6 object-contain object-top ml-1 mr-1 sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ </div>
+ </div>
+
+
{router.pathname != '/' && product?.flashSale?.id > 0 && (
<div className='absolute bottom-0 w-full grid'>
<div className='absolute bottom-0 w-full h-full'>
@@ -171,11 +200,37 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
<div className='flex bg-white'>
<div className='w-4/12'>
<Link href={URL.product} className='relative'>
+ <div className="relative">
<Image
src={image}
alt={product?.name}
- className='w-full object-contain object-center h-36'
+ className="gambarA w-full object-contain object-center h-36 sm:h-48"
/>
+ <div className="absolute top-0 right-0 flex mt-3">
+ <div className="gambarB ">
+ {product?.isSni && (
+ <ImageNext
+ src="/images/sni-logo.png"
+ alt="SNI Logo"
+ className="w-4 h-5 object-contain object-top sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ <div className="gambarC ">
+ {product?.isTkdn && (
+ <ImageNext
+ src="/images/TKDN.png"
+ alt="TKDN"
+ className="w-11 h-6 object-contain object-top ml-1 sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ </div>
+ </div>
{product.variantTotal > 1 && (
<div className='absolute badge-gray bottom-1.5 left-1.5'>
{product.variantTotal} Varian
diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx
index e4a62abb..a8073036 100644
--- a/src/lib/product/components/ProductFilterDesktop.jsx
+++ b/src/lib/product/components/ProductFilterDesktop.jsx
@@ -21,6 +21,7 @@ import Image from '@/core/components/elements/Image/Image'
import { formatCurrency } from '@/core/utils/formatValue'
const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = null }) => {
+
const router = useRouter()
const { query } = router
const [order, setOrder] = useState(query?.orderBy)
@@ -102,7 +103,14 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu
}
params = _.pickBy(params, _.identity)
params = toQuery(params)
- router.push(`${prefixUrl}?${params}`)
+
+ const slug = Array.isArray(router.query.slug) ? router.query.slug[0] : router.query.slug;
+
+ if (slug) {
+ router.push(`${prefixUrl}/${slug}?${params}`)
+ } else {
+ router.push(`${prefixUrl}?${params}`)
+ }
}
diff --git a/src/lib/product/components/ProductFilterDesktopPromotion.jsx b/src/lib/product/components/ProductFilterDesktopPromotion.jsx
new file mode 100644
index 00000000..0815b881
--- /dev/null
+++ b/src/lib/product/components/ProductFilterDesktopPromotion.jsx
@@ -0,0 +1,132 @@
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+import _ from 'lodash';
+import { toQuery } from 'lodash-contrib';
+import { Button } from '@chakra-ui/react';
+import { MultiSelect } from 'react-multi-select-component';
+
+const ProductFilterDesktop = ({ brands, categories, prefixUrl }) => {
+ const router = useRouter();
+ const { query } = router;
+ const [order, setOrder] = useState(query?.orderBy);
+ const [brandValues, setBrand] = useState([]);
+ const [categoryValues, setCategory] = useState([]);
+ const [priceFrom, setPriceFrom] = useState(query?.priceFrom);
+ const [priceTo, setPriceTo] = useState(query?.priceTo);
+ const [stock, setStock] = useState(query?.stock);
+ const [activeRange, setActiveRange] = useState(null);
+ const [isBrandDropdownClicked, setIsBrandDropdownClicked] = useState(false);
+ const [isCategoryDropdownClicked, setIsCategoryDropdownClicked] = useState(false);
+
+ // Effect to set brandValues from query parameter 'brand'
+ useEffect(() => {
+ const brandParam = query?.brand;
+ if (brandParam) {
+ const brandsArray = brandParam.split(',').map((b) => ({
+ label: b,
+ value: b,
+ }));
+ setBrand(brandsArray);
+ }
+
+ }, [query.brand]); // Trigger effect whenever query.brand changes
+
+ useEffect(() => {
+ const categoryParam = query?.category;
+ if (categoryParam) {
+ const categoriesArray = categoryParam.split(',').map((c) => ({
+ label: c,
+ value: c,
+ }));
+ setCategory(categoriesArray);
+ }
+ }, [query.category]); // Trigger effect whenever query.category changes
+
+ const handleSubmit = () => {
+ let params = {
+ q: router.query.q,
+ orderBy: order,
+ brand: brandValues.map((b) => b.value).join(','),
+ category: categoryValues.map((c) => c.value).join(','),
+ priceFrom,
+ priceTo,
+ stock: stock,
+ };
+ params = _.pickBy(params, _.identity);
+ params = toQuery(params);
+
+ const slug = Array.isArray(router.query.slug)
+ ? router.query.slug[0]
+ : router.query.slug;
+
+ if (slug) {
+ router.push(`${prefixUrl}/${slug}?${params}`);
+ } else {
+ router.push(`${prefixUrl}?${params}`);
+ }
+ };
+
+
+ const brandOptions = brands.map((brand) => ({
+ label: `${brand.brand} (${brand.qty})`,
+ value: brand.brand,
+ }));
+
+ const categoryOptions = categories.map((category) => ({
+ label: `${category.name} (${category.qty})`,
+ value: category.name,
+ }));
+
+ return (
+ <>
+ <div className='flex h-full w-[100%] justify-end '>
+ {/* Brand MultiSelect */}
+ <div className='mb-[20px] mr-4 w-64 h-full flex justify-start '>
+ <div className='relative'>
+ <label>Brand</label>
+ <div className='h-auto z-50 w-64 '>
+ <MultiSelect
+ options={brandOptions}
+ value={brandValues}
+ onChange={setBrand}
+ labelledBy='Select Brand'
+ onMenuToggle={(isOpen) => setIsBrandDropdownClicked(isOpen)}
+ hasSelectAll={false}
+ />
+ </div>
+ </div>
+ </div>
+
+ {/* Category MultiSelect */}
+ <div className='mb-[20px] mr-4 w-64 h-full flex justify-start '>
+ <div className='relative'>
+ <label>Kategori</label>
+ <div className=' h-auto w-64'>
+ <MultiSelect
+ options={categoryOptions}
+ value={categoryValues}
+ onChange={setCategory}
+ labelledBy='Select Kategori'
+ onMenuToggle={() =>
+ setIsCategoryDropdownClicked(!isCategoryDropdownClicked)
+ }
+ hasSelectAll={false}
+ />
+ </div>
+ </div>
+ </div>
+
+ {/* Apply Button */}
+ <div className='TOMBOL mb-1 h-24 flex justify-center items-center w-24'>
+ <div className=' bottom-1 pb-1 left-0 right-0 flex justify-center rounded' >
+ <Button colorScheme='red' width={"full"} onClick={handleSubmit}>
+ Terapkan
+ </Button>
+ </div>
+ </div>
+ </div>
+ </>
+ );
+};
+
+export default ProductFilterDesktop;
diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx
index ee4ec2de..b1a5d409 100644
--- a/src/lib/product/components/ProductSearch.jsx
+++ b/src/lib/product/components/ProductSearch.jsx
@@ -24,6 +24,9 @@ import ProductFilter from './ProductFilter';
import ProductFilterDesktop from './ProductFilterDesktop';
import ProductSearchSkeleton from './Skeleton/ProductSearchSkeleton';
+import SideBanner from '~/modules/side-banner';
+import FooterBanner from '~/modules/footer-banner';
+
const ProductSearch = ({
query,
prefixUrl,
@@ -127,6 +130,7 @@ const ProductSearch = ({
brands.push({ brand, qty });
}
}
+
const categories = [];
for (
@@ -141,6 +145,7 @@ const ProductSearch = ({
categories.push({ name, qty });
}
}
+
const orderOptions = [
{ value: 'price-asc', label: 'Harga Terendah' },
@@ -401,6 +406,10 @@ const ProductSearch = ({
prefixUrl={prefixUrl}
defaultBrand={defaultBrand}
/>
+
+ <div className='h-6' />
+
+ <SideBanner />
</div>
<div className='w-9/12 pl-6'>
{bannerPromotionHeader && bannerPromotionHeader?.image && (
@@ -552,6 +561,7 @@ const ProductSearch = ({
/>
</div>
)}
+ <FooterBanner />
</div>
</div>
</DesktopView>
diff --git a/src/lib/promo/components/Promocrumb.jsx b/src/lib/promo/components/Promocrumb.jsx
new file mode 100644
index 00000000..4f5cf346
--- /dev/null
+++ b/src/lib/promo/components/Promocrumb.jsx
@@ -0,0 +1,40 @@
+import { Breadcrumb as ChakraBreadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'
+import Link from 'next/link'
+import React from 'react'
+
+/**
+ * Renders a breadcrumb component with links to navigate through different pages.
+ *
+ * @param {Object} props - The props object containing the brand name.
+ * @param {string} props.brandName - The name of the brand to display in the breadcrumb.
+ * @return {JSX.Element} The rendered breadcrumb component.
+ */
+const Breadcrumb = ({ brandName }) => {
+ return (
+ <div className='container mx-auto py-4 md:py-6'>
+ <ChakraBreadcrumb>
+ <BreadcrumbItem>
+ <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'>
+ Shop
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+
+ {/* <BreadcrumbItem>
+ <BreadcrumbLink
+ as={Link}
+ href='/shop/promo'
+ className='!text-danger-500 whitespace-nowrap'
+ >
+ Promo
+ </BreadcrumbLink>
+ </BreadcrumbItem> */}
+
+ <BreadcrumbItem isCurrentPage>
+ <BreadcrumbLink className='whitespace-nowrap'>{brandName}</BreadcrumbLink>
+ </BreadcrumbItem>
+ </ChakraBreadcrumb>
+ </div>
+ )
+}
+
+export default Breadcrumb
diff --git a/src/lib/quotation/components/Quotation.jsx b/src/lib/quotation/components/Quotation.jsx
index 09d55e92..8855c6c4 100644
--- a/src/lib/quotation/components/Quotation.jsx
+++ b/src/lib/quotation/components/Quotation.jsx
@@ -68,6 +68,8 @@ const Quotation = () => {
const [etd, setEtd] = useState(null);
const [etdFix, setEtdFix] = useState(null);
+ const [isApproval, setIsApproval] = useState(false);
+
const expedisiValidation = useRef(null);
const [selectedAddress, setSelectedAddress] = useState({
@@ -86,6 +88,7 @@ const Quotation = () => {
};
getAddresses();
+ setIsApproval(auth?.feature?.soApproval);
}, [auth]);
useEffect(() => {
@@ -185,6 +188,13 @@ const Quotation = () => {
if (etd) setEtdFix(calculateEstimatedArrival(etd));
}, [etd]);
+ useEffect(() => {
+ if (isApproval) {
+ setselectedCarrierId(1);
+ setselectedExpedisiService('indoteknik');
+ }
+ }, [isApproval]);
+
// end set up address and carrier
useEffect(() => {
@@ -235,7 +245,7 @@ const Quotation = () => {
const checkout = async () => {
// validation checkout
- if (selectedExpedisi === 0) {
+ if (selectedExpedisi === 0 && !isApproval) {
setCheckoutValidation(true);
if (expedisiValidation.current) {
const position = expedisiValidation.current.getBoundingClientRect();
@@ -246,7 +256,7 @@ const Quotation = () => {
}
return;
}
- if (selectedCarrier != 1 && biayaKirim == 0) {
+ if (selectedCarrier != 1 && biayaKirim == 0 && !isApproval) {
toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.');
return;
}
@@ -267,7 +277,9 @@ const Quotation = () => {
estimated_arrival_days: splitDuration(etd),
delivery_service_type: selectedExpedisiService,
};
+ console.log('data checkout', data);
const isSuccess = await checkoutApi({ data });
+ console.log('isSuccess', isSuccess);
setIsLoading(false);
if (isSuccess?.id) {
for (const product of products) deleteItemCart({ productId: product.id });
@@ -343,16 +355,21 @@ const Quotation = () => {
)}
<Divider />
<SectionValidation address={selectedAddress.invoicing} />
- <SectionExpedisi
- address={selectedAddress.shipping}
- listExpedisi={listExpedisi}
- setSelectedExpedisi={setSelectedExpedisi}
- checkWeigth={checkWeigth}
- checkoutValidation={checkoutValidation}
- expedisiValidation={expedisiValidation}
- loadingRajaOngkir={loadingRajaOngkir}
- />
- <Divider />
+ {!isApproval && (
+ <>
+ <SectionExpedisi
+ address={selectedAddress.shipping}
+ listExpedisi={listExpedisi}
+ setSelectedExpedisi={setSelectedExpedisi}
+ checkWeigth={checkWeigth}
+ checkoutValidation={checkoutValidation}
+ expedisiValidation={expedisiValidation}
+ loadingRajaOngkir={loadingRajaOngkir}
+ />
+ <Divider />
+ </>
+ )}
+
<SectionListService
listserviceExpedisi={listserviceExpedisi}
setSelectedServiceType={setSelectedServiceType}
@@ -468,15 +485,18 @@ const Quotation = () => {
)}
<Divider />
<SectionValidation address={selectedAddress.invoicing} />
- <SectionExpedisi
- address={selectedAddress.shipping}
- listExpedisi={listExpedisi}
- setSelectedExpedisi={setSelectedExpedisi}
- checkWeigth={checkWeigth}
- checkoutValidation={checkoutValidation}
- expedisiValidation={expedisiValidation}
- loadingRajaOngkir={loadingRajaOngkir}
- />
+ {!isApproval && (
+ <SectionExpedisi
+ address={selectedAddress.shipping}
+ listExpedisi={listExpedisi}
+ setSelectedExpedisi={setSelectedExpedisi}
+ checkWeigth={checkWeigth}
+ checkoutValidation={checkoutValidation}
+ expedisiValidation={expedisiValidation}
+ loadingRajaOngkir={loadingRajaOngkir}
+ />
+ )}
+
<Divider />
<SectionListService
listserviceExpedisi={listserviceExpedisi}
diff --git a/src/lib/transaction/api/listSiteApi.js b/src/lib/transaction/api/listSiteApi.js
new file mode 100644
index 00000000..8b7740c5
--- /dev/null
+++ b/src/lib/transaction/api/listSiteApi.js
@@ -0,0 +1,10 @@
+import odooApi from '@/core/api/odooApi'
+import { getAuth } from '@/core/utils/auth'
+
+const getSite = async () => {
+ const auth = getAuth()
+ const dataSite = await odooApi('GET', `/api/v1/partner/${auth?.partnerId}/list/site`)
+ return dataSite
+}
+
+export default getSite \ No newline at end of file
diff --git a/src/lib/transaction/api/rejectProductApi.js b/src/lib/transaction/api/rejectProductApi.js
new file mode 100644
index 00000000..e03c7975
--- /dev/null
+++ b/src/lib/transaction/api/rejectProductApi.js
@@ -0,0 +1,9 @@
+import odooApi from '@/core/api/odooApi'
+
+const rejectProductApi = async ({ idSo, idProduct, reason }) => {
+ const dataCheckout = await odooApi('POST', `/api/v1/sale_order/${idSo}/reject/${idProduct}`, {
+ reason_reject: reason
+ });
+ return dataCheckout
+}
+export default rejectProductApi \ No newline at end of file
diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx
index 8f4b2038..cd3613c0 100644
--- a/src/lib/transaction/components/Transaction.jsx
+++ b/src/lib/transaction/components/Transaction.jsx
@@ -2,7 +2,8 @@ import Spinner from '@/core/components/elements/Spinner/Spinner';
import useTransaction from '../hooks/useTransaction';
import TransactionStatusBadge from './TransactionStatusBadge';
import Divider from '@/core/components/elements/Divider/Divider';
-import { useMemo, useRef, useState } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import ImageNext from 'next/image';
import {
downloadPurchaseOrder,
downloadQuotation,
@@ -33,12 +34,18 @@ import useAuth from '@/core/hooks/useAuth';
import StepApproval from './stepper';
import aprpoveApi from '../api/approveApi';
import rejectApi from '../api/rejectApi';
+import rejectProductApi from '../api/rejectProductApi';
+import { useRouter } from 'next/router';
const Transaction = ({ id }) => {
+ const router = useRouter()
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [selectedProduct, setSelectedProduct] = useState(null);
+ const [reason, setReason] = useState('');
const auth = useAuth();
const { transaction } = useTransaction({ id });
-
- const statusApprovalWeb = transaction.data?.approvalStep
+
+ const statusApprovalWeb = transaction.data?.approvalStep;
const { queryAirwayBill } = useAirwayBill({ orderId: id });
const [airwayBillPopup, setAirwayBillPopup] = useState(null);
@@ -49,6 +56,7 @@ const Transaction = ({ id }) => {
const [idAWB, setIdAWB] = useState(null);
const openUploadPo = () => setUploadPo(true);
const closeUploadPo = () => setUploadPo(false);
+
const submitUploadPo = async () => {
const file = poFile.current.files[0];
const name = poNumber.current.value;
@@ -78,7 +86,7 @@ const Transaction = ({ id }) => {
const closeCancelTransaction = () => setCancelTransaction(false);
const [rejectTransaction, setRejectTransaction] = useState(false);
-
+
const openRejectTransaction = () => setRejectTransaction(true);
const closeRejectTransaction = () => setRejectTransaction(false);
const submitCancelTransaction = async () => {
@@ -106,13 +114,13 @@ const Transaction = ({ id }) => {
await aprpoveApi({ id });
toast.success('Berhasil melanjutkan approval');
transaction.refetch();
- }
+ };
const handleReject = async () => {
- await rejectApi({ id });
- closeRejectTransaction()
- transaction.refetch();
- }
+ await rejectApi({ id });
+ closeRejectTransaction();
+ transaction.refetch();
+ };
const memoizeVariantGroupCard = useMemo(
() => (
@@ -139,6 +147,15 @@ const Transaction = ({ id }) => {
[transaction.data]
);
+ const memoizeVariantGroupCardReject = useMemo(
+ () => (
+ <div className='p-4 pt-0 flex flex-col gap-y-3'>
+ <VariantGroupCard variants={transaction.data?.productsRejectLine} buyMore />
+ </div>
+ ),
+ [transaction.data]
+ );
+
if (transaction.isLoading) {
return (
<div className='flex justify-center my-6'>
@@ -151,6 +168,38 @@ const Transaction = ({ id }) => {
setIdAWB(null);
};
+ const openModal = (product) => {
+ setSelectedProduct(product);
+ setIsModalOpen(true);
+ };
+
+ const closeModal = () => {
+ setIsModalOpen(false);
+ setSelectedProduct(null);
+ setReason('');
+ };
+
+ const handleRejectProduct = async () => {
+ try{
+ if (!reason.trim()) {
+ toast.error('Masukkan alasan terlebih dahulu');
+ return;
+ }else{
+ let idSo = transaction?.data.id
+ let idProduct = selectedProduct?.id
+ await rejectProductApi({ idSo, idProduct, reason});
+ closeModal();
+ toast.success("Produk berhasil di reject")
+ setTimeout(() => {
+ window.location.reload();
+ }, 1500);
+ }
+ }catch(error){
+ toast.error('Gagal reject produk. Silakan coba lagi.');
+ }
+ };
+
+
return (
transaction.data?.name && (
<>
@@ -238,7 +287,13 @@ const Transaction = ({ id }) => {
<MobileView>
<div className='p-4'>
- <StepApproval layer={2} status={'cancel'} className='ml-auto' />
+ {auth?.feature?.soApproval && (
+ <StepApproval
+ layer={statusApprovalWeb}
+ status={transaction?.data?.status}
+ className='ml-auto'
+ />
+ )}
</div>
<div className='flex flex-col gap-y-4 p-4'>
<DescriptionRow label='Status Transaksi'>
@@ -293,40 +348,6 @@ const Transaction = ({ id }) => {
<Divider />
- {!auth?.feature.soApproval && (
- <div className='p-4 flex flex-col gap-y-4'>
- <DescriptionRow label='Purchase Order'>
- {transaction.data?.purchaseOrderName || '-'}
- </DescriptionRow>
- <div className='flex items-center'>
- <p className='text-gray_r-11 leading-none'>Dokumen PO</p>
- <button
- type='button'
- className='btn-light py-1.5 px-3 ml-auto'
- onClick={
- transaction.data?.purchaseOrderFile
- ? () => downloadPurchaseOrder(transaction.data)
- : openUploadPo
- }
- >
- {transaction.data?.purchaseOrderFile ? 'Download' : 'Upload'}
- </button>
- </div>
- </div>
- )}
-
- <Divider />
-
- <div className='font-medium p-4'>Detail Produk</div>
-
- {memoizeVariantGroupCard}
-
- <Divider />
-
- <SectionAddress address={transaction.data?.address} />
-
- <Divider />
-
<div className='p-4'>
<p className='font-medium'>Invoice</p>
<div className='flex flex-col gap-y-3 mt-4'>
@@ -358,34 +379,90 @@ const Transaction = ({ id }) => {
<Divider />
- <div className='p-4 pt-0'>
- {transaction.data?.status == 'draft' && auth?.feature.soApproval && (
- <div className='flex gap-x-2'>
- <button
- className='btn-yellow w-full'
- onClick={checkout}
- disabled={
- transaction.data?.status === 'cancel' ? true : false || auth?.webRole === statusApprovalWeb ? true : false
- }
- >
- Approve
- </button>
+ {!auth?.feature.soApproval && (
+ <div className='p-4 flex flex-col gap-y-4'>
+ <DescriptionRow label='Purchase Order'>
+ {transaction.data?.purchaseOrderName || '-'}
+ </DescriptionRow>
+ <div className='flex items-center'>
+ <p className='text-gray_r-11 leading-none'>Dokumen PO</p>
<button
- className='btn-solid-red px-7 w-full'
- onClick={checkout}
- disabled={
- transaction.data?.status === 'cancel' ? true : false || auth?.webRole === statusApprovalWeb ? true : false
+ type='button'
+ className='btn-light py-1.5 px-3 ml-auto'
+ onClick={
+ transaction.data?.purchaseOrderFile
+ ? () => downloadPurchaseOrder(transaction.data)
+ : openUploadPo
}
>
- Reject
+ {transaction.data?.purchaseOrderFile ? 'Download' : 'Upload'}
</button>
</div>
+ </div>
+ )}
+
+ <Divider />
+
+ <div className='font-medium p-4'>Detail Produk</div>
+ {transaction?.data?.products.length > 0? (
+ <div>
+ {memoizeVariantGroupCard}
+ </div>
+ ) : (
+ <div className='badge-red text-sm px-2 ml-4'>Semua produk telah di reject</div>
)}
- {transaction.data?.status == 'draft' && !auth?.feature?.soApproval && (
- <button className='btn-yellow w-full mt-4' onClick={checkout}>
- Lanjutkan Transaksi
- </button>
- )}
+
+ {transaction?.data?.productsRejectLine.length > 0 && (
+ <div>
+ <div className='font-medium p-4'>Detail Produk Reject</div>
+ {memoizeVariantGroupCardReject}
+ </div>
+ )}
+
+ <Divider />
+
+ <SectionAddress address={transaction.data?.address} />
+
+ <Divider />
+
+ <div className='p-4 pt-0'>
+ {transaction.data?.status == 'draft' &&
+ auth?.feature.soApproval && (
+ <div className='flex gap-x-2'>
+ <button
+ className='btn-yellow w-full'
+ onClick={checkout}
+ disabled={
+ transaction.data?.status === 'cancel'
+ ? true
+ : false || auth?.webRole === statusApprovalWeb
+ ? true
+ : false
+ }
+ >
+ Approve
+ </button>
+ <button
+ className='btn-solid-red px-7 w-full'
+ onClick={checkout}
+ disabled={
+ transaction.data?.status === 'cancel'
+ ? true
+ : false || auth?.webRole === statusApprovalWeb
+ ? true
+ : false
+ }
+ >
+ Reject
+ </button>
+ </div>
+ )}
+ {transaction.data?.status == 'draft' &&
+ !auth?.feature?.soApproval && (
+ <button className='btn-yellow w-full mt-4' onClick={checkout}>
+ Lanjutkan Transaksi
+ </button>
+ )}
<button
className='btn-light w-full mt-4'
disabled={transaction.data?.status != 'draft'}
@@ -439,13 +516,20 @@ const Transaction = ({ id }) => {
Download
</button>
{transaction.data?.status == 'draft' &&
- auth?.feature?.soApproval && auth?.webRole && (
+ auth?.feature?.soApproval &&
+ auth?.webRole && (
<div className='flex gap-x-2'>
<button
className='btn-yellow'
onClick={handleApproval}
disabled={
- transaction.data?.status === 'cancel' ? true : false || auth?.webRole === statusApprovalWeb ? true : false || statusApprovalWeb < 1 ? true : false
+ transaction.data?.status === 'cancel'
+ ? true
+ : false || auth?.webRole === statusApprovalWeb
+ ? true
+ : false || statusApprovalWeb < 1
+ ? true
+ : false
}
>
Approve
@@ -454,7 +538,13 @@ const Transaction = ({ id }) => {
className='btn-solid-red px-7'
onClick={openRejectTransaction}
disabled={
- transaction.data?.status === 'cancel' ? true : false || auth?.webRole === statusApprovalWeb ? true : false || statusApprovalWeb < 1 ? true : false
+ transaction.data?.status === 'cancel'
+ ? true
+ : false || auth?.webRole === statusApprovalWeb
+ ? true
+ : false || statusApprovalWeb < 1
+ ? true
+ : false
}
>
Reject
@@ -467,15 +557,16 @@ const Transaction = ({ id }) => {
Lanjutkan Transaksi
</button>
)}
- {transaction.data?.status != 'draft' && !auth?.feature.soApproval && (
- <button
- className='btn-light'
- disabled={transaction.data?.status != 'waiting'}
- onClick={openCancelTransaction}
- >
- Batalkan Transaksi
- </button>
- )}
+ {transaction.data?.status != 'draft' &&
+ !auth?.feature.soApproval && (
+ <button
+ className='btn-light'
+ disabled={transaction.data?.status != 'waiting'}
+ onClick={openCancelTransaction}
+ >
+ Batalkan Transaksi
+ </button>
+ )}
</div>
<div className='grid grid-cols-2 gap-x-6 mt-6'>
@@ -490,7 +581,7 @@ const Transaction = ({ id }) => {
<div>Ketentuan Pembayaran</div>
<div>: {transaction?.data?.paymentTerm}</div>
- {!auth?.feature?.soApproval && (
+ {!auth?.feature?.soApproval ? (
<>
<div>Purchase Order</div>
<div>
@@ -510,6 +601,11 @@ const Transaction = ({ id }) => {
</button>
</div>
</>
+ ) : (
+ <>
+ <div>Site</div>
+ <div>: {transaction?.data?.sitePartner}</div>
+ </>
)}
</div>
</div>
@@ -525,50 +621,83 @@ const Transaction = ({ id }) => {
/>
</div>
</div>
-
- <div className='text-h-sm font-semibold mt-10 mb-4'>
- Pengiriman
- </div>
- <div className='grid grid-cols-3 gap-1'>
- {transaction?.data?.pickings?.map((airway) => (
- <button
- className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left'
- key={airway?.id}
- onClick={() => setIdAWB(airway?.id)}
- >
- <div>
- <span className='text-sm text-gray_r-11'>
- No Resi : {airway?.trackingNumber || '-'}{' '}
- </span>
- <p className='mt-1 font-medium'>{airway?.name}</p>
- </div>
- <div className='flex gap-x-2'>
- <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1'>
- {airway?.delivered ? 'Pesanan Tiba' : 'Sedang Dikirim'}
- </div>
- <ChevronRightIcon className='w-5 stroke-2' />
+ <div className='flex '>
+ <div className='w-1/2'>
+ <div className='text-h-sm font-semibold mt-10 mb-4'>
+ Pengiriman
+ </div>
+ {transaction?.data?.pickings.length == 0 && (
+ <div className='badge-red text-sm'>Belum ada pengiriman</div>
+ )}
+ <div className='grid grid-cols-1 gap-1 w-2/3'>
+ {transaction?.data?.pickings?.map((airway) => (
+ <button
+ className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left h-20'
+ key={airway?.id}
+ onClick={() => setIdAWB(airway?.id)}
+ >
+ <div>
+ <span className='text-sm text-gray_r-11'>
+ No Resi : {airway?.trackingNumber || '-'}{' '}
+ </span>
+ <p className='mt-1 font-medium'>{airway?.name}</p>
+ </div>
+ <div className='flex gap-x-2'>
+ <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1'>
+ {airway?.delivered ? 'Pesanan Tiba' : 'Sedang Dikirim'}
+ </div>
+ <ChevronRightIcon className='w-5 stroke-2' />
+ </div>
+ </button>
+ ))}
+ </div>
+ </div>
+ <div className='invoice w-1/2 '>
+ <div className='text-h-sm font-semibold mt-10 mb-4 '>Invoice</div>
+ {transaction.data?.invoices?.length === 0 && (
+ <div className='badge-red text-sm'>Belum ada invoice</div>
+ )}
+ <div className='grid grid-cols-1 gap-1 w-2/3 '>
+ {transaction.data?.invoices?.map((invoice, index) => (
+ <Link href={`/my/invoices/${invoice.id}`} key={index}>
+ <div className='shadow rounded-md p-4 text-gray_r-12 font-normal flex justify-between'>
+ <div>
+ <p className='mb-1'>{invoice?.name}</p>
+ <div className='flex items-center gap-x-1'>
+ {invoice.amountResidual > 0 ? (
+ <div className='badge-red'>Belum Lunas</div>
+ ) : (
+ <div className='badge-green'>Lunas</div>
+ )}
+ <p className='text-caption-2 text-gray_r-11'>
+ {currencyFormat(invoice.amountTotal)}
+ </p>
+ </div>
+ </div>
+ <ChevronRightIcon className='w-5 stroke-2' />
+ </div>
+ </Link>
+ ))}
</div>
- </button>
- ))}
+ </div>
</div>
- {transaction?.data?.pickings.length == 0 && (
- <div className='badge-red text-sm'>Belum ada pengiriman</div>
- )}
- <div className='text-h-sm font-semibold mt-10 mb-4'>
+ <div className='text-h-sm font-semibold mt-4 mb-4'>
Rincian Pembelian
</div>
- <table className='table-data'>
- <thead>
- <tr>
- <th>Nama Produk</th>
- <th>Diskon</th>
- <th>Jumlah</th>
- <th>Harga</th>
- <th>Subtotal</th>
- </tr>
- </thead>
- <tbody>
+ {transaction?.data?.products?.length > 0? (
+ <table className='table-data'>
+ <thead>
+ <tr>
+ <th>Nama Produk</th>
+ {/* <th>Diskon</th> */}
+ <th>Jumlah</th>
+ <th>Harga</th>
+ <th>Subtotal</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
{transaction?.data?.products?.map((product) => (
<tr key={product.id}>
<td className='flex'>
@@ -580,11 +709,37 @@ const Transaction = ({ id }) => {
)}
className='w-[20%] flex-shrink-0'
>
- <Image
- src={product?.parent?.image}
- alt={product?.name}
- className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md'
- />
+ <div className='relative'>
+ <Image
+ src={product?.parent?.image}
+ alt={product?.name}
+ className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md'
+ />
+ <div className='absolute top-0 right-4 flex mt-3'>
+ <div className='gambarB '>
+ {product.isSni && (
+ <ImageNext
+ src='/images/sni-logo.png'
+ alt='SNI Logo'
+ className='w-2 h-4 object-contain object-top sm:h-4'
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ <div className='gambarC '>
+ {product.isTkdn && (
+ <ImageNext
+ src='/images/TKDN.png'
+ alt='TKDN'
+ className='w-5 h-4 object-contain object-top ml-1 sm:h-4'
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ </div>
+ </div>
</Link>
<div className='px-2 text-left'>
<Link
@@ -605,26 +760,71 @@ const Transaction = ({ id }) => {
</div>
</div>
</td>
- <td>
+ {/* <td>
{product.price.discountPercentage > 0
? `${product.price.discountPercentage}%`
: ''}
- </td>
+ </td> */}
<td>{product.quantity}</td>
<td>
- {product.price.discountPercentage > 0 && (
+ {/* {product.price.discountPercentage > 0 && (
<div className='line-through mb-1 text-caption-1 text-gray_r-12/70'>
{currencyFormat(product.price.price)}
</div>
- )}
+ )} */}
<div>{currencyFormat(product.price.priceDiscount)}</div>
</td>
<td>{currencyFormat(product.price.subtotal)}</td>
+ {/* {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (transaction.data.isReaject == false) && ( */}
+ {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (router.asPath.includes("/my/quotations/")) && (
+ <td>
+ <button
+ className="bg-red-500 text-white py-1 px-3 rounded"
+ onClick={() => openModal(product)}
+ >
+ Reject
+ </button>
+ </td>
+ )}
</tr>
))}
</tbody>
</table>
+ ) : (
+ <div className='badge-red text-sm'>Semua produk telah di reject</div>
+ )}
+
+ {isModalOpen && (
+ <div className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center'>
+ <div className='bg-white p-4 rounded w-96
+ ease-in-out opacity-100
+ transform transition-transform duration-300 scale-100'>
+ <h2 className='text-lg mb-2'>Berikan Alasan</h2>
+ <textarea
+ value={reason}
+ onChange={(e) => setReason(e.target.value)}
+ className='w-full p-2 border rounded'
+ rows='4'
+ ></textarea>
+ <div className='mt-4 flex justify-end'>
+ <button
+ className='bg-gray-300 text-black py-1 px-3 rounded mr-2'
+ onClick={closeModal}
+ >
+ Batal
+ </button>
+ <button
+ className='bg-red-500 text-white py-1 px-3 rounded'
+ onClick={handleRejectProduct}
+ >
+ Reject
+ </button>
+ </div>
+ </div>
+ </div>
+ )}
+ {transaction?.data?.products?.map((product) => (
<div className='flex justify-end mt-4'>
<div className='w-1/4 grid grid-cols-2 gap-y-3 text-gray_r-12/80'>
<div className='text-right'>Subtotal</div>
@@ -650,33 +850,84 @@ const Transaction = ({ id }) => {
</div>
</div>
</div>
+ ))}
- <div className='text-h-sm font-semibold mt-10 mb-4'>Invoice</div>
- <div className='grid grid-cols-3 gap-4'>
- {transaction.data?.invoices?.map((invoice, index) => (
- <Link href={`/my/invoices/${invoice.id}`} key={index}>
- <div className='shadow rounded-md p-4 text-gray_r-12 font-normal flex justify-between'>
- <div>
- <p className='mb-2'>{invoice?.name}</p>
- <div className='flex items-center gap-x-1'>
- {invoice.amountResidual > 0 ? (
- <div className='badge-red'>Belum Lunas</div>
- ) : (
- <div className='badge-green'>Lunas</div>
- )}
- <p className='text-caption-2 text-gray_r-11'>
- {currencyFormat(invoice.amountTotal)}
- </p>
- </div>
- </div>
- <ChevronRightIcon className='w-5 stroke-2' />
- </div>
- </Link>
- ))}
- </div>
- {transaction.data?.invoices?.length === 0 && (
- <div className='badge-red text-sm'>Belum ada invoice</div>
+
+
+ {transaction?.data?.productsRejectLine.length > 0 && (
+ <div className='text-h-sm font-semibold mt-10 mb-4'>
+ Rincian Produk Reject
+ </div>
+ )}
+ {transaction?.data?.productsRejectLine.length > 0 && (
+ <table className='table-data'>
+ <thead>
+ <tr>
+ <th>Nama Produk</th>
+ {/* <th>Diskon</th> */}
+ <th>Jumlah</th>
+ <th>Harga</th>
+ <th>Subtotal</th>
+ </tr>
+ </thead>
+ <tbody>
+ {transaction?.data?.productsRejectLine?.map((product) => (
+ <tr key={product.id}>
+ <td className='flex'>
+ <Link
+ href={createSlug(
+ '/shop/product/',
+ product?.parent.name,
+ product?.parent.id
+ )}
+ className='w-[20%] flex-shrink-0'
+ >
+ <Image
+ src={product?.parent?.image}
+ alt={product?.name}
+ className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md'
+ />
+ </Link>
+ <div className='px-2 text-left'>
+ <Link
+ href={createSlug(
+ '/shop/product/',
+ product?.parent.name,
+ product?.parent.id
+ )}
+ className='line-clamp-2 leading-6 !text-gray_r-12 font-normal'
+ >
+ {product?.parent?.name}
+ </Link>
+ <div className='text-gray_r-11 mt-2'>
+ {product?.code}{' '}
+ {product?.attributes.length > 0
+ ? `| ${product?.attributes.join(', ')}`
+ : ''}
+ </div>
+ </div>
+ </td>
+ {/* <td>
+ {product.price.discountPercentage > 0
+ ? `${product.price.discountPercentage}%`
+ : ''}
+ </td> */}
+ <td>{product.quantity}</td>
+ <td>
+ {/* {product.price.discountPercentage > 0 && (
+ <div className='line-through mb-1 text-caption-1 text-gray_r-12/70'>
+ {currencyFormat(product.price.price)}
+ </div>
+ )} */}
+ <div>{currencyFormat(product.price.priceDiscount)}</div>
+ </td>
+ <td>{currencyFormat(product.quantity * product.price.priceDiscount)}</td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
)}
+
</div>
</div>
</DesktopView>
diff --git a/src/lib/transaction/components/Transactions.jsx b/src/lib/transaction/components/Transactions.jsx
index be63effd..92bdd276 100644
--- a/src/lib/transaction/components/Transactions.jsx
+++ b/src/lib/transaction/components/Transactions.jsx
@@ -1,63 +1,163 @@
-import { useRouter } from 'next/router'
-import { useState } from 'react'
-import { toast } from 'react-hot-toast'
-import { EllipsisVerticalIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-hot-toast';
+import {
+ EllipsisVerticalIcon,
+ MagnifyingGlassIcon,
+} from '@heroicons/react/24/outline';
+import useAuth from '@/core/hooks/useAuth';
-import { downloadPurchaseOrder, downloadQuotation } from '../utils/transactions'
-import useTransactions from '../hooks/useTransactions'
-import currencyFormat from '@/core/utils/currencyFormat'
-import cancelTransactionApi from '../api/cancelTransactionApi'
-import TransactionStatusBadge from './TransactionStatusBadge'
-import Spinner from '@/core/components/elements/Spinner/Spinner'
-import Link from '@/core/components/elements/Link/Link'
-import BottomPopup from '@/core/components/elements/Popup/BottomPopup'
-import Pagination from '@/core/components/elements/Pagination/Pagination'
-import { toQuery } from 'lodash-contrib'
-import _ from 'lodash'
-import Alert from '@/core/components/elements/Alert/Alert'
-import MobileView from '@/core/components/views/MobileView'
-import DesktopView from '@/core/components/views/DesktopView'
-import Menu from '@/lib/auth/components/Menu'
+import {
+ downloadPurchaseOrder,
+ downloadQuotation,
+} from '../utils/transactions';
+import useTransactions from '../hooks/useTransactions';
+import currencyFormat from '@/core/utils/currencyFormat';
+import cancelTransactionApi from '../api/cancelTransactionApi';
+import TransactionStatusBadge from './TransactionStatusBadge';
+import Spinner from '@/core/components/elements/Spinner/Spinner';
+import Link from '@/core/components/elements/Link/Link';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import Pagination from '@/core/components/elements/Pagination/Pagination';
+import { toQuery } from 'lodash-contrib';
+import _ from 'lodash';
+import Alert from '@/core/components/elements/Alert/Alert';
+import MobileView from '@/core/components/views/MobileView';
+import DesktopView from '@/core/components/views/DesktopView';
+import Menu from '@/lib/auth/components/Menu';
+import * as XLSX from 'xlsx';
+import getSite from '../api/listSiteApi';
+import transactionsApi from '../api/transactionsApi';
const Transactions = ({ context = '' }) => {
- const router = useRouter()
- const { q = '', page = 1 } = router.query
+ const auth = useAuth();
+ const router = useRouter();
+ const { q = '', page = 1, site = null } = router.query;
- const limit = 15
+ const limit = 15;
+
+ const [inputQuery, setInputQuery] = useState(q);
+ const [toOthers, setToOthers] = useState(null);
+ const [toCancel, setToCancel] = useState(null);
+ const [listSites, setListSites] = useState([]);
+
+ const [siteFilter, setSiteFilter] = useState(site);
const query = {
name: q,
offset: (page - 1) * limit,
context,
- limit
- }
- const { transactions } = useTransactions({ query })
+ limit,
+ site:
+ siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null),
+ };
+
+ const { transactions } = useTransactions({ query });
- const [inputQuery, setInputQuery] = useState(q)
- const [toOthers, setToOthers] = useState(null)
- const [toCancel, setToCancel] = useState(null)
+ const fetchSite = async () => {
+ const site = await getSite();
+ setListSites(site.sites);
+ };
const submitCancelTransaction = async () => {
const isCancelled = await cancelTransactionApi({
- transaction: toCancel
- })
+ transaction: toCancel,
+ });
if (isCancelled) {
- toast.success('Berhasil batalkan transaksi')
- transactions.refetch()
+ toast.success('Berhasil batalkan transaksi');
+ transactions.refetch();
}
- setToCancel(null)
- }
+ setToCancel(null);
+ };
- const pageCount = Math.ceil(transactions?.data?.saleOrderTotal / limit)
- let pageQuery = _.omit(query, ['limit', 'offset', 'context'])
- pageQuery = _.pickBy(pageQuery, _.identity)
- pageQuery = toQuery(pageQuery)
+ const pageCount = Math.ceil(transactions?.data?.saleOrderTotal / limit);
+ let pageQuery = _.omit(query, ['limit', 'offset', 'context']);
+ pageQuery = _.pickBy(
+ pageQuery,
+ (value, key) => value !== '' && !(key === 'page' && value === '1')
+ );
+ pageQuery = toQuery(pageQuery);
const handleSubmit = (e) => {
- e.preventDefault()
- router.push(`${router.pathname}?q=${inputQuery}`)
- }
+ e.preventDefault();
+ const queryParams = {};
+ if (inputQuery) queryParams.q = inputQuery;
+ if (siteFilter) queryParams.site = siteFilter;
+ router.push({
+ pathname: router.pathname,
+ query: queryParams,
+ });
+ };
+
+ const handleSiteFilterChange = (e) => {
+ setSiteFilter(e.target.value);
+ const queryParams = {};
+ if (inputQuery) queryParams.q = inputQuery;
+ if (e.target.value) queryParams.site = e.target.value;
+ router.push({
+ pathname: router.pathname,
+ query: queryParams,
+ });
+ };
+
+ const exportToExcel = (data, siteFilter) => {
+ const fieldsToExport = [
+ 'No. Transaksi',
+ 'No. PO',
+ 'Tanggal',
+ 'Created By',
+ 'Salesperson',
+ 'Total',
+ 'Status',
+ ];
+ const rowsToExport = [];
+
+ data.forEach((saleOrder) => {
+ const row = {
+ 'No. Transaksi': saleOrder.name,
+ 'No. PO': saleOrder.purchaseOrderName || '-',
+ Tanggal: saleOrder.dateOrder || '-',
+ 'Created By': saleOrder.address.customer?.name || '-',
+ Salesperson: saleOrder.sales,
+ Total: currencyFormat(saleOrder.amountTotal),
+ Status: saleOrder.status,
+ };
+ if (siteFilter) {
+ row['Site'] = siteFilter;
+ }
+ rowsToExport.push(row);
+ });
+ const worksheet = XLSX.utils.json_to_sheet(rowsToExport, {
+ header: fieldsToExport,
+ });
+
+ const workbook = XLSX.utils.book_new();
+ XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
+ XLSX.writeFile(workbook, 'transactions.xlsx');
+ };
+
+ const getAllData = async () => {
+ const query = {
+ name: q,
+ context,
+ site:
+ siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null),
+ };
+ const queryString = toQuery(query)
+ const data = await transactionsApi({ query: queryString });
+ return data;
+ };
+
+ const handleExportExcel = async () => {
+ const dataToExport = await getAllData();
+
+ exportToExcel(dataToExport?.saleOrders, siteFilter);
+ };
+
+ useEffect(() => {
+ fetchSite();
+ }, []);
return (
<>
<MobileView>
@@ -81,17 +181,23 @@ const Transactions = ({ context = '' }) => {
</div>
)}
- {!transactions.isLoading && transactions.data?.saleOrders?.length === 0 && (
- <Alert type='info' className='text-center'>
- Tidak ada transaksi
- </Alert>
- )}
+ {!transactions.isLoading &&
+ transactions.data?.saleOrders?.length === 0 && (
+ <Alert type='info' className='text-center'>
+ Tidak ada transaksi
+ </Alert>
+ )}
{transactions.data?.saleOrders?.map((saleOrder, index) => (
- <div className='p-4 shadow border border-gray_r-3 rounded-md' key={index}>
+ <div
+ className='p-4 shadow border border-gray_r-3 rounded-md'
+ key={index}
+ >
<div className='grid grid-cols-2'>
<Link href={`${router.pathname}/${saleOrder.id}`}>
- <span className='text-caption-2 text-gray_r-11'>No. Transaksi</span>
+ <span className='text-caption-2 text-gray_r-11'>
+ No. Transaksi
+ </span>
<h2 className='text-danger-500 mt-1'>{saleOrder.name}</h2>
</Link>
<div className='flex gap-x-1 justify-end'>
@@ -105,13 +211,17 @@ const Transactions = ({ context = '' }) => {
<Link href={`${router.pathname}/${saleOrder.id}`}>
<div className='grid grid-cols-2 mt-3'>
<div>
- <span className='text-caption-2 text-gray_r-11'>No. Purchase Order</span>
+ <span className='text-caption-2 text-gray_r-11'>
+ No. Purchase Order
+ </span>
<p className='mt-1 font-medium text-gray_r-12'>
{saleOrder.purchaseOrderName || '-'}
</p>
</div>
<div className='text-right'>
- <span className='text-caption-2 text-gray_r-11'>Total Invoice</span>
+ <span className='text-caption-2 text-gray_r-11'>
+ Total Invoice
+ </span>
<p className='mt-1 font-medium text-gray_r-12'>
{saleOrder.invoiceCount} Invoice
</p>
@@ -120,10 +230,14 @@ const Transactions = ({ context = '' }) => {
<div className='grid grid-cols-2 mt-3'>
<div>
<span className='text-caption-2 text-gray_r-11'>Sales</span>
- <p className='mt-1 font-medium text-gray_r-12'>{saleOrder.sales}</p>
+ <p className='mt-1 font-medium text-gray_r-12'>
+ {saleOrder.sales}
+ </p>
</div>
<div className='text-right'>
- <span className='text-caption-2 text-gray_r-11'>Total Harga</span>
+ <span className='text-caption-2 text-gray_r-11'>
+ Total Harga
+ </span>
<p className='mt-1 font-medium text-gray_r-12'>
{currencyFormat(saleOrder.amountTotal)}
</p>
@@ -140,14 +254,18 @@ const Transactions = ({ context = '' }) => {
className='mt-2 mb-2'
/>
- <BottomPopup title='Lainnya' active={toOthers} close={() => setToOthers(null)}>
+ <BottomPopup
+ title='Lainnya'
+ active={toOthers}
+ close={() => setToOthers(null)}
+ >
<div className='flex flex-col gap-y-4 mt-2'>
<button
className='text-left disabled:opacity-60'
disabled={!toOthers?.purchaseOrderFile}
onClick={() => {
- downloadPurchaseOrder(toOthers)
- setToOthers(null)
+ downloadPurchaseOrder(toOthers);
+ setToOthers(null);
}}
>
Download PO
@@ -156,8 +274,8 @@ const Transactions = ({ context = '' }) => {
className='text-left disabled:opacity-60'
disabled={toOthers?.status != 'draft'}
onClick={() => {
- downloadQuotation(toOthers)
- setToOthers(null)
+ downloadQuotation(toOthers);
+ setToOthers(null);
}}
>
Download Quotation
@@ -166,8 +284,8 @@ const Transactions = ({ context = '' }) => {
className='text-left disabled:opacity-60'
disabled={toOthers?.status != 'waiting'}
onClick={() => {
- setToCancel(toOthers)
- setToOthers(null)
+ setToCancel(toOthers);
+ setToOthers(null);
}}
>
Batalkan Transaksi
@@ -175,7 +293,11 @@ const Transactions = ({ context = '' }) => {
</div>
</BottomPopup>
- <BottomPopup active={toCancel} close={() => setToCancel(null)} title='Batalkan Transaksi'>
+ <BottomPopup
+ active={toCancel}
+ close={() => setToCancel(null)}
+ title='Batalkan Transaksi'
+ >
<div className='leading-7 text-gray_r-12/80'>
Apakah anda yakin membatalkan transaksi{' '}
<span className='underline'>{toCancel?.name}</span>?
@@ -188,7 +310,11 @@ const Transactions = ({ context = '' }) => {
>
Ya, Batalkan
</button>
- <button className='btn-light flex-1' type='button' onClick={() => setToCancel(null)}>
+ <button
+ className='btn-light flex-1'
+ type='button'
+ onClick={() => setToCancel(null)}
+ >
Batal
</button>
</div>
@@ -205,21 +331,50 @@ const Transactions = ({ context = '' }) => {
<div className='flex mb-6 items-center justify-between'>
<h1 className='text-title-sm font-semibold'>
Daftar Transaksi{' '}
- {transactions?.data?.saleOrders ? `(${transactions?.data?.saleOrders.length})` : ''}
+ {transactions?.data?.saleOrders
+ ? `(${transactions?.data?.saleOrders.length})`
+ : ''}
</h1>
- <form className='flex gap-x-2' onSubmit={handleSubmit}>
- <input
- type='text'
- className='form-input'
- placeholder='Cari Transaksi...'
- value={inputQuery}
- onChange={(e) => setInputQuery(e.target.value)}
- />
- <button className='btn-light bg-transparent px-3' type='submit'>
- <MagnifyingGlassIcon className='w-6' />
- </button>
- </form>
+ <div className='grid grid-cols-2 gap-2'>
+ {listSites?.length > 0 ? (
+ <select
+ value={siteFilter}
+ onChange={handleSiteFilterChange}
+ className='form-input'
+ >
+ <option value=''>Pilih Site</option>
+ {listSites.map((site) => (
+ <option value={site} key={site}>
+ {site}
+ </option>
+ ))}
+ </select>
+ ) : (<div></div>)}
+
+ <form className='flex gap-x-1' onSubmit={handleSubmit}>
+ <input
+ type='text'
+ className='form-input'
+ placeholder='Cari Transaksi...'
+ value={inputQuery}
+ onChange={(e) => setInputQuery(e.target.value)}
+ />
+ <button
+ className='btn-light bg-transparent px-3'
+ type='submit'
+ >
+ <MagnifyingGlassIcon className='w-6' />
+ </button>
+ </form>
+ </div>
</div>
+ <button
+ onClick={handleExportExcel}
+ type='button'
+ className='btn-solid-red px-3 py-2 mr-auto mb-2'
+ >
+ <span>Download</span>
+ </button>
<table className='table-data'>
<thead>
<tr>
@@ -227,6 +382,9 @@ const Transactions = ({ context = '' }) => {
<th>No. PO</th>
<th>Tanggal</th>
<th>Created By</th>
+ {auth?.feature?.soApproval && (
+ <th>Site</th>
+ )}
<th className='!text-left'>Salesperson</th>
<th className='!text-left'>Total</th>
<th>Status</th>
@@ -252,13 +410,23 @@ const Transactions = ({ context = '' }) => {
{transactions.data?.saleOrders?.map((saleOrder) => (
<tr key={saleOrder.id}>
<td>
- <Link className='whitespace-nowrap' href={`${router.pathname}/${saleOrder.id}`}>{saleOrder.name}</Link>
+ <Link
+ className='whitespace-nowrap'
+ href={`${router.pathname}/${saleOrder.id}`}
+ >
+ {saleOrder.name}
+ </Link>
</td>
<td>{saleOrder.purchaseOrderName || '-'}</td>
<td>{saleOrder.dateOrder || '-'}</td>
<td>{saleOrder.address.customer?.name || '-'}</td>
+ {auth?.feature?.soApproval && (
+ <td>{saleOrder.sitePartner || '-'}</td>
+ )}
<td className='!text-left'>{saleOrder.sales}</td>
- <td className='!text-left'>{currencyFormat(saleOrder.amountTotal)}</td>
+ <td className='!text-left'>
+ {currencyFormat(saleOrder.amountTotal)}
+ </td>
<td>
<div className='flex justify-center'>
<TransactionStatusBadge status={saleOrder.status} />
@@ -272,14 +440,14 @@ const Transactions = ({ context = '' }) => {
<Pagination
pageCount={pageCount}
currentPage={parseInt(page)}
- url={router.pathname + pageQuery}
+ url={router.pathname + (pageQuery ? `?${pageQuery}` : '')}
className='mt-2 mb-2'
/>
</div>
</div>
</DesktopView>
</>
- )
-}
+ );
+};
-export default Transactions
+export default Transactions;
diff --git a/src/lib/variant/components/VariantCard.jsx b/src/lib/variant/components/VariantCard.jsx
index 9f1b5733..68cdf54f 100644
--- a/src/lib/variant/components/VariantCard.jsx
+++ b/src/lib/variant/components/VariantCard.jsx
@@ -1,15 +1,28 @@
import { useRouter } from 'next/router'
import { toast } from 'react-hot-toast'
-
+import useAuth from '@/core/hooks/useAuth';
import Image from '@/core/components/elements/Image/Image'
import Link from '@/core/components/elements/Link/Link'
import { createSlug } from '@/core/utils/slug'
import currencyFormat from '@/core/utils/currencyFormat'
import { updateItemCart } from '@/core/utils/cart'
import whatsappUrl from '@/core/utils/whatsappUrl'
+import {useState } from 'react';
+import rejectProductApi from '../../../lib/transaction/api/rejectProductApi'
+// import {useTransaction} from 'C:\Users\Indoteknik\next-indoteknik\src\lib\transaction\hooks\useTransaction.js'
+import useTransaction from '../../../lib/transaction/hooks/useTransaction';
+import ImageNext from 'next/image';
const VariantCard = ({ product, openOnClick = true, buyMore = false }) => {
const router = useRouter()
+ const id = router.query.id
+ const auth = useAuth();
+ const { transaction } = useTransaction({id});
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [selectedProduct, setSelectedProduct] = useState(null);
+ const [reason, setReason] = useState('');
+
+
const addItemToCart = () => {
toast.success('Berhasil menambahkan ke keranjang', { duration: 1500 })
@@ -19,19 +32,78 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => {
})
return
}
-
+
const checkoutItem = () => {
router.push(`/shop/checkout?product_id=${product.id}&qty=${product.quantity}`)
}
-
+
+ const openModal = (product) => {
+ setSelectedProduct(product);
+ setIsModalOpen(true);
+ };
+
+ const closeModal = () => {
+ setIsModalOpen(false);
+ setSelectedProduct(null);
+ setReason('');
+ };
+
+ const handleRejectProduct = async () => {
+ try {
+ if (!reason.trim()) {
+ toast.error('Masukkan alasan terlebih dahulu');
+ return;
+ }else{
+ let idSo = transaction?.data.id
+ let idProduct = selectedProduct.id
+ await rejectProductApi({ idSo, idProduct, reason});
+ closeModal();
+ toast.success("Produk berhasil di reject")
+ setTimeout(() => {
+ window.location.reload();
+ }, 1500);
+ }
+ } catch (error) {
+ toast.error('Gagal reject produk. Silakan coba lagi.');
+ }
+ };
+
const Card = () => (
<div className='flex gap-x-3'>
<div className='w-4/12 flex items-center gap-x-2'>
- <Image
+
+ <div className="relative">
+ <Image
src={product.parent.image}
alt={product.parent.name}
className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md'
/>
+ <div className="absolute top-0 right-4 flex mt-3">
+ <div className="gambarB ">
+ {product.isSni && (
+ <ImageNext
+ src="/images/sni-logo.png"
+ alt="SNI Logo"
+ className="w-2 h-5 object-contain object-top sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ <div className="gambarC ">
+ {product.isTkdn && (
+ <ImageNext
+ src="/images/TKDN.png"
+ alt="TKDN"
+ className="w-5 h-6 object-contain object-top ml-1 mr-1 sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ </div>
+ </div>
+
</div>
<div className='w-8/12 flex flex-col'>
<p className='product-card__title wrap-line-ellipsis-2'>{product.parent.name}</p>
@@ -82,7 +154,7 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => {
<Link href={createSlug('/shop/product/', product.parent.name, product.parent.id)}>
<Card />
</Link>
- {buyMore && (
+ {buyMore && (!transaction?.data?.productsRejectLine.some(pr => pr.id === product.id)) && (
<div className='flex justify-end gap-x-2 mb-2'>
<button
type='button'
@@ -91,14 +163,48 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => {
>
Tambah Keranjang
</button>
- <button
- type='button'
- onClick={checkoutItem}
- className='btn-solid-red py-2 px-3 text-caption-1'
- >
- Beli Lagi
- </button>
+ {/* {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (transaction.data.isReaject == false) && ( */}
+ {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (router.asPath.includes("/my/quotations/")) && (
+ !transaction?.data?.productsRejectLine.some(pr => pr.id === product.id) && (
+ <button
+ className="bg-red-500 text-white py-1 px-3 rounded"
+ onClick={() => openModal(product)}
+ >
+ Reject
+ </button>
+ )
+ )}
+ {isModalOpen && (
+ <div className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center'>
+ <div className='bg-white p-4 rounded w-96
+ ease-in-out opacity-100
+ transform transition-transform duration-300 scale-100'>
+ <h2 className='text-lg mb-2'>Berikan Alasan</h2>
+ <textarea
+ value={reason}
+ onChange={(e) => setReason(e.target.value)}
+ className='w-full p-2 border rounded'
+ rows='4'
+ ></textarea>
+ <div className='mt-4 flex justify-end'>
+ <button
+ className='bg-gray-300 text-black py-1 px-3 rounded mr-2'
+ onClick={closeModal}
+ >
+ Batal
+ </button>
+ <button
+ className='bg-red-500 text-white py-1 px-3 rounded'
+ onClick={handleRejectProduct}
+ >
+ Reject
+ </button>
+ </div>
+ </div>
+ </div>
+ )}
</div>
+
)}
</>
)
diff --git a/src/pages/api/shop/generate-recomendation.js b/src/pages/api/shop/generate-recomendation.js
new file mode 100644
index 00000000..dce8ae72
--- /dev/null
+++ b/src/pages/api/shop/generate-recomendation.js
@@ -0,0 +1,64 @@
+import { productMappingSolr } from '@/utils/solrMapping'
+import axios from 'axios';
+import camelcaseObjectDeep from 'camelcase-object-deep';
+
+export default async function handler(req, res) {
+ const { q = null, op = 'AND' } = req.query
+
+ if (!q) {
+ return res.status(422).json({ error: 'parameter missing' })
+ }
+
+ /*let parameter = [
+ `q=${escapeSolrQuery(q)}`,
+ `q.op=${op}`,
+ `indent=true`,
+ `fq=-publish_b:false`,
+ `qf=name_s^2 description_s`,
+ `facetch=true`,
+ `fq=price_tier1_v2_f:[1 TO *]`,
+ `rows=10`,
+ `sort=product_rating_f DESC, price_discount_f DESC`,
+ ];
+ let result = await axios(
+ process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&')
+ );*/
+ let parameter = [
+ `q=${q}`,
+ `q.op=${op}`,
+ `debugQuery=on`,
+ `defType=edismax`,
+ `df=display_name_s`,
+ `fq=-publish_b:false`,
+ `rows=5`,
+ ];
+ if(op == 'AND'){
+ parameter.push(`sort=product_rating_f DESC, price_discount_f DESC`);
+ parameter.push(`rows=1`);
+ }
+
+ let result = await axios(
+ process.env.SOLR_HOST + '/solr/recommendation/select?' + parameter.join('&')
+ );
+ try {
+ result.data = camelcaseObjectDeep(result.data)
+ res.status(200).json(result.data)
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+}
+
+const escapeSolrQuery = (query) => {
+ if (query == '*') return query;
+
+ const specialChars = /([\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g;
+ const words = query.split(/\s+/);
+ const escapedWords = words.map((word) => {
+ if (specialChars.test(word)) {
+ return `"${word.replace(specialChars, '\\$1')}"`;
+ }
+ return word;
+ });
+
+ return escapedWords.join(' ');
+};
diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js
index 10575d37..b6b8c795 100644
--- a/src/pages/api/shop/search.js
+++ b/src/pages/api/shop/search.js
@@ -77,14 +77,14 @@ export default async function handler(req, res) {
parameter.push(
`fq=${brand
.split(',')
- .map((manufacturer) => `manufacture_name:"${manufacturer}"`)
+ .map((manufacturer) => `manufacture_name:"${encodeURIComponent(manufacturer)}"`)
.join(' OR ')}`
);
if (category)
parameter.push(
`fq=${category
.split(',')
- .map((cat) => `category_name:"${cat}"`)
+ .map((cat) => `category_name:"${encodeURIComponent(cat)}"`)
.join(' OR ')}`
);
// if (category) parameter.push(`fq=category_name:${capitalizeFirstLetter(category.replace(/,/g, ' OR '))}`)
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
index c097530c..8af963fb 100644
--- a/src/pages/index.jsx
+++ b/src/pages/index.jsx
@@ -11,6 +11,8 @@ import { FlashSaleSkeleton } from '@/lib/flashSale/skeleton/FlashSaleSkeleton';
import PreferredBrandSkeleton from '@/lib/home/components/Skeleton/PreferredBrandSkeleton';
import PromotinProgram from '@/lib/promotinProgram/components/HomePage';
import PagePopupIformation from '~/modules/popup-information';
+import useProductDetail from '~/modules/product-detail/stores/useProductDetail';
+import { getAuth } from '~/libs/auth';
const BasicLayout = dynamic(() =>
import('@/core/components/layouts/BasicLayout')
@@ -41,6 +43,11 @@ const FlashSale = dynamic(
loading: () => <FlashSaleSkeleton />,
}
);
+
+const ProgramPromotion = dynamic(() =>
+ import('@/lib/home/components/PromotionProgram')
+);
+
const BannerSection = dynamic(() =>
import('@/lib/home/components/BannerSection')
);
@@ -56,6 +63,8 @@ export default function Home() {
const bannerRef = useRef(null);
const wrapperRef = useRef(null);
+ const auth = getAuth();
+
const handleOnLoad = () => {
wrapperRef.current.style.height =
bannerRef.current?.querySelector(':first-child')?.clientHeight + 'px';
@@ -97,12 +106,16 @@ 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 />
<div id='flashsale'>
<PreferredBrand />
</div>
- <FlashSale />
+ {!auth?.feature?.soApproval && (
+ <>
+ <ProgramPromotion /> <FlashSale />
+ </>
+ )}
<PromotinProgram />
<CategoryHomeId />
<BannerSection />
@@ -124,9 +137,16 @@ export default function Home() {
<PreferredBrand />
</div>
</DelayRender>
- <DelayRender renderAfter={600}>
- <FlashSale />
- </DelayRender>
+ {!auth?.feature?.soApproval && (
+ <>
+ <DelayRender renderAfter={400}>
+ <ProgramPromotion />
+ </DelayRender>
+ <DelayRender renderAfter={600}>
+ <FlashSale />
+ </DelayRender>
+ </>
+ )}
<DelayRender renderAfter={600}>
<PromotinProgram />
</DelayRender>
diff --git a/src/pages/my/recomendation/api/recomendation.js b/src/pages/my/recomendation/api/recomendation.js
new file mode 100644
index 00000000..8ff760d0
--- /dev/null
+++ b/src/pages/my/recomendation/api/recomendation.js
@@ -0,0 +1,17 @@
+import axios from 'axios';
+import { useQuery } from 'react-query';
+
+const GenerateRecomendations = ({ query }) => {
+ const queryString = _.toQuery(query);
+ const GenerateRecomendationProducts = async () =>
+ await axios(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/recomendation?${queryString}`
+ );
+ const productSearch = useQuery(
+ `generateRecomendation-${ququeryStringery}`,
+ GenerateRecomendationProducts
+ );
+
+ return productSearch;
+};
+export default GenerateRecomendations;
diff --git a/src/pages/my/recomendation/components/products-recomendatison.jsx b/src/pages/my/recomendation/components/products-recomendatison.jsx
new file mode 100644
index 00000000..d39d2a99
--- /dev/null
+++ b/src/pages/my/recomendation/components/products-recomendatison.jsx
@@ -0,0 +1,477 @@
+import Menu from '@/lib/auth/components/Menu';
+import { useEffect, useState } from 'react';
+import * as XLSX from 'xlsx';
+import GenerateRecomendations from '../api/recomendation';
+import axios from 'axios';
+import { Button, Link } from '@chakra-ui/react';
+import Image from 'next/image';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import formatCurrency from '~/libs/formatCurrency';
+
+const exportToExcel = (data) => {
+ const worksheet = XLSX.utils.json_to_sheet(data);
+ const workbook = XLSX.utils.book_new();
+ XLSX.utils.book_append_sheet(workbook, worksheet, 'Results');
+
+ // Generate Excel file and trigger download in the browser
+ XLSX.writeFile(workbook, 'ProductRecommendations.xlsx');
+};
+
+const ProductsRecomendation = ({ id }) => {
+ const [excelData, setExcelData] = useState(null);
+ const [products, setProducts] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isOpen, setIsOpen] = useState(false);
+ const [variantsOpen, setVariantsOpen] = useState([]);
+ const [otherRec, setOtherRec] = useState(false);
+
+ const mappingProducts = async ({ index, product, result, variants }) => {
+ const resultMapping = {
+ index: index,
+ product: product,
+ result: {
+ id: result?.id || '-',
+ name: result?.nameS || '-',
+ code: result?.defaultCodeS || '-',
+ },
+ };
+
+ return resultMapping;
+ };
+
+ const searchRecomendation = async ({ product, index, operator = 'AND' }) => {
+ let variants = [];
+ let resultMapping = {};
+ const searchProduct = await axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/generate-recomendation?q=${product}&op=${operator}`
+ );
+
+ if (operator === 'AND') {
+ const result =
+ searchProduct.data.response.numFound > 0
+ ? searchProduct.data.response.products[0]
+ : null;
+
+ if (result?.variantTotal > 1) {
+ const searchVariants = await axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/product-detail?id=${result.id}`
+ );
+ variants = searchVariants.data[0].variants;
+ }
+
+ resultMapping = await mappingProducts({
+ index,
+ product,
+ result,
+ variants,
+ });
+ } else {
+ const result =
+ searchProduct.data.response.numFound > 0
+ ? searchProduct.data.response.products
+ : null;
+
+ result.map((item) => {
+ if (item.variantTotal > 1) {
+ const searchVariants = axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/product-detail?id=${item.id}`
+ );
+ variants = searchVariants.data[0].variants;
+ }
+ });
+
+ console.log('ini result', searchProduct.data.response);
+ }
+
+ return resultMapping;
+ };
+
+ const handleSubmit = async (e) => {
+ setIsLoading(true);
+ e.preventDefault();
+ if (excelData) {
+ const results = await Promise.all(
+ excelData.map(async (row, i) => {
+ const index = i + 1;
+ const product = row['product'];
+ return await generateProductRecomendation({ product, index });
+ })
+ );
+
+ const formattedResults = results.map((result) => {
+ const formattedResult = { product: result.product };
+ for (let i = 0; i <= 5; i++) {
+ formattedResult[`recomendation product ${i + 1} - code`] = result.result[i] == null ? '-' : result.result[i]?.code;
+ formattedResult[`recomendation product ${i + 1} - name`] = result.result[i] == null ? '-' : result.result[i]?.name ;
+ }
+ return formattedResult;
+ });
+
+ exportToExcel(formattedResults);
+ setProducts(results);
+ setIsLoading(false);
+ } else {
+ setIsLoading(false);
+ console.log('No excel data available');
+ }
+ };
+
+ const handleFileChange = (e) => {
+ setIsLoading(true);
+ const file = e.target.files[0];
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ const data = new Uint8Array(event.target.result);
+ const workbook = XLSX.read(data, { type: 'array' });
+
+ const firstSheetName = workbook.SheetNames[0];
+ const worksheet = workbook.Sheets[firstSheetName];
+ const jsonData = XLSX.utils.sheet_to_json(worksheet);
+
+ setExcelData(jsonData);
+ console.log('ini json data', jsonData);
+
+ setIsLoading(false);
+ };
+ reader.readAsArrayBuffer(file);
+ };
+
+ const handleVariantsOpen = ({ variants }) => {
+ setVariantsOpen(variants);
+ setIsOpen(true);
+ };
+ const hadnliChooseVariants = ({ id, variant }) => {
+ let foundIndex = products.findIndex((item) => item.result.id === id);
+ if (foundIndex !== -1) {
+ products[foundIndex].result.code = variant?.code;
+ products[foundIndex].result.name = variant?.name;
+ } else {
+ console.log('Data not found.');
+ }
+ setIsOpen(false);
+ };
+
+ const handlingOtherRec = ({ product }) => {
+ console.log('ini product', product);
+ const result = async () =>
+ await searchRecomendation({ product, index: 0, operator: 'OR' });
+
+ result();
+ };
+
+ const generateProductRecomendation = async ({ product, index }) => {
+ let variants = [];
+ let resultMapping = {};
+ const searchProduct = await axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/generate-recomendation?q=${product}&op=AND`
+ );
+ const searchProductOR = await axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/generate-recomendation?q=${product}&op=OR`
+ );
+ const resultAND =
+ searchProduct.data.response.numFound > 0
+ ? searchProduct.data.response.docs[0]
+ : null; // hasil satu
+ const resultOR =
+ searchProductOR.data.response.numFound > 0
+ ? searchProductOR.data.response.docs
+ : []; // hasil 5
+
+ resultMapping = {
+ index: index,
+ product: product,
+ result: {},
+ };
+
+ // Add resultAND to resultMapping if it exists
+ resultMapping.result[0] = resultAND
+ ? {
+ id: resultAND?.id || '-',
+ name: resultAND?.nameS || '-',
+ code: resultAND?.defaultCodeS || '-',
+ }
+ : null;
+
+ // Add resultOR to resultMapping
+ if (resultOR.length > 0) {
+ resultOR.forEach((item, idx) => {
+ resultMapping.result[idx + 1] = {
+ id: item?.id || '-',
+ name: item?.nameS || '-',
+ code: item?.defaultCodeS || '-',
+ };
+ });
+ } else {
+ for (let i = 0; i <= 5; i++) {
+ resultMapping.result[i + 1] = null;
+ }
+ }
+ return resultMapping;
+ };
+ return (
+ <>
+ <BottomPopup
+ active={isOpen}
+ close={() => setIsOpen(false)}
+ className='w-full md:!w-[60%]'
+ title='List Variants'
+ >
+ <div className='container'>
+ <table className='table-data'>
+ <thead>
+ <tr>
+ <th>Part Number</th>
+ <th>Variants </th>
+ <th>Harga </th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {variantsOpen?.map((variant, index) => (
+ <tr key={index}>
+ <td>{variant.code}</td>
+ <td>{variant.attributes.join(', ') || '-'}</td>
+ <td>
+ {variant.price.discountPercentage > 0 && (
+ <div className='flex items-center gap-x-1'>
+ <div className={style['disc-badge']}>
+ {Math.floor(variant.price.discountPercentage)}%
+ </div>
+ <div className={style['disc-price']}>
+ Rp {formatCurrency(variant.price.price)}
+ </div>
+ </div>
+ )}
+ {variant.price.priceDiscount > 0 &&
+ `Rp ${formatCurrency(variant.price.priceDiscount)}`}
+ {variant.price.priceDiscount === 0 && '-'}
+ </td>
+ <td>
+ <Button
+ size='sm'
+ w='100%'
+ onClick={() =>
+ hadnliChooseVariants({
+ id: variant.parent.id,
+ variant: variant,
+ })
+ }
+ >
+ Pilih
+ </Button>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ </BottomPopup>
+ <BottomPopup
+ active={otherRec}
+ close={() => setOtherRec(false)}
+ className='w-full md:!w-[60%]'
+ title='Other Recomendations'
+ >
+ <div className='container'>
+ <table className='table-data'>
+ <thead>
+ <tr>
+ <th>Product</th>
+ <th>Item Code </th>
+ <th>Description</th>
+ <th>Brand</th>
+ <th>Price</th>
+ <th>Image</th>
+ </tr>
+ </thead>
+ <tbody>
+ {variantsOpen?.map((variant, index) => (
+ <tr key={index}>
+ <td>{variant.code}</td>
+ <td>{variant.attributes.join(', ') || '-'}</td>
+ <td>
+ {variant.price.discountPercentage > 0 && (
+ <div className='flex items-center gap-x-1'>
+ <div className={style['disc-badge']}>
+ {Math.floor(variant.price.discountPercentage)}%
+ </div>
+ <div className={style['disc-price']}>
+ Rp {formatCurrency(variant.price.price)}
+ </div>
+ </div>
+ )}
+ {variant.price.priceDiscount > 0 &&
+ `Rp ${formatCurrency(variant.price.priceDiscount)}`}
+ {variant.price.priceDiscount === 0 && '-'}
+ </td>
+ <td>
+ <Button
+ size='sm'
+ w='100%'
+ onClick={() =>
+ hadnliChooseVariants({
+ id: variant.parent.id,
+ variant: variant,
+ })
+ }
+ >
+ Pilih
+ </Button>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ </BottomPopup>
+ <div className='container mx-auto flex py-10'>
+ <div className='w-3/12 pr-4'>
+ <Menu />
+ </div>
+ <div className='w-9/12 p-4 bg-white border border-gray_r-6 rounded'>
+ <div className='flex mb-6 items-center justify-between'>
+ <h1 className='text-title-sm font-semibold'>
+ Generate Recomendation
+ </h1>
+ </div>
+ <div className='group'>
+ <h1 className='text-sm font-semibold'>Contoh Excel</h1>
+ <table className='table-data'>
+ <thead>
+ <tr>
+ <th>Product</th>
+ <th>Qty</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>Tekiro Long Nose Pliers Tang Lancip</td>
+ <td>10</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div className='container mx-auto mt-8'>
+ <div className='mb-4'>
+ <label htmlFor='excelFile' className='text-sm font-semibold'>
+ Upload Excel File (.xlsx)
+ </label>
+ <input
+ type='file'
+ id='excelFile'
+ accept='.xlsx'
+ onChange={handleFileChange}
+ className='mt-1 p-2 block w-full border border-gray-300 rounded-md focus:outline-none focus:border-blue-500'
+ />
+ </div>
+ <Button
+ colorScheme='red'
+ w='l'
+ isDisabled={isLoading}
+ onClick={handleSubmit}
+ >
+ Generate
+ </Button>
+ </div>
+ {/* <div className='grup mt-8'>
+ {products && products.length > 0 && (
+ <div className='group'>
+ <table className='table-data'>
+ <thead>
+ <tr>
+ <th>Product</th>
+ <th>Item Code </th>
+ <th>Description</th>
+ <th>Brand</th>
+ <th>Price</th>
+ <th>Image</th>
+ <th>lainnya</th>
+ </tr>
+ </thead>
+ <tbody>
+ {products.map((product, index) => (
+ <tr key={index}>
+ <td>{product?.product}</td>
+ <td>
+ {product?.result?.code === '-' &&
+ product.result.variantTotal > 1 && (
+ <Button
+ border='2px'
+ borderColor='yellow.300'
+ size='sm'
+ onClick={() =>
+ handleVariantsOpen({
+ variants: product?.result?.variants,
+ })
+ }
+ >
+ Lihat Variants
+ </Button>
+ )}
+ {product?.result.code !== '-' &&
+ product?.result.variantTotal > 1 ? (
+ <>
+ {product?.result.code}
+ <Button
+ variant='link'
+ colorScheme='yellow'
+ size='sm'
+ onClick={() =>
+ handleVariantsOpen({
+ variants: product?.result?.variants,
+ })
+ }
+ >
+ Variants lainya
+ </Button>
+ </>
+ ) : (
+ <>{product?.result.code}</>
+ )}
+ </td>
+ <td>{product?.result.name}</td>
+ <td>{product?.result.manufacture}</td>
+ <td>
+ {product?.result.price !== '-'
+ ? `Rp ${formatCurrency(product?.result.price)}`
+ : '-'}
+ </td>
+ <td>
+ {product?.result.image !== '-' ? (
+ <Image
+ src={product?.result.image}
+ width={100}
+ height={100}
+ alt={product?.result.name}
+ />
+ ) : (
+ '-'
+ )}
+ </td>
+ <td>
+ {' '}
+ <Button
+ border='2px'
+ borderColor='red.500'
+ size='sm'
+ onClick={() =>
+ handlingOtherRec({ product: product.product })
+ }
+ >
+ Other
+ </Button>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ )}
+ </div> */}
+ </div>
+ </div>
+ </>
+ );
+};
+
+export default ProductsRecomendation;
diff --git a/src/pages/my/recomendation/index.jsx b/src/pages/my/recomendation/index.jsx
new file mode 100644
index 00000000..684b30c2
--- /dev/null
+++ b/src/pages/my/recomendation/index.jsx
@@ -0,0 +1,26 @@
+import DesktopView from '@/core/components/views/DesktopView';
+import MobileView from '@/core/components/views/MobileView';
+import IsAuth from '../../../lib/auth/components/IsAuth';
+import AppLayout from '@/core/components/layouts/AppLayout';
+import BasicLayout from '@/core/components/layouts/BasicLayout';
+import dynamic from 'next/dynamic';
+import Seo from '@/core/components/Seo'
+
+const ProductsRecomendation = dynamic(() => import('./components/products-recomendatison'))
+export default function MyRecomendation() {
+ return (
+ <IsAuth>
+
+ <Seo title='Dashboard Rekomendasi - Indoteknik.com' />
+
+ <MobileView>
+ <AppLayout></AppLayout>
+ </MobileView>
+ <DesktopView>
+ <BasicLayout>
+ <ProductsRecomendation />
+ </BasicLayout>
+ </DesktopView>
+ </IsAuth>
+ );
+}
diff --git a/src/pages/shop/promo/[slug].tsx b/src/pages/shop/promo/[slug].tsx
new file mode 100644
index 00000000..aaee1249
--- /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}`,0,100);
+ 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}`,0,100);
+ return response2;
+ }else if(combinedQuery){
+ const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`);
+ const product = response.response.docs[0];
+ const product_id = product.id;
+ const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} `,0,100);
+ return response2;
+ } else {
+ const response = await fetchPromoItemsSolr(`id:${item.id}`,0,100);
+ 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.jsx b/src/pages/shop/promo/index.jsx
new file mode 100644
index 00000000..01a11aad
--- /dev/null
+++ b/src/pages/shop/promo/index.jsx
@@ -0,0 +1,40 @@
+import Seo from '@/core/components/Seo'
+import BasicLayout from '@/core/components/layouts/BasicLayout';
+import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react';
+import Link from 'next/link';
+import Promo from '~/pages/shop/promo';
+
+import React from 'react';
+
+const PromoPage = () => {
+ return (
+ <BasicLayout>
+ <Seo title='Promo Indoteknik.com' />
+ <div className='container mx-auto py-4 md:py-6 pb-0'>
+ <Breadcrumb>
+ <BreadcrumbItem>
+ <BreadcrumbLink
+ as={Link}
+ href='/'
+ className='!text-danger-500 whitespace-nowrap'
+ >
+ Home
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+
+ <BreadcrumbItem isCurrentPage>
+ <BreadcrumbLink className='whitespace-nowrap'>
+ Promo
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+ </Breadcrumb>
+
+ <div className='h-10' />
+
+ <Promo />
+ </div>
+ </BasicLayout>
+ );
+};
+
+export default PromoPage;
diff --git a/src/utils/solrMapping.js b/src/utils/solrMapping.js
index 199a7f35..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) {