diff options
| author | FIN-IT_AndriFP <andrifebriyadiputra@gmail.com> | 2026-01-09 14:35:06 +0700 |
|---|---|---|
| committer | FIN-IT_AndriFP <andrifebriyadiputra@gmail.com> | 2026-01-09 14:35:06 +0700 |
| commit | 13a6ccd99722464342942eeb7fbc73c2f0f27542 (patch) | |
| tree | 4b4340021a0e6d936cb35fe4511a81b07ad4f076 /src-migrate/modules | |
| parent | 983aae0a27ed93009964edf2cdf2c4d0f1ea072e (diff) | |
| parent | 6c2de1aa1cc655b1da525015ac0280fd4c72731f (diff) | |
fix merge
Diffstat (limited to 'src-migrate/modules')
| -rw-r--r-- | src-migrate/modules/product-detail/components/Information.tsx | 87 | ||||
| -rw-r--r-- | src-migrate/modules/product-detail/components/ProductDetail.tsx | 326 |
2 files changed, 280 insertions, 133 deletions
diff --git a/src-migrate/modules/product-detail/components/Information.tsx b/src-migrate/modules/product-detail/components/Information.tsx index b7d3401e..c565682f 100644 --- a/src-migrate/modules/product-detail/components/Information.tsx +++ b/src-migrate/modules/product-detail/components/Information.tsx @@ -9,6 +9,7 @@ import style from '../styles/information.module.css'; import dynamic from 'next/dynamic'; import Link from 'next/link'; import { useEffect, useRef, useState } from 'react'; +import axios from 'axios'; // <--- 1. TAMBAHAN IMPORT AXIOS import currencyFormat from '@/core/utils/currencyFormat'; import { InputGroup, InputRightElement, SimpleGrid, Flex, Text, Box } from '@chakra-ui/react'; @@ -44,6 +45,44 @@ const Information = ({ product }: Props) => { const variantId = selectedVariant?.id; const { slaVariant, isLoading } = useVariant({ variantId }); + const [warranties, setWarranties] = useState<Record<string, string>>({}); + const [loadingWarranty, setLoadingWarranty] = useState(false); + + useEffect(() => { + const fetchWarrantyDirectly = async () => { + if (!product?.variants || product.variants.length === 0) return; + + setLoadingWarranty(true); + try { + // Ambil semua SKU untuk dikirim ke API + const skus = product.variants.map((v) => v.id).join(','); + const mainSku = product.variants[0].id; + console.log("Fetching warranties for SKUs:", skus); + console.log("Main SKU:", mainSku); + + // Panggil API magento-product + const res = await axios.get('/api/magento-product', { + params: { skus, main_sku: mainSku } + }); + + // Simpan hasil ke state lokal + if (res.data && res.data.warranties) { + setWarranties(res.data.warranties); + } + console.log("Warranties API Response:", res); + console.log("Warranties fetched:", res.data.warranties); + } catch (error) { + console.error("Gagal ambil garansi:", error); + } finally { + setLoadingWarranty(false); + } + }; + + fetchWarrantyDirectly(); + }, [product]); + // ====================================================== + + useEffect(() => { if (selectedVariant) { setInputValue( @@ -64,23 +103,20 @@ const Information = ({ product }: Props) => { } }, [slaVariant, isLoading, setSla]); - const handleOnChange = (vals: any) => { - setDisableFilter(true); - let code = vals.replace(/\s-\s.*$/, '').trim(); - let variant = variantOptions.find((item) => item.code === code); - setSelectedVariant(variant); - setInputValue( - variant?.code + - (variant?.attributes[0] ? ' - ' + variant?.attributes[0] : '') - ); +const handleOnChange = (vals: any) => { + setDisableFilter(true); + let code = vals.replace(/\s-\s.*$/, '').trim(); + let variant = product?.variants.find((item) => item.code === code); + if (variant) { - const filteredOptions = product?.variants.filter( - (item) => item !== variant - ); - const newOptions = [variant, ...filteredOptions]; - setVariantOptions(newOptions); - } - }; + setSelectedVariant(variant); + setInputValue( + variant?.code + + (variant?.attributes[0] ? ' - ' + variant?.attributes[0] : '') + ); + setVariantOptions(product?.variants); + } + }; const handleOnKeyUp = (e: any) => { setDisableFilter(false); @@ -127,7 +163,11 @@ const Information = ({ product }: Props) => { </InputGroup> <AutoCompleteList> - {variantOptions.map((option, cid) => ( + {variantOptions + .sort((a: any, b: any) => { + return a.code.localeCompare(b.code, undefined, { numeric: true, sensitivity: 'base' }); + }) + .map((option, cid) => ( <AutoCompleteItem key={`option-${cid}`} value={ @@ -239,7 +279,6 @@ const Information = ({ product }: Props) => { <div className="mt-6 border-t pt-4"> <h2 className="font-bold text-gray-800 text-sm mb-4">Detail Informasi Produk</h2> - {/* Perubahan: Spacing diperbesar menjadi 10 agar estimasi bergeser ke kanan */} <SimpleGrid columns={{ base: 1, md: 3 }} spacing={10}> {/* 1. Distributor Resmi */} <Flex align="center" className="gap-3"> @@ -282,7 +321,17 @@ const Information = ({ product }: Props) => { /> <Box> <Text fontSize="11px" color="gray.500" lineHeight="short" mb="1px">Garansi Produk</Text> - <Text fontSize="12px" fontWeight="bold" color="gray.800" lineHeight="short">24 Bulan</Text> + + {/* Menggunakan Loading State & Data dari 'warranties' */} + {loadingWarranty ? ( + <Skeleton height="12px" width="80px" mt="2px" /> + ) : ( + <Text fontSize="12px" fontWeight="bold" color="gray.800" lineHeight="short"> + {selectedVariant && warranties[selectedVariant.id] + ? warranties[selectedVariant.id] + : '-'} + </Text> + )} </Box> </Flex> </SimpleGrid> diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index 58276f3c..dcd97ef1 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -2,7 +2,8 @@ import style from '../styles/product-detail.module.css'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { useEffect, useRef, useState, UIEvent } from 'react'; +// Import useMemo +import { useEffect, useRef, useState, UIEvent, useMemo } from 'react'; // Import komponen Chakra UI import { @@ -76,6 +77,7 @@ const ProductDetail = ({ product }: Props) => { const [specsMatrix, setSpecsMatrix] = useState<any[]>([]); const [upsellIds, setUpsellIds] = useState<number[]>([]); const [relatedIds, setRelatedIds] = useState<number[]>([]); + const [descriptionMap, setDescriptionMap] = useState<Record<string, string>>({}); const [loadingSpecs, setLoadingSpecs] = useState(false); @@ -176,12 +178,21 @@ const ProductDetail = ({ product }: Props) => { // 1. Specs Matrix (Processed Grouping) if (data.specsMatrix && Array.isArray(data.specsMatrix)) { - const processed = processMatrixData(data.specsMatrix); - setSpecsMatrix(processed); + const filteredMatrix = data.specsMatrix.filter((item: any) => { + const code = item.code || ''; + return !code.includes('z_brand'); + }); + + const processed = processMatrixData(filteredMatrix); + setSpecsMatrix(processed); } else { setSpecsMatrix([]); } + if (data.descriptions){ + setDescriptionMap(data.descriptions); + } + // 2. Upsell & Related if (data.upsell_ids && Array.isArray(data.upsell_ids)) setUpsellIds(data.upsell_ids); else setUpsellIds([]); @@ -320,14 +331,48 @@ const ProductDetail = ({ product }: Props) => { }; const scrollToIndex = (i: number) => { - const el = sliderRef.current; - if (!el) return; - const elRef = sliderRef.current; - elRef.scrollTo({ left: i * elRef.clientWidth, behavior: 'smooth' }); + const el = sliderRef.current; + if (!el) return; + el.scrollTo({ left: i * el.clientWidth, behavior: 'smooth' }); setCurrentIdx(i); setMainImage(allImages[i] || ''); }; + const sortedVariants = useMemo(() => { + if (!product?.variants) return []; + + return [...product.variants].sort((a, b) => { + const labelA = a.attributes && a.attributes.length > 0 + ? a.attributes.join(' - ') + : a.code || ''; + + const labelB = b.attributes && b.attributes.length > 0 + ? b.attributes.join(' - ') + : b.code || ''; + + const getNumber = (str: string) => { + const match = String(str).match(/(\d+(\.\d+)?)/); + return match ? parseFloat(match[0]) : null; + }; + + const numA = getNumber(labelA); + const numB = getNumber(labelB); + + if (numA !== null && numB !== null && numA !== numB) { + return numA - numB; + } + + return String(labelA).localeCompare(String(labelB), undefined, { + numeric: true, + sensitivity: 'base' + }); + }); + }, [product.variants]); + + const activeMagentoDesc = selectedVariant?.id ? descriptionMap[String(selectedVariant.id)] : ''; + const finalDescription = activeMagentoDesc || product.description || 'Deskripsi produk tidak tersedia.'; + const cleanDescription = finalDescription === '<p><br></p>' ? 'Deskripsi produk tidak tersedia.' : finalDescription; + return ( <> {/* 3. MODAL POPUP DIRENDER DISINI */} @@ -501,117 +546,170 @@ const ProductDetail = ({ product }: Props) => { {/* DESKRIPSI */} <TabPanel px={0} py={6}> <div className='overflow-x-auto text-sm text-gray-700'> - <div - className={style['description']} - dangerouslySetInnerHTML={{ - __html: - !product.description || product.description === '<p><br></p>' - ? '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>' - : product.description, - }} - /> + <div className={style['description']} dangerouslySetInnerHTML={{ __html: cleanDescription }} /> </div> </TabPanel> - {/* SPESIFIKASI (MATRIKS & SPLIT PIPA) */} + {/* SPESIFIKASI */} <TabPanel px={0} py={2}> - <Box border="1px solid" borderColor="gray.200" borderRadius="sm" overflowX="auto"> - {loadingSpecs ? ( - <Center py={6}><Spinner color='red.500' /></Center> - ) : specsMatrix.length > 0 ? ( - // Cek Single vs Multi Variant - (() => { - const isSingleVariant = product.variants.length === 1; - const globalAlign = isSingleVariant ? "left" : "center"; - - return ( - <Table variant="simple" size="md"> - <Thead bg="red.600"> - <Tr> - <Th - width={isSingleVariant ? "30%" : "20%"} - borderColor="whiteAlpha.300" - color="white" - fontSize="sm" - textTransform="none" - verticalAlign="middle" - > - Spesifikasi - </Th> - - {product.variants.map(v => ( - <Th - key={v.id} - borderColor="whiteAlpha.300" - color="white" - textAlign={globalAlign} - fontSize="sm" - textTransform="none" - verticalAlign="middle" - > - {isSingleVariant ? 'Detail' : (v.attributes && v.attributes.length > 0 ? v.attributes.join(' - ') : v.code)} - </Th> - ))} - </Tr> - </Thead> - <Tbody> - {specsMatrix.map((row, i) => { - - // CASE 1: GROUPING (Label punya ' : ') - if (row.type === 'group') { - return ( - <Tr key={i}> - {/* Header Group Kiri */} - <Td fontWeight="bold" borderColor="gray.200" fontSize="sm" verticalAlign="middle">{row.label}</Td> - - {/* Content Group Kanan */} - {product.variants.map(v => ( - <Td key={v.id} borderColor="gray.200" textAlign={globalAlign} fontSize="sm" verticalAlign="middle"> - <div className={`inline-block text-left`}> - <div className="flex flex-col gap-0"> - {row.children.map((child: any, idx: number) => { - const rawVal = child.values[v.id]; - if (!rawVal || rawVal === '-') return null; - - return ( - <div key={idx} className="grid grid-cols-[auto_auto] gap-x-2"> - <span className="font-semibold text-gray-600 whitespace-nowrap"> - {child.label}: - </span> - <span>{renderSpecValue(rawVal)}</span> - </div> - ); - })} - </div> - </div> - </Td> - ))} - </Tr> - ); - } - - // CASE 2: SINGLE ITEM - return ( - <Tr key={i}> - <Td fontWeight="bold" borderColor="gray.200" fontSize="sm" verticalAlign="middle">{row.label}</Td> - {product.variants.map(v => { - const rawValue = row.values[v.id] || '-'; - return ( - <Td key={v.id} borderColor="gray.200" textAlign={globalAlign} fontSize="sm" verticalAlign="middle"> - {renderSpecValue(rawValue)} - </Td> - ); - })} - </Tr> - ); - })} - </Tbody> - </Table> - ); - })() - ) : ( - <Box p={4} color="gray.500" fontSize="sm"><Text>Spesifikasi teknis belum tersedia.</Text></Box> - )} + <Box + border="1px solid" + borderColor="gray.200" + borderRadius="sm" + overflowX="auto" + overflowY="auto" + maxHeight="500px" + css={{ + '&::-webkit-scrollbar': { + width: '12px', + height: '12px', + }, + '&::-webkit-scrollbar-track': { + background: '#f1f1f1', + }, + '&::-webkit-scrollbar-thumb': { + backgroundColor: '#a0aec0', + borderRadius: '8px', + border: '4px solid transparent', + backgroundClip: 'content-box', + }, + '&::-webkit-scrollbar-thumb:hover': { + backgroundColor: '#718096', + }, + }} + > + {loadingSpecs ? ( + <Center py={6}><Spinner color='red.500' /></Center> + ) : specsMatrix.length > 0 ? ( + (() => { + const topHeaders: any[] = []; + const subHeaders: any[] = []; + const flatSpecs: any[] = []; + + specsMatrix.forEach(row => { + if (row.type === 'group') { + topHeaders.push({ + label: row.label, + type: 'group', + colSpan: row.children.length, + rowSpan: 1 + }); + row.children.forEach((child: any) => { + subHeaders.push(child); + flatSpecs.push(child); + }); + } else { + topHeaders.push({ + label: row.label, + type: 'single', + colSpan: 1, + rowSpan: 2 + }); + flatSpecs.push(row); + } + }); + + return ( + <Table variant="simple" size="md"> + <Thead bg="red.600" position="sticky" top={0} zIndex={20}> + {/* Baris 1: Header Utama */} + <Tr> + {topHeaders.map((th, idx) => ( + <Th + key={`top-${idx}`} + position={idx === 0 ? "sticky" : "static"} + left={idx === 0 ? 0 : undefined} + zIndex={idx === 0 ? 22 : 20} + boxShadow={idx === 0 ? "2px 0 5px -2px rgba(0,0,0,0.2)" : "none"} + + bg="red.600" + colSpan={th.colSpan} + rowSpan={th.rowSpan} + color="white" + textAlign="center" + fontSize="sm" + textTransform="none" + fontWeight="800" + letterSpacing="wide" + verticalAlign="middle" + borderBottom="none" + > + {th.label} + </Th> + ))} + </Tr> + + {/* Baris 2: Sub Header */} + <Tr> + {subHeaders.map((sub, idx) => { + const isFirstHeaderGroup = topHeaders[0]?.type === 'group'; + const shouldSticky = idx === 0 && isFirstHeaderGroup; + return ( + <Th + key={`sub-${idx}`} + position={shouldSticky ? "sticky" : "static"} + left={shouldSticky ? 0 : undefined} + zIndex={shouldSticky ? 21 : 1} + boxShadow={shouldSticky ? "2px 0 5px -2px rgba(0,0,0,0.2)" : "none"} + + color="white" + textAlign="center" + fontSize="xs" + textTransform="none" + verticalAlign="middle" + whiteSpace="nowrap" + bg="red.600" + pt={1} pb={1} + > + {sub.label} + </Th> + ); + })} + </Tr> + </Thead> + + <Tbody> + {sortedVariants.map((v, vIdx) => ( + <Tr key={v.id} bg={vIdx % 2 === 0 ? 'white' : 'gray.50'}> + {flatSpecs.map((spec, sIdx) => { + const rawValue = spec.values[v.id] || '-'; + const isFirstCol = sIdx === 0; + return ( + <Td + key={sIdx} + // === LOGIC STICKY DATA PERTAMA === + position={isFirstCol ? "sticky" : "static"} + left={isFirstCol ? 0 : undefined} + zIndex={isFirstCol ? 10 : 1} + bg={vIdx % 2 === 0 ? 'white' : 'gray.50'} + boxShadow={isFirstCol ? "2px 0 5px -2px rgba(0,0,0,0.1)" : "none"} + // ================================= + + borderColor="gray.200" + textAlign="center" + fontSize="sm" + verticalAlign="middle" + px={2} + py={3} + minW="100px" + maxW="150px" + whiteSpace="normal" + overflowWrap="break-word" + fontWeight={isFirstCol ? "bold" : "normal"} + > + {renderSpecValue(rawValue)} + </Td> + ); + })} + </Tr> + ))} + </Tbody> + </Table> + ); + })() + ) : ( + <Box p={4} color="gray.500" fontSize="sm"><Text>Spesifikasi teknis belum tersedia.</Text></Box> + )} </Box> </TabPanel> |
