From 561c3c992bdbe743bbf2a4afe16d0019b4e0269c Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Wed, 14 Jan 2026 16:16:06 +0700 Subject: (andri) fix --- .../components/ProductComparisonModal.tsx | 776 ++++++++++----------- 1 file changed, 380 insertions(+), 396 deletions(-) (limited to 'src-migrate') diff --git a/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx b/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx index 4f1fcd82..f2d7ca57 100644 --- a/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx +++ b/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useRef } from 'react'; // Tambah useRef +import React, { useEffect, useState, useRef } from 'react'; import { Modal, ModalOverlay, @@ -26,7 +26,7 @@ import { ListItem, useToast, Select, - useOutsideClick // Tambah import ini + useOutsideClick } from '@chakra-ui/react'; import { Search, Trash2 } from 'lucide-react'; @@ -41,20 +41,20 @@ const formatPrice = (price: number) => { }; const renderSpecValue = (val: any) => { - if (!val || val === '-') return '-'; - return String(val).replace(/<[^>]*>?/gm, ''); + if (!val || val === '-') return '-'; + return String(val).replace(/<[^>]*>?/gm, ''); }; type Props = { isOpen: boolean; onClose: () => void; - mainProduct: any; - selectedVariant: any; + mainProduct: any; + selectedVariant: any; }; const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant }: Props) => { const toast = useToast(); - + // --- STATE --- const [products, setProducts] = useState<(any | null)[]>([null, null, null, null]); const [specsMatrix, setSpecsMatrix] = useState([]); @@ -67,16 +67,15 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant const [isSearching, setIsSearching] = useState(false); // --- REF & OUTSIDE CLICK --- - const searchWrapperRef = useRef(null); // Ref untuk mendeteksi klik + const searchWrapperRef = useRef(null); useOutsideClick({ ref: searchWrapperRef, handler: () => { - // Jika user klik di luar area search yang aktif, tutup search - if (activeSearchSlot !== null) { - setActiveSearchSlot(null); - setSearchResults([]); // Opsional: bersihkan hasil juga - } + if (activeSearchSlot !== null) { + setActiveSearchSlot(null); + setSearchResults([]); + } }, }); @@ -86,53 +85,53 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant useEffect(() => { if (isOpen && mainProduct) { let activeItem = selectedVariant; - + if (!activeItem && mainProduct.variants && mainProduct.variants.length > 0) { - activeItem = mainProduct.variants[0]; + activeItem = mainProduct.variants[0]; } if (!activeItem) { - activeItem = mainProduct; + activeItem = mainProduct; } const targetId = activeItem.id; const displayCode = activeItem.default_code || activeItem.code || activeItem.sku || mainProduct.default_code || mainProduct.code; const variantOptions = mainProduct.variants?.map((v: any) => ({ - id: v.id, - code: v.default_code || v.code || v.sku, - name: v.name || v.displayName || v.display_name, - price: v.price?.price || v.price || 0, - image: v.image + id: v.id, + code: v.default_code || v.code || v.sku, + name: v.name || v.displayName || v.display_name, + price: v.price?.price || v.price || 0, + image: v.image })) || []; if (variantOptions.length === 0) { - variantOptions.push({ - id: targetId, - code: displayCode, - name: mainProduct.name, - price: activeItem.price?.price || activeItem.price || 0, - image: activeItem.image || mainProduct.image - }); + variantOptions.push({ + id: targetId, + code: displayCode, + name: mainProduct.name, + price: activeItem.price?.price || activeItem.price || 0, + image: activeItem.image || mainProduct.image + }); } const displayName = activeItem.name || activeItem.displayName || mainProduct.name; const productSlot1 = { - id: targetId, - sku: targetId, - realCode: displayCode, - name: displayName, - price: activeItem.price?.price || activeItem.price || mainProduct.lowest_price?.price || 0, - image: activeItem.image || mainProduct.image, - variants: variantOptions + id: targetId, + sku: targetId, + realCode: displayCode, + name: displayName, + price: activeItem.price?.price || activeItem.price || mainProduct.lowest_price?.price || 0, + image: activeItem.image || mainProduct.image, + variants: variantOptions }; setProducts((prev) => { - const newSlots = [...prev]; - if (!newSlots[0] || String(newSlots[0].id) !== String(targetId)) { - newSlots[0] = productSlot1; - } - return newSlots; + const newSlots = [...prev]; + if (!newSlots[0] || String(newSlots[0].id) !== String(targetId)) { + newSlots[0] = productSlot1; + } + return newSlots; }); } }, [isOpen, mainProduct, selectedVariant]); @@ -145,226 +144,223 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant if (!isOpen || validProducts.length === 0) return; const fetchSpecs = async () => { - setIsLoadingMatrix(true); - try { - const allSkus = validProducts.map(p => p.sku).join(','); - const mainSku = validProducts[0]?.sku; + setIsLoadingMatrix(true); + try { + const allSkus = validProducts.map(p => p.sku).join(','); + const mainSku = validProducts[0]?.sku; - const res = await fetch(`/api/magento-product?skus=${allSkus}&main_sku=${mainSku}`); - if (!res.ok) return; + const res = await fetch(`/api/magento-product?skus=${allSkus}&main_sku=${mainSku}`); + if (!res.ok) return; - const data = await res.json(); - if (data.specsMatrix) { - setSpecsMatrix(data.specsMatrix); - } - } catch (err) { - console.error(err); - } finally { - setIsLoadingMatrix(false); + const data = await res.json(); + if (data.specsMatrix) { + setSpecsMatrix(data.specsMatrix); } + } catch (err) { + console.error(err); + } finally { + setIsLoadingMatrix(false); + } }; fetchSpecs(); }, [products, isOpen]); // =========================================================================== - // 3. SEARCH LOGIC + // 3. SEARCH LOGIC // =========================================================================== useEffect(() => { - const delayDebounceFn = setTimeout(async () => { - if (searchQuery.length > 0 && searchQuery.length < 3) { - setSearchResults([]); - return; - } + const delayDebounceFn = setTimeout(async () => { + if (searchQuery.length > 0 && searchQuery.length < 3) { + setSearchResults([]); + return; + } - if (activeSearchSlot === null) return; + if (activeSearchSlot === null) return; - const attrSetId = selectedVariant?.attribute_set_id || mainProduct?.attribute_set_id; + const attrSetId = selectedVariant?.attribute_set_id || mainProduct?.attribute_set_id; - if (!attrSetId) { - console.warn("Search dibatalkan: Produk utama tidak memiliki attribute_set_id"); - setSearchResults([]); - setIsSearching(false); - return; - } + if (!attrSetId) { + console.warn("Search dibatalkan: Produk utama tidak memiliki attribute_set_id"); + setSearchResults([]); + setIsSearching(false); + return; + } - setIsSearching(true); - try { - const queryParam = searchQuery === '' ? '*' : searchQuery; + setIsSearching(true); + try { + const queryParam = searchQuery === '' ? '*' : searchQuery; - const params = new URLSearchParams({ - source: 'compare', - q: queryParam, - limit: '20', - fq: `attribute_set_id_i:${attrSetId}` - }); + const params = new URLSearchParams({ + source: 'compare', + q: queryParam, + limit: '20', + fq: `attribute_set_id_i:${attrSetId}` + }); - const res = await fetch(`/api/shop/search?${params.toString()}`); - - if (res.ok) { - const data = await res.json(); - setSearchResults(data.response?.products || []); - } else { - setSearchResults([]); - } - } catch (e) { - setSearchResults([]); - } finally { - setIsSearching(false); + const res = await fetch(`/api/shop/search?${params.toString()}`); + + if (res.ok) { + const data = await res.json(); + setSearchResults(data.response?.products || []); + } else { + setSearchResults([]); } - }, 500); + } catch (e) { + setSearchResults([]); + } finally { + setIsSearching(false); + } + }, 500); - return () => clearTimeout(delayDebounceFn); + return () => clearTimeout(delayDebounceFn); }, [searchQuery, mainProduct, selectedVariant, activeSearchSlot]); // =========================================================================== // 4. HANDLERS // =========================================================================== const handleVariantChange = (slotIndex: number, newId: string) => { - const currentProduct = products[slotIndex]; - if (!currentProduct || !currentProduct.variants) return; - - const selectedVar = currentProduct.variants.find((v: any) => String(v.id) === String(newId)); - - if (selectedVar) { - const newProducts = [...products]; - newProducts[slotIndex] = { - ...currentProduct, - id: selectedVar.id, - sku: selectedVar.id, - name: selectedVar.name, - realCode: selectedVar.code, - price: selectedVar.price, - image: selectedVar.image - }; - setProducts(newProducts); - } - }; + const currentProduct = products[slotIndex]; + if (!currentProduct || !currentProduct.variants) return; + const selectedVar = currentProduct.variants.find((v: any) => String(v.id) === String(newId)); + if (selectedVar) { + const newProducts = [...products]; + newProducts[slotIndex] = { + ...currentProduct, + id: selectedVar.id, + sku: selectedVar.id, + name: selectedVar.name, + realCode: selectedVar.code, + price: selectedVar.price, + image: selectedVar.image + }; + setProducts(newProducts); + } + }; const handleAddProduct = async (searchItem: any, slotIndex: number) => { + if (products.find(p => p && String(p.id) === String(searchItem.id))) { + toast({ title: "Produk sudah ada", status: "warning", position: "top" }); + return; + } - if (products.find(p => p && String(p.id) === String(searchItem.id))) { - toast({ title: "Produk sudah ada", status: "warning", position: "top" }); - return; - } - - const idToAdd = searchItem.id; - const codeToAdd = searchItem.defaultCode || searchItem.default_code || searchItem.code; - const nameToAdd = searchItem.displayName || searchItem.name; - const imageToAdd = searchItem.image || searchItem.imageS || searchItem.image_s; - const priceToAdd = searchItem.lowestPrice?.price || searchItem.priceTier1V2F || searchItem.price || 0; - - let parentId = searchItem.templateId || - searchItem.templateIdI || - searchItem.template_id_i || - searchItem.template_id; - - if (!parentId) { - try { - const checkParams = new URLSearchParams({ - source: 'upsell', - q: '*:*', - fq: `id:${idToAdd}` - }); - - const checkRes = await fetch(`/api/shop/search?${checkParams.toString()}`); - if (checkRes.ok) { - const checkData = await checkRes.json(); - const freshItem = checkData.response?.products?.[0]; - - if (freshItem) { - const serverReturnedId = freshItem.id; - if (String(serverReturnedId) !== String(idToAdd)) { - parentId = serverReturnedId; - } else { - parentId = freshItem.templateId || freshItem.templateIdI || idToAdd; - } - } - } - } catch (e) { - console.error("Gagal validasi parent:", e); - parentId = idToAdd; + const idToAdd = searchItem.id; + const codeToAdd = searchItem.defaultCode || searchItem.default_code || searchItem.code; + const nameToAdd = searchItem.displayName || searchItem.name; + const imageToAdd = searchItem.image || searchItem.imageS || searchItem.image_s; + const priceToAdd = searchItem.lowestPrice?.price || searchItem.priceTier1V2F || searchItem.price || 0; + + let parentId = searchItem.templateId || + searchItem.templateIdI || + searchItem.template_id_i || + searchItem.template_id; + + if (!parentId) { + try { + const checkParams = new URLSearchParams({ + source: 'upsell', + q: '*:*', + fq: `id:${idToAdd}` + }); + + const checkRes = await fetch(`/api/shop/search?${checkParams.toString()}`); + if (checkRes.ok) { + const checkData = await checkRes.json(); + const freshItem = checkData.response?.products?.[0]; + + if (freshItem) { + const serverReturnedId = freshItem.id; + if (String(serverReturnedId) !== String(idToAdd)) { + parentId = serverReturnedId; + } else { + parentId = freshItem.templateId || freshItem.templateIdI || idToAdd; + } } + } + } catch (e) { + console.error("Gagal validasi parent:", e); + parentId = idToAdd; } - - const newProductEntry = { - id: idToAdd, - sku: idToAdd, - realCode: codeToAdd, - name: nameToAdd, - price: priceToAdd, - image: imageToAdd, - variants: [{ - id: idToAdd, - code: codeToAdd, - name: nameToAdd, - price: priceToAdd, - image: imageToAdd - }] - }; + } - setProducts((prev) => { - const newSlots = [...prev]; - newSlots[slotIndex] = newProductEntry; - return newSlots; - }); + const newProductEntry = { + id: idToAdd, + sku: idToAdd, + realCode: codeToAdd, + name: nameToAdd, + price: priceToAdd, + image: imageToAdd, + variants: [{ + id: idToAdd, + code: codeToAdd, + name: nameToAdd, + price: priceToAdd, + image: imageToAdd + }] + }; - setActiveSearchSlot(null); - setSearchQuery(''); - setSearchResults([]); - - if (parentId) { - try { - const params = new URLSearchParams({ - source: 'upsell', - limit: '100', - fq: `template_id_i:${parentId}` - }); - - const res = await fetch(`/api/shop/search?${params.toString()}`); - - if (res.ok) { - const data = await res.json(); - const siblings = data.response?.products || []; - - if (siblings.length > 0) { - const allVariants = siblings.map((s: any) => ({ - id: s.variantId || s.productIdI || s.id, - code: s.defaultCode || s.default_code || s.code, - name: s.displayName || s.name || s.nameS, - price: s.lowestPrice?.price || s.priceTier1V2F || 0, - image: s.image || s.imageS - })); - - allVariants.sort((a: any, b: any) => - String(a.code).localeCompare(String(b.code)) - ); - - setProducts((prev) => { - const updated = [...prev]; - if (updated[slotIndex] && String(updated[slotIndex].id) === String(idToAdd)) { - updated[slotIndex] = { - ...updated[slotIndex], - variants: allVariants - }; - } - return updated; - }); - } + setProducts((prev) => { + const newSlots = [...prev]; + newSlots[slotIndex] = newProductEntry; + return newSlots; + }); + + setActiveSearchSlot(null); + setSearchQuery(''); + setSearchResults([]); + + if (parentId) { + try { + const params = new URLSearchParams({ + source: 'upsell', + limit: '100', + fq: `template_id_i:${parentId}` + }); + + const res = await fetch(`/api/shop/search?${params.toString()}`); + + if (res.ok) { + const data = await res.json(); + const siblings = data.response?.products || []; + + if (siblings.length > 0) { + const allVariants = siblings.map((s: any) => ({ + id: s.variantId || s.productIdI || s.id, + code: s.defaultCode || s.default_code || s.code, + name: s.displayName || s.name || s.nameS, + price: s.lowestPrice?.price || s.priceTier1V2F || 0, + image: s.image || s.imageS + })); + + allVariants.sort((a: any, b: any) => + String(a.code).localeCompare(String(b.code)) + ); + + setProducts((prev) => { + const updated = [...prev]; + if (updated[slotIndex] && String(updated[slotIndex].id) === String(idToAdd)) { + updated[slotIndex] = { + ...updated[slotIndex], + variants: allVariants + }; } - } catch (error) { - console.error("Gagal fetch variant lain:", error); + return updated; + }); } + } + } catch (error) { + console.error("Gagal fetch variant lain:", error); } + } }; const handleRemoveProduct = (index: number) => { - const newProducts = [...products]; - newProducts[index] = null; - setProducts(newProducts); - if (newProducts.every(p => p === null)) setSpecsMatrix([]); + const newProducts = [...products]; + newProducts[index] = null; + setProducts(newProducts); + if (newProducts.every(p => p === null)) setSpecsMatrix([]); }; return ( @@ -379,7 +375,7 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant - Detail Spesifikasi Produk yang kamu pilih + Detail Spesifikasi Produk yang kamu pilih @@ -392,16 +388,16 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant {product ? ( {index !== 0 && ( - } - size="xs" position="absolute" top={-2} right={-2} - colorScheme="red" onClick={() => handleRemoveProduct(index)} zIndex={2} - /> + } + size="xs" position="absolute" top={-2} right={-2} + colorScheme="red" onClick={() => handleRemoveProduct(index)} zIndex={2} + /> )} - {product.name} { (e.target as HTMLImageElement).src = '/images/no-image-compare.svg'; }} /> @@ -413,200 +409,188 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant {product.name} - - handleVariantChange(index, e.target.value)} + isDisabled={false} bg="white" > - {product.variants && product.variants.map((v: any) => ( - - ))} + {product.variants && product.variants.map((v: any) => ( + + ))} - } - variant="outline" - colorScheme="red" - size="sm" - /> - + } + variant="outline" + colorScheme="red" + size="sm" + /> + ) : ( - - {/* WRAPPER SEARCH DENGAN REF */} - {/* Hanya berikan ref jika ini adalah slot yang sedang aktif dicari */} - - - { setActiveSearchSlot(index); setSearchQuery(''); }} - onChange={(e) => setSearchQuery(e.target.value)} - /> - - - {/* HASIL SEARCH */} - {activeSearchSlot === index && ( - - - {/* CEK ATTRIBUTE SET ID */} - {!selectedVariant?.attribute_set_id && !mainProduct?.attribute_set_id ? ( - - Perbandingan Tidak Tersedia - Produk utama tidak memiliki data kategori yang valid untuk dibandingkan. - - ) : ( - <> - {isSearching ? ( - - ) : searchResults.length > 0 ? ( - - {searchResults.map((res) => ( - handleAddProduct(res, index)} - > - {/* Align top biar rapi kalau teks panjang */} - {/* GAMBAR */} - { (e.target as HTMLImageElement).src = '/images/no-image-compare.svg'; }} - flexShrink={0} // Mencegah gambar gepeng - mt={1} - /> - - {/* TEXT CONTAINER */} - {/* w="0" adalah trik CSS agar text-overflow bekerja di dalam Flex */} - - {res.displayName || res.name} - - - {formatPrice(res.lowestPrice?.price || 0)} - - - - - ))} - - ) : ( - - {searchQuery === '' ? 'Menampilkan rekomendasi...' : 'Produk tidak ditemukan.'} - - )} - - )} + + + { setActiveSearchSlot(index); setSearchQuery(''); }} + onChange={(e) => setSearchQuery(e.target.value)} + /> + + + {activeSearchSlot === index && ( + + {!selectedVariant?.attribute_set_id && !mainProduct?.attribute_set_id ? ( + + Perbandingan Tidak Tersedia + Produk utama tidak memiliki data kategori yang valid untuk dibandingkan. - )} + ) : ( + <> + {isSearching ? ( + + ) : searchResults.length > 0 ? ( + + {searchResults.map((res) => ( + handleAddProduct(res, index)} + > + + { (e.target as HTMLImageElement).src = '/images/no-image-compare.svg'; }} + flexShrink={0} + mt={1} + /> + + + {res.displayName || res.name} + + + {formatPrice(res.lowestPrice?.price || 0)} + + + + + ))} + + ) : ( + + {searchQuery === '' ? 'Menampilkan rekomendasi...' : 'Produk tidak ditemukan.'} + + )} + + )} + + )} - {/* SLOT KOSONG */} - - Empty Slot - - Produk Belum Ditambahkan - + Empty Slot + + Produk Belum Ditambahkan + )} ))} - {/* --- HEADER SPESIFIKASI --- */} - - - Spesifikasi Teknis - {isLoadingMatrix && specsMatrix.length > 0 && ( - - - Updating... - - )} - - + + + Spesifikasi Teknis + {isLoadingMatrix && specsMatrix.length > 0 && ( + + + Updating... + + )} + + - {/* --- MATRIX SPEK --- */} {isLoadingMatrix && specsMatrix.length === 0 ? ( - - - Memuat data... - + + + Memuat data... + ) : specsMatrix.length > 0 ? ( - specsMatrix.map((row, rowIndex) => ( - - ( + + + {row.label} + + + {products.map((product, colIndex) => { + const val = product ? (row.values[String(product.sku)] || '-') : ''; + return ( + - {row.label} - - - {products.map((product, colIndex) => { - const val = product ? (row.values[String(product.sku)] || '-') : ''; - return ( - - {isLoadingMatrix && product && !row.values[String(product.sku)] ? ( - - ) : ( - {renderSpecValue(val)} - )} - - ); - })} - - )) + > + {isLoadingMatrix && product && !row.values[String(product.sku)] ? ( + + ) : ( + {renderSpecValue(val)} + )} + + ); + })} + + )) ) : ( - - Data spesifikasi belum tersedia untuk produk ini. - + + Data spesifikasi belum tersedia untuk produk ini. + )} -- cgit v1.2.3