From d885bbb998c31c809b0ff77faa4695c1335a3717 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Wed, 14 Jan 2026 17:47:45 +0700 Subject: (andri) dropdown menggunakan autocomplete & fix --- .../components/ProductComparisonModal.tsx | 201 +++++++++++++++++---- 1 file changed, 168 insertions(+), 33 deletions(-) diff --git a/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx b/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx index f2d7ca57..00eaebe5 100644 --- a/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx +++ b/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx @@ -16,6 +16,7 @@ import { Input, InputGroup, InputLeftElement, + InputRightElement, VStack, HStack, IconButton, @@ -25,11 +26,17 @@ import { List, ListItem, useToast, - Select, useOutsideClick } from '@chakra-ui/react'; -import { Search, Trash2 } from 'lucide-react'; +import { + AutoComplete, + AutoCompleteInput, + AutoCompleteItem, + AutoCompleteList, +} from '@choc-ui/chakra-autocomplete'; + +import { Search, Trash2, ChevronDown } from 'lucide-react'; // --- HELPER FORMATTING --- const formatPrice = (price: number) => { @@ -45,6 +52,27 @@ const renderSpecValue = (val: any) => { return String(val).replace(/<[^>]*>?/gm, ''); }; +const extractAttribute = (item: any) => { + if (item.attributes && item.attributes.length > 0) { + return item.attributes[0]; + } + + const textToParse = item.displayName || item.name || ''; + const match = textToParse.match(/\(([^)]+)\)$/); + + if (match) { + return match[1]; + } + + const code = item.code || item.defaultCode || ''; + return textToParse.replace(`[${code}]`, '').replace(code, '').trim(); +}; + +const getVariantLabel = (v: any) => { + const attr = extractAttribute(v); + return `${v.code} - ${attr}`; +}; + type Props = { isOpen: boolean; onClose: () => void; @@ -55,7 +83,6 @@ type Props = { 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([]); const [isLoadingMatrix, setIsLoadingMatrix] = useState(false); @@ -66,7 +93,8 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); - // --- REF & OUTSIDE CLICK --- + const [disableVariantFilter, setDisableVariantFilter] = useState(false); + const searchWrapperRef = useRef(null); useOutsideClick({ @@ -100,8 +128,10 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant id: v.id, code: v.default_code || v.code || v.sku, name: v.name || v.displayName || v.display_name, + displayName: v.displayName || v.name, price: v.price?.price || v.price || 0, - image: v.image + image: v.image, + attributes: v.attributes || [] })) || []; if (variantOptions.length === 0) { @@ -109,12 +139,21 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant id: targetId, code: displayCode, name: mainProduct.name, + displayName: mainProduct.displayName || mainProduct.name, price: activeItem.price?.price || activeItem.price || 0, - image: activeItem.image || mainProduct.image + image: activeItem.image || mainProduct.image, + attributes: [] }); } const displayName = activeItem.name || activeItem.displayName || mainProduct.name; + + const tempActiveVar = { + code: displayCode, + name: displayName, + displayName: displayName, + attributes: activeItem.attributes || [] + }; const productSlot1 = { id: targetId, @@ -123,7 +162,8 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant name: displayName, price: activeItem.price?.price || activeItem.price || mainProduct.lowest_price?.price || 0, image: activeItem.image || mainProduct.image, - variants: variantOptions + variants: variantOptions, + inputValue: getVariantLabel(tempActiveVar) }; setProducts((prev) => { @@ -181,7 +221,6 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant 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; @@ -190,7 +229,6 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant setIsSearching(true); try { const queryParam = searchQuery === '' ? '*' : searchQuery; - const params = new URLSearchParams({ source: 'compare', q: queryParam, @@ -199,7 +237,6 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant }); const res = await fetch(`/api/shop/search?${params.toString()}`); - if (res.ok) { const data = await res.json(); setSearchResults(data.response?.products || []); @@ -219,13 +256,30 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant // =========================================================================== // 4. HANDLERS // =========================================================================== - const handleVariantChange = (slotIndex: number, newId: string) => { + + const handleInputChange = (slotIndex: number, newValue: string) => { + setDisableVariantFilter(false); + const newProducts = [...products]; + if (newProducts[slotIndex]) { + newProducts[slotIndex] = { + ...newProducts[slotIndex], + inputValue: newValue + }; + setProducts(newProducts); + } + }; + + const handleVariantChange = (slotIndex: number, selectedValueString: string) => { const currentProduct = products[slotIndex]; if (!currentProduct || !currentProduct.variants) return; - const selectedVar = currentProduct.variants.find((v: any) => String(v.id) === String(newId)); + // Cari varian yang labelnya cocok dengan string input + const selectedVar = currentProduct.variants.find((v: any) => { + return getVariantLabel(v) === selectedValueString; + }); if (selectedVar) { + setDisableVariantFilter(true); const newProducts = [...products]; newProducts[slotIndex] = { ...currentProduct, @@ -234,7 +288,8 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant name: selectedVar.name, realCode: selectedVar.code, price: selectedVar.price, - image: selectedVar.image + image: selectedVar.image, + inputValue: getVariantLabel(selectedVar) }; setProducts(newProducts); } @@ -259,17 +314,11 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant if (!parentId) { try { - const checkParams = new URLSearchParams({ - source: 'upsell', - q: '*:*', - fq: `id:${idToAdd}` - }); - + 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)) { @@ -285,6 +334,15 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant } } + // Siapkan object sementara untuk generate Label (Memanfaatkan Extract Attribute) + const tempVar = { + code: codeToAdd, + name: nameToAdd, + displayName: searchItem.displayName || nameToAdd, // Pastikan ada displayName untuk diparsing + attributes: searchItem.attributes || [] + }; + const initialLabel = getVariantLabel(tempVar); + const newProductEntry = { id: idToAdd, sku: idToAdd, @@ -296,9 +354,12 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant id: idToAdd, code: codeToAdd, name: nameToAdd, + displayName: searchItem.displayName || nameToAdd, // Simpan untuk varian sendiri price: priceToAdd, - image: imageToAdd - }] + image: imageToAdd, + attributes: searchItem.attributes || [] + }], + inputValue: initialLabel }; setProducts((prev) => { @@ -330,8 +391,10 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant id: s.variantId || s.productIdI || s.id, code: s.defaultCode || s.default_code || s.code, name: s.displayName || s.name || s.nameS, + displayName: s.displayName, // Wajib disimpan agar extractAttribute bisa bekerja price: s.lowestPrice?.price || s.priceTier1V2F || 0, - image: s.image || s.imageS + image: s.image || s.imageS, + attributes: [] // Biarkan kosong, nanti dihandle extractAttribute via displayName })); allVariants.sort((a: any, b: any) => @@ -410,16 +473,88 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant - + {/* --- AUTOCOMPLETE DROPDOWN (FINAL FIX) --- */} + + handleVariantChange(index, val)} + value={product.inputValue} + > + + handleInputChange(index, e.target.value)} + onFocus={() => setDisableVariantFilter(true)} + /> + + + + + + {/* Dropdown List dengan Style Information.tsx */} + + {product.variants && product.variants.map((v: any, vIdx: number) => { + + // Gunakan Helper extractAttribute untuk mendapatkan teks spesifikasi + // Ini akan bekerja baik untuk Slot 1 (pakai attributes) + // maupun Slot Search (parsing displayName) + const attributeText = extractAttribute(v); + const label = `${v.code} - ${attributeText}`; + + return ( + + + {/* KIRI: KODE & ATRIBUT */} + + + {v.code} + + + {attributeText} + + + + {/* KANAN: HARGA */} + + {formatPrice(v.price)} + + + + ); + })} + + +