From a5e695f82e03577cc85c4a1dded9f6021f0235fc Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 28 Nov 2025 09:36:15 +0700 Subject: (andri) try get detail product from magento --- src/pages/api/magento-product.ts | 114 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/pages/api/magento-product.ts (limited to 'src') diff --git a/src/pages/api/magento-product.ts b/src/pages/api/magento-product.ts new file mode 100644 index 00000000..c906079e --- /dev/null +++ b/src/pages/api/magento-product.ts @@ -0,0 +1,114 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { sku } = req.query; + + if (!sku) { + return res.status(400).json({ error: 'SKU is required' }); + } + + // Token Magento + const token = 'vxrtcjvztv1icgjzsui45de9kmwlz0lf'; + const baseUrl = 'https://pimdev.1211.my.id/rest/V1/products'; + + try { + // 1. Pastikan SKU menjadi string dan hapus spasi kiri/kanan + const cleanSku = String(sku).trim(); + + // 2. Encode SKU + const encodedSku = encodeURIComponent(cleanSku); + + // 3. Bentuk URL Final + const finalUrl = `${baseUrl}/${encodedSku}`; + + // --- DEBUGGING LOG --- + console.log('Fetching URL:', finalUrl); + + // Request ke Product Endpoint + const response = await fetch(finalUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + }); + + if (!response.ok) { + console.error(`Magento Error: ${response.status} ${response.statusText}`); + return res.status(response.status).json({ + error: 'Failed to fetch from Magento', + magentoStatus: response.status, + checkedUrl: finalUrl + }); + } + + const data = await response.json(); + + // ===================================================================== + // TAMBAHAN: FETCH LABEL ATRIBUT (z_*) + // ===================================================================== + + let specsWithLabels: any[] = []; + + // Cek apakah ada custom_attributes + if (data.custom_attributes) { + // Filter atribut yang kodenya dimulai dengan 'z' + const zAttributes = data.custom_attributes.filter((attr: any) => + attr.attribute_code.startsWith('z') + ); + + // Fetch detail label untuk setiap atribut secara paralel + specsWithLabels = await Promise.all( + zAttributes.map(async (attr: any) => { + try { + // Endpoint untuk ambil detail atribut (Label): /V1/products/attributes/{attributeCode} + const attrUrl = `${baseUrl}/attributes/${attr.attribute_code}`; + + const attrRes = await fetch(attrUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + } + }); + + if (attrRes.ok) { + const attrData = await attrRes.json(); + // AMBIL NILAI 'default_frontend_label' + return { + label: attrData.default_frontend_label || attr.attribute_code, + value: attr.value + }; + } + } catch (err) { + console.error(`Failed to fetch label for ${attr.attribute_code}`); + } + + // Fallback: Jika gagal ambil label, format manual dari kode + const fallbackLabel = attr.attribute_code + .substring(1).replace(/_/g, ' ').trim(); // z_size_ml -> size ml + + return { + label: fallbackLabel, + value: attr.value + }; + }) + ); + } + + // Gabungkan data asli dengan data specs yang sudah ada labelnya + const responseData = { + ...data, + specs: specsWithLabels // Frontend tinggal pakai field ini + }; + + res.status(200).json(responseData); + + } catch (error) { + console.error('Proxy Server Error:', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +} \ No newline at end of file -- cgit v1.2.3 From 825d86bb50f48f9a21d740d474c0dddee858dffb Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Mon, 1 Dec 2025 11:31:30 +0700 Subject: (andri) show upsells product from magento in similarbottom product --- src/pages/api/magento-product.ts | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/pages/api/magento-product.ts b/src/pages/api/magento-product.ts index c906079e..c494b05d 100644 --- a/src/pages/api/magento-product.ts +++ b/src/pages/api/magento-product.ts @@ -1,3 +1,4 @@ +// pages/api/magento-product.ts import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler( @@ -48,7 +49,7 @@ export default async function handler( const data = await response.json(); // ===================================================================== - // TAMBAHAN: FETCH LABEL ATRIBUT (z_*) + // TAMBAHAN 1: FETCH LABEL ATRIBUT (z_*) // ===================================================================== let specsWithLabels: any[] = []; @@ -56,6 +57,7 @@ export default async function handler( // Cek apakah ada custom_attributes if (data.custom_attributes) { // Filter atribut yang kodenya dimulai dengan 'z' + // FIX: Menghapus filter 'Pakai link' agar MSDS tetap muncul const zAttributes = data.custom_attributes.filter((attr: any) => attr.attribute_code.startsWith('z') ); @@ -64,8 +66,8 @@ export default async function handler( specsWithLabels = await Promise.all( zAttributes.map(async (attr: any) => { try { - // Endpoint untuk ambil detail atribut (Label): /V1/products/attributes/{attributeCode} - const attrUrl = `${baseUrl}/attributes/${attr.attribute_code}`; + // Endpoint untuk ambil detail atribut (Label) + const attrUrl = `https://pimdev.1211.my.id/rest/V1/products/attributes/${attr.attribute_code}`; const attrRes = await fetch(attrUrl, { method: 'GET', @@ -77,8 +79,8 @@ export default async function handler( if (attrRes.ok) { const attrData = await attrRes.json(); - // AMBIL NILAI 'default_frontend_label' return { + code: attr.attribute_code, // FIX: Kirim code agar bisa dideteksi frontend (z_doc_) label: attrData.default_frontend_label || attr.attribute_code, value: attr.value }; @@ -87,11 +89,12 @@ export default async function handler( console.error(`Failed to fetch label for ${attr.attribute_code}`); } - // Fallback: Jika gagal ambil label, format manual dari kode + // Fallback: Format manual const fallbackLabel = attr.attribute_code - .substring(1).replace(/_/g, ' ').trim(); // z_size_ml -> size ml + .substring(1).replace(/_/g, ' ').trim(); return { + code: attr.attribute_code, // FIX: Kirim code di fallback juga label: fallbackLabel, value: attr.value }; @@ -99,10 +102,26 @@ export default async function handler( ); } - // Gabungkan data asli dengan data specs yang sudah ada labelnya + // ===================================================================== + // TAMBAHAN 2: AMBIL UP-SELLS (product_links) + // ===================================================================== + let upsellIds: number[] = []; + + if (data.product_links && Array.isArray(data.product_links)) { + upsellIds = data.product_links + // Filter hanya link type 'upsell' + .filter((link: any) => link.link_type === 'upsell') + // Ambil SKU (yang isinya ID Odoo) dan ubah ke number + .map((link: any) => Number(link.linked_product_sku)); + } + + // ===================================================================== + // RESPONSE GABUNGAN + // ===================================================================== const responseData = { ...data, - specs: specsWithLabels // Frontend tinggal pakai field ini + specs: specsWithLabels, // Data Spesifikasi (z_*) + upsell_ids: upsellIds // Data Upsell ID (product_links) }; res.status(200).json(responseData); -- cgit v1.2.3 From 219c61c5c14e3a8dfed3d7158d59d11c476e3586 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Wed, 3 Dec 2025 11:49:18 +0700 Subject: (andri) fix search for similar product side dan bottom pada detail product --- src/pages/api/magento-product.ts | 29 +++-- src/pages/api/shop/search.js | 230 ++++++++++++++++++--------------------- 2 files changed, 121 insertions(+), 138 deletions(-) (limited to 'src') diff --git a/src/pages/api/magento-product.ts b/src/pages/api/magento-product.ts index c494b05d..32dd0e5c 100644 --- a/src/pages/api/magento-product.ts +++ b/src/pages/api/magento-product.ts @@ -54,19 +54,14 @@ export default async function handler( let specsWithLabels: any[] = []; - // Cek apakah ada custom_attributes if (data.custom_attributes) { - // Filter atribut yang kodenya dimulai dengan 'z' - // FIX: Menghapus filter 'Pakai link' agar MSDS tetap muncul const zAttributes = data.custom_attributes.filter((attr: any) => attr.attribute_code.startsWith('z') ); - // Fetch detail label untuk setiap atribut secara paralel specsWithLabels = await Promise.all( zAttributes.map(async (attr: any) => { try { - // Endpoint untuk ambil detail atribut (Label) const attrUrl = `https://pimdev.1211.my.id/rest/V1/products/attributes/${attr.attribute_code}`; const attrRes = await fetch(attrUrl, { @@ -80,7 +75,7 @@ export default async function handler( if (attrRes.ok) { const attrData = await attrRes.json(); return { - code: attr.attribute_code, // FIX: Kirim code agar bisa dideteksi frontend (z_doc_) + code: attr.attribute_code, label: attrData.default_frontend_label || attr.attribute_code, value: attr.value }; @@ -89,12 +84,11 @@ export default async function handler( console.error(`Failed to fetch label for ${attr.attribute_code}`); } - // Fallback: Format manual const fallbackLabel = attr.attribute_code .substring(1).replace(/_/g, ' ').trim(); return { - code: attr.attribute_code, // FIX: Kirim code di fallback juga + code: attr.attribute_code, label: fallbackLabel, value: attr.value }; @@ -103,15 +97,24 @@ export default async function handler( } // ===================================================================== - // TAMBAHAN 2: AMBIL UP-SELLS (product_links) + // TAMBAHAN 2: AMBIL UP-SELLS (product_links type = upsell) // ===================================================================== let upsellIds: number[] = []; if (data.product_links && Array.isArray(data.product_links)) { upsellIds = data.product_links - // Filter hanya link type 'upsell' .filter((link: any) => link.link_type === 'upsell') - // Ambil SKU (yang isinya ID Odoo) dan ubah ke number + .map((link: any) => Number(link.linked_product_sku)); + } + + // ===================================================================== + // TAMBAHAN 3: AMBIL RELATED PRODUCTS (product_links type = related) + // ===================================================================== + let relatedIds: number[] = []; + + if (data.product_links && Array.isArray(data.product_links)) { + relatedIds = data.product_links + .filter((link: any) => link.link_type === 'related') .map((link: any) => Number(link.linked_product_sku)); } @@ -121,10 +124,12 @@ export default async function handler( const responseData = { ...data, specs: specsWithLabels, // Data Spesifikasi (z_*) - upsell_ids: upsellIds // Data Upsell ID (product_links) + upsell_ids: upsellIds, // Data Upsell ID + related_ids: relatedIds // Data Related ID (BARU) }; res.status(200).json(responseData); + console.log(responseData); } catch (error) { console.error('Proxy Server Error:', error); diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 42d16100..7d4adfcb 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -12,7 +12,7 @@ export default async function handler(req, res) { priceTo = 0, orderBy = '', operation = 'AND', - fq = '', + fq = '', // bisa berupa string atau array limit = 30, source = '', } = req.query; @@ -20,107 +20,119 @@ export default async function handler(req, res) { let { stock = '' } = req.query; // ============================================================ - // SITEMAP + // LOGIC KHUSUS UPSELL (Simple & Direct) // ============================================================ - if (source === 'sitemap') { + if (source === 'upsell') { try { - const offset = (page - 1) * limit; + // Ambil fq dari query (format: product_id_i:(...)) + // Pastikan fq adalah string tunggal + let fqUpsell = Array.isArray(fq) ? fq.join(' OR ') : fq; + fqUpsell = decodeURIComponent(fqUpsell); const parameter = [ 'q=*:*', `rows=${limit}`, - `start=${offset}`, - 'fl=product_id_i,name_s,default_code_s,image_s,category_name', 'wt=json', - 'omitHeader=true', + 'indent=true', + 'defType=edismax', + // Filter Query khusus Upsell + `fq=${encodeURIComponent(fqUpsell)}`, + // Tetap filter yang publish & ada harga agar produk valid + `fq=${encodeURIComponent('-publish_b:false')}`, + `fq=${encodeURIComponent('price_tier1_v2_f:[1 TO *]')}` ]; - // const parameter = [ - // 'q=*:*', - // `rows=${limit}`, - // `start=${offset}`, - - // // ❌ EXCLUDE PROMOTION - // 'fq=-(name_s:*promotion* OR display_name_s:*promotion* OR variants_name_t:*promotion*)', - - // // ❌ EXCLUDE DUMMY PRODUCT - // 'fq=-(name_s:*dummy* OR display_name_s:*dummy* OR variants_name_t:*dummy* OR default_code_s:A.*)', + // PENTING: SEARCH DI CORE 'VARIANTS' + const solrUrl = process.env.SOLR_HOST + '/solr/variants/select?' + parameter.join('&'); - // 'fl=product_id_i,name_s,default_code_s,image_s,category_name', - // 'wt=json', - // 'omitHeader=true', - // ]; + const result = await axios(solrUrl); - const solrUrl = - process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&'); - - // console.log('[SITEMAP SOLR QUERY]', solrUrl); - - const result = await axios(solrUrl, { timeout: 25000 }); - - // mapping seperti biasa - result.data.response.products = productMappingSolr( + // 1. Mapping dasar + const mappedProducts = productMappingSolr( result.data.response.docs, false ); - delete result.data.response.docs; + // 2. FIX URL LINK: Override ID Varian dengan Template ID + const rawDocs = result.data.response.docs; + + const fixedProducts = mappedProducts.map((p, index) => { + const raw = rawDocs[index]; + if (raw && raw.template_id_i) { + return { + ...p, + id: raw.template_id_i, // Ganti ID Varian jadi ID Template agar link valid + variantId: raw.product_id_i + }; + } + return p; + }); + + const finalResponse = { + ...result.data, + response: { + ...result.data.response, + products: fixedProducts + } + }; + + delete finalResponse.response.docs; + const camelCasedData = camelcaseObjectDeep(finalResponse); + + return res.status(200).json(camelCasedData); - result.data = camelcaseObjectDeep(result.data); + } catch (e) { + console.error('[UPSELL ERROR]', e.response?.data || e.message); + return res.status(200).json({ response: { products: [], numFound: 0 } }); + } + } + // ============================================================ + // SITEMAP (Biarkan tetap sama) + // ============================================================ + if (source === 'sitemap') { + try { + const offset = (page - 1) * limit; + const parameter = [ + 'q=*:*', + `rows=${limit}`, + `start=${offset}`, + 'fl=product_id_i,name_s,default_code_s,image_s,category_name', + 'wt=json', + 'omitHeader=true', + ]; + const solrUrl = process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&'); + const result = await axios(solrUrl, { timeout: 25000 }); + result.data.response.products = productMappingSolr(result.data.response.docs, false); + delete result.data.response.docs; + result.data = camelcaseObjectDeep(result.data); return res.status(200).json(result.data); } catch (e) { - console.error('[SITEMAP ERROR]', e); return res.status(500).json({ error: 'Sitemap query failed' }); } } // ============================================================ - // SEARCH NORMAL + // SEARCH NORMAL (LOGIKA LAMA) // ============================================================ let paramOrderBy = ''; switch (orderBy) { - case 'flashsale-discount-desc': - paramOrderBy += 'flashsale_discount_f DESC'; - break; - case 'price-asc': - paramOrderBy += 'price_tier1_v2_f ASC'; - break; - case 'price-desc': - paramOrderBy += 'price_tier1_v2_f DESC'; - break; - case 'popular': - paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,'; - break; - case 'popular-weekly': - paramOrderBy += 'search_rank_weekly_i DESC'; - break; - case 'stock': - paramOrderBy += 'product_rating_f DESC, stock_total_f DESC'; - break; - case 'flashsale-price-asc': - paramOrderBy += 'flashsale_price_f ASC'; - break; - default: - paramOrderBy += ''; - break; + case 'flashsale-discount-desc': paramOrderBy += 'flashsale_discount_f DESC'; break; + case 'price-asc': paramOrderBy += 'price_tier1_v2_f ASC'; break; + case 'price-desc': paramOrderBy += 'price_tier1_v2_f DESC'; break; + case 'popular': paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,'; break; + case 'popular-weekly': paramOrderBy += 'search_rank_weekly_i DESC'; break; + case 'stock': paramOrderBy += 'product_rating_f DESC, stock_total_f DESC'; break; + case 'flashsale-price-asc': paramOrderBy += 'flashsale_price_f ASC'; break; + default: paramOrderBy += ''; break; } let checkQ = q.trim().split(/[\s\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/]+/); let newQ = escapeSolrQuery(q); - const formattedQuery = `(${newQ - .split(' ') - .map((term) => (term.length < 2 ? term : `${term}*`)) - .join(' ')})`; - - const mm = - checkQ.length > 2 - ? checkQ.length > 5 - ? '55%' - : '85%' - : `${checkQ.length}`; + const formattedQuery = `(${newQ.split(' ').map((term) => (term.length < 2 ? term : `${term}*`)).join(' ')})`; + const mm = checkQ.length > 2 ? (checkQ.length > 5 ? '55%' : '85%') : `${checkQ.length}`; const filterQueries = [ '-publish_b:false', @@ -128,23 +140,19 @@ export default async function handler(req, res) { 'price_tier1_v2_f:[1 TO *]', ]; - if (orderBy === 'stock') { - filterQueries.push('stock_total_f:[1 TO *]'); - } + if (orderBy === 'stock') filterQueries.push('stock_total_f:[1 TO *]'); - if (fq && source != 'similar' && typeof fq != 'string') { - fq.push(...filterQueries); + // Handle 'fq' parameter from request + let finalFq = [...filterQueries]; + if (fq) { + if (Array.isArray(fq)) finalFq.push(...fq); + else finalFq.push(fq); } - const fq_ = filterQueries.join(' AND '); - let keywords = newQ; if (source === 'similar' || checkQ.length < 3) { - if (checkQ.length < 2 || checkQ[1].length < 2) { - keywords = newQ; - } else { - keywords = newQ + '*'; - } + if (checkQ.length < 2 || checkQ[1].length < 2) keywords = newQ; + else keywords = newQ + '*'; } else { keywords = formattedQuery; } @@ -164,16 +172,16 @@ export default async function handler(req, res) { `start=${parseInt(offset)}`, `rows=${limit}`, `sort=${paramOrderBy}`, - `fq=${encodeURIComponent(fq_)}`, `mm=${encodeURIComponent(mm)}`, ]; + // Masukkan semua Filter Query (fq) + finalFq.forEach(f => { + parameter.push(`fq=${encodeURIComponent(f)}`); + }); + if (priceFrom > 0 || priceTo > 0) { - parameter.push( - `fq=price_tier1_v2_f:[${priceFrom == '' ? '*' : priceFrom} TO ${ - priceTo == '' ? '*' : priceTo - }]` - ); + parameter.push(`fq=price_tier1_v2_f:[${priceFrom || '*'} TO ${priceTo || '*'}]`); } let { auth } = req.cookies; @@ -183,49 +191,28 @@ export default async function handler(req, res) { } if (brand) { - const brandExpr = brand - .split(',') - .map( - (manufacturer) => - `manufacture_name:"${encodeURIComponent(manufacturer)}"` - ) - .join(' OR '); + const brandExpr = brand.split(',').map(m => `manufacture_name:"${encodeURIComponent(m)}"`).join(' OR '); parameter.push(`fq={!tag=brand}(${brandExpr})`); } if (category) { - const catExpr = category - .split(',') - .map((cat) => `category_name:"${encodeURIComponent(cat)}"`) - .join(' OR '); + const catExpr = category.split(',').map(c => `category_name:"${encodeURIComponent(c)}"`).join(' OR '); parameter.push(`fq={!tag=cat}(${catExpr})`); } if (stock) parameter.push(`fq=stock_total_f:(1 TO *)`); - if (typeof fq === 'string') parameter.push(`fq=${encodeURIComponent(fq)}`); - - if (Array.isArray(fq)) - parameter = parameter.concat( - fq.map((val) => `fq=${encodeURIComponent(val)}`) - ); - - const solrUrl = - process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&'); - - const result = await axios(solrUrl); + // SEARCH NORMAL: DEFAULT KE CORE 'PRODUCT' + const solrUrl = process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&'); try { + const result = await axios(solrUrl); result.data.response.products = productMappingSolr( result.data.response.docs, auth?.pricelist || false ); - result.data.responseHeader.params.start = parseInt( - result.data.responseHeader.params.start - ); - result.data.responseHeader.params.rows = parseInt( - result.data.responseHeader.params.rows - ); + result.data.responseHeader.params.start = parseInt(result.data.responseHeader.params.start); + result.data.responseHeader.params.rows = parseInt(result.data.responseHeader.params.rows); delete result.data.response.docs; result.data = camelcaseObjectDeep(result.data); res.status(200).json(result.data); @@ -236,17 +223,8 @@ export default async function handler(req, res) { const escapeSolrQuery = (query) => { if (query == '*') return query; - query = query.replace(/-/g, ' '); - 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(' '); -}; + return words.map((word) => specialChars.test(word) ? word.replace(specialChars, '\\$1') : word).join(' '); +}; \ No newline at end of file -- cgit v1.2.3 From 43b2ce4d59c153655eb9b7a2190b83050fd48855 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Wed, 3 Dec 2025 17:06:01 +0700 Subject: (andri) table spek untuk product dengan variant lebih dari 1 --- src/pages/api/magento-product.ts | 186 +++++++++++++++++++-------------------- 1 file changed, 92 insertions(+), 94 deletions(-) (limited to 'src') diff --git a/src/pages/api/magento-product.ts b/src/pages/api/magento-product.ts index 32dd0e5c..297f0ebc 100644 --- a/src/pages/api/magento-product.ts +++ b/src/pages/api/magento-product.ts @@ -5,31 +5,32 @@ export default async function handler( req: NextApiRequest, res: NextApiResponse ) { - const { sku } = req.query; + // Kita terima 'skus' (banyak) dan 'main_sku' (utama/pertama) + const { skus, main_sku } = req.query; - if (!sku) { - return res.status(400).json({ error: 'SKU is required' }); + if (!skus) { + return res.status(400).json({ error: 'SKUs are required' }); } - // Token Magento const token = 'vxrtcjvztv1icgjzsui45de9kmwlz0lf'; - const baseUrl = 'https://pimdev.1211.my.id/rest/V1/products'; + const baseUrl = 'https://pimdev.1211.my.id/rest/V1'; try { - // 1. Pastikan SKU menjadi string dan hapus spasi kiri/kanan - const cleanSku = String(sku).trim(); - - // 2. Encode SKU - const encodedSku = encodeURIComponent(cleanSku); - - // 3. Bentuk URL Final - const finalUrl = `${baseUrl}/${encodedSku}`; + const skuList = String(skus).split(','); // Contoh: ['221', '222', '223'] + const mainSku = String(main_sku || skuList[0]).trim(); // Fallback ke yang pertama - // --- DEBUGGING LOG --- - console.log('Fetching URL:', finalUrl); + // ===================================================================== + // 1. FETCH SEMUA VARIAN SEKALIGUS (Optimasi 'IN' Operator) + // ===================================================================== + const searchParams = new URLSearchParams({ + 'searchCriteria[filter_groups][0][filters][0][field]': 'sku', + 'searchCriteria[filter_groups][0][filters][0][value]': skuList.join(','), + 'searchCriteria[filter_groups][0][filters][0][condition_type]': 'in' + }); - // Request ke Product Endpoint - const response = await fetch(finalUrl, { + const productUrl = `${baseUrl}/products?${searchParams.toString()}`; + + const productResponse = await fetch(productUrl, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -37,102 +38,99 @@ export default async function handler( }, }); - if (!response.ok) { - console.error(`Magento Error: ${response.status} ${response.statusText}`); - return res.status(response.status).json({ - error: 'Failed to fetch from Magento', - magentoStatus: response.status, - checkedUrl: finalUrl - }); + if (!productResponse.ok) { + return res.status(200).json({ specsMatrix: [], upsell_ids: [], related_ids: [] }); } - const data = await response.json(); + const productData = await productResponse.json(); + const items = productData.items || []; + + if (items.length === 0) { + return res.status(200).json({ specsMatrix: [], upsell_ids: [], related_ids: [] }); + } // ===================================================================== - // TAMBAHAN 1: FETCH LABEL ATRIBUT (z_*) + // 2. BUILD SPECS MATRIX + // Kita butuh daftar semua atribut unik (z_*) dari seluruh varian // ===================================================================== - let specsWithLabels: any[] = []; - - if (data.custom_attributes) { - const zAttributes = data.custom_attributes.filter((attr: any) => - attr.attribute_code.startsWith('z') - ); - - specsWithLabels = await Promise.all( - zAttributes.map(async (attr: any) => { - try { - const attrUrl = `https://pimdev.1211.my.id/rest/V1/products/attributes/${attr.attribute_code}`; - - const attrRes = await fetch(attrUrl, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - } - }); - - if (attrRes.ok) { - const attrData = await attrRes.json(); - return { - code: attr.attribute_code, - label: attrData.default_frontend_label || attr.attribute_code, - value: attr.value - }; - } - } catch (err) { - console.error(`Failed to fetch label for ${attr.attribute_code}`); + // Kumpulkan semua kode atribut unik + const allAttributeCodes = new Set(); + items.forEach((p: any) => { + if (p.custom_attributes) { + p.custom_attributes.forEach((attr: any) => { + if (attr.attribute_code.startsWith('z')) { + allAttributeCodes.add(attr.attribute_code); } + }); + } + }); - const fallbackLabel = attr.attribute_code - .substring(1).replace(/_/g, ' ').trim(); + // Fetch Label untuk atribut-atribut tersebut (Sekali jalan) + const labelsMap: Record = {}; + await Promise.all(Array.from(allAttributeCodes).map(async (code) => { + try { + const attrUrl = `${baseUrl}/products/attributes/${code}`; + const res = await fetch(attrUrl, { headers: { 'Authorization': `Bearer ${token}` } }); + if (res.ok) { + const json = await res.json(); + labelsMap[code] = json.default_frontend_label || code; + } + } catch (e) {} + + // Fallback label jika gagal + if (!labelsMap[code]) { + labelsMap[code] = code.substring(1).replace(/_/g, ' ').trim(); + } + })); + + // Susun Matrix + // Struktur: { code, label, values: { [sku]: value } } + const matrix: any[] = []; + Array.from(allAttributeCodes).forEach((code) => { + const row: any = { + code: code, + label: labelsMap[code], + values: {} // Key = SKU/ID Variant, Value = Isi Atribut + }; + + items.forEach((p: any) => { + const attr = p.custom_attributes.find((a: any) => a.attribute_code === code); + // Simpan value berdasarkan SKU (ID Variant dari Odoo) + row.values[p.sku] = attr ? attr.value : '-'; + }); - return { - code: attr.attribute_code, - label: fallbackLabel, - value: attr.value - }; - }) - ); - } + matrix.push(row); + }); // ===================================================================== - // TAMBAHAN 2: AMBIL UP-SELLS (product_links type = upsell) + // 3. AMBIL LINKS (UPSELL & RELATED) DARI MAIN VARIANT SAJA // ===================================================================== - let upsellIds: number[] = []; - - if (data.product_links && Array.isArray(data.product_links)) { - upsellIds = data.product_links - .filter((link: any) => link.link_type === 'upsell') - .map((link: any) => Number(link.linked_product_sku)); - } + // Cari data milik varian utama (varian pertama) + const mainProduct = items.find((p: any) => String(p.sku) === mainSku) || items[0]; - // ===================================================================== - // TAMBAHAN 3: AMBIL RELATED PRODUCTS (product_links type = related) - // ===================================================================== + let upsellIds: number[] = []; let relatedIds: number[] = []; - if (data.product_links && Array.isArray(data.product_links)) { - relatedIds = data.product_links - .filter((link: any) => link.link_type === 'related') - .map((link: any) => Number(link.linked_product_sku)); + if (mainProduct && mainProduct.product_links) { + mainProduct.product_links.forEach((link: any) => { + if (link.link_type === 'upsell') { + upsellIds.push(Number(link.linked_product_sku)); + } else if (link.link_type === 'related') { + relatedIds.push(Number(link.linked_product_sku)); + } + }); } - // ===================================================================== - // RESPONSE GABUNGAN - // ===================================================================== - const responseData = { - ...data, - specs: specsWithLabels, // Data Spesifikasi (z_*) - upsell_ids: upsellIds, // Data Upsell ID - related_ids: relatedIds // Data Related ID (BARU) - }; - - res.status(200).json(responseData); - console.log(responseData); + // Response + res.status(200).json({ + specsMatrix: matrix, + upsell_ids: upsellIds, + related_ids: relatedIds + }); } catch (error) { - console.error('Proxy Server Error:', error); + console.error('Proxy Error:', error); res.status(500).json({ error: 'Internal Server Error' }); } } \ No newline at end of file -- cgit v1.2.3 From f77a4beee4e668b26f69a50e318a74ca8dcbbf33 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Thu, 11 Dec 2025 16:23:00 +0700 Subject: (andri) fix mapping solr --- src/utils/solrMapping.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/utils/solrMapping.js b/src/utils/solrMapping.js index 33f0cbaf..419e0c61 100644 --- a/src/utils/solrMapping.js +++ b/src/utils/solrMapping.js @@ -127,6 +127,8 @@ export const variantsMappingSolr = (parent, products, pricelist) => { variantTotal: product.variant_total_i || 0, stockTotal: product.stock_total_f || 0, weight: product.weight_f || 0, + attribute_set_id: product.attribute_set_id_i || 0, + attribute_set_name: product.attribute_set_name_s || '', manufacture: {}, parent: {}, qtySold: product?.qty_sold_f || 0, -- cgit v1.2.3 From 31853dc731c6e4105c9cf9bd373c63e6e989caa4 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Tue, 16 Dec 2025 14:35:33 +0700 Subject: (andri) try compare with data --- src/pages/api/shop/search.js | 94 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 7d4adfcb..90841e09 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -2,6 +2,14 @@ import { productMappingSolr } from '@/utils/solrMapping'; import axios from 'axios'; import camelcaseObjectDeep from 'camelcase-object-deep'; +const escapeSolrQuery = (query) => { + if (query == '*') return query; + query = query.replace(/-/g, ' '); + const specialChars = /([\+\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g; + const words = query.split(/\s+/); + return words.map((word) => specialChars.test(word) ? word.replace(specialChars, '\\$1') : word).join(' '); +}; + export default async function handler(req, res) { const { q = '*', @@ -19,6 +27,82 @@ export default async function handler(req, res) { let { stock = '' } = req.query; + // ============================================================ + // [BARU] 1. LOGIC KHUSUS COMPARE (Wajib ditaruh paling atas) + // ============================================================ + if (source === 'compare') { + try { + let qCompare = q === '*' ? '*:*' : q; + + // Sanitasi Query + if (qCompare !== '*:*') { + const escaped = escapeSolrQuery(qCompare); + qCompare = `*${escaped}*`; + } + + // Susun Parameter Solr + const parameter = [ + `q=${encodeURIComponent(qCompare)}`, + `rows=${limit}`, + 'wt=json', + 'indent=true', + 'defType=edismax', + + // Grouping agar varian tidak banjir (per template) + 'group=true', + 'group.field=template_id_i', + 'group.limit=1', + 'group.main=true', + + // Field Wajib (Perhatikan: kita butuh product_id_i/default_code_s) + 'fl=id,display_name_s,default_code_s,image_s,price_tier1_v2_f,attribute_set_id_i,attribute_set_name_s,template_id_i,product_id_i', + + // Filter Dasar + 'fq=-publish_b:false', + 'fq=price_tier1_v2_f:[1 TO *]' + ]; + + // Logic Locking (Filter Attribute Set ID dari Frontend) + // Frontend akan mengirim fq="attribute_set_id_i:9" + if (fq) { + if (Array.isArray(fq)) { + fq.forEach(f => parameter.push(`fq=${encodeURIComponent(f)}`)); + } else { + parameter.push(`fq=${encodeURIComponent(fq)}`); + } + } + + // Target Core: VARIANTS (Karena compare butuh data spesifik) + const solrUrl = process.env.SOLR_HOST + '/solr/variants/select?' + parameter.join('&'); + + const result = await axios(solrUrl); + + // Mapping Result + const mappedProducts = productMappingSolr( + result.data.response.docs, + false + ); + + const finalResponse = { + ...result.data, + response: { + ...result.data.response, + products: mappedProducts + } + }; + + delete finalResponse.response.docs; + const camelCasedData = camelcaseObjectDeep(finalResponse); + + return res.status(200).json(camelCasedData); + + } catch (e) { + console.error('[COMPARE SEARCH ERROR]', e.message); + // Return JSON valid meski kosong, agar frontend tidak error syntax + return res.status(200).json({ response: { products: [], numFound: 0 } }); + } + } + // ============================================================ // LOGIC KHUSUS UPSELL (Simple & Direct) // ============================================================ @@ -219,12 +303,4 @@ export default async function handler(req, res) { } catch (error) { res.status(400).json({ error: error.message }); } -} - -const escapeSolrQuery = (query) => { - if (query == '*') return query; - query = query.replace(/-/g, ' '); - const specialChars = /([\+\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g; - const words = query.split(/\s+/); - return words.map((word) => specialChars.test(word) ? word.replace(specialChars, '\\$1') : word).join(' '); -}; \ No newline at end of file +} \ No newline at end of file -- cgit v1.2.3 From 1876d142f492714c37efdc1eabb72709917a0c1d Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 19 Dec 2025 15:20:31 +0700 Subject: (andri) searching comparable --- src/pages/api/shop/search.js | 106 +++++++++++++++++++++++++------------------ src/utils/solrMapping.js | 1 + 2 files changed, 62 insertions(+), 45 deletions(-) (limited to 'src') diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 90841e09..5ea6a70a 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -28,54 +28,67 @@ export default async function handler(req, res) { let { stock = '' } = req.query; // ============================================================ - // [BARU] 1. LOGIC KHUSUS COMPARE (Wajib ditaruh paling atas) + // [PERBAIKAN] 1. LOGIC KHUSUS COMPARE (PAKAI URLSearchParams) // ============================================================ if (source === 'compare') { try { let qCompare = q === '*' ? '*:*' : q; - + // Sanitasi Query if (qCompare !== '*:*') { - const escaped = escapeSolrQuery(qCompare); - qCompare = `*${escaped}*`; + // Kita escape, tapi biarkan stringnya bersih (jangan ditambah wildcard * manual) + // karena kita serahkan ke 'edismax' parser + qCompare = escapeSolrQuery(qCompare); } - // Susun Parameter Solr - const parameter = [ - `q=${encodeURIComponent(qCompare)}`, - `rows=${limit}`, - 'wt=json', - 'indent=true', - 'defType=edismax', - - // Grouping agar varian tidak banjir (per template) - 'group=true', - 'group.field=template_id_i', - 'group.limit=1', - 'group.main=true', - - // Field Wajib (Perhatikan: kita butuh product_id_i/default_code_s) - 'fl=id,display_name_s,default_code_s,image_s,price_tier1_v2_f,attribute_set_id_i,attribute_set_name_s,template_id_i,product_id_i', - - // Filter Dasar - 'fq=-publish_b:false', - 'fq=price_tier1_v2_f:[1 TO *]' - ]; + // [SOLUSI] Gunakan URLSearchParams untuk menyusun URL dengan aman + const params = new URLSearchParams(); + + params.append('q', qCompare); + params.append('rows', limit); + params.append('wt', 'json'); + params.append('indent', 'true'); + + // Gunakan eDisMax parser (Otak Cerdas) + params.append('defType', 'edismax'); + + // Set Prioritas Pencarian (Boost ^) + // 1. default_code_s^20 : SKU persis (Prioritas Tertinggi) + // 2. search_keywords_t^10 : Field baru (Case insensitive) + // 3. display_name_s^1 : Cadangan + params.append('qf', 'default_code_s^20 search_keywords_t^10 display_name_s^1'); + + // Minimum Match 100% (Semua kata harus ada), ubah jika ingin lebih longgar + params.append('mm', '100%'); + + // Grouping + params.append('group', 'true'); + params.append('group.field', 'template_id_i'); + params.append('group.limit', '1'); + params.append('group.main', 'true'); + + // Field List (fl) + params.append('fl', 'id,display_name_s,default_code_s,image_s,price_tier1_v2_f,attribute_set_id_i,attribute_set_name_s,template_id_i,product_id_i'); + + // Filter Query (fq) Dasar + params.append('fq', '-publish_b:false'); + params.append('fq', 'price_tier1_v2_f:[1 TO *]'); // Logic Locking (Filter Attribute Set ID dari Frontend) - // Frontend akan mengirim fq="attribute_set_id_i:9" if (fq) { - if (Array.isArray(fq)) { - fq.forEach(f => parameter.push(`fq=${encodeURIComponent(f)}`)); - } else { - parameter.push(`fq=${encodeURIComponent(fq)}`); - } + if (Array.isArray(fq)) { + fq.forEach(f => params.append('fq', f)); + } else { + params.append('fq', fq); + } } - // Target Core: VARIANTS (Karena compare butuh data spesifik) - const solrUrl = process.env.SOLR_HOST + '/solr/variants/select?' + parameter.join('&'); - - const result = await axios(solrUrl); + // Target Core: VARIANTS + // HAPUS parameter manual dari string URL, gunakan params object + const solrUrl = process.env.SOLR_HOST + '/solr/variants/select'; + + // Axios akan otomatis handle encoding % dan & dengan benar + const result = await axios.get(solrUrl, { params: params }); // Mapping Result const mappedProducts = productMappingSolr( @@ -84,13 +97,13 @@ export default async function handler(req, res) { ); const finalResponse = { - ...result.data, - response: { - ...result.data.response, - products: mappedProducts - } + ...result.data, + response: { + ...result.data.response, + products: mappedProducts + } }; - + delete finalResponse.response.docs; const camelCasedData = camelcaseObjectDeep(finalResponse); @@ -98,13 +111,16 @@ export default async function handler(req, res) { } catch (e) { console.error('[COMPARE SEARCH ERROR]', e.message); - // Return JSON valid meski kosong, agar frontend tidak error syntax + if (e.response && e.response.data) { + // Log detail error dari Solr + console.error('[SOLR DETAILS]:', JSON.stringify(e.response.data, null, 2)); + } return res.status(200).json({ response: { products: [], numFound: 0 } }); } } // ============================================================ - // LOGIC KHUSUS UPSELL (Simple & Direct) + // LOGIC KHUSUS UPSELL (KODE LAMA ANDA) // ============================================================ if (source === 'upsell') { try { @@ -172,7 +188,7 @@ export default async function handler(req, res) { } // ============================================================ - // SITEMAP (Biarkan tetap sama) + // SITEMAP (KODE LAMA ANDA) // ============================================================ if (source === 'sitemap') { try { @@ -197,7 +213,7 @@ export default async function handler(req, res) { } // ============================================================ - // SEARCH NORMAL (LOGIKA LAMA) + // SEARCH NORMAL (KODE LAMA ANDA) // ============================================================ let paramOrderBy = ''; diff --git a/src/utils/solrMapping.js b/src/utils/solrMapping.js index 419e0c61..8c0abcf1 100644 --- a/src/utils/solrMapping.js +++ b/src/utils/solrMapping.js @@ -129,6 +129,7 @@ export const variantsMappingSolr = (parent, products, pricelist) => { weight: product.weight_f || 0, attribute_set_id: product.attribute_set_id_i || 0, attribute_set_name: product.attribute_set_name_s || '', + search_keywords: product.search_keywords_t || '', manufacture: {}, parent: {}, qtySold: product?.qty_sold_f || 0, -- cgit v1.2.3 From fd50180ac301916c6feb5e93598f3fc59ecb78a8 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Mon, 5 Jan 2026 10:41:39 +0700 Subject: (andri) get desc variant product from magento --- src/pages/api/magento-product.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/pages/api/magento-product.ts b/src/pages/api/magento-product.ts index 297f0ebc..ef5fd3bc 100644 --- a/src/pages/api/magento-product.ts +++ b/src/pages/api/magento-product.ts @@ -103,6 +103,15 @@ export default async function handler( matrix.push(row); }); + // Deskripsi produk per varian + const descriptions:Record = {}; + items.forEach((p: any) => { + const descAttr = p.custom_attributes.find((a: any) => a.attribute_code === 'description' || a.attribute_code === 'short_description'); + descriptions[p.sku] = descAttr ? descAttr.value : ''; + }); + + + // ===================================================================== // 3. AMBIL LINKS (UPSELL & RELATED) DARI MAIN VARIANT SAJA // ===================================================================== @@ -126,7 +135,8 @@ export default async function handler( res.status(200).json({ specsMatrix: matrix, upsell_ids: upsellIds, - related_ids: relatedIds + related_ids: relatedIds, + descriptions: descriptions }); } catch (error) { -- cgit v1.2.3 From 4c9f6b642871f180de70f38e6c05c88c234b7d32 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Mon, 5 Jan 2026 11:01:11 +0700 Subject: (andri) add garansi --- src/pages/api/magento-product.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/pages/api/magento-product.ts b/src/pages/api/magento-product.ts index ef5fd3bc..4e963844 100644 --- a/src/pages/api/magento-product.ts +++ b/src/pages/api/magento-product.ts @@ -109,6 +109,12 @@ export default async function handler( const descAttr = p.custom_attributes.find((a: any) => a.attribute_code === 'description' || a.attribute_code === 'short_description'); descriptions[p.sku] = descAttr ? descAttr.value : ''; }); + + const warranties: Record = {}; + items.forEach((p: any) => { + const warAttr = p.custom_attributes.find((a: any) => a.attribute_code === 'z_warranty'); + warranties[p.sku] = warAttr ? warAttr.value : ''; + }); @@ -136,7 +142,8 @@ export default async function handler( specsMatrix: matrix, upsell_ids: upsellIds, related_ids: relatedIds, - descriptions: descriptions + descriptions: descriptions, + warranties: warranties, }); } catch (error) { -- cgit v1.2.3 From bdc571812ce174ff218c77bed0b5e06bf31e3196 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Mon, 12 Jan 2026 15:54:42 +0700 Subject: (andri) check nilai suatu attribute + bersihkan tanda petik --- src/pages/api/magento-product.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/pages/api/magento-product.ts b/src/pages/api/magento-product.ts index 4e963844..f61daf69 100644 --- a/src/pages/api/magento-product.ts +++ b/src/pages/api/magento-product.ts @@ -87,20 +87,31 @@ export default async function handler( // Susun Matrix // Struktur: { code, label, values: { [sku]: value } } const matrix: any[] = []; + Array.from(allAttributeCodes).forEach((code) => { const row: any = { code: code, label: labelsMap[code], - values: {} // Key = SKU/ID Variant, Value = Isi Atribut + values: {} }; + let hasData = false; + items.forEach((p: any) => { const attr = p.custom_attributes.find((a: any) => a.attribute_code === code); - // Simpan value berdasarkan SKU (ID Variant dari Odoo) - row.values[p.sku] = attr ? attr.value : '-'; + let rawVal = attr && attr.value !== null ? String(attr.value).trim() : ''; + if (rawVal.length >= 2 && rawVal.startsWith('"') && rawVal.endsWith('"')) { + rawVal = rawVal.slice(1, -1).trim(); + } + if (rawVal !== '' && rawVal !== '-') { + hasData = true; + } + row.values[p.sku] = rawVal; }); - matrix.push(row); + if (hasData) { + matrix.push(row); + } }); // Deskripsi produk per varian -- cgit v1.2.3 From ec7ab4c654fc5b29b277d42ad84986f4c1220134 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 13 Jan 2026 17:56:33 +0700 Subject: add short desc category website --- src/lib/category/components/Breadcrumb.jsx | 66 +++++++++++++++++----- .../components/styles/breadcrumb.module.css | 3 + src/pages/shop/category/[slug].jsx | 30 ++++++++-- 3 files changed, 79 insertions(+), 20 deletions(-) create mode 100644 src/lib/category/components/styles/breadcrumb.module.css (limited to 'src') diff --git a/src/lib/category/components/Breadcrumb.jsx b/src/lib/category/components/Breadcrumb.jsx index 50557c3e..8579bb14 100644 --- a/src/lib/category/components/Breadcrumb.jsx +++ b/src/lib/category/components/Breadcrumb.jsx @@ -11,17 +11,21 @@ import React from 'react'; import { useQuery } from 'react-query'; import useDevice from '@/core/hooks/useDevice'; -const Breadcrumb = ({ categoryId }) => { +const Breadcrumb = ({ categoryId, shortDesc }) => { const breadcrumbs = useQuery( ['category-breadcrumbs', categoryId], async () => await odooApi('GET', `/api/v1/category/${categoryId}/category-breadcrumb`) ); + const { isDesktop, isMobile } = useDevice(); const items = breadcrumbs.data ?? []; const lastIdx = items.length - 1; + /* ========================= + DESKTOP + ========================= */ if (isDesktop) { return (
@@ -79,16 +83,34 @@ const Breadcrumb = ({ categoryId }) => { })} + {shortDesc && ( +
+ {shortDesc} +
+ )}
); } + /* ========================= + MOBILE + ========================= */ if (isMobile) { - const items = breadcrumbs.data ?? []; const n = items.length; - const lastCat = n >= 1 ? items[n - 1] : null; // terakhir (current) - const secondLast = n >= 2 ? items[n - 2] : null; // sebelum current - const beforeSecond = n >= 3 ? items[n - 3] : null; // sebelum secondLast + const lastCat = n >= 1 ? items[n - 1] : null; + const secondLast = n >= 2 ? items[n - 2] : null; + const beforeSecond = n >= 3 ? items[n - 3] : null; + const hiddenText = n >= 3 ? items @@ -101,15 +123,15 @@ const Breadcrumb = ({ categoryId }) => {
/} // lebih rapat + separator={/} spacing='4px' sx={{ '& ol': { display: 'flex', alignItems: 'center', - overflow: 'hidden', // untuk ellipsis - whiteSpace: 'nowrap', // untuk ellipsis - gap: '0', // no extra gap + overflow: 'hidden', + whiteSpace: 'nowrap', + gap: '0', }, '& li': { display: 'inline-flex', alignItems: 'center' }, '& li:not(:last-of-type)': { @@ -117,7 +139,7 @@ const Breadcrumb = ({ categoryId }) => { whiteSpace: 'nowrap', }, '& li:last-of-type': { - flex: '0 1 auto', // jangan ambil full space biar gak keliatan “space kosong” + flex: '0 1 auto', minWidth: 0, }, }} @@ -130,7 +152,6 @@ const Breadcrumb = ({ categoryId }) => { - {/* Jika ada kategori sebelum secondLast, tampilkan '..' (link ke beforeSecond) */} {beforeSecond && ( { beforeSecond.id )} title={hiddenText} - aria-label={`Kembali ke ${beforeSecond.name}`} className='!text-danger-500' > .. @@ -149,7 +169,6 @@ const Breadcrumb = ({ categoryId }) => { )} - {/* secondLast sebagai link (kalau ada) */} {secondLast && ( { )} - {/* lastCat (current) dengan truncate & lebar dibatasi */} {lastCat && ( {lastCat.name} @@ -180,9 +198,27 @@ const Breadcrumb = ({ categoryId }) => { )} + + {shortDesc && ( +
+ {shortDesc} +
+ )}
); } + + return null; }; export default Breadcrumb; diff --git a/src/lib/category/components/styles/breadcrumb.module.css b/src/lib/category/components/styles/breadcrumb.module.css new file mode 100644 index 00000000..dee4e1b4 --- /dev/null +++ b/src/lib/category/components/styles/breadcrumb.module.css @@ -0,0 +1,3 @@ +.category-short-desc { + flex: 0 0 100%; +} diff --git a/src/pages/shop/category/[slug].jsx b/src/pages/shop/category/[slug].jsx index 11840d47..e515e3f4 100644 --- a/src/pages/shop/category/[slug].jsx +++ b/src/pages/shop/category/[slug].jsx @@ -16,13 +16,14 @@ const ProductSearch = dynamic(() => ); const CategorySection = dynamic(() => import('@/lib/product/components/CategorySection') -) +); export default function CategoryDetail() { const router = useRouter(); const { slug = '', page = 1 } = router.query; - const [dataCategories, setDataCategories] = useState([]) + const [dataCategories, setDataCategories] = useState([]); + const [shortDesc, setShortDesc] = useState(''); const categoryName = getNameFromSlug(slug); const categoryId = getIdFromSlug(slug); const q = router?.query.q || null; @@ -33,6 +34,22 @@ export default function CategoryDetail() { if (q) { query.q = q; } + useEffect(() => { + if (!router.isReady) return; + if (!categoryId) return; + + const loadShortDesc = async () => { + const res = await odooApi( + 'GET', + `/api/v1/category/${categoryId}/short-desc` + ); + + const desc = res?.shortDesc || ''; + setShortDesc(desc); + }; + + loadShortDesc(); + }, [router.isReady, categoryId]); return ( @@ -47,11 +64,14 @@ export default function CategoryDetail() { ]} /> - - + {!_.isEmpty(router.query) && ( - + )} ); -- cgit v1.2.3 From b878d3369fe7a43a85c8410c50d163a9e326644e Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Mon, 19 Jan 2026 10:38:20 +0700 Subject: (andri) clean attribute --- src/pages/api/magento-product.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/pages/api/magento-product.ts b/src/pages/api/magento-product.ts index f61daf69..51ca1732 100644 --- a/src/pages/api/magento-product.ts +++ b/src/pages/api/magento-product.ts @@ -49,6 +49,17 @@ export default async function handler( return res.status(200).json({ specsMatrix: [], upsell_ids: [], related_ids: [] }); } + // --- HELPER: Fungsi untuk membersihkan nilai dari tanda kutip --- + const cleanAttributeValue = (val: any) => { + if (val === null || val === undefined) return ''; + let str = String(val).trim(); + // Cek apakah diawali dan diakhiri tanda kutip + if (str.length >= 2 && str.startsWith('"') && str.endsWith('"')) { + str = str.slice(1, -1).trim(); + } + return str; + }; + // ===================================================================== // 2. BUILD SPECS MATRIX // Kita butuh daftar semua atribut unik (z_*) dari seluruh varian @@ -99,10 +110,9 @@ export default async function handler( items.forEach((p: any) => { const attr = p.custom_attributes.find((a: any) => a.attribute_code === code); - let rawVal = attr && attr.value !== null ? String(attr.value).trim() : ''; - if (rawVal.length >= 2 && rawVal.startsWith('"') && rawVal.endsWith('"')) { - rawVal = rawVal.slice(1, -1).trim(); - } + // Gunakan helper cleanAttributeValue disini + const rawVal = attr ? cleanAttributeValue(attr.value) : ''; + if (rawVal !== '' && rawVal !== '-') { hasData = true; } @@ -121,14 +131,14 @@ export default async function handler( descriptions[p.sku] = descAttr ? descAttr.value : ''; }); + // [UPDATED] Warranties dengan filter tanda kutip const warranties: Record = {}; items.forEach((p: any) => { const warAttr = p.custom_attributes.find((a: any) => a.attribute_code === 'z_warranty'); - warranties[p.sku] = warAttr ? warAttr.value : ''; + // Gunakan helper cleanAttributeValue agar tanda kutip hilang + warranties[p.sku] = warAttr ? cleanAttributeValue(warAttr.value) : ''; }); - - // ===================================================================== // 3. AMBIL LINKS (UPSELL & RELATED) DARI MAIN VARIANT SAJA // ===================================================================== -- cgit v1.2.3 From 72f9f0204e82ebf6fd79bde774dc0a186275b32d Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Mon, 19 Jan 2026 10:59:35 +0700 Subject: fix --- src/pages/api/magento-product.ts | 6 ------ 1 file changed, 6 deletions(-) (limited to 'src') diff --git a/src/pages/api/magento-product.ts b/src/pages/api/magento-product.ts index 51ca1732..551a4a5e 100644 --- a/src/pages/api/magento-product.ts +++ b/src/pages/api/magento-product.ts @@ -49,11 +49,9 @@ export default async function handler( return res.status(200).json({ specsMatrix: [], upsell_ids: [], related_ids: [] }); } - // --- HELPER: Fungsi untuk membersihkan nilai dari tanda kutip --- const cleanAttributeValue = (val: any) => { if (val === null || val === undefined) return ''; let str = String(val).trim(); - // Cek apakah diawali dan diakhiri tanda kutip if (str.length >= 2 && str.startsWith('"') && str.endsWith('"')) { str = str.slice(1, -1).trim(); } @@ -62,7 +60,6 @@ export default async function handler( // ===================================================================== // 2. BUILD SPECS MATRIX - // Kita butuh daftar semua atribut unik (z_*) dari seluruh varian // ===================================================================== // Kumpulkan semua kode atribut unik @@ -131,18 +128,15 @@ export default async function handler( descriptions[p.sku] = descAttr ? descAttr.value : ''; }); - // [UPDATED] Warranties dengan filter tanda kutip const warranties: Record = {}; items.forEach((p: any) => { const warAttr = p.custom_attributes.find((a: any) => a.attribute_code === 'z_warranty'); - // Gunakan helper cleanAttributeValue agar tanda kutip hilang warranties[p.sku] = warAttr ? cleanAttributeValue(warAttr.value) : ''; }); // ===================================================================== // 3. AMBIL LINKS (UPSELL & RELATED) DARI MAIN VARIANT SAJA // ===================================================================== - // Cari data milik varian utama (varian pertama) const mainProduct = items.find((p: any) => String(p.sku) === mainSku) || items[0]; let upsellIds: number[] = []; -- cgit v1.2.3 From 8ea5e52346fff91462e63e3e881f65a47ffd1354 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Tue, 27 Jan 2026 11:47:24 +0700 Subject: (andri) fix search non grouping compare --- src/pages/api/shop/search.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 5ea6a70a..37cfa1bd 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -23,6 +23,7 @@ export default async function handler(req, res) { fq = '', // bisa berupa string atau array limit = 30, source = '', + group = 'true', } = req.query; let { stock = '' } = req.query; @@ -61,11 +62,14 @@ export default async function handler(req, res) { // Minimum Match 100% (Semua kata harus ada), ubah jika ingin lebih longgar params.append('mm', '100%'); - // Grouping - params.append('group', 'true'); - params.append('group.field', 'template_id_i'); - params.append('group.limit', '1'); - params.append('group.main', 'true'); + if (group === 'false') { + params.append('group', 'false'); + } else { + params.append('group', 'true'); + params.append('group.field', 'template_id_i'); + params.append('group.limit', '1'); + params.append('group.main', 'true'); + } // Field List (fl) params.append('fl', 'id,display_name_s,default_code_s,image_s,price_tier1_v2_f,attribute_set_id_i,attribute_set_name_s,template_id_i,product_id_i'); -- cgit v1.2.3 From dac398aa33f8e57fd17fc9b115cf435c2d877b19 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Wed, 28 Jan 2026 11:05:37 +0700 Subject: fix --- src/pages/api/magento-product.ts | 4 ++-- src/pages/api/shop/search.js | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/pages/api/magento-product.ts b/src/pages/api/magento-product.ts index 551a4a5e..28738878 100644 --- a/src/pages/api/magento-product.ts +++ b/src/pages/api/magento-product.ts @@ -12,8 +12,8 @@ export default async function handler( return res.status(400).json({ error: 'SKUs are required' }); } - const token = 'vxrtcjvztv1icgjzsui45de9kmwlz0lf'; - const baseUrl = 'https://pimdev.1211.my.id/rest/V1'; + const token = process.env.MAGENTO_API_KEY || ''; + const baseUrl = process.env.MAGENTO_API_HOST || ''; try { const skuList = String(skus).split(','); // Contoh: ['221', '222', '223'] diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 37cfa1bd..89d75cd0 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -35,11 +35,14 @@ export default async function handler(req, res) { try { let qCompare = q === '*' ? '*:*' : q; - // Sanitasi Query if (qCompare !== '*:*') { - // Kita escape, tapi biarkan stringnya bersih (jangan ditambah wildcard * manual) - // karena kita serahkan ke 'edismax' parser qCompare = escapeSolrQuery(qCompare); + qCompare = qCompare.split(/\s+/).map(term => { + if (term && !term.includes('*')) { + return term + '*'; + } + return term; + }).join(' '); } // [SOLUSI] Gunakan URLSearchParams untuk menyusun URL dengan aman @@ -59,8 +62,12 @@ export default async function handler(req, res) { // 3. display_name_s^1 : Cadangan params.append('qf', 'default_code_s^20 search_keywords_t^10 display_name_s^1'); - // Minimum Match 100% (Semua kata harus ada), ubah jika ingin lebih longgar - params.append('mm', '100%'); + const compareWords = qCompare.split(/\s+/).filter(w => w.length > 0); + let compareMm = '100%'; + if (compareWords.length >= 3) { + compareMm = '75%'; + } + params.append('mm', compareMm); if (group === 'false') { params.append('group', 'false'); -- cgit v1.2.3 From 4bb444b880d7677ba60f163c78440c2357fb16a4 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 30 Jan 2026 09:12:55 +0700 Subject: add --- src/lib/category/components/styles/breadcrumb.module.css | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/lib/category/components/styles/breadcrumb.module.css (limited to 'src') diff --git a/src/lib/category/components/styles/breadcrumb.module.css b/src/lib/category/components/styles/breadcrumb.module.css new file mode 100644 index 00000000..dee4e1b4 --- /dev/null +++ b/src/lib/category/components/styles/breadcrumb.module.css @@ -0,0 +1,3 @@ +.category-short-desc { + flex: 0 0 100%; +} -- cgit v1.2.3 From 1ba8b2ae2c360f6e43218efb96cbd616765e4d59 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Wed, 11 Feb 2026 11:53:40 +0700 Subject: (andri) ganti button navbar pilihan semua promo --- src/core/components/elements/Navbar/NavbarDesktop.jsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx index db4fcbb8..c320c43a 100644 --- a/src/core/components/elements/Navbar/NavbarDesktop.jsx +++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx @@ -13,6 +13,7 @@ import { MenuItem, MenuList, useDisclosure, + Badge, } from '@chakra-ui/react'; import { ChevronDownIcon, HeartIcon } from '@heroicons/react/24/outline'; import dynamic from 'next/dynamic'; @@ -271,7 +272,7 @@ const NavbarDesktop = () => { aria-label='Promo' className={`${ router.asPath === '/shop/promo' && 'bg-gray_r-3' - } flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition group relative`} // Added relative position + } flex-[1.5] flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition group relative`} target='_blank' rel='noreferrer' > @@ -289,8 +290,11 @@ const NavbarDesktop = () => { /> )} - + Semua Promo + + Baru + {/* {showPopup && router.pathname === '/' && ( @@ -306,7 +310,7 @@ const NavbarDesktop = () => { aria-label='Brand' className={`${ router.asPath === '/shop/brands' && 'bg-gray_r-3' - } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition group`} + } px-2 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition group`} target='_blank' rel='noreferrer' > @@ -320,7 +324,7 @@ const NavbarDesktop = () => { className={`${ router.asPath.includes('/shop/search?orderBy=stock') && 'bg-gray_r-3' - } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition group`} + } px-2 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition group`} target='_blank' rel='noreferrer' > @@ -331,7 +335,7 @@ const NavbarDesktop = () => { @@ -506,4 +510,4 @@ const SocialMedias = () => ( ); -export default NavbarDesktop; +export default NavbarDesktop; \ No newline at end of file -- cgit v1.2.3 From 244bbbb00aa9acb9665c1981855a7e5302083762 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Wed, 11 Feb 2026 17:56:50 +0700 Subject: (andri) hide penawaran terbatas --- src/core/components/elements/Navbar/NavbarDesktop.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx index c320c43a..2f3f8682 100644 --- a/src/core/components/elements/Navbar/NavbarDesktop.jsx +++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx @@ -276,7 +276,7 @@ const NavbarDesktop = () => { target='_blank' rel='noreferrer' > - {showPopup && ( + {/* {showPopup && (
{ loading='eager' />
- )} + )} */} Semua Promo -- cgit v1.2.3 From af3b8a74d1f851c9c754814c900749e5bd6ee4cc Mon Sep 17 00:00:00 2001 From: Mqdd Date: Sun, 15 Feb 2026 01:10:14 +0700 Subject: auto set to popular when search --- src/lib/product/components/ProductSearch.jsx | 76 +++++++++++++++++----------- 1 file changed, 47 insertions(+), 29 deletions(-) (limited to 'src') diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index 850d00cc..c73c7036 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -6,7 +6,10 @@ import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react'; import axios from 'axios'; import _ from 'lodash'; import { toQuery } from 'lodash-contrib'; -import { FunnelIcon, AdjustmentsHorizontalIcon } from '@heroicons/react/24/outline'; +import { + FunnelIcon, + AdjustmentsHorizontalIcon, +} from '@heroicons/react/24/outline'; import odooApi from '@/core/api/odooApi'; import searchSpellApi from '@/core/api/searchSpellApi'; import Link from '@/core/components/elements/Link/Link'; @@ -57,9 +60,15 @@ const ProductSearch = ({ if (!router.isReady) return; const onBrandsPage = router.pathname.includes('brands'); - const hasOrder = typeof router.query?.orderBy === 'string' && router.query.orderBy !== ''; - - if (onBrandsPage && !hasOrder && !appliedDefaultBrandOrder.current) { + const onSearchPage = prefixUrl === '/shop/search'; + const hasOrder = + typeof router.query?.orderBy === 'string' && router.query.orderBy !== ''; + + if ( + (onBrandsPage || onSearchPage) && + !hasOrder && + !appliedDefaultBrandOrder.current + ) { let params = { ...router.query, orderBy: 'popular', @@ -83,7 +92,7 @@ const ProductSearch = ({ const loadProduct = async () => { const getCategoriesId = await odooApi( 'GET', - `/api/v1/category/numFound?parent_id=${categoryId}` + `/api/v1/category/numFound?parent_id=${categoryId}`, ); if (getCategoriesId) { setDataCategoriesProduct(getCategoriesId); @@ -94,7 +103,7 @@ const ProductSearch = ({ const loadProduct = async () => { const lobData = await odooApi( 'GET', - `/api/v1/lob_homepage/${categoryId}/category_id` + `/api/v1/lob_homepage/${categoryId}/category_id`, ); if (lobData) { @@ -175,7 +184,11 @@ const ProductSearch = ({ }, [dataCategoriesProduct, dataLob]); useEffect(() => { - if (prefixUrl.includes('category') || prefixUrl.includes('lob') || router.asPath.includes('penawaran')) { + if ( + prefixUrl.includes('category') || + prefixUrl.includes('lob') || + router.asPath.includes('penawaran') + ) { setQueryFinal({ ...finalQuery, q, limit, orderBy }); } else { setQueryFinal({ ...query, q, limit, orderBy }); @@ -198,10 +211,10 @@ const ProductSearch = ({ ? router.query.brand ? router.query.brand.split(',') : [] - : [] + : [], ); const [categoryValues, setCategory] = useState( - router.query?.category?.split(',') || router.query?.category?.split(',') + router.query?.category?.split(',') || router.query?.category?.split(','), ); const [priceFrom, setPriceFrom] = useState(router.query?.priceFrom || null); @@ -217,11 +230,11 @@ const ProductSearch = ({ if (productFound == 0 && query.q && !spellings) { searchSpellApi({ query: query.q }).then((response) => { const oddIndexSuggestions = response.data.spellcheck.suggestions.filter( - (_, index) => index % 2 === 1 + (_, index) => index % 2 === 1, ); const oddIndexCollations = response.data.spellcheck.collations.filter( - (_, index) => index % 2 === 1 + (_, index) => index % 2 === 1, ); const dataSpellings = oddIndexSuggestions.reduce((acc, curr) => { @@ -246,7 +259,7 @@ const ProductSearch = ({ useEffect(() => { const checkIfBrand = async () => { const brand = await axios( - `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/brands?params=search&q=${search}` + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/brands?params=search&q=${search}`, ); if (brand.data.length > 0) { @@ -265,7 +278,7 @@ const ProductSearch = ({ const loadCategories = async () => { const getCategories = await odooApi( 'GET', - `/api/v1/category/child?parent_id=${categoryId}` + `/api/v1/category/child?parent_id=${categoryId}`, ); if (getCategories) { setDataCategories(getCategories); @@ -335,15 +348,15 @@ const ProductSearch = ({ if (router.pathname.includes('search')) { const getBannerHeader = await odooApi( 'GET', - '/api/v1/banner?type=promotion-header' + '/api/v1/banner?type=promotion-header', ); const getBannerFooter = await odooApi( 'GET', - '/api/v1/banner?type=promotion-footer' + '/api/v1/banner?type=promotion-footer', ); var randomIndex = Math.floor(Math.random() * getBannerHeader.length); var randomIndexFooter = Math.floor( - Math.random() * getBannerFooter.length + Math.random() * getBannerFooter.length, ); setBannerPromotionHeader(getBannerHeader[randomIndex]); setBannerPromotionFooter(getBannerFooter[randomIndexFooter]); @@ -430,7 +443,9 @@ const ProductSearch = ({
{isNotReadyStockPage && isBrand && isBrand.logo && (
-

Brand Pencarian {q}

+

+ Brand Pencarian {q} +

1 ? ( <> {productStart + 1}- - {parseInt(productStart) + parseInt(productRows) > productFound + {parseInt(productStart) + parseInt(productRows) > + productFound ? productFound : parseInt(productStart) + parseInt(productRows)}  dari  @@ -474,7 +490,8 @@ const ProductSearch = ({  produk{' '} {query.q && ( <> - untuk pencarian {query.q} + untuk pencarian{' '} + {query.q} )} @@ -512,7 +529,9 @@ const ProductSearch = ({
)} {!!dataLob?.length && } - {!!dataCategories?.length && } + {!!dataCategories?.length && ( + + )}
{products && products.map((product) => ( @@ -621,7 +640,7 @@ const ProductSearch = ({ <> {productStart + 1}- {parseInt(productStart) + parseInt(productRows) > - productFound + productFound ? productFound : parseInt(productStart) + parseInt(productRows)}  dari  @@ -629,12 +648,11 @@ const ProductSearch = ({ ) : ( '' )} - {productFound} + {productFound}  produk{' '} {query.q && ( <> - untuk pencarian{' '} - {query.q} + untuk pencarian {query.q} )} @@ -697,8 +715,8 @@ const ProductSearch = ({ href={ query?.q ? whatsappUrl('productSearch', { - name: query.q, - }) + name: query.q, + }) : whatsappUrl() } className='text-danger-500' @@ -783,9 +801,9 @@ const FilterChoicesComponent = ({ )} {brandValues?.length > 0 || - categoryValues?.length > 0 || - priceFrom || - priceTo ? ( + categoryValues?.length > 0 || + priceFrom || + priceTo ? (