diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/core/components/elements/Navbar/NavbarUserDropdown.jsx | 5 | ||||
| -rw-r--r-- | src/pages/api/shop/generate-recomendation.js | 64 | ||||
| -rw-r--r-- | src/pages/my/recomendation/api/recomendation.js | 17 | ||||
| -rw-r--r-- | src/pages/my/recomendation/components/products-recomendatison.jsx | 477 | ||||
| -rw-r--r-- | src/pages/my/recomendation/index.jsx | 26 |
5 files changed, 589 insertions, 0 deletions
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> + ); +} |
