summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortrisusilo <tri.susilo@altama.co.id>2024-07-02 07:35:02 +0000
committertrisusilo <tri.susilo@altama.co.id>2024-07-02 07:35:02 +0000
commitf7b024585b70f1bd600ba5e0d26368c532ac9723 (patch)
treefafea81669ea00f824260ecb4a0acc9e1096499f
parent66d787499d0751365c1cda9d79b31e9f3c3c28bc (diff)
parente8ad23dbad5e96dddcd6b10bdc46400c6721e80b (diff)
Merged in feature/generate_recomendation (pull request #148)
Feature/generate recomendation
-rw-r--r--package.json1
-rw-r--r--src/core/components/elements/Navbar/NavbarUserDropdown.jsx5
-rw-r--r--src/pages/api/shop/generate-recomendation.js64
-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
6 files changed, 590 insertions, 0 deletions
diff --git a/package.json b/package.json
index 054a30e4..28fbc5d8 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
"swiper": "^8.4.4",
"tw-merge": "^0.0.1-alpha.3",
"usehooks-ts": "^2.9.1",
+ "xlsx": "^0.18.5",
"xmlbuilder": "^15.1.1",
"yup": "^0.32.11",
"zod": "^3.22.4",
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/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/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>
+ );
+}