summaryrefslogtreecommitdiff
path: root/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src-migrate/modules/product-detail/components/ProductComparisonModal.tsx')
-rw-r--r--src-migrate/modules/product-detail/components/ProductComparisonModal.tsx201
1 files 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<any[]>([]);
const [isLoadingMatrix, setIsLoadingMatrix] = useState(false);
@@ -66,7 +93,8 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
const [searchResults, setSearchResults] = useState<any[]>([]);
const [isSearching, setIsSearching] = useState(false);
- // --- REF & OUTSIDE CLICK ---
+ const [disableVariantFilter, setDisableVariantFilter] = useState(false);
+
const searchWrapperRef = useRef<HTMLDivElement>(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
</Text>
</Box>
- <Select
- size="sm" borderRadius="md" fontSize="xs"
- value={product.id}
- onChange={(e) => handleVariantChange(index, e.target.value)}
- isDisabled={false} bg="white"
- >
- {product.variants && product.variants.map((v: any) => (
- <option key={v.id} value={v.id}>{v.code}</option>
- ))}
- </Select>
+ {/* --- AUTOCOMPLETE DROPDOWN (FINAL FIX) --- */}
+ <Box w="100%">
+ <AutoComplete
+ openOnFocus
+ disableFilter={disableVariantFilter}
+ onChange={(val) => handleVariantChange(index, val)}
+ value={product.inputValue}
+ >
+ <InputGroup size="sm">
+ <AutoCompleteInput
+ variant="outline"
+ fontSize="xs"
+ borderRadius="md"
+ placeholder="Cari Varian..."
+ value={product.inputValue}
+ onChange={(e) => handleInputChange(index, e.target.value)}
+ onFocus={() => setDisableVariantFilter(true)}
+ />
+ <InputRightElement h="100%" pointerEvents="none">
+ <Icon as={ChevronDown} color="gray.400" size={14} />
+ </InputRightElement>
+ </InputGroup>
+
+ {/* Dropdown List dengan Style Information.tsx */}
+ <AutoCompleteList fontSize="xs" maxH="250px" overflowY="auto" p={0}>
+ {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 (
+ <AutoCompleteItem
+ key={`option-${vIdx}`}
+ value={label}
+ textTransform="capitalize"
+ _selected={{ bg: 'red.50', borderLeft: '3px solid red' }}
+ _focus={{ bg: 'gray.50' }}
+ p={2}
+ borderBottom="1px dashed" // Style dashed border
+ borderColor="gray.200"
+ >
+ <Flex justify="space-between" align="start" w="100%">
+ {/* KIRI: KODE & ATRIBUT */}
+ <Box flex={1} mr={2} overflow="hidden">
+ <Text fontWeight="bold" fontSize="xs" color="gray.700">
+ {v.code}
+ </Text>
+ <Text
+ fontSize="xs"
+ color="gray.500"
+ noOfLines={1}
+ title={attributeText}
+ textTransform="capitalize"
+ >
+ {attributeText}
+ </Text>
+ </Box>
+
+ {/* KANAN: HARGA */}
+ <Text
+ color="red.600"
+ fontWeight="bold"
+ fontSize="xs"
+ whiteSpace="nowrap"
+ bg="red.50"
+ px={2}
+ py={0.5}
+ borderRadius="md"
+ h="fit-content"
+ >
+ {formatPrice(v.price)}
+ </Text>
+ </Flex>
+ </AutoCompleteItem>
+ );
+ })}
+ </AutoCompleteList>
+ </AutoComplete>
+ </Box>
<HStack spacing={2}>
<IconButton