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 --- .../product-detail/components/ProductDetail.tsx | 385 +++++++++++---------- src/pages/api/magento-product.ts | 186 +++++----- 2 files changed, 285 insertions(+), 286 deletions(-) diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index cfe73628..3beb75b4 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -16,6 +16,8 @@ import { Tbody, Tr, Td, + Th, + Thead, Box, Spinner, Center, @@ -27,6 +29,7 @@ import { AlertTriangle, MessageCircleIcon, Share2Icon, + ExternalLink } from 'lucide-react'; import { LazyLoadComponent } from 'react-lazy-load-image-component'; @@ -65,12 +68,11 @@ const ProductDetail = ({ product }: Props) => { const [auth, setAuth] = useState(null); // State Data dari Magento - const [specs, setSpecs] = useState<{ code: string; label: string; value: string }[]>([]); + const [specsMatrix, setSpecsMatrix] = useState([]); const [upsellIds, setUpsellIds] = useState([]); const [relatedIds, setRelatedIds] = useState([]); const [loadingSpecs, setLoadingSpecs] = useState(false); - const [errorSpecs, setErrorSpecs] = useState(false); useEffect(() => { try { @@ -111,7 +113,7 @@ const ProductDetail = ({ product }: Props) => { }, [router.asPath, product.manufacture.name, product.name, setAskAdminUrl]); // ========================================================================= - // LOGIC INISIALISASI VARIANT (RESET SAAT NAVIGASI) + // 1. LOGIC INISIALISASI VARIANT // ========================================================================= useEffect(() => { if (typeof auth === 'object') { @@ -123,30 +125,32 @@ const ProductDetail = ({ product }: Props) => { setSelectedVariant(variantInit); - // Reset data Magento - setSpecs([]); + setSpecsMatrix([]); setUpsellIds([]); setRelatedIds([]); }, [product, auth]); // ========================================================================= - // LOGIC FETCH: SPECS, UPSELLS, RELATED + // 2. LOGIC FETCH DATA // ========================================================================= useEffect(() => { const fetchMagentoData = async () => { - // Validasi kepemilikan varian - if (!selectedVariant?.id) return; - const isVariantOwner = product.variants.some(v => Number(v.id) === Number(selectedVariant.id)); - if (!isVariantOwner) return; + const allVariantIds = product.variants.map(v => v.id); + + if (allVariantIds.length === 0) return; - const idToFetch = selectedVariant.id; + const mainId = allVariantIds[0]; setLoadingSpecs(true); - setErrorSpecs(false); try { - const endpoint = `/api/magento-product?sku=${encodeURIComponent(String(idToFetch))}`; + const params = new URLSearchParams({ + skus: allVariantIds.join(','), + main_sku: String(mainId) + }); + + const endpoint = `/api/magento-product?${params.toString()}`; const response = await fetch(endpoint, { method: 'GET', @@ -154,7 +158,7 @@ const ProductDetail = ({ product }: Props) => { }); if (!response.ok) { - setSpecs([]); + setSpecsMatrix([]); setUpsellIds([]); setRelatedIds([]); return; @@ -162,67 +166,108 @@ const ProductDetail = ({ product }: Props) => { const data = await response.json(); - // Double Check - if (Number(idToFetch) !== Number(selectedVariant.id)) return; - - // 1. Specs - if (data.specs && Array.isArray(data.specs)) { - setSpecs(data.specs); + // 1. Specs Matrix (Processed Grouping) + if (data.specsMatrix && Array.isArray(data.specsMatrix)) { + const processed = processMatrixData(data.specsMatrix); + setSpecsMatrix(processed); } else { - setSpecs([]); + setSpecsMatrix([]); } - // 2. Upsell - if (data.upsell_ids && Array.isArray(data.upsell_ids)) { - setUpsellIds(data.upsell_ids); - } else { - setUpsellIds([]); - } + // 2. Upsell & Related + if (data.upsell_ids && Array.isArray(data.upsell_ids)) setUpsellIds(data.upsell_ids); + else setUpsellIds([]); - // 3. Related - if (data.related_ids && Array.isArray(data.related_ids)) { - setRelatedIds(data.related_ids); - } else { - setRelatedIds([]); - } + if (data.related_ids && Array.isArray(data.related_ids)) setRelatedIds(data.related_ids); + else setRelatedIds([]); } catch (error) { console.error("Gagal mengambil data Magento:", error); - setErrorSpecs(true); - setSpecs([]); - setUpsellIds([]); - setRelatedIds([]); + setSpecsMatrix([]); } finally { setLoadingSpecs(false); } }; fetchMagentoData(); - }, [selectedVariant, product]); + }, [product.id]); // ========================================================================= - // HELPER: RENDER SPEC VALUE (SIMPLE - NO LINK DETECT) + // HELPER 1: GROUPING DATA BY LABEL (Separator ':') // ========================================================================= - const renderSpecValue = (item: { code: string; label: string; value: string }) => { - const val = item.value; + const processMatrixData = (rawMatrix: any[]) => { + const groups: any = {}; + const result: any[] = []; + + rawMatrix.forEach(item => { + // Cek Label: "Group Name : Sub Label" + if (item.label && item.label.includes(' : ')) { + const parts = item.label.split(' : '); + const groupName = parts[0].trim(); + const childLabel = parts.slice(1).join(' : ').trim(); + + if (!groups[groupName]) { + groups[groupName] = { + type: 'group', + label: groupName, + children: [] + }; + result.push(groups[groupName]); + } + + groups[groupName].children.push({ + ...item, + label: childLabel // Override label jadi pendek + }); + + } else { + result.push({ ...item, type: 'single' }); + } + }); + + return result; + }; + + + // ========================================================================= + // HELPER 2: RENDER SPEC VALUE + // ========================================================================= + const renderSpecValue = (val: any) => { if (!val) return '-'; - - const cleanVal = val.trim(); + const strVal = String(val).trim(); + + // URL Link + const isUrl = !strVal.includes(' ') && ( + strVal.startsWith('http') || + strVal.startsWith('www.') + ); + if (isUrl) { + const href = strVal.startsWith('http') ? strVal : `https://${strVal}`; + return ( + + Link + + ); + } - // 1. JIKA HTML (Legacy Data) - // Deteksi tag HTML sederhana - if (cleanVal.includes('<') && cleanVal.includes('>')) { + // HTML + if (strVal.includes('<') && strVal.includes('>')) { return (
); } - // 2. TEKS BIASA - return cleanVal; + // Teks Biasa + return strVal; }; @@ -310,90 +355,39 @@ const ProductDetail = ({ product }: Props) => {
{/* ===== Kolom kiri: gambar ===== */}
- {/* === MOBILE: Slider swipeable === */} + {/* ... Image Slider ... */} {isMobile ? (
-
+
{allImages.length > 0 ? ( allImages.map((img, i) => ( -
- {`Gambar { - (e.target as HTMLImageElement).src = - '/images/noimage.jpeg'; - }} - /> +
+ {`Gambar { (e.target as HTMLImageElement).src = '/images/noimage.jpeg'; }} />
)) ) : (
- Gambar produk + Gambar produk
)}
- - {/* Dots indicator */} {allImages.length > 1 && (
{allImages.map((_, i) => ( -
)}
) : ( <> - {/* === DESKTOP === */} {allImages.length > 0 && (
{allImages.map((img, index) => ( -
setMainImage(img)} - > - {`Thumbnail { - (e.target as HTMLImageElement).src = - '/images/noimage.jpeg'; - }} - /> +
setMainImage(img)}> + {`Thumbnail { (e.target as HTMLImageElement).src = '/images/noimage.jpeg'; }} />
))}
@@ -408,13 +402,8 @@ const ProductDetail = ({ product }: Props) => {
{!hasPrice && (
- -

- Maaf untuk saat ini Produk yang anda cari tidak tersedia -

+ +

Maaf untuk saat ini Produk yang anda cari tidak tersedia

)}
@@ -428,13 +417,8 @@ const ProductDetail = ({ product }: Props) => {
{!hasPrice && (
- -

- Maaf untuk saat ini Produk yang anda cari tidak tersedia -

+ +

Maaf untuk saat ini Produk yang anda cari tidak tersedia

)}

{product.name}

@@ -454,110 +438,128 @@ const ProductDetail = ({ product }: Props) => {
{!!activeVariantId && !isApproval && ( - + )}
- {/* === SECTION TABS === */} + {/* === SECTION TABS: DESKRIPSI & SPESIFIKASI === */}
- - Deskripsi - - - Spesifikasi - - - Detail Lainnya - + Deskripsi + Spesifikasi + Detail Lainnya {/* DESKRIPSI */}
-

' - ? '

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

' - : product.description, - }} - /> +

' ? '

Lorem ipsum dolor sit amet.

' : product.description, }} />
- {/* SPESIFIKASI */} + {/* SPESIFIKASI (LOGIKA GROUPING + RATA TENGAH) */} - + {loadingSpecs ? (
- ) : specs.length > 0 ? ( - - - {specs.map((item, index) => ( - - - - - ))} - -
- {item.label} - - {renderSpecValue(item)} -
+ ) : specsMatrix.length > 0 ? ( + (() => { + const isSingleVariant = product.variants.length === 1; + const globalAlign = isSingleVariant ? "left" : "center"; + + return ( + + + + + {product.variants.map(v => ( + + ))} + + + + {specsMatrix.map((row, i) => { + + // CASE 1: GROUPING (Label punya ' : ') + if (row.type === 'group') { + return ( + + {/* Header Group Kiri */} + + + {/* Content Group Kanan */} + {product.variants.map(v => ( + + ))} + + ); + } + + // CASE 2: SINGLE ITEM + return ( + + + {product.variants.map(v => { + const rawValue = row.values[v.id] || '-'; + return ( + + ); + })} + + ); + })} + +
+ Spesifikasi + + {isSingleVariant ? 'Detail' : (v.attributes && v.attributes.length > 0 ? v.attributes.join(' - ') : v.code)} +
{row.label} +
+
+ {row.children.map((child: any, idx: number) => { + const rawVal = child.values[v.id]; + if (!rawVal || rawVal === '-') return null; + + return ( +
+ + {child.label}: + + {renderSpecValue(rawVal)} +
+ ); + })} +
+
+
{row.label} + {renderSpecValue(rawValue)} +
+ ); + })() ) : ( - - Spesifikasi teknis belum tersedia. - + Spesifikasi teknis belum tersedia. )}
{/* DETAIL LAINNYA */} - -

Informasi tambahan belum tersedia.

-
- +

Informasi tambahan belum tersedia.

+ {/* ... (Bagian Sidebar & Bottom SAMA) ... */} {isDesktop && (
-
- - | -
- -
- | - {canShare && ( - - - - )} +
+ {/* ... Buttons ... */}
-
Produk Serupa
@@ -572,7 +574,6 @@ const ProductDetail = ({ product }: Props) => {
-
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