From d23604144b9465dcf21d8c032802075f3b1429fb Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Thu, 27 Nov 2025 14:29:39 +0700 Subject: (andri) add icon --- public/images/estimasi.svg | 3 +++ public/images/garansi.svg | 5 +++++ public/images/produk_asli.svg | 3 +++ 3 files changed, 11 insertions(+) create mode 100644 public/images/estimasi.svg create mode 100644 public/images/garansi.svg create mode 100644 public/images/produk_asli.svg diff --git a/public/images/estimasi.svg b/public/images/estimasi.svg new file mode 100644 index 00000000..b4e1eb02 --- /dev/null +++ b/public/images/estimasi.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/garansi.svg b/public/images/garansi.svg new file mode 100644 index 00000000..e7ac6c59 --- /dev/null +++ b/public/images/garansi.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/produk_asli.svg b/public/images/produk_asli.svg new file mode 100644 index 00000000..2b4cdae5 --- /dev/null +++ b/public/images/produk_asli.svg @@ -0,0 +1,3 @@ + + + -- cgit v1.2.3 From 629133beeb7f5668b63db61e415cf844d513cde7 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Thu, 27 Nov 2025 14:30:30 +0700 Subject: (andri) view from magento --- .../product-detail/components/Information.tsx | 114 ++++++++++++------ .../product-detail/components/ProductDetail.tsx | 127 +++++++++++++++++---- 2 files changed, 185 insertions(+), 56 deletions(-) diff --git a/src-migrate/modules/product-detail/components/Information.tsx b/src-migrate/modules/product-detail/components/Information.tsx index a7a58cbc..b7d3401e 100644 --- a/src-migrate/modules/product-detail/components/Information.tsx +++ b/src-migrate/modules/product-detail/components/Information.tsx @@ -11,12 +11,11 @@ import Link from 'next/link'; import { useEffect, useRef, useState } from 'react'; import currencyFormat from '@/core/utils/currencyFormat'; -import { InputGroup, InputRightElement, Spinner } from '@chakra-ui/react'; +import { InputGroup, InputRightElement, SimpleGrid, Flex, Text, Box } from '@chakra-ui/react'; import { ChevronDownIcon } from '@heroicons/react/24/outline'; import Image from 'next/image'; import { formatToShortText } from '~/libs/formatNumber'; import { createSlug } from '~/libs/slug'; -import { getVariantSLA } from '~/services/productVariant'; import { IProductDetail } from '~/types/product'; import { useProductDetail } from '../stores/useProductDetail'; import useVariant from '../hook/useVariant'; @@ -30,7 +29,7 @@ type Props = { }; const Information = ({ product }: Props) => { - const { selectedVariant, setSelectedVariant, setSla, setActive, sla } = + const { selectedVariant, setSelectedVariant, setSla, sla } = useProductDetail(); const [inputValue, setInputValue] = useState( @@ -45,15 +44,6 @@ const Information = ({ product }: Props) => { const variantId = selectedVariant?.id; const { slaVariant, isLoading } = useVariant({ variantId }); - // let variantOptions = product?.variants; - - // const querySLA = useQuery({ - // queryKey: ['variant-sla', selectedVariant?.id], - // queryFn: () => getVariantSLA(selectedVariant?.id), - // enabled: !!selectedVariant?.id, - // }); - // const sla = querySLA?.data; - useEffect(() => { if (selectedVariant) { setInputValue( @@ -66,14 +56,13 @@ const Information = ({ product }: Props) => { }, [selectedVariant]); useEffect(() => { - if (isLoading){ + if (isLoading) { setSla(null); } if (slaVariant) { setSla(slaVariant); } - }, [slaVariant, isLoading]); - + }, [slaVariant, isLoading, setSla]); const handleOnChange = (vals: any) => { setDisableFilter(true); @@ -98,6 +87,15 @@ const Information = ({ product }: Props) => { setInputValue(e.target.value); }; + // STYLE CUSTOM UNTUK BARIS (Item Code, dll) + const rowStyle = { + backgroundColor: '#ffffff', + fontSize: '13px', + borderBottom: '1px dashed #e2e8f0', + padding: '8px 0', + marginBottom: '0px' + }; + return (
@@ -183,12 +181,15 @@ const Information = ({ product }: Props) => {
-
-
Item Code
+ {/* ITEM CODE */} +
+
Item Code
{selectedVariant?.code}
-
-
Manufacture
+ + {/* MANUFACTURE */} +
+
Manufacture
{!!product.manufacture.name ? ( { )}
-
-
Berat Barang
+ + {/* BERAT BARANG */} +
+
Berat Barang
{selectedVariant?.weight > 0 ? `${selectedVariant?.weight} Kg` : '-'}
-
-
Terjual
+ + {/* TERJUAL */} +
+
Terjual
{product.qty_sold > 0 ? formatToShortText(product.qty_sold) : '-'}
-
-
Persiapan Barang
- {isLoading && ( -
- -
- )} - {!isLoading &&
{sla?.sla_date}
} + + {/* === DETAIL INFORMASI PRODUK (Updated Layout) === */} +
+

Detail Informasi Produk

+ + {/* Perubahan: Spacing diperbesar menjadi 10 agar estimasi bergeser ke kanan */} + + {/* 1. Distributor Resmi */} + + Distributor Resmi + + Distributor Resmi + Jaminan Produk Asli + + + + {/* 2. Estimasi Penyiapan */} + + Estimasi Penyiapan + + Estimasi Penyiapan + {isLoading ? ( + + ) : ( + + {sla?.sla_date || '3 - 7 Hari'} + + )} + + + + {/* 3. Garansi Produk */} + + Garansi Produk + + Garansi Produk + 24 Bulan + + +
+
); }; -export default Information; +export default Information; \ No newline at end of file diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index e4ba2b2f..1bacd2e2 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -4,9 +4,22 @@ import Link from 'next/link'; import { useRouter } from 'next/router'; import { useEffect, useRef, useState, UIEvent } from 'react'; -import { Button } from '@chakra-ui/react'; +// Import komponen Chakra UI yang dibutuhkan +import { + Button, + Tabs, + TabList, + TabPanels, + Tab, + TabPanel, + Table, + Tbody, + Tr, + Td, + Box +} from '@chakra-ui/react'; + import { - AlertCircle, AlertTriangle, MessageCircleIcon, Share2Icon, @@ -62,7 +75,6 @@ const ProductDetail = ({ product }: Props) => { setIsApproval, isApproval, setSelectedVariant, - setSla, } = useProductDetail(); useEffect(() => { @@ -136,11 +148,15 @@ const ProductDetail = ({ product }: Props) => { const scrollToIndex = (i: number) => { const el = sliderRef.current; if (!el) return; - el.scrollTo({ left: i * el.clientWidth, behavior: 'smooth' }); + const elRef = sliderRef.current; + elRef.scrollTo({ left: i * elRef.clientWidth, behavior: 'smooth' }); setCurrentIdx(i); setMainImage(allImages[i] || ''); }; + console.log('detail product render'); + console.log('product: ', product); + return ( <>
@@ -195,7 +211,6 @@ const ProductDetail = ({ product }: Props) => { key={i} className='w-full flex-shrink-0 snap-center flex justify-center items-center' > - {/* gambar diperkecil */} {`Gambar { )}
- {/* Dots indicator */} {allImages.length > 1 && (
{allImages.map((_, i) => ( @@ -238,8 +252,6 @@ const ProductDetail = ({ product }: Props) => { <> {/* === DESKTOP: Tetap seperti sebelumnya === */} - - {/* Carousel horizontal (thumbnail) – hanya desktop */} {allImages.length > 0 && (
@@ -271,7 +283,6 @@ const ProductDetail = ({ product }: Props) => { )}
- {/* <<=== TUTUP kolom kiri */} {/* ===== Kolom kanan: info ===== */} {isDesktop && ( @@ -332,21 +343,89 @@ const ProductDetail = ({ product }: Props) => {
+ {/* === SECTION TABS: DESKRIPSI & SPESIFIKASI === */}
-

Informasi Produk

-
-
-

' - ? 'Belum ada deskripsi' - : product.description, - }} - /> -
+ + {/* Header Tabs */} + + + Deskripsi + + + Spesifikasi + + + Detail Lainnya + + + + + {/* PANEL 1: DESKRIPSI */} + +
+

' + ? '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

' + : product.description, + }} + /> +
+ + + {/* PANEL 2: SPESIFIKASI (Sesuai Gambar) */} + +

Informasi tambahan belum tersedia.

+
+ + {/* PANEL 3: DETAIL LAINNYA */} + +

Informasi tambahan belum tersedia.

+
+ + +
@@ -422,4 +501,4 @@ const ProductDetail = ({ product }: Props) => { ); }; -export default ProductDetail; +export default ProductDetail; \ No newline at end of file -- cgit v1.2.3 From a5e695f82e03577cc85c4a1dded9f6021f0235fc Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 28 Nov 2025 09:36:15 +0700 Subject: (andri) try get detail product from magento --- .../product-detail/components/ProductDetail.tsx | 114 ++++++++++++++++++--- src/pages/api/magento-product.ts | 114 +++++++++++++++++++++ 2 files changed, 216 insertions(+), 12 deletions(-) create mode 100644 src/pages/api/magento-product.ts diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index 1bacd2e2..b0950194 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -4,7 +4,7 @@ import Link from 'next/link'; import { useRouter } from 'next/router'; import { useEffect, useRef, useState, UIEvent } from 'react'; -// Import komponen Chakra UI yang dibutuhkan +// Import komponen Chakra UI import { Button, Tabs, @@ -16,7 +16,10 @@ import { Tbody, Tr, Td, - Box + Box, + Spinner, + Center, + Text } from '@chakra-ui/react'; import { @@ -58,6 +61,12 @@ const ProductDetail = ({ product }: Props) => { const { isDesktop, isMobile } = useDevice(); const router = useRouter(); const [auth, setAuth] = useState(null); + + // State untuk Spesifikasi dari Magento + const [specs, setSpecs] = useState<{ label: string; value: string }[]>([]); + const [loadingSpecs, setLoadingSpecs] = useState(false); + const [errorSpecs, setErrorSpecs] = useState(false); + useEffect(() => { try { setAuth(getAuth() ?? null); @@ -74,6 +83,7 @@ const ProductDetail = ({ product }: Props) => { activeVariantId, setIsApproval, isApproval, + selectedVariant, setSelectedVariant, } = useProductDetail(); @@ -99,12 +109,65 @@ const ProductDetail = ({ product }: Props) => { if (typeof auth === 'object') { setIsApproval(auth?.feature?.soApproval); } - const selectedVariant = + const variantInit = product?.variants?.find((variant) => variant.is_in_bu) || product?.variants?.[0]; - setSelectedVariant(selectedVariant); + setSelectedVariant(variantInit); }, []); + // ========================================================================= + // LOGIC FETCH: MENGGUNAKAN ID VARIANT + // ========================================================================= + useEffect(() => { + const fetchMagentoSpecs = async () => { + // MENGGUNAKAN ID VARIANT SESUAI REQUEST + const skuToFetch = selectedVariant?.id; + + if (!skuToFetch) return; + + setLoadingSpecs(true); + setErrorSpecs(false); + + try { + console.log("Fetching Magento specs via Proxy for Variant ID:", skuToFetch); + + // Pastikan dikonversi ke string + const endpoint = `/api/magento-product?sku=${encodeURIComponent(String(skuToFetch))}`; + + const response = await fetch(endpoint, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + } + }); + + if (!response.ok) { + console.warn(`Spec API status: ${response.status}`); + setSpecs([]); + return; + } + + const data = await response.json(); + + if (data.specs && Array.isArray(data.specs)) { + setSpecs(data.specs); + } else { + setSpecs([]); + } + + } catch (error) { + console.error("Gagal mengambil data spesifikasi:", error); + setErrorSpecs(true); + setSpecs([]); + } finally { + setLoadingSpecs(false); + } + }; + + fetchMagentoSpecs(); + }, [selectedVariant]); + + const allImages = (() => { const arr: string[] = []; if (product?.image) arr.push(product.image); @@ -154,9 +217,6 @@ const ProductDetail = ({ product }: Props) => { setMainImage(allImages[i] || ''); }; - console.log('detail product render'); - console.log('product: ', product); - return ( <>
@@ -192,7 +252,7 @@ const ProductDetail = ({ product }: Props) => {
{/* ===== Kolom kiri: gambar ===== */}
- {/* === MOBILE: Slider swipeable, tanpa thumbnail carousel === */} + {/* === MOBILE: Slider swipeable === */} {isMobile ? (
{ )}
+ {/* Dots indicator */} {allImages.length > 1 && (
{allImages.map((_, i) => ( @@ -250,7 +311,7 @@ const ProductDetail = ({ product }: Props) => {
) : ( <> - {/* === DESKTOP: Tetap seperti sebelumnya === */} + {/* === DESKTOP === */} {allImages.length > 0 && (
@@ -414,9 +475,38 @@ const ProductDetail = ({ product }: Props) => {
- {/* PANEL 2: SPESIFIKASI (Sesuai Gambar) */} - -

Informasi tambahan belum tersedia.

+ {/* PANEL 2: SPESIFIKASI DARI MAGENTO */} + + + {loadingSpecs ? ( +
+ +
+ ) : errorSpecs ? ( + + Gagal memuat data spesifikasi. Silakan coba lagi nanti. + + ) : specs.length > 0 ? ( + + + {specs.map((item, index) => ( + + + + + ))} + +
+ {item.label} + + {item.value} +
+ ) : ( + + Spesifikasi teknis belum tersedia untuk varian ID: {selectedVariant?.id} + + )} +
{/* PANEL 3: DETAIL LAINNYA */} diff --git a/src/pages/api/magento-product.ts b/src/pages/api/magento-product.ts new file mode 100644 index 00000000..c906079e --- /dev/null +++ b/src/pages/api/magento-product.ts @@ -0,0 +1,114 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { sku } = req.query; + + if (!sku) { + return res.status(400).json({ error: 'SKU is required' }); + } + + // Token Magento + const token = 'vxrtcjvztv1icgjzsui45de9kmwlz0lf'; + const baseUrl = 'https://pimdev.1211.my.id/rest/V1/products'; + + 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}`; + + // --- DEBUGGING LOG --- + console.log('Fetching URL:', finalUrl); + + // Request ke Product Endpoint + const response = await fetch(finalUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + }); + + 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 + }); + } + + const data = await response.json(); + + // ===================================================================== + // TAMBAHAN: FETCH LABEL ATRIBUT (z_*) + // ===================================================================== + + let specsWithLabels: any[] = []; + + // Cek apakah ada custom_attributes + if (data.custom_attributes) { + // Filter atribut yang kodenya dimulai dengan 'z' + const zAttributes = data.custom_attributes.filter((attr: any) => + attr.attribute_code.startsWith('z') + ); + + // Fetch detail label untuk setiap atribut secara paralel + specsWithLabels = await Promise.all( + zAttributes.map(async (attr: any) => { + try { + // Endpoint untuk ambil detail atribut (Label): /V1/products/attributes/{attributeCode} + const attrUrl = `${baseUrl}/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(); + // AMBIL NILAI 'default_frontend_label' + return { + label: attrData.default_frontend_label || attr.attribute_code, + value: attr.value + }; + } + } catch (err) { + console.error(`Failed to fetch label for ${attr.attribute_code}`); + } + + // Fallback: Jika gagal ambil label, format manual dari kode + const fallbackLabel = attr.attribute_code + .substring(1).replace(/_/g, ' ').trim(); // z_size_ml -> size ml + + return { + label: fallbackLabel, + value: attr.value + }; + }) + ); + } + + // Gabungkan data asli dengan data specs yang sudah ada labelnya + const responseData = { + ...data, + specs: specsWithLabels // Frontend tinggal pakai field ini + }; + + res.status(200).json(responseData); + + } catch (error) { + console.error('Proxy Server Error:', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +} \ No newline at end of file -- cgit v1.2.3 From 825d86bb50f48f9a21d740d474c0dddee858dffb Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Mon, 1 Dec 2025 11:31:30 +0700 Subject: (andri) show upsells product from magento in similarbottom product --- .../product-detail/components/ProductDetail.tsx | 183 +++++++++------------ .../product-detail/components/SimilarBottom.tsx | 55 +++++-- src-migrate/services/product.ts | 38 +++++ src/pages/api/magento-product.ts | 35 +++- 4 files changed, 187 insertions(+), 124 deletions(-) diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index b0950194..5f930117 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -22,11 +22,13 @@ import { Text } from '@chakra-ui/react'; +// Import Icons import { AlertTriangle, MessageCircleIcon, Share2Icon, } from 'lucide-react'; + import { LazyLoadComponent } from 'react-lazy-load-image-component'; import useDevice from '@/core/hooks/useDevice'; @@ -62,8 +64,9 @@ const ProductDetail = ({ product }: Props) => { const router = useRouter(); const [auth, setAuth] = useState(null); - // State untuk Spesifikasi dari Magento - const [specs, setSpecs] = useState<{ label: string; value: string }[]>([]); + // State Data dari Magento + const [specs, setSpecs] = useState<{ code: string; label: string; value: string }[]>([]); + const [upsellIds, setUpsellIds] = useState([]); const [loadingSpecs, setLoadingSpecs] = useState(false); const [errorSpecs, setErrorSpecs] = useState(false); @@ -105,6 +108,9 @@ const ProductDetail = ({ product }: Props) => { setAskAdminUrl(createdAskUrl); }, [router.asPath, product.manufacture.name, product.name, setAskAdminUrl]); + // ========================================================================= + // LOGIC INISIALISASI VARIANT (HANDLE NAVIGASI) + // ========================================================================= useEffect(() => { if (typeof auth === 'object') { setIsApproval(auth?.feature?.soApproval); @@ -112,60 +118,95 @@ const ProductDetail = ({ product }: Props) => { const variantInit = product?.variants?.find((variant) => variant.is_in_bu) || product?.variants?.[0]; + setSelectedVariant(variantInit); - }, []); + + // Reset data Magento saat produk berubah + setSpecs([]); + setUpsellIds([]); + + }, [product, auth]); // ========================================================================= - // LOGIC FETCH: MENGGUNAKAN ID VARIANT + // LOGIC FETCH: SPECS & UPSELLS // ========================================================================= useEffect(() => { - const fetchMagentoSpecs = async () => { - // MENGGUNAKAN ID VARIANT SESUAI REQUEST - const skuToFetch = selectedVariant?.id; + const fetchMagentoData = async () => { + // Gunakan ID Variant (SKU Odoo) + const idToFetch = selectedVariant?.id; - if (!skuToFetch) return; + if (!idToFetch) return; setLoadingSpecs(true); setErrorSpecs(false); try { - console.log("Fetching Magento specs via Proxy for Variant ID:", skuToFetch); + console.log("Fetching Magento data via Proxy for ID:", idToFetch); - // Pastikan dikonversi ke string - const endpoint = `/api/magento-product?sku=${encodeURIComponent(String(skuToFetch))}`; + const endpoint = `/api/magento-product?sku=${encodeURIComponent(String(idToFetch))}`; const response = await fetch(endpoint, { method: 'GET', - headers: { - 'Content-Type': 'application/json', - } + headers: { 'Content-Type': 'application/json' } }); if (!response.ok) { - console.warn(`Spec API status: ${response.status}`); + console.warn(`Magento API status: ${response.status}`); setSpecs([]); + setUpsellIds([]); return; } const data = await response.json(); + // 1. Simpan Data Spesifikasi if (data.specs && Array.isArray(data.specs)) { setSpecs(data.specs); } else { setSpecs([]); } + // 2. Simpan Data Up-Sells (ID) + if (data.upsell_ids && Array.isArray(data.upsell_ids)) { + setUpsellIds(data.upsell_ids); + } else { + setUpsellIds([]); + } + } catch (error) { - console.error("Gagal mengambil data spesifikasi:", error); + console.error("Gagal mengambil data Magento:", error); setErrorSpecs(true); setSpecs([]); + setUpsellIds([]); } finally { setLoadingSpecs(false); } }; - fetchMagentoSpecs(); - }, [selectedVariant]); + fetchMagentoData(); + }, [selectedVariant, product.id]); + + + // ========================================================================= + // HELPER: RENDER SPEC VALUE (SIMPLE TEXT/HTML ONLY) + // ========================================================================= + const renderSpecValue = (item: { code: string; label: string; value: string }) => { + const val = item.value; + if (!val) return '-'; + + // Cek apakah mengandung tag HTML sederhana (

, ,