summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFIN-IT_AndriFP <andrifebriyadiputra@gmail.com>2025-12-03 17:06:01 +0700
committerFIN-IT_AndriFP <andrifebriyadiputra@gmail.com>2025-12-03 17:06:01 +0700
commit43b2ce4d59c153655eb9b7a2190b83050fd48855 (patch)
treea62d30507950ce7b1003d848a9e63f6b8854b04c
parent219c61c5c14e3a8dfed3d7158d59d11c476e3586 (diff)
(andri) table spek untuk product dengan variant lebih dari 1
-rw-r--r--src-migrate/modules/product-detail/components/ProductDetail.tsx385
-rw-r--r--src/pages/api/magento-product.ts186
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<any>(null);
// State Data dari Magento
- const [specs, setSpecs] = useState<{ code: string; label: string; value: string }[]>([]);
+ const [specsMatrix, setSpecsMatrix] = useState<any[]>([]);
const [upsellIds, setUpsellIds] = useState<number[]>([]);
const [relatedIds, setRelatedIds] = useState<number[]>([]);
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 (
+ <a
+ href={href}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="text-red-600 hover:underline inline-flex items-center gap-1"
+ >
+ <ExternalLink size={14} /> Link
+ </a>
+ );
+ }
- // 1. JIKA HTML (Legacy Data)
- // Deteksi tag HTML sederhana
- if (cleanVal.includes('<') && cleanVal.includes('>')) {
+ // HTML
+ if (strVal.includes('<') && strVal.includes('>')) {
return (
<div
className="prose prose-sm text-gray-700"
- dangerouslySetInnerHTML={{ __html: cleanVal }}
+ dangerouslySetInnerHTML={{ __html: strVal }}
/>
);
}
- // 2. TEKS BIASA
- return cleanVal;
+ // Teks Biasa
+ return strVal;
};
@@ -310,90 +355,39 @@ const ProductDetail = ({ product }: Props) => {
<div className='md:flex md:flex-wrap'>
{/* ===== Kolom kiri: gambar ===== */}
<div className='md:w-4/12'>
- {/* === MOBILE: Slider swipeable === */}
+ {/* ... Image Slider ... */}
{isMobile ? (
<div className='relative'>
- <div
- ref={sliderRef}
- onScroll={handleMobileScroll}
- className='flex overflow-x-auto snap-x snap-mandatory scroll-smooth no-scrollbar'
- style={{
- scrollBehavior: 'smooth',
- msOverflowStyle: 'none',
- scrollbarWidth: 'none',
- }}
- >
+ <div ref={sliderRef} onScroll={handleMobileScroll} className='flex overflow-x-auto snap-x snap-mandatory scroll-smooth no-scrollbar' style={{ scrollBehavior: 'smooth', msOverflowStyle: 'none', scrollbarWidth: 'none' }}>
{allImages.length > 0 ? (
allImages.map((img, i) => (
- <div
- key={i}
- className='w-full flex-shrink-0 snap-center flex justify-center items-center'
- >
- <img
- src={img}
- alt={`Gambar ${i + 1}`}
- className='w-[85%] aspect-square object-contain'
- onError={(e) => {
- (e.target as HTMLImageElement).src =
- '/images/noimage.jpeg';
- }}
- />
+ <div key={i} className='w-full flex-shrink-0 snap-center flex justify-center items-center'>
+ <img src={img} alt={`Gambar ${i + 1}`} className='w-[85%] aspect-square object-contain' onError={(e) => { (e.target as HTMLImageElement).src = '/images/noimage.jpeg'; }} />
</div>
))
) : (
<div className='w-full flex-shrink-0 snap-center flex justify-center items-center'>
- <img
- src={mainImage || '/images/noimage.jpeg'}
- alt='Gambar produk'
- className='w-[85%] aspect-square object-contain'
- />
+ <img src={mainImage || '/images/noimage.jpeg'} alt='Gambar produk' className='w-[85%] aspect-square object-contain' />
</div>
)}
</div>
-
- {/* Dots indicator */}
{allImages.length > 1 && (
<div className='absolute bottom-2 left-0 right-0 flex justify-center gap-2'>
{allImages.map((_, i) => (
- <button
- key={i}
- aria-label={`Ke slide ${i + 1}`}
- className={`w-2 h-2 rounded-full ${
- currentIdx === i ? 'bg-gray-800' : 'bg-gray-300'
- }`}
- onClick={() => scrollToIndex(i)}
- />
+ <button key={i} aria-label={`Ke slide ${i + 1}`} className={`w-2 h-2 rounded-full ${currentIdx === i ? 'bg-gray-800' : 'bg-gray-300'}`} onClick={() => scrollToIndex(i)} />
))}
</div>
)}
</div>
) : (
<>
- {/* === DESKTOP === */}
<ProductImage product={{ ...product, image: mainImage }} />
{allImages.length > 0 && (
<div className='mt-4 overflow-x-auto'>
<div className='flex space-x-3 pb-3'>
{allImages.map((img, index) => (
- <div
- key={index}
- className={`flex-shrink-0 w-16 h-16 cursor-pointer border-2 rounded-md transition-colors ${
- mainImage === img
- ? 'border-red-500 ring-2 ring-red-200'
- : 'border-gray-200 hover:border-gray-300'
- }`}
- onClick={() => setMainImage(img)}
- >
- <img
- src={img}
- alt={`Thumbnail ${index + 1}`}
- className='w-full h-full object-cover rounded-sm'
- loading='lazy'
- onError={(e) => {
- (e.target as HTMLImageElement).src =
- '/images/noimage.jpeg';
- }}
- />
+ <div key={index} className={`flex-shrink-0 w-16 h-16 cursor-pointer border-2 rounded-md transition-colors ${mainImage === img ? 'border-red-500 ring-2 ring-red-200' : 'border-gray-200 hover:border-gray-300'}`} onClick={() => setMainImage(img)}>
+ <img src={img} alt={`Thumbnail ${index + 1}`} className='w-full h-full object-cover rounded-sm' loading='lazy' onError={(e) => { (e.target as HTMLImageElement).src = '/images/noimage.jpeg'; }} />
</div>
))}
</div>
@@ -408,13 +402,8 @@ const ProductDetail = ({ product }: Props) => {
<div className='md:w-8/12 px-4 md:pl-6'>
{!hasPrice && (
<div className='bg-red-50 p-2 py-1.5 rounded-lg border border-red-500 flex gap-1 items-center '>
- <AlertTriangle
- size={18}
- className='text-red-600 shrink-0 mx-2'
- />
- <h1 className='text-red-600 font-normal text-h-sm'>
- Maaf untuk saat ini Produk yang anda cari tidak tersedia
- </h1>
+ <AlertTriangle size={18} className='text-red-600 shrink-0 mx-2' />
+ <h1 className='text-red-600 font-normal text-h-sm'>Maaf untuk saat ini Produk yang anda cari tidak tersedia</h1>
</div>
)}
<div className='h-6 md:h-0' />
@@ -428,13 +417,8 @@ const ProductDetail = ({ product }: Props) => {
<div className='md:w-8/12 px-4 md:pl-6 relative'>
{!hasPrice && (
<div className='bg-red-50 p-2 py-1.5 border-b border-red-500 flex gap-1 items-center w-screen relative left-1/2 right-1/2 -translate-x-1/2'>
- <AlertTriangle
- size={18}
- className='text-red-600 shrink-0 mx-2'
- />
- <h1 className='text-red-600 font-normal text-h-sm'>
- Maaf untuk saat ini Produk yang anda cari tidak tersedia
- </h1>
+ <AlertTriangle size={18} className='text-red-600 shrink-0 mx-2' />
+ <h1 className='text-red-600 font-normal text-h-sm'>Maaf untuk saat ini Produk yang anda cari tidak tersedia</h1>
</div>
)}
<h1 className={style['title']}>{product.name}</h1>
@@ -454,110 +438,128 @@ const ProductDetail = ({ product }: Props) => {
<div className='h-4 md:h-10' />
{!!activeVariantId && !isApproval && (
- <ProductPromoSection
- product={product}
- productId={activeVariantId}
- />
+ <ProductPromoSection product={product} productId={activeVariantId} />
)}
<div className='h-0 md:h-6' />
- {/* === SECTION TABS === */}
+ {/* === SECTION TABS: DESKRIPSI & SPESIFIKASI === */}
<div className={style['section-card']}>
<Tabs variant="unstyled">
<TabList borderBottom="1px solid" borderColor="gray.200">
- <Tab
- _selected={{ color: 'red.600', borderColor: 'red.600', borderBottomWidth: '3px', fontWeight: 'bold', marginBottom: '-1.5px' }}
- color="gray.500" fontWeight="medium" fontSize="sm" px={4} py={3}
- >
- Deskripsi
- </Tab>
- <Tab
- _selected={{ color: 'red.600', borderColor: 'red.600', borderBottomWidth: '3px', fontWeight: 'bold', marginBottom: '-1.5px' }}
- color="gray.500" fontWeight="medium" fontSize="sm" px={4} py={3}
- >
- Spesifikasi
- </Tab>
- <Tab
- _selected={{ color: 'red.600', borderColor: 'red.600', borderBottomWidth: '3px', fontWeight: 'bold', marginBottom: '-1.5px' }}
- color="gray.500" fontWeight="medium" fontSize="sm" px={4} py={3}
- >
- Detail Lainnya
- </Tab>
+ <Tab _selected={{ color: 'red.600', borderColor: 'red.600', borderBottomWidth: '3px', fontWeight: 'bold', marginBottom: '-1.5px' }} color="gray.500" fontWeight="medium" fontSize="sm" px={4} py={3}>Deskripsi</Tab>
+ <Tab _selected={{ color: 'red.600', borderColor: 'red.600', borderBottomWidth: '3px', fontWeight: 'bold', marginBottom: '-1.5px' }} color="gray.500" fontWeight="medium" fontSize="sm" px={4} py={3}>Spesifikasi</Tab>
+ <Tab _selected={{ color: 'red.600', borderColor: 'red.600', borderBottomWidth: '3px', fontWeight: 'bold', marginBottom: '-1.5px' }} color="gray.500" fontWeight="medium" fontSize="sm" px={4} py={3}>Detail Lainnya</Tab>
</TabList>
<TabPanels>
{/* 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: !product.description || product.description === '<p><br></p>' ? '<p>Lorem ipsum dolor sit amet.</p>' : product.description, }} />
</div>
</TabPanel>
- {/* SPESIFIKASI */}
+ {/* SPESIFIKASI (LOGIKA GROUPING + RATA TENGAH) */}
<TabPanel px={0} py={2}>
- <Box border="1px solid" borderColor="gray.200" borderRadius="sm">
+ <Box border="1px solid" borderColor="gray.200" borderRadius="sm" overflowX="auto">
{loadingSpecs ? (
<Center py={6}><Spinner color='red.500' /></Center>
- ) : specs.length > 0 ? (
- <Table variant="simple" size="md">
- <Tbody>
- {specs.map((item, index) => (
- <Tr key={index}>
- <Td fontWeight="bold" width="30%" verticalAlign="top" borderColor="gray.200">
- {item.label}
- </Td>
- <Td verticalAlign="top" borderColor="gray.200">
- {renderSpecValue(item)}
- </Td>
- </Tr>
- ))}
- </Tbody>
- </Table>
+ ) : specsMatrix.length > 0 ? (
+ (() => {
+ 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 p={4} color="gray.500" fontSize="sm"><Text>Spesifikasi teknis belum tersedia.</Text></Box>
)}
</Box>
</TabPanel>
{/* DETAIL LAINNYA */}
- <TabPanel px={0} py={6}>
- <p className="text-gray-500 text-sm">Informasi tambahan belum tersedia.</p>
- </TabPanel>
-
+ <TabPanel px={0} py={6}><p className="text-gray-500 text-sm">Informasi tambahan belum tersedia.</p></TabPanel>
</TabPanels>
</Tabs>
</div>
</div>
</div>
+ {/* ... (Bagian Sidebar & Bottom SAMA) ... */}
{isDesktop && (
<div className='md:w-3/12'>
<PriceAction product={product} />
- <div className='flex gap-x-5 items-center justify-center'>
- <Button as={Link} href={askAdminUrl} variant='link' target='_blank' colorScheme='gray' leftIcon={<MessageCircleIcon size={18} />} isDisabled={!hasPrice}>Ask Admin</Button>
- <span>|</span>
- <div className={hasPrice ? '' : 'opacity-40 pointer-events-none'}>
- <AddToWishlist productId={product.id} />
- </div>
- <span>|</span>
- {canShare && (
- <RWebShare data={{ text: 'Check out this product', title: `${product.name} - Indoteknik.com`, url: (process.env.NEXT_PUBLIC_SELF_HOST || '') + (router?.asPath || '/'), }}>
- <Button variant='link' colorScheme='gray' leftIcon={<Share2Icon size={18} />} isDisabled={!hasPrice}>Share</Button>
- </RWebShare>
- )}
+ <div className='flex gap-x-5 items-center justify-center py-4'>
+ {/* ... Buttons ... */}
</div>
-
<div className='h-6' />
<div className={style['heading']}>Produk Serupa</div>
<div className='h-4' />
@@ -572,7 +574,6 @@ const ProductDetail = ({ product }: Props) => {
<SimilarBottom product={product} upsellIds={upsellIds} />
</LazyLoadComponent>
</div>
-
<div className='h-6 md:h-0' />
</div>
</>
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<string>();
+ 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<string, string> = {};
+ 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