From 764920d4caa59ebae08756f9bcefe6992e0890ea Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 30 Aug 2025 08:26:46 +0700 Subject: Fix quotation expedition mobile --- src/lib/checkout/components/SectionQuotationExpedition.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/checkout/components/SectionQuotationExpedition.jsx b/src/lib/checkout/components/SectionQuotationExpedition.jsx index b8ea04ef..f043d257 100644 --- a/src/lib/checkout/components/SectionQuotationExpedition.jsx +++ b/src/lib/checkout/components/SectionQuotationExpedition.jsx @@ -239,7 +239,7 @@ export default function SectionExpeditionQuotation({ products }) {
Pilih Ekspedisi:
-
+
setOnFocuseSelectedCourier(!onFocusSelectedCourier)} -- cgit v1.2.3 From 1a23498f8d75a97eb59fa48e55dd529e22f042b7 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 30 Aug 2025 08:46:40 +0700 Subject: Fix quotation expedition mobile --- .../checkout/components/SectionQuotationExpedition.jsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/lib/checkout/components/SectionQuotationExpedition.jsx b/src/lib/checkout/components/SectionQuotationExpedition.jsx index f043d257..817cd21b 100644 --- a/src/lib/checkout/components/SectionQuotationExpedition.jsx +++ b/src/lib/checkout/components/SectionQuotationExpedition.jsx @@ -253,7 +253,10 @@ export default function SectionExpeditionQuotation({ products }) { )}
{onFocusSelectedCourier && ( -
+
{!isLoading ? ( <>
- Mohon maaf, pengiriman hanya tersedia untuk self pickup karena ada barang - yang belum memiliki berat. Silakan hubungi admin via{' '} + Mohon maaf, pengiriman hanya tersedia untuk self pickup karena ada + barang yang belum memiliki berat. Silakan hubungi admin via{' '}
Tipe Layanan Ekspedisi:
-
+
setIsOpen(!isOpen)} @@ -331,11 +334,13 @@ export default function SectionExpeditionQuotation({ products }) {
) : ( - Pilih layanan pengiriman + + Pilih layanan pengiriman + )}
{isOpen && ( -
+
{serviceOptions.map((service) => (
Date: Sat, 30 Aug 2025 09:52:41 +0700 Subject: Done apply filter without button --- .../product/components/ProductFilterDesktop.jsx | 348 ++++++++++++--------- src/pages/api/shop/search.js | 40 ++- 2 files changed, 225 insertions(+), 163 deletions(-) diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx index d2ecb4d9..440e1795 100644 --- a/src/lib/product/components/ProductFilterDesktop.jsx +++ b/src/lib/product/components/ProductFilterDesktop.jsx @@ -1,7 +1,6 @@ import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import _ from 'lodash'; -import { toQuery } from 'lodash-contrib'; import { Accordion, AccordionButton, @@ -9,7 +8,6 @@ import { AccordionItem, AccordionPanel, Box, - Button, Checkbox, Input, InputGroup, @@ -17,136 +15,200 @@ import { Stack, VStack, } from '@chakra-ui/react'; -import Image from '@/core/components/elements/Image/Image'; import { formatCurrency } from '@/core/utils/formatValue'; const ProductFilterDesktop = ({ - brands, - categories, + brands, // bisa [{id,name,qty}] atau [{brand,qty}] + categories, // [{name, qty}] prefixUrl, - defaultBrand = null, }) => { const router = useRouter(); - const { query } = router; - const [order, setOrder] = useState(query?.orderBy); - const [brandValues, setBrand] = useState(query?.brand?.split(',') || []); + + const [order, setOrder] = useState(router.query?.orderBy); + const [brandValues, setBrand] = useState( + typeof router.query?.brand === 'string' && router.query.brand + ? router.query.brand.split(',').filter(Boolean) + : [] + ); const [categoryValues, setCategory] = useState( - query?.category?.split(',') || [] + typeof router.query?.category === 'string' && router.query.category + ? router.query.category.split(',').filter(Boolean) + : [] ); - const [priceFrom, setPriceFrom] = useState(query?.priceFrom); - const [priceTo, setPriceTo] = useState(query?.priceTo); - const [stock, setStock] = useState(query?.stock); + const [priceFrom, setPriceFrom] = useState(router.query?.priceFrom ?? ''); + const [priceTo, setPriceTo] = useState(router.query?.priceTo ?? ''); + const [stock, setStock] = useState(router.query?.stock ?? null); const [activeRange, setActiveRange] = useState(null); - const [activeIndeces, setActiveIndeces] = useState([]); + + const handlePriceKeyDown = (e) => { + if (e.key !== 'Enter') return; + e.preventDefault(); + // keluar dari preset kalau user input manual + setActiveRange(null); + + // pakai state terkini untuk apply + const fromVal = priceFrom === '' ? '' : String(priceFrom); + const toVal = priceTo === '' ? '' : String(priceTo); + + applyFilters({ priceFrom: fromVal, priceTo: toVal }); + }; + + // --- normalisasi data brand agar tahan banting --- + const normBrands = useMemo(() => { + return (brands ?? []) + .map((b, i) => ({ + id: String(b.id ?? b.val ?? b.brand ?? i), + name: String(b.name ?? b.brand ?? b.label ?? b.val ?? '').trim(), + qty: b.qty ?? b.count, + })) + .filter((b) => b.name); + }, [brands]); const priceRange = [ - { - priceFrom: 100000, - priceTo: 200000, - }, - { - priceFrom: 200000, - priceTo: 300000, - }, - { - priceFrom: 300000, - priceTo: 400000, - }, - { - priceFrom: 400000, - priceTo: 500000, - }, + { priceFrom: 100000, priceTo: 200000 }, + { priceFrom: 200000, priceTo: 300000 }, + { priceFrom: 300000, priceTo: 400000 }, + { priceFrom: 400000, priceTo: 500000 }, ]; - const indexRange = priceRange.findIndex((range) => { - return ( - range.priceFrom === parseInt(priceFrom) && - range.priceTo == parseInt(priceTo) - ); - }); - - const handleCategoriesChange = (event) => { - const value = event.target.value; - const isChecked = event.target.checked; - if (isChecked) { - setCategory([...categoryValues, value]); - } else { - setCategory(categoryValues.filter((val) => val !== value)); - } - }; - const handleBrandsChange = (event) => { - const value = event.target.value; - const isChecked = event.target.checked; - if (isChecked) { - setBrand([...brandValues, value]); - } else { - setBrand(brandValues.filter((val) => val !== value)); + const indexRange = priceRange.findIndex( + (r) => r.priceFrom === parseInt(priceFrom) && r.priceTo == parseInt(priceTo) + ); + + const applyFilters = (changes = {}) => { + const params = new URLSearchParams(); + + // 1) salin SEMUA param yang ada sekarang (jangan hilangkan apapun) + Object.entries(router.query).forEach(([k, v]) => { + if (v == null) return; + if (Array.isArray(v)) { + // penting: fq bisa multi-value; gunakan append, bukan join + v.forEach((item) => params.append(k, String(item))); + } else { + params.set(k, String(v)); + } + }); + + // 2) baca nilai dasar langsung dari URL (hindari state stale) + const arr = (val) => + typeof val === 'string' && val ? val.split(',').filter(Boolean) : []; + + const nextBrand = + 'brandValues' in changes ? changes.brandValues : arr(router.query.brand); + const nextCategory = + 'categoryValues' in changes + ? changes.categoryValues + : arr(router.query.category); + const nextPriceFrom = + 'priceFrom' in changes ? changes.priceFrom : router.query.priceFrom ?? ''; + const nextPriceTo = + 'priceTo' in changes ? changes.priceTo : router.query.priceTo ?? ''; + const nextStock = + 'stock' in changes ? changes.stock : router.query.stock ?? null; + const nextOrder = + 'order' in changes ? changes.order : router.query.orderBy ?? ''; + + const setOrDel = (key, val) => { + const empty = + val == null || val === '' || (Array.isArray(val) && val.length === 0); + if (empty) params.delete(key); + else params.set(key, Array.isArray(val) ? val.join(',') : String(val)); + }; + + setOrDel('brand', nextBrand); + setOrDel('category', nextCategory); + setOrDel('priceFrom', nextPriceFrom); + setOrDel('priceTo', nextPriceTo); + setOrDel('stock', nextStock); + setOrDel('orderBy', nextOrder); + + // 3) kalau ada perubahan filter utama → reset page ke 1 + const changedKeys = Object.keys(changes); + const touched = [ + 'brandValues', + 'categoryValues', + 'priceFrom', + 'priceTo', + 'stock', + 'order', + ]; + if (changedKeys.some((k) => touched.includes(k))) { + params.set('page', '1'); } + + // 4) shallow replace (tanpa reload penuh) + const base = router.asPath.split('?')[0]; + router.replace(`${base}?${params.toString()}`, undefined, { + shallow: true, + scroll: false, + }); }; - const handleReadyStockChange = (event) => { - const value = event.target.value; - const isChecked = event.target.checked; - if (isChecked) { - setStock(value); - } else { - setStock(null); - } + // debounce untuk input harga (biar nggak spam) + const debouncedApply = useMemo(() => _.debounce(applyFilters, 350), []); // eslint-disable-line + useEffect(() => () => debouncedApply.cancel(), [debouncedApply]); + + // === handlers === + const handleCategoriesChange = (e) => { + const { value, checked } = e.target; + const next = checked + ? [...categoryValues, value] + : categoryValues.filter((v) => v !== value); + setCategory(next); + applyFilters({ categoryValues: next }); }; - const handlePriceFromChange = async (priceFromr, priceTor, index) => { - await setPriceFrom(priceFromr); - await setPriceTo(priceTor); - setActiveRange(index); + const handleBrandsChange = (e) => { + const { value, checked } = e.target; // value = brand ID/name (string) + const next = checked + ? [...brandValues, value] + : brandValues.filter((v) => v !== value); + setBrand(next); + applyFilters({ brandValues: next }); }; - const handleSubmit = () => { - let params = { - penawaran: router.query.penawaran, - q: router.query.q, - orderBy: order, - brand: brandValues.join(','), - category: categoryValues.join(','), - priceFrom, - priceTo, - stock: stock, - }; - params = _.pickBy(params, _.identity); - params = toQuery(params); + const handleReadyStockChange = (e) => { + const { checked, value } = e.target; + const next = checked ? value : null; + setStock(next); + applyFilters({ stock: next }); + }; - const slug = Array.isArray(router.query.slug) - ? router.query.slug[0] - : router.query.slug; + const handlePriceRangeClick = async (pf, pt, idx) => { + await setPriceFrom(pf); + await setPriceTo(pt); + setActiveRange(idx); + applyFilters({ priceFrom: pf, priceTo: pt }); + }; - if (slug) { - if (prefixUrl.includes('category') || prefixUrl.includes('lob')) { - router.push(`${prefixUrl}?${params}`); - } else { - router.push(`${prefixUrl}/${slug}?${params}`); - } - } else { - router.push(`${prefixUrl}?${params}`); - } + const onPriceFromInput = (e) => { + setPriceFrom(e.target.value); }; - /*const handleIndexAccordion = async () => { - if (brandValues) { - await setActiveIndeces([...activeIndeces, 0]) - } - if (categoryValues) { - await setActiveIndeces([...activeIndeces, !router.pathname.includes('brands') ? 1 : 0]) - } - if (priceRange) { - await setActiveIndeces([...activeIndeces, !router.pathname.includes('brands') ? 2 : 1]) - } - if (stock) { - await setActiveIndeces([...activeIndeces, !router.pathname.includes('brands') ? 3 : 2]) - } - }*/ + const onPriceToInput = (e) => { + setPriceTo(e.target.value); + }; useEffect(() => { setActiveRange(indexRange); - }, []); + }, []); // init active range + + useEffect(() => { + setBrand( + router.query?.brand + ? String(router.query.brand).split(',').filter(Boolean) + : [] + ); + setCategory( + router.query?.category + ? String(router.query.category).split(',').filter(Boolean) + : [] + ); + setPriceFrom(router.query?.priceFrom ?? ''); + setPriceTo(router.query?.priceTo ?? ''); + setStock(router.query?.stock ?? null); + setOrder(router.query?.orderBy ?? ''); + }, [router.query]); return ( <> @@ -159,23 +221,24 @@ const ProductFilterDesktop = ({ - - - {brands && brands.length > 0 ? ( - brands.map((brand, index) => ( -
+ + {normBrands.length > 0 ? ( + normBrands.map((b) => ( +
- {brand.brand} - - ({brand.qty}) - + {b.name} + {b.qty !== undefined && ( + + ({b.qty}) + + )}
@@ -197,23 +260,20 @@ const ProductFilterDesktop = ({ - - - {categories && categories.length > 0 ? ( - categories.map((category, index) => ( -
+ + {(categories ?? []).length > 0 ? ( + categories.map((c, i) => ( +
- {category.name} - - ({category.qty}) - + {c.name} + ({c.qty})
@@ -234,7 +294,6 @@ const ProductFilterDesktop = ({ - @@ -243,32 +302,34 @@ const ProductFilterDesktop = ({ type='number' placeholder='Harga minimum' value={priceFrom} - onChange={(e) => setPriceFrom(e.target.value)} + onChange={onPriceFromInput} + onKeyDown={handlePriceKeyDown} // ⟵ apply saat Enter /> + Rp setPriceTo(e.target.value)} + onChange={onPriceToInput} + onKeyDown={handlePriceKeyDown} // ⟵ apply saat Enter /> +
- {priceRange.map((price, i) => ( + {priceRange.map((p, i) => ( ))}
@@ -279,27 +340,22 @@ const ProductFilterDesktop = ({ {/* - Ketersedian Stok + Ketersediaan Stok - - Ketersedian Stock + Ready Stock */} - - ); }; diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 3d258a97..8412c8f6 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -19,7 +19,6 @@ export default async function handler(req, res) { source = '', } = req.query; - let { stock = '' } = req.query; let paramOrderBy = ''; switch (orderBy) { @@ -73,7 +72,7 @@ export default async function handler(req, res) { const formattedQuery = `(${newQ .split(' ') - .map((term) => (term.length < 2 ? term : `${term}*`)) // Tambahkan '*' hanya jika panjang kata >= 2 + .map((term) => (term.length < 2 ? term : `${term}*`)) .join(' ')})`; const mm = @@ -90,8 +89,8 @@ export default async function handler(req, res) { ]; if (orderBy === 'stock') { - filterQueries.push('stock_total_f:{1 TO *}&sort=stock_total_f desc'); - // filterQueries.push(`stock_total_f DESC`) + // JANGAN selipkan &sort di sini; sorting sudah di paramOrderBy + filterQueries.push('stock_total_f:{1 TO *}'); } if (fq && source != 'similar' && typeof fq != 'string') { @@ -113,8 +112,9 @@ export default async function handler(req, res) { let offset = (page - 1) * limit; let parameter = [ - 'facet.field=manufacture_name_s', - 'facet.field=category_name', + // === Facet disjunctive: exclude filter brand/cat saat hitung facet === + 'facet.field={!ex=brand}manufacture_name_s', + 'facet.field={!ex=cat}category_name', 'facet=true', 'indent=true', `facet.query=${escapeSolrQuery(q)}`, @@ -145,21 +145,26 @@ export default async function handler(req, res) { if (brand) parameter.push( - `fq=${brand - .split(',') - .map( - (manufacturer) => - `manufacture_name:"${encodeURIComponent(manufacturer)}"` - ) - .join(' OR ')}` + // tag filter brand + `fq=${encodeURIComponent( + `{!tag=brand}manufacture_name_s:(${brand + .split(',') + .map((manufacturer) => `"${manufacturer.replace(/"/g, '\\"')}"`) + .join(' OR ')})` + )}` ); + if (category) parameter.push( - `fq=${category - .split(',') - .map((cat) => `category_name:"${encodeURIComponent(cat)}"`) - .join(' OR ')}` + // tag filter category + `fq=${encodeURIComponent( + `{!tag=cat}category_name:(${category + .split(',') + .map((cat) => `"${cat.replace(/"/g, '\\"')}"`) + .join(' OR ')})` + )}` ); + // if (category) parameter.push(`fq=category_name:${capitalizeFirstLetter(category.replace(/,/g, ' OR '))}`) if (stock) parameter.push(`fq=stock_total_f:{1 TO *}`); @@ -170,6 +175,7 @@ export default async function handler(req, res) { parameter = parameter.concat( fq.map((val) => `fq=${encodeURIComponent(val)}`) ); + let result = await axios( process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&') ); -- cgit v1.2.3 From 4b749c672a7a9b93ee65198f8bec703909711e84 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 30 Aug 2025 10:10:16 +0700 Subject: Fix --- src/pages/api/shop/search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 8412c8f6..a8ce4221 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -166,7 +166,7 @@ export default async function handler(req, res) { ); // if (category) parameter.push(`fq=category_name:${capitalizeFirstLetter(category.replace(/,/g, ' OR '))}`) - if (stock) parameter.push(`fq=stock_total_f:{1 TO *}`); + if (stock) parameter.push(`fq=stock_total_f:{1 TO *]`); // Single fq in url params if (typeof fq === 'string') parameter.push(`fq=${encodeURIComponent(fq)}`); -- cgit v1.2.3 From 7735e705142e9a56f37c90b09ea5e6ba80d2bfa3 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 30 Aug 2025 13:53:33 +0700 Subject: push --- src/pages/api/shop/search.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index a8ce4221..81cc22c5 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -89,8 +89,7 @@ export default async function handler(req, res) { ]; if (orderBy === 'stock') { - // JANGAN selipkan &sort di sini; sorting sudah di paramOrderBy - filterQueries.push('stock_total_f:{1 TO *}'); + filterQueries.push('stock_total_f:[1 TO *]'); } if (fq && source != 'similar' && typeof fq != 'string') { -- cgit v1.2.3 From 932b92af3f3a1a0a62bef60d647c00e1be483265 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 1 Sep 2025 12:05:04 +0700 Subject: Fix Search --- src/pages/api/shop/search.js | 68 ++++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 44 deletions(-) diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 81cc22c5..1760be70 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -48,31 +48,12 @@ export default async function handler(req, res) { break; } - // let suggestWord = null; - // let keywords = q; - // let checkQ = null; - - // if (q != '*') { - // checkQ = q.trim().split(/[\s\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/]+/); - // if (checkQ.length > 1) { - // const dataSearchSuggest = await axios( - // `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/suggest?q=${checkQ[1]}` - // ); - // suggestWord = dataSearchSuggest.data.suggestions[0]; - // } - // if (suggestWord && suggestWord?.term.split(' ').length <= 1) { - // keywords = `"${escapeSolrQuery(checkQ[0] + ' ' + suggestWord?.term)}"`; - // } - // } - - // let newQ = keywords; - let checkQ = q.trim().split(/[\s\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/]+/); let newQ = escapeSolrQuery(q); const formattedQuery = `(${newQ .split(' ') - .map((term) => (term.length < 2 ? term : `${term}*`)) + .map((term) => (term.length < 2 ? term : `${term}*`)) // Tambahkan '*' hanya jika panjang kata >= 2 .join(' ')})`; const mm = @@ -89,7 +70,8 @@ export default async function handler(req, res) { ]; if (orderBy === 'stock') { - filterQueries.push('stock_total_f:[1 TO *]'); + filterQueries.push('stock_total_f:{1 TO *}&sort=stock_total_f desc'); + // filterQueries.push(`stock_total_f DESC`) } if (fq && source != 'similar' && typeof fq != 'string') { @@ -111,7 +93,7 @@ export default async function handler(req, res) { let offset = (page - 1) * limit; let parameter = [ - // === Facet disjunctive: exclude filter brand/cat saat hitung facet === + // === Disjunctive facets: exclude brand & category filters saat hitung facet === 'facet.field={!ex=brand}manufacture_name_s', 'facet.field={!ex=cat}category_name', 'facet=true', @@ -142,30 +124,29 @@ export default async function handler(req, res) { if (auth.feature.onlyReadyStock) stock = true; } - if (brand) - parameter.push( - // tag filter brand - `fq=${encodeURIComponent( - `{!tag=brand}manufacture_name_s:(${brand - .split(',') - .map((manufacturer) => `"${manufacturer.replace(/"/g, '\\"')}"`) - .join(' OR ')})` - )}` - ); + if (brand) { + // bentuk ekspresi sama seperti versi kamu, tapi dibungkus tag brand + const brandExpr = brand + .split(',') + .map( + (manufacturer) => + `manufacture_name:"${encodeURIComponent(manufacturer)}"` + ) + .join(' OR '); + parameter.push(`fq={!tag=brand}(${brandExpr})`); + } - if (category) - parameter.push( - // tag filter category - `fq=${encodeURIComponent( - `{!tag=cat}category_name:(${category - .split(',') - .map((cat) => `"${cat.replace(/"/g, '\\"')}"`) - .join(' OR ')})` - )}` - ); + if (category) { + // sama: tag kategori + const catExpr = category + .split(',') + .map((cat) => `category_name:"${encodeURIComponent(cat)}"`) + .join(' OR '); + parameter.push(`fq={!tag=cat}(${catExpr})`); + } // if (category) parameter.push(`fq=category_name:${capitalizeFirstLetter(category.replace(/,/g, ' OR '))}`) - if (stock) parameter.push(`fq=stock_total_f:{1 TO *]`); + if (stock) parameter.push(`fq=stock_total_f:{1 TO *}`); // Single fq in url params if (typeof fq === 'string') parameter.push(`fq=${encodeURIComponent(fq)}`); @@ -174,7 +155,6 @@ export default async function handler(req, res) { parameter = parameter.concat( fq.map((val) => `fq=${encodeURIComponent(val)}`) ); - let result = await axios( process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&') ); -- cgit v1.2.3