import React, { useEffect, useState, useRef } from 'react'; import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, Drawer, DrawerOverlay, DrawerContent, DrawerHeader, DrawerBody, DrawerCloseButton, Button, Text, Box, Badge, Grid, GridItem, Image, Input, InputGroup, InputLeftElement, InputRightElement, VStack, HStack, IconButton, Flex, Icon, Spinner, List, ListItem, useToast, useOutsideClick, useBreakpointValue, Divider, ScaleFade } from '@chakra-ui/react'; import { AutoComplete, AutoCompleteInput, AutoCompleteItem, AutoCompleteList, } from '@choc-ui/chakra-autocomplete'; import { Search, Trash2, ChevronDown, X, Plus } from 'lucide-react'; import AddToCart from './AddToCart'; // --- HELPER FORMATTING --- const formatPrice = (price: number) => { return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0, }).format(price); }; const renderSpecValue = (val: any) => { if (!val || val === '-') return '-'; // return String(val).replace(/<[^>]*>?/gm, ''); return String(val); }; 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; mainProduct: any; selectedVariant: any; }; const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant }: Props) => { const toast = useToast(); const isMobile = useBreakpointValue({ base: true, md: false }); const [products, setProducts] = useState<(any | null)[]>([null, null]); const [specsMatrix, setSpecsMatrix] = useState([]); const [isLoadingMatrix, setIsLoadingMatrix] = useState(false); // Search State const [activeSearchSlot, setActiveSearchSlot] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [disableVariantFilter, setDisableVariantFilter] = useState(false); const searchWrapperRef = useRef(null); useOutsideClick({ ref: searchWrapperRef, handler: () => { if (activeSearchSlot !== null) { setActiveSearchSlot(null); setSearchResults([]); } }, }); // =========================================================================== // 1. LOGIC UTAMA: ISI SLOT 1 // =========================================================================== useEffect(() => { if (isOpen && mainProduct) { let activeItem = selectedVariant; if (!activeItem && mainProduct.variants && mainProduct.variants.length > 0) { activeItem = mainProduct.variants[0]; } if (!activeItem) { 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, displayName: v.displayName || v.name, price: v.price?.price || v.price || 0, image: v.image, attributes: v.attributes || [] })) || []; if (variantOptions.length === 0) { variantOptions.push({ id: targetId, code: displayCode, name: mainProduct.name, displayName: mainProduct.displayName || mainProduct.name, price: activeItem.price?.price || activeItem.price || 0, 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, sku: targetId, realCode: displayCode, name: displayName, price: activeItem.price?.price || activeItem.price || mainProduct.lowest_price?.price || 0, image: activeItem.image || mainProduct.image, variants: variantOptions, inputValue: getVariantLabel(tempActiveVar) }; setProducts((prev) => { const newSlots = [...prev]; if (!newSlots[0] || String(newSlots[0].id) !== String(targetId)) { newSlots[0] = productSlot1; } return newSlots; }); } }, [isOpen, mainProduct, selectedVariant]); // =========================================================================== // 2. FETCH SPECS // =========================================================================== useEffect(() => { const validProducts = products.filter(p => p !== null); 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; 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); } }; fetchSpecs(); }, [products, isOpen]); // =========================================================================== // 3. SEARCH LOGIC // =========================================================================== useEffect(() => { const delayDebounceFn = setTimeout(async () => { if (searchQuery.length > 0 && searchQuery.length < 3) { setSearchResults([]); return; } if (activeSearchSlot === null) return; const attrSetId = selectedVariant?.attribute_set_id || mainProduct?.attribute_set_id; if (!attrSetId) { setSearchResults([]); setIsSearching(false); return; } setIsSearching(true); try { let queryParam = '*'; if (searchQuery !== '') { const words = searchQuery.trim().split(/\s+/); queryParam = words.map(w => `*${w}*`).join(' '); } const params = new URLSearchParams({ source: 'compare', q: queryParam, limit: '20', fq: `attribute_set_id_i:${attrSetId}`, group: 'false' }); 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); } }, 500); return () => clearTimeout(delayDebounceFn); }, [searchQuery, mainProduct, selectedVariant, activeSearchSlot]); // =========================================================================== // 4. HANDLERS // =========================================================================== 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) => { return getVariantLabel(v) === selectedValueString; }); if (selectedVar) { const isDuplicate = products.some((p, idx) => idx !== slotIndex && p !== null && String(p.id) === String(selectedVar.id) ); if (isDuplicate) { toast({ title: "Varian sudah ada", description: "Varian produk ini sudah ada di slot perbandingan lain.", status: "warning", position: "top", duration: 3000 }); return; } setDisableVariantFilter(true); const newProducts = [...products]; newProducts[slotIndex] = { ...currentProduct, id: selectedVar.id, sku: selectedVar.id, name: selectedVar.name, realCode: selectedVar.code, price: selectedVar.price, image: selectedVar.image, inputValue: getVariantLabel(selectedVar) }; setProducts(newProducts); } }; const handleAddProduct = async (searchItem: any, slotIndex: number) => { if (products.some(p => p !== null && String(p.id) === String(searchItem.id))) { toast({ title: "Produk sudah ada", status: "warning", position: "top" }); return; } setDisableVariantFilter(true); 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 tempVar = { code: codeToAdd, name: nameToAdd, displayName: searchItem.displayName || nameToAdd, attributes: searchItem.attributes || [] }; const initialLabel = getVariantLabel(tempVar); const newProductEntry = { id: idToAdd, sku: idToAdd, realCode: codeToAdd, name: nameToAdd, price: priceToAdd, image: imageToAdd, variants: [{ id: idToAdd, code: codeToAdd, name: nameToAdd, displayName: searchItem.displayName || nameToAdd, price: priceToAdd, image: imageToAdd, attributes: searchItem.attributes || [] }], inputValue: initialLabel }; 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, displayName: s.displayName, price: s.lowestPrice?.price || s.priceTier1V2F || 0, image: s.image || s.imageS, attributes: [] })); 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; }); } } } catch (error) { console.error("Gagal fetch variant lain:", error); } } }; const handleRemoveProduct = (index: number) => { const newProducts = [...products]; if (newProducts.length > 2) { newProducts.splice(index, 1); } else { newProducts[index] = null; } setProducts(newProducts); if (newProducts.every(p => p === null)) setSpecsMatrix([]); }; const handleAddSlot = () => { if (products.length < 4) { setProducts([...products, null]); } }; // --- RENDER SLOT ITEM (REUSABLE) --- const renderProductSlot = (product: any, index: number) => { let content; if (product) { const productPayload = { ...mainProduct, id: product.id, name: product.name, price: product.price, image: product.image }; content = ( {index !== 0 && ( } 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'; }} /> {product.price > 0 ? formatPrice(product.price) : 'Hubungi Admin'} {product.name} handleVariantChange(index, val)} value={product.inputValue} > handleInputChange(index, e.target.value)} onFocus={() => setDisableVariantFilter(true)} /> {product.variants && product.variants.map((v: any, vIdx: number) => { const attributeText = extractAttribute(v); const label = `${v.code} - ${attributeText}`; return ( {v.code} {attributeText} {formatPrice(v.price)} ); })} {/* [UBAH BAGIAN TOMBOL ACTION INI] */} {/* 1. TOMBOL KERANJANG */} {/* Bungkus dengan Box w="auto" agar ukurannya pas mengikuti icon */} {({ onClick, isLoading }) => ( } variant="outline" colorScheme="red" size="sm" onClick={onClick} isLoading={isLoading} isDisabled={!product.price} /> )} {/* 2. TOMBOL BELI SEKARANG */} {/* Bungkus dengan Box flex={1} agar mengisi sisa ruang (Sesuai kode lama) */} {({ onClick, isLoading }) => ( )} ); } else { // TAMPILAN KOSONG content = ( {index !== 0 && products.length > 2 && ( } size="xs" position="absolute" top={-2} right={-2} colorScheme="gray" variant="solid" borderRadius="full" zIndex={2} onClick={() => handleRemoveProduct(index)} /> )} { setActiveSearchSlot(index); setSearchQuery(''); }} onChange={(e) => setSearchQuery(e.target.value)} /> {activeSearchSlot === index && searchQuery && ( { setSearchQuery(''); setActiveSearchSlot(null); }}> )} {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.'} )} )} )} Empty Slot Produk Belum Ditambahkan ); } // Animasi Wrapper ScaleFade return ( {content} ); }; // --- RENDER MOBILE CONTENT --- const renderMobileContent = () => { const mobileProducts = products.slice(0, 2); return ( {/* Sticky Header */} {mobileProducts.map((p, i) => ( {renderProductSlot(p, i)} ))} Spesifikasi Teknis {/* Specs List with Loader per Line */} {specsMatrix.length > 0 ? ( }> {specsMatrix.map((row, rIdx) => ( {mobileProducts.map((p, cIdx) => { const val = p ? (row.values[String(p.sku)] || '-') : '-'; const isItemLoading = isLoadingMatrix && p && !row.values[String(p.sku)]; return ( {isItemLoading ? ( ) : ( )} {row.label} ) })} ))} ) : ( {isLoadingMatrix ? Memuat data... : "Data spesifikasi tidak tersedia"} )} ); }; // --- MAIN RENDER --- if (isMobile) { return ( Bandingkan Produk {renderMobileContent()} ); } // Tampilan Desktop (Modal 6XL) - DYNAMIC GRID const totalColumns = 1 + products.length + (products.length < 4 ? 1 : 0); const productColumnsCount = products.length + (products.length < 4 ? 1 : 0); const SLOT_WIDTH_PX = 200; const LABEL_WIDTH_PX = 200; const GAP_PX = 16; const PADDING_PX = 48; const calculatedWidth = LABEL_WIDTH_PX + (productColumnsCount * SLOT_WIDTH_PX) + (productColumnsCount * GAP_PX) + PADDING_PX + 'px'; return ( Bandingkan Produk Baru Detail Spesifikasi Produk yang kamu pilih {products.map((product, index) => ( {renderProductSlot(product, index)} ))} {/* Render Tombol Tambah Slot (Jika slot < 4) */} {products.length < 4 && ( {/* [TAMBAH] Animasi Wrapper ScaleFade untuk Tombol */} )} Spesifikasi Teknis {isLoadingMatrix && specsMatrix.length > 0 && ( Updating... )} {isLoadingMatrix && specsMatrix.length === 0 ? ( Memuat data... ) : specsMatrix.length > 0 ? ( specsMatrix.map((row, rowIndex) => ( {row.label} {products.map((product, colIndex) => { const val = product ? (row.values[String(product.sku)] || '-') : ''; return ( {isLoadingMatrix && product && !row.values[String(product.sku)] ? ( ) : ( )} ); })} {products.length < 4 && ( )} )) ) : ( Data spesifikasi belum tersedia untuk produk ini. )} ); }; export default ProductComparisonModal;