import { useState, useRef, useEffect, useMemo, useCallback } from 'react'; import { Trash2, Plus, RotateCcw, Search, Save } from 'lucide-react'; import { useDispatch, useSelector } from 'react-redux'; import '../../Styles/Product/TurboAddForm.scss'; import { getAdmin, taxSelector } from '../../Features/Tax/Tax.js'; import { getSession } from '../../Services/Others.js'; import { isMobile } from 'react-device-detect'; import { ApplicationPreferences, getCommonAppPreference, } from '../../Features/BrachLogin/BranchLogin.js'; import { allProdSubCatDataSelector, bulkpostdata, getBrandData, getFieldSetupData, getHsnData, getProdCatData, getProdSubCatData, getProductTypeData, getQrcodeData, getUomData, onlineimages, postFieldSetup, prodCatDataSelector, prodSubCatDataSelector, prodTaxDataSelector, productTypeDataSelector, putProductData, uomDataSelector, } from '../../Features/ProductPage/ProductPage.js'; import { Messages } from '../../Components/Notifications/Messages.jsx'; import { changeSearchedData, getLayoutsearch, GlobalSearchData, } from '../../Features/BookingScreen/BookingData/BookingData.js'; import debounce from 'lodash.debounce'; import { MdOutlineAppRegistration } from 'react-icons/md'; import { IoClose } from 'react-icons/io5'; import { Form, Radio, Tooltip } from 'antd'; import { DatePicker, TimePicker } from 'antd'; import { DefaultModal } from '../../Components/Modal/DefaultModal.jsx'; import FormHeader from '../PageComponents/FormHeader.jsx'; import { DropDowns } from '../../Components/Forms/DropDown.jsx'; import Buttons from '../../Components/Forms/Buttons.jsx'; import { ArrowRightOutlined } from '@ant-design/icons'; import DefaultSettingPopover from './Utils/DefaultSettingPopover.jsx'; import CellSelect from './Utils/CellSelect.jsx'; import CellInput from './Utils/CellInput.jsx'; import dayjs from 'dayjs'; import EnhancedVoiceForm from './Utils/EnhancedVoiceForm.jsx'; import CropUpload from '../../Components/Forms/CropUpload.jsx'; import { uploadImage } from '../../Features/upload/upload.js'; import { FaRegEye } from 'react-icons/fa'; import { changeBreadCrumb } from '../../Features/AppPage/CenterPage.js'; import { BiBarcodeReader } from 'react-icons/bi'; import BarcodeScanner from '../BookingScreen/Components/UtillComponents/BarcodeScanner.jsx'; import BarcodeScannerTurbo from '../BookingScreen/Components/UtillComponents/BarcodeScannerTurbo.jsx'; import BarCodeScan from '../../Services/BarCodeScan.jsx'; import AddCategoryModal from './Utils/AddCategoryModal.jsx'; import { getConfigTypeData, postConfiguration } from '../../Features/ConfigMasterPage/ConfigMasterPage.js'; import AddSubCategoryModal from './Utils/AddSubCategoryModal.jsx'; import AddBrandModal from './Utils/AddBrandModal.jsx'; import AddTaxModal from './Utils/AddTaxModal.jsx'; const subDirectory = import.meta.env.ENV_BASE_URL; const items = [ { name: 'Home', link: `${subDirectory}app-page/home`, }, { name: 'Product', link: `${subDirectory}setting/product-master`, }, { name: 'Turbo Add', link: `${subDirectory}setting/product-master/turbo-add`, }, ]; // --- Constants --- const FIELD_NAMES = { productName: 'Product Name', uom: 'UOM', nou: 'No. of. Units', mrp: 'MRP', salePrice: 'Sale Price', category: 'Category', subCategory: 'Sub-Category', tax: 'Tax', }; const REQUIRED_FIELDS = [ 'productName', 'uom', 'nou', 'mrp', 'salePrice', 'category', 'tax', ]; const TOTAL_COLS = 8; // --- Validation --- const createValidationRules = (products) => ({ productName: (v, row) => { if (!v?.trim()) return 'Product Name is required'; // Duplicate check: productName + uom + nou const isDuplicate = products?.some( (p) => p.id !== row.id && p.productName?.trim().toLowerCase() === v.trim().toLowerCase() && p.uom === row.uom && p.nou === row.nou && p.category === row.category && p.subCategory === row.subCategory ); if (isDuplicate) return 'Product already exists'; return ''; }, uom: (v) => (!v ? 'UOM is required' : ''), nou: (v) => { if (!v || v.toString().trim() === '') return 'No. of. Units is required'; const num = parseFloat(v); return num < 0 ? 'No. of. Units cannot be negative' : ''; }, mrp: (v) => { if (!v || v.toString().trim() === '') return 'MRP is required'; const num = parseFloat(v); return num < 0 ? 'MRP cannot be negative' : ''; }, salePrice: (v, row) => { if (!v || v.toString().trim() === '') return 'Sale Price is required'; const num = parseFloat(v); if (num < 0) return 'Sale Price cannot be negative'; if (row?.mrp && num > parseFloat(row.mrp)) return 'Sale Price cannot be greater than MRP'; return ''; }, discountLimit: (v, row) => { const num = parseFloat(v); if (row.discountType === 'P' && num >= 100) return 'Percentage must be less than 100'; if (row.discountType === 'F' && num >= parseFloat(row.salePrice || 0)) return 'Fixed must be less than Sale Price'; return ''; }, imei1: (v) => (v && !/^\d{15}$/.test(v) ? 'IMEI must be 15 digits' : ''), imei2: (v) => (v && !/^\d{15}$/.test(v) ? 'IMEI must be 15 digits' : ''), macId: (v) => v && !/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.test(v) ? 'Invalid MacId' : '', }); // --- Main Form --- const TurboAddForm = () => { const dispatch = useDispatch(); // Redux selectors const taxRef = useRef(); const brandRef = useRef(); const categoryRef = useRef(); const subCategoryRef = useRef(); const scannerRef = useRef(); const prodTaxData = useSelector(prodTaxDataSelector); const taxData = useSelector(taxSelector); const UomData = useSelector(uomDataSelector); const productTypeData = useSelector(productTypeDataSelector); const ApplicationPreferenceData = useSelector(ApplicationPreferences); const categoryId = ApplicationPreferenceData?.find( (p) => p?.PreferredCatName?.toLowerCase() === 'product form fields' )?.PreferredCatId; const prodCategories = useSelector(prodCatDataSelector); const prodSubCategories = useSelector(prodSubCatDataSelector); const allProdSubCategories = useSelector(allProdSubCatDataSelector); const searchText = useSelector(GlobalSearchData); // Session data const AppId = useMemo(() => getSession('AppId'), []); const CompId = useMemo(() => getSession('CompId'), []); const BranchId = useMemo(() => getSession('BranchId'), []); const UserId = useMemo(() => getSession('UserId'), []); // State const [openTaxModal, setOpenTaxModal] = useState(false); const [openCategoryModal, setOpenCategoryModal] = useState(false); const [openSubCategoryModal, setOpenSubCategoryModal] = useState(false); const [openBrandModal, setOpenBrandModal] = useState(false); const [imageCategoryUrl, setImageCategoryUrl] = useState(''); const [voiceData, setVoiceData] = useState(null); const [messageType, setMessageType] = useState(null); const [messageData, setMessageData] = useState(null); const [products, setProducts] = useState([]); const [selectedCell, setSelectedCell] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [errors, setErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const [fieldSetup, setFieldSetup] = useState(false); const [screenWidth, setScreenWidth] = useState(window.innerWidth); const [selectedFields, setSelectedFields] = useState([]); const [defaultCategories, setDefaultCategories] = useState([]); const [selectedDefaultCategory, setSelectedDefaultCategory] = useState(null); const [defaultSubCategories, setDefaultSubCategories] = useState([]); const [selectedDefaultSubCategory, setSelectedDefaultSubCategory] = useState(null); const [defaultBrands, setDefaultBrands] = useState([]); const [selectedDefaultBrand, setSelectedDefaultBrand] = useState(null); const [tableFieldPreferences, setTableFieldPreferences] = useState([]); const [hsnDetails, setHsnDetails] = useState([]); const [selectedHsn, setSelectedHsn] = useState(''); const [selectedImage, setSelectedImage] = useState(null); const [onlineImage, setOnlineImage] = useState(''); const [imageModal, setImageModal] = useState(false); const [uploadImageModal, setUploadImageModal] = useState(false); const [onlineImageData, setOnlineImageData] = useState([]); const [imageModalRowIndex, setImageModalRowIndex] = useState(null); const [openScanner, setOpenScanner] = useState(false); console.log(imageModalRowIndex, 'onlineImage'); const hasField = (label) => tableFieldPreferences?.some((f) => f.label === label && f.access === 'Y'); // Defaults const [defaults, setDefaults] = useState({ category: null, subCategory: null, brand: null, tax: null, uom: null, availableSubCategories: null, availableBrands: null, }); const debouncedProductLookup = useRef(); const debouncedHSNLookup = useRef(); const formRef = useRef(null); const defaultsSetRef = useRef({ category: false, subCategory: false, tax: false, uom: false, }); const tableRef = useRef(null); const validationRules = useMemo( () => createValidationRules(products), [products] ); // Memoized UOM preference const { UomPreference, fieldPreferences, productDtlsFields, qrAndTokenDtlsFields, priceDtlsFields, hsnAndMoreDtlsFields, dateAndTimeDtlsFields, } = useMemo(() => { // UOM Preference const uomPref = ApplicationPreferenceData?.find( (p) => p?.PreferredCatName === 'Product Uom' )?.PreferenceCatDetails; // Field Preferences const fieldsPref = ApplicationPreferenceData?.find( (p) => p?.PreferredCatName?.toLowerCase() === 'product form fields' )?.PreferenceCatDetails; const UomPreference = UomData?.filter((item) => uomPref?.some?.( (e) => e?.PreferredSubCatName?.toLowerCase() === item?.ConfigName?.toLowerCase() && e?.PreferredStatus === 'Y' ) ); const fieldPreferences = fieldsPref?.filter( (e) => e?.PreferredStatus === 'Y' ); // Category mapping const categoryMap = { productDtls: [ 'Sub Category', 'Brand', 'Product Image', 'EMI Allowed', 'WholeSale Price', ], qrAndTokenDtls: [ 'Auto Generate QRCode', 'Stock Maintenance', 'Token Maintenance', 'Amount Per Piece', ], priceDtls: [ 'Product Type', 'Tax', 'Cess', 'Discount Type', 'Discount Limit', 'Add Variant', ], hsnAndMoreDtls: [ 'HSN', 'Part Number', 'Rack', 'Batch Number', 'Model Number', 'IMEI 1', 'IMEI 2', 'Serial Number', 'MacId', ], dateAndTimeDtls: [ 'Manufacture Date', 'Expire Date', 'Expiry Notification Days', 'Available From', 'Available To', ], }; // Build reverse map const reverseMap = Object.entries(categoryMap).reduce( (acc, [key, values]) => { values.forEach((val) => { acc[val] = key; }); return acc; }, {} ); // Build result fields const result = { productDtlsFields: [], qrAndTokenDtlsFields: [], priceDtlsFields: [], hsnAndMoreDtlsFields: [], dateAndTimeDtlsFields: [], }; fieldPreferences?.forEach((field) => { const key = reverseMap[field?.PreferredSubCatName]; if (key) { result[key + 'Fields'].push(field.PreferredSubCatName); } }); return { UomPreference, fieldPreferences, ...result, }; }, [ApplicationPreferenceData, UomData]); // Memoized select options const selectOptions = useMemo( () => ({ uom: UomPreference?.map((uom) => ({ label: uom?.ConfigName, value: uom?.ConfigId, })) || [], tax: taxData?.map((tax) => ({ label: `${tax?.TaxIdName} - ${tax?.TaxPercentage}%`, value: tax?.TaxId, name: tax?.TaxIdName, percentage: tax?.TaxPercentage, })) || [], categories: prodCategories?.map((cat) => ({ label: cat?.ConfigName, value: cat?.ConfigId, })) || [], subCategories: prodSubCategories ?.map((subCat) => ({ label: subCat?.ConfigName, value: subCat?.ConfigId, })) .filter((item) => item.label) || [], }), [UomPreference, taxData, prodCategories, prodSubCategories] ); console.log(selectOptions?.tax, taxData, "selectOptions tax"); // Filtered products for search const filteredProducts = useMemo(() => { if (!searchTerm.trim()) return products; const term = searchTerm.toLowerCase(); return products.filter( (p) => p?.productName?.toLowerCase()?.includes(term) || p?.barcode?.toLowerCase()?.includes(term) ); }, [products, searchTerm]); console.log( productTypeData ?.filter((pt) => pt?.ConfigName !== 'Others') ?.map((pt) => ({ label: pt.ConfigName, value: pt.ConfigId })), 'productTypeData' ); // --- Effects: Data fetching and defaults --- useEffect(() => { dispatch(getUomData()); dispatch(getAdmin({ AppId, CompId })); dispatch(getProdCatData({ AppId })); dispatch(getProductTypeData()); dispatch(getCommonAppPreference(AppId)); }, [AppId, CompId, dispatch]); useEffect(() => { const setGeneralDefaults = async () => { setDefaultCategories( prodCategories?.map((cat) => ({ label: cat?.ConfigName, value: cat?.ConfigId, })) || [] ); if (prodCategories?.length > 0 && !defaultsSetRef.current.category) { const generalCat = prodCategories.find( (c) => c?.ConfigName?.toLowerCase() === 'general' ); if (generalCat) { defaultsSetRef.current.category = true; try { const response = await dispatch( getProdSubCatData({ ConfigId: generalCat.ConfigId }) ).unwrap(); const subCategories = response?.data?.data || []; const generalSubCat = subCategories.find( (s) => s?.ConfigName?.toLowerCase() === 'general' ); let availableBrands = []; if (generalSubCat?.ConfigId) { const brandResponse = await dispatch( getBrandData({ ConfigId: generalSubCat.ConfigId }) ).unwrap(); availableBrands = brandResponse?.data?.data || []; } setDefaults((prev) => ({ ...prev, category: generalCat.ConfigId, subCategory: generalSubCat?.ConfigId || null, availableSubCategories: subCategories .filter( (subCat) => subCat.NumFId === parseInt(generalCat.ConfigId) && !!subCat?.ConfigName ) .map((subCat) => ({ label: subCat?.ConfigName, value: subCat.ConfigId, })), availableBrands: availableBrands .filter( (brand) => brand.NumFId === parseInt(generalSubCat.ConfigId) && !!brand?.ConfigName ) .map((brand) => ({ label: brand?.ConfigName, value: brand.ConfigId, })), })); } catch (error) { console.error('Failed to set defaults', error); } } } }; setGeneralDefaults(); }, [prodCategories, dispatch]); useEffect(() => { if (taxData?.length > 0 && !defaultsSetRef.current.tax) { const nilTax = taxData.find( (t) => t?.TaxPercentage === 0 && t?.TaxIdName === 'NIL' ); if (nilTax) { setDefaults((prev) => ({ ...prev, tax: nilTax?.TaxId })); defaultsSetRef.current.tax = true; } } }, [taxData]); useEffect(() => { if (UomPreference?.length > 0 && !defaultsSetRef.current.uom) { const pcsDt = UomPreference.find((u) => u?.ConfigName === 'PCS'); if (pcsDt) { setDefaults((prev) => ({ ...prev, uom: pcsDt?.ConfigId })); defaultsSetRef.current.uom = true; } } }, [UomPreference]); useEffect(() => { getFieldSetup(); }, [categoryId]); const getFieldSetup = async () => { try { const response = await dispatch( getFieldSetupData({ AppId, CompId, BranchId, categoryId, Type: 'GB' }) ).unwrap(); if (response?.data?.statusCode === 1) { console.log(response?.data?.data?.[0]?.ConfigDtl, 'Field Setup Data'); setSelectedFields( response?.data?.data?.[0]?.ConfigDtl?.filter( (c) => c.ConfigId && c.Access === 'Y' )?.map((c) => c.ConfigId) || [] ); setTableFieldPreferences( response?.data?.data?.[0]?.ConfigDtl?.map((c) => ({ value: c.ConfigId, label: c.ConfigName, access: c.Access, })) || [] ); } } catch (error) { console.error('Error fetching field setup:', error); } }; // --- Validation --- const validateField = useCallback( (id, field, value, rowData = {}) => { const validator = validationRules[field]; const error = validator ? validator(value, rowData) : ''; setErrors((prev) => ({ ...prev, [`${id}-${field}`]: error })); return !error; }, [validationRules] ); const validateRow = useCallback( (rowData) => { const rowErrors = {}; let isValid = true; REQUIRED_FIELDS.forEach((field) => { const error = validationRules[field]?.(rowData[field], rowData) || ''; if (error) { rowErrors[`${rowData.id}-${field}`] = error; isValid = false; } }); setErrors((prev) => ({ ...prev, ...rowErrors })); return isValid; }, [validationRules] ); // --- Default Setting Handler --- const handleDefaultSetting = useCallback( async (field, selectedValue, applyToAll) => { // Step 1: Always update raw default field immediately setDefaults((prev) => ({ ...prev, [field]: selectedValue })); let newDefaults = { ...defaults, [field]: selectedValue }; let updatedProducts = products; // Base update when applyToAll is true if (applyToAll && products?.length > 0) { updatedProducts = products?.map((product) => { if ( field === 'subCategory' && product.category !== selectedDefaultCategory ) return product; if ( field === 'brand' && product.subCategory !== selectedDefaultSubCategory ) return product; const updatedProduct = { ...product, [field]: selectedValue }; switch (field) { case 'uom': { const u = selectOptions.uom.find( (x) => x.value === selectedValue ); updatedProduct.uomName = u?.label || ''; break; } case 'category': { const c = selectOptions.categories.find( (x) => x.value === selectedValue ); updatedProduct.categoryName = c?.label || ''; updatedProduct.subCategory = ''; updatedProduct.subCategoryName = ''; updatedProduct.brand = ''; updatedProduct.brandName = ''; break; } case 'subCategory': { const sc = product.availableSubCategories?.find( (x) => x.value === selectedValue ); updatedProduct.subCategoryName = sc?.label || ''; updatedProduct.brand = ''; updatedProduct.brandName = ''; break; } case 'brand': { const b = product.availableBrands?.find( (x) => x.value === selectedValue ); updatedProduct.brandName = b?.label || ''; break; } case 'tax': { const t = selectOptions.tax.find( (x) => x.value === selectedValue ); updatedProduct.taxName = t?.label || ''; break; } default: break; } return updatedProduct; }); } // Helper: fetch subcategories const fetchSubCategories = async (categoryId) => { try { const response = await dispatch( getProdSubCatData({ ConfigId: categoryId }) ).unwrap(); if (response?.status !== 200) return { availableSubCategories: [], generalSubCat: null }; const subCategories = response?.data?.data || []; const availableSubCategories = subCategories .filter((s) => s.NumFId === parseInt(categoryId) && !!s?.ConfigName) .map((s) => ({ label: s.ConfigName, value: s.ConfigId })); const generalSubCat = availableSubCategories.find( (s) => s.label?.toLowerCase() === 'general' ); return { availableSubCategories, generalSubCat }; } catch (err) { console.error('Error fetching subcategories:', err); return { availableSubCategories: [], generalSubCat: null }; } }; // Helper: fetch brands const fetchBrands = async (subCategoryId) => { try { const response = await dispatch( getBrandData({ ConfigId: subCategoryId }) ).unwrap(); if (response?.status !== 200) return []; return ( response?.data?.data ?.filter( (b) => b.NumFId === parseInt(subCategoryId) && !!b?.ConfigName ) ?.map((b) => ({ label: b?.ConfigName, value: b.ConfigId })) || [] ); } catch (err) { console.error('Error fetching brands:', err); return []; } }; // Handle CATEGORY change → fetch subcategories (+auto "General") if (field === 'category' && selectedValue) { const { availableSubCategories, generalSubCat } = await fetchSubCategories(selectedValue); let availableBrands = []; if (generalSubCat) { availableBrands = await fetchBrands(generalSubCat.value); } newDefaults = { ...newDefaults, availableSubCategories, subCategory: generalSubCat?.value || null, availableBrands, brand: null, }; if (applyToAll) { updatedProducts = updatedProducts.map((p) => ({ ...p, availableSubCategories, subCategory: generalSubCat?.value || '', subCategoryName: generalSubCat?.label || '', availableBrands, brand: '', brandName: '', })); } handleDefaultCategoryChange(selectedValue); } // Handle SUBCATEGORY change → fetch brands if (field === 'subCategory' && selectedValue) { const availableBrands = await fetchBrands(selectedValue); newDefaults = { ...newDefaults, availableBrands, brand: null, }; if (applyToAll) { updatedProducts = updatedProducts.map((p) => { if ( field === 'subCategory' && p.category !== selectedDefaultCategory ) return p; return { ...p, availableBrands, brand: '', brandName: '', }; }); } handleDefaultSubCategoryChange(selectedValue); } if (field === 'brand' && selectedValue) setSelectedDefaultBrand(selectedValue); // Final state updates if (applyToAll) setProducts(updatedProducts); setDefaults(newDefaults); setMessageType('success'); setMessageData( `Default ${field.toUpperCase()} set successfully${applyToAll ? ' and applied to all rows' : ''}` ); }, [products, selectOptions, dispatch, defaults] ); const handleDefaultCategoryChange = async (selectedCategory) => { setSelectedDefaultCategory(selectedCategory); setSelectedDefaultSubCategory(null); setSelectedDefaultBrand(null); const subCategoryResponse = await dispatch( getProdSubCatData({ ConfigId: selectedCategory }) )?.unwrap(); setDefaultSubCategories( subCategoryResponse?.data?.data ?.filter((sub) => sub.NumFId === selectedCategory && !!sub.ConfigName) ?.map((sub) => ({ label: sub.ConfigName, value: sub.ConfigId })) ); }; const handleDefaultSubCategoryChange = async (selectedSubCategory) => { setSelectedDefaultSubCategory(selectedSubCategory); setSelectedDefaultBrand(null); const brandResponse = await dispatch( getBrandData({ ConfigId: selectedSubCategory }) )?.unwrap(); setDefaultBrands( brandResponse?.data?.data ?.filter( (brand) => brand.NumFId === selectedSubCategory && !!brand.ConfigName ) ?.map((brand) => ({ label: brand.ConfigName, value: brand.ConfigId })) ); }; // --- Handlers --- const handleInputChange = useCallback( (id, field, value) => { setProducts((prev) => prev.map((p) => { if (p.id !== id) return p; let updatedRow = { ...p, [field]: value }; // Update label fields if (field === 'uom') { const selectedUOM = selectOptions.uom.find( (u) => u.value === value ); updatedRow.uomName = selectedUOM?.label || ''; } else if (field === 'category') { const selectedCategory = selectOptions.categories.find( (c) => c.value === value ); updatedRow.categoryName = selectedCategory?.label || ''; } else if (field === 'subCategory') { const selectedSubCategory = p.availableSubCategories?.find( (s) => s.value === value ); updatedRow.subCategoryName = selectedSubCategory?.label || ''; } else if (field === 'tax') { const selectedTax = selectOptions.tax.find( (t) => t.value === value ); updatedRow.taxName = selectedTax?.label || ''; } else if (field === 'mrp') { updatedRow.salePrice = ''; setErrors((prevErrors) => { const { [`${id}-salePrice`]: _, ...rest } = prevErrors; return rest; }); } // Duplicate check for productName, uom, nou if ( ['productName', 'uom', 'nou', 'category', 'subCategory'].includes( field ) ) { const trimmedName = field === 'productName' ? value?.trim().toLowerCase() : updatedRow.productName?.trim().toLowerCase(); const uomValue = field === 'uom' ? value : updatedRow.uom; const nouValue = field === 'nou' ? value : updatedRow.nou; const categoryValue = field === 'category' ? value : updatedRow.category; const subCategoryValue = field === 'subCategory' ? value : updatedRow.subCategory; const duplicate = products.some( (prod) => prod.id !== id && prod.productName?.trim().toLowerCase() === trimmedName && prod.uom === uomValue && prod.nou === nouValue && prod.category === categoryValue && prod.subCategory === subCategoryValue ); if (duplicate && trimmedName) { setErrors((prev) => ({ ...prev, [`${id}-productName`]: 'Product already exists', })); } else { setErrors((prev) => { const { [`${id}-productName`]: _, ...rest } = prev; return rest; }); } } setTimeout(() => validateField(id, field, value, updatedRow), 0); if (['category', 'subCategory'].includes(field)) { setTimeout( () => validateField( id, 'productName', updatedRow.productName, updatedRow ), 0 ); } return updatedRow; }) ); }, [ validateField, selectOptions.uom, selectOptions.categories, selectOptions.tax, products, ] ); const handleCategoryChange = useCallback( async (productId, categoryId) => { const selectedCategory = selectOptions.categories.find( (c) => c.value === categoryId ); // Reset product fields handleInputChange(productId, 'category', categoryId); handleInputChange( productId, 'categoryName', selectedCategory?.label || '' ); handleInputChange(productId, 'subCategory', ''); handleInputChange(productId, 'subCategoryName', ''); handleInputChange(productId, 'brand', ''); handleInputChange(productId, 'brandName', ''); handleInputChange(productId, 'availableBrands', []); if (!categoryId) return; try { const response = await dispatch( getProdSubCatData({ ConfigId: categoryId }) ).unwrap(); if (response?.status !== 200) return; const allSubCats = response?.data?.data || []; const availableSubCategories = allSubCats .filter( (subCat) => subCat.NumFId === parseInt(categoryId) && !!subCat?.ConfigName ) .map((subCat) => ({ label: subCat.ConfigName, value: subCat.ConfigId, })); let subCategoryId = ''; let subCategoryName = ''; let availableBrands = []; // ✅ If selected category is "general", try to auto-select "general" subCategory + fetch brands const isGeneralCategory = prodCategories.some( (cat) => cat.ConfigId === parseInt(categoryId) && cat.ConfigName?.toLowerCase() === 'general' ); if (isGeneralCategory) { const generalSubCat = availableSubCategories.find( (subCat) => subCat.label?.toLowerCase() === 'general' ); if (generalSubCat) { subCategoryId = generalSubCat.value; subCategoryName = generalSubCat.label; try { const brandResponse = await dispatch( getBrandData({ ConfigId: generalSubCat.value }) ).unwrap(); availableBrands = brandResponse?.data?.data ?.filter( (b) => b.NumFId === parseInt(generalSubCat.value) && !!b?.ConfigName ) ?.map((b) => ({ label: b.ConfigName, value: b.ConfigId })) || []; } catch (err) { console.error('Error fetching brands:', err); } } } // ✅ Apply updates in one go (no async inside setProducts) setProducts((prev) => prev.map((p) => p.id === productId ? { ...p, availableSubCategories, subCategory: subCategoryId, subCategoryName, availableBrands, brand: '', brandName: '', } : p ) ); handleInputChange(productId, 'subCategory', subCategoryId); } catch (error) { console.error('Error fetching subcategories:', error); } }, [handleInputChange, dispatch, prodCategories, selectOptions.categories] ); const handleSubCategoryChange = useCallback( async (productId, subCategoryId) => { const selectedSubCategory = selectOptions.subCategories.find( (s) => s.value === subCategoryId ); handleInputChange(productId, 'subCategory', subCategoryId); handleInputChange( productId, 'subCategoryName', selectedSubCategory?.label || '' ); handleInputChange(productId, 'brand', ''); handleInputChange(productId, 'brandName', ''); if (subCategoryId) { try { const response = await dispatch( getBrandData({ ConfigId: subCategoryId }) ).unwrap(); setProducts((prev) => { return prev.map((p) => { if (p.id === productId) { return { ...p, availableBrands: response?.data?.data ?.filter( (brand) => brand.NumFId === parseInt(subCategoryId) && !!brand?.ConfigName ) ?.map((brand) => ({ label: brand.ConfigName, value: brand.ConfigId, })), brand: '', brandName: '', }; } return p; }); }); } catch (error) { } } }, [handleInputChange, selectOptions.subCategories] ); // --- Row operations --- const createNewRow = useCallback( (overrides = {}) => { const newId = Math.max(0, ...products.map((p) => p.id), 0) + 1; const defaultUOM = selectOptions.uom.find( (u) => u.value === defaults.uom ); const defaultCategory = selectOptions.categories.find( (c) => c.value === defaults.category ); const defaultSubCategory = defaults.availableSubCategories?.find( (s) => s.value === defaults.subCategory ); const defaultTax = selectOptions.tax.find( (t) => t.value === defaults.tax ); // Clean overrides: drop null/undefined so defaults are preserved const cleanedOverrides = Object.fromEntries( Object.entries(overrides).filter( ([_, v]) => v !== null && v !== undefined && v !== '' ) ); return { id: newId, productName: '', uom: defaults.uom || '', uomName: defaultUOM?.label || '', nou: '1', mrp: '', salePrice: '', category: defaults.category, categoryName: defaultCategory?.label || '', subCategory: defaults.subCategory, subCategoryName: defaultSubCategory?.label || '', tax: defaults.tax, taxName: defaultTax?.label || '', barcode: '', availableSubCategories: defaults.availableSubCategories, availableBrands: defaults.availableBrands, emiAllowed: 'No', autoGenerateQrcode: 'No', stockMaintenance: 'No', tokenMaintenance: 'No', amountPerPiece: 'No', autoGenerateOnePcQrcode: 'No', productType: productTypeData?.find((pt) => pt?.ConfigName === 'Product') ?.ConfigId, cess: '', discountType: 'F', discountLimit: '', hsnCode: '', partNumber: '', batchNumber: '', modelNumber: '', rack: '', imei1: '', imei2: '', serialNumber: '', macId: '', manufactureDate: null, expireDate: null, expiryNotificationDays: 0, availableFrom: null, availableTo: null, productImage: '', ...cleanedOverrides, }; }, [ products, defaults, selectOptions.uom, selectOptions.categories, selectOptions.tax, ] ); useEffect(() => { // Only add if products is empty and defaults are available if ( products?.length === 0 && defaults.uom && defaults.category && defaults.subCategory && defaults.tax ) { setProducts([createNewRow()]); } }, [ defaults.uom, defaults.category, defaults.subCategory, defaults.tax, products?.length, createNewRow, ]); const addNewRow = useCallback(() => { // Fields to check const requiredFields = ['productName', 'nou', 'uom']; // Find the first incomplete row const incompleteIndex = products.findIndex((row) => requiredFields.some( (field) => row[field] === null || row[field] === undefined || row[field] === '' ) ); if (incompleteIndex !== -1) { // Move the incomplete row to the top setProducts((prev) => { const newProducts = [...prev]; const [incompleteRow] = newProducts.splice(incompleteIndex, 1); newProducts.unshift(incompleteRow); return newProducts; }); setMessageType('error'); setMessageData( 'Please fill all required fields in the highlighted row before adding a new row.' ); return; } setProducts((prev) => [createNewRow(), ...prev]); }, [products, createNewRow]); const insertRowAbove = useCallback( (targetIndex) => { const newRow = createNewRow(); setProducts((prev) => { const newProducts = [...prev]; newProducts.splice(targetIndex, 0, newRow); return newProducts; }); }, [createNewRow] ); const deleteRow = useCallback((id) => { setProducts((prev) => prev.filter((p) => p.id !== id)); setErrors((prev) => { const newErrors = { ...prev }; Object.keys(newErrors).forEach((key) => { if (key.startsWith(`${id}-`)) delete newErrors[key]; }); return newErrors; }); }, []); const resetAll = useCallback(() => { setProducts([]); setSearchTerm(''); setSelectedCell(null); setErrors({}); }, []); // --- Submit handler --- const handleSubmit = useCallback(async () => { if (products?.length === 0) { setMessageType('error'); setMessageData('Please add at least one product before submitting.'); return; } // Duplicate check: productName + uom + nou let duplicateError = {}; products.forEach((product, idx) => { const isDuplicate = products?.some( (p, i) => i !== idx && p.productName?.trim().toLowerCase() === product.productName?.trim().toLowerCase() && p.uom === product.uom && p.nou === product.nou && p?.category === product?.category && p?.subCategory === product?.subCategory ); if (isDuplicate && product.productName?.trim()) { duplicateError[`${product.id}-productName`] = 'Product already exists'; } }); if (Object.keys(duplicateError)?.length > 0) { setErrors((prev) => ({ ...prev, ...duplicateError })); setMessageType('error'); setMessageData('Duplicate products found. Please fix before submitting.'); return; } setIsSubmitting(true); try { let hasErrors = false; const allErrors = {}; products.forEach((product) => { REQUIRED_FIELDS.forEach((field) => { const error = validationRules[field]?.(product[field], product) || ''; if (error) { allErrors[`${product.id}-${field}`] = error; hasErrors = true; } }); }); if (hasErrors) { setErrors(allErrors); setMessageType('error'); setMessageData('Please fix all validation errors before submitting.'); return; } const postData = products.map((product) => ({ AppId: AppId || 0, CompId: CompId || '', BranchId: BranchId || '', CreatedBy: UserId || 0, ProdName: product.productName?.trim() || '', ProdVariantName: product.prodVariantName || '', Size: parseFloat(product.nou) || '', UOM: product.uomName || '', MRP: parseFloat(product.mrp) || 0, WhSalePrice: parseFloat(product.whSalePrice) || 0, SellPrice: parseFloat(product.salePrice) || 0, ProdCat: product.categoryName || '', ProdSubCat: product.subCategoryName || '', Brand: product.brandName || product.brand || '', AutoGenerateQr: product.autoGenerateQrcode || '', QRCode: product.barcode || product.qrcode || '', StockAvailable: product.stockAvailable || 'N', TaxId: product.taxName || product.tax || '', HSNCode: product.hsnCode || '', PartNumber: product.partNumber || '', Rack: product.rack ? parseInt(product.rack) : 0, ManufDate: product.manufactureDate || '', ExpDate: product.expireDate || '', AvailableFrom: product.availableFrom || '', AvailableTo: product.availableTo || '', ProdLogo: product.productImage || '', OnePcsAvailable: product.onePcsAvailable || 'No', AutoGenerateSingleQr: product.autoGenerateOnePcQrcode || 'N', OnePcQR: product.onePcQrcode || '', TokenAvailable: product.tokenAvailable || product.tokenMaintenance || 'N', OpeningQty: product.openingQty ? parseFloat(product.openingQty) : 0, QtyBasedPrice: product.qtyBasedPrice || '', InwardDate: product.inwardDate || '', SuppId: product.suppId || '', Reference: product.reference || '', ReceivedQty: product.receivedQty ? parseFloat(product.receivedQty) : 0, AcceptedQty: product.acceptedQty ? parseFloat(product.acceptedQty) : 0, RejectedQty: product.rejectedQty ? parseFloat(product.rejectedQty) : 0, RejectionReason: product.rejectionReason || '', IssuedQty: product.issuedQty ? parseFloat(product.issuedQty) : 0, BalanceQty: product.balanceQty ? parseFloat(product.balanceQty) : 0, InwardPrice: product.inwardPrice ? parseFloat(product.inwardPrice) : 0, OfferPrice: product.offerPrice ? parseFloat(product.offerPrice) : 0, SpecialPrice: product.specialPrice ? parseFloat(product.specialPrice) : 0, Cess: product.cess ? parseFloat(product.cess) : 0, })); await dispatch(bulkpostdata({ ProdDetails: postData })).unwrap(); setMessageType('success'); setMessageData( `Successfully added ${postData?.length > 1 ? `${postData?.length} products` : '1 product'}` ); resetAll(); } catch (error) { console.error('Bulk product submit error:', error); setMessageType('error'); setMessageData('Failed to add products. Please try again.'); } finally { setIsSubmitting(false); } }, [ products, validationRules, AppId, CompId, resetAll, dispatch, UserId, BranchId, ]); const buildColumnMapping = useCallback(() => { const columns = [ { id: 'productName', index: 0 }, { id: 'uom', index: 1 }, { id: 'nou', index: 2 }, { id: 'mrp', index: 3 }, { id: 'salePrice', index: 4 }, { id: 'category', index: 5 }, ]; // Add conditional columns based on field visibility if (hasField('Sub Category')) { columns.push({ id: 'subCategory', index: 6 }); } if (hasField('Sub Category') && hasField('Brand')) { columns.push({ id: 'brand', index: 'brand' }); } if (hasField('WholeSale Price')) { columns?.push({ id: 'whSalePrice', index: 'whSalePrice' }); } if (hasField('EMI Allowed')) { columns.push({ id: 'emiAllowed', index: 'emiAllowed' }); } if (hasField('Auto Generate QRCode')) { columns.push({ id: 'autoGenerateQrcode', index: 'autoGenerateQrcode' }); columns.push({ id: 'qrcode', index: 'qrcode' }); } if (hasField('Stock Maintenance')) { columns.push({ id: 'stockMaintenance', index: 'stockMaintenance' }); columns.push({ id: 'lowStockCount', index: 'lowStockCount' }); } if (hasField('Token Maintenance')) { columns.push({ id: 'tokenMaintenance', index: 'tokenMaintenance' }); } if (hasField('Amount Per Piece')) { columns.push({ id: 'amountPerPiece', index: 'amountPerPiece' }); columns.push({ id: 'perPieceAmount', index: 'perPieceAmount' }); columns.push({ id: 'autoGenerateOnePcQrcode', index: 'autoGenerateOnePcQrcode', }); columns.push({ id: 'onePcQrcode', index: 'onePcQrcode' }); columns.push({ id: 'noOfPieceInside', index: 'noOfPieceInside' }); } if (hasField('Product Type')) { columns.push({ id: 'productType', index: 'productType' }); } if (hasField('Tax')) { columns.push({ id: 'tax', index: 7 }); } if (hasField('Cess')) { columns.push({ id: 'cess', index: 'cess' }); } if (hasField('Discount Type')) { columns.push({ id: 'discountType', index: 'discountType' }); } if (hasField('Discount Limit')) { columns.push({ id: 'discountLimit', index: 'discountLimit' }); } if (hasField('HSN')) { columns.push({ id: 'hsnCode', index: 'hsnCode' }); } if (hasField('Part Number')) { columns.push({ id: 'partNumber', index: 'partNumber' }); } if (hasField('Rack')) { columns.push({ id: 'rack', index: 'rack' }); } if (hasField('Batch Number')) { columns.push({ id: 'batchNumber', index: 'batchNumber' }); } if (hasField('Model Number')) { columns.push({ id: 'modelNumber', index: 'modelNumber' }); } if (hasField('IMEI 1')) { columns.push({ id: 'imei1', index: 'imei1' }); } if (hasField('IMEI 2')) { columns.push({ id: 'imei2', index: 'imei2' }); } if (hasField('Serial Number')) { columns.push({ id: 'serialNumber', index: 'serialNumber' }); } if (hasField('MacId')) { columns.push({ id: 'macId', index: 'macId' }); } if (hasField('Manufacture Date')) { columns.push({ id: 'manufactureDate', index: 'manufactureDate' }); } if (hasField('Expire Date')) { columns.push({ id: 'expireDate', index: 'expireDate' }); } if (hasField('Expiry Notification Days')) { columns.push({ id: 'expiryNotificationDays', index: 'expiryNotificationDays', }); } if (hasField('Available From')) { columns.push({ id: 'availableFrom', index: 'availableFrom' }); } if (hasField('Available To')) { columns.push({ id: 'availableTo', index: 'availableTo' }); } return columns; }, [hasField]); // Memoize the column mapping const columnMapping = useMemo( () => buildColumnMapping(), [buildColumnMapping] ); // Helper function to get current column position const getCurrentColumnIndex = useCallback( (cellId) => { // Extract field name from cellId (e.g., "0-brand" -> "brand", "0-1" -> find by index 1) const [rowStr, fieldStr] = cellId?.split('-'); if (isNaN(fieldStr)) { // It's a field name like "brand" return columnMapping.findIndex((col) => col.id === fieldStr); } else { // It's a numeric index const numericIndex = parseInt(fieldStr); return columnMapping.findIndex((col) => col.index === numericIndex); } }, [columnMapping] ); // Helper function to build cell ID const buildCellId = useCallback( (rowIndex, columnIndex) => { const column = columnMapping[columnIndex]; if (!column) return null; return `${rowIndex}-${column.index}`; }, [columnMapping] ); // Updated handleKeyDown function const handleKeyDown = useCallback( async (e, rowIndex, currentCellId) => { const totalRows = products?.length; const totalCols = columnMapping?.length; // Get current column index const currentColIndex = getCurrentColumnIndex(currentCellId); const moveCell = (r, c) => { const cellId = buildCellId(r, c); if (cellId) { setSelectedCell(cellId); } }; switch (e.key) { case 'Tab': e.preventDefault(); if (currentColIndex < totalCols - 1) { moveCell(rowIndex, currentColIndex + 1); } else if (rowIndex < totalRows - 1) { moveCell(rowIndex + 1, 0); } else { addNewRow(); setTimeout(() => moveCell(totalRows, 0), 0); } break; case 'Enter': e.preventDefault(); const prodName = products?.find( (p, idx) => idx === rowIndex )?.productName; if (prodName) { const qrResponse = await dispatch( getQrcodeData({ QRCode: prodName }) ).unwrap(); if ( qrResponse?.data?.statusCode === 1 && qrResponse?.data?.data?.filter( (item) => item?.AppId === AppId && item?.CompId === CompId && item?.BranchId === BranchId )?.length > 0 ) { setMessageType('error'); setMessageData('Barcode already exists'); return; } } if (rowIndex > 0) { moveCell(rowIndex - 1, currentColIndex); } else { addNewRow(); setSelectedCell(null); setTimeout(() => moveCell(rowIndex, currentColIndex), 0); } break; case 'ArrowUp': if (rowIndex > 0) { moveCell(rowIndex - 1, currentColIndex); } else { addNewRow(); setSelectedCell(null); setTimeout(() => moveCell(rowIndex, currentColIndex), 0); } break; case 'ArrowDown': if (rowIndex < totalRows - 1) { moveCell(rowIndex + 1, currentColIndex); } break; // case "ArrowLeft": // if (currentColIndex > 0) { // moveCell(rowIndex, currentColIndex - 1); // } // break; // case "ArrowRight": // if (currentColIndex < totalCols - 1) { // moveCell(rowIndex, currentColIndex + 1); // } // break; default: break; } }, [ products?.length, addNewRow, columnMapping, getCurrentColumnIndex, buildCellId, dispatch, AppId, CompId, BranchId, setMessageType, setMessageData, ] ); // --- Focus management --- useEffect(() => { if (selectedCell) { const el = document.querySelector(`[data-cell="${selectedCell}"]`); el?.focus(); } }, [selectedCell]); useEffect(() => { debouncedProductLookup.current = debounce( async (enteredValue, rowIndex) => { // Use refs or stable values for dispatch, AppId, etc. if (enteredValue && enteredValue?.length >= 6) { try { const response = await dispatch( getLayoutsearch({ AppId, CompId, BranchId, ProdName: enteredValue, }) ).unwrap(); const found = response?.data?.data?.[0]; if ( response?.data?.statusCode === 1 && found?.ProdName && found?.QrBasedSearch === null && found?.SearchType === 'N' ) { setProducts((prev) => prev.map((p, idx) => { const index = rowIndex === 0 && !isMobile ? rowIndex + 1 : rowIndex; return idx === index ? { ...p, productName: found.ProdName, barcode: enteredValue, productImage: found.ImageUrl, } : p; }) ); // setRowToSubmit(rowIndex); } else if ( response?.data?.statusCode === 1 && found?.ProdName && found?.QrBasedSearch === 'Y' && found?.SearchType === 'Y' ) { setProducts((prev) => prev.map((p, idx) => idx === rowIndex ? { ...p, productName: '' } : p ) ); setMessageType('error'); setMessageData('Product Already Exists'); } // setTimeout(() => validateField(id, field, value, updatedRow), 0); // else { // setProducts(prev => // prev.map((p, idx) => // idx === rowIndex ? { ...p, productName: "" } : p // ) // ); // setMessageType("error"); // setMessageData("Product Details not found"); // } } catch { setProducts((prev) => prev.map((p, idx) => idx === rowIndex ? { ...p, productName: '' } : p ) ); setMessageType('error'); setMessageData('Product Details not found'); } } }, 300 ); try { dispatch(changeBreadCrumb({ items: items })); } catch (err) { console.log(err, 'err'); } }, []); useEffect(() => { if (!voiceData) return; // Extract UOM once const matchedUom = selectOptions.uom?.find( (u) => u.label?.toLowerCase() === voiceData.uom?.toLowerCase() ); const baseData = { productName: voiceData.product, nou: voiceData.qty || '1', uom: matchedUom?.value, uomName: matchedUom?.label, mrp: voiceData.price, salePrice: voiceData.price, }; // Check for duplicate const duplicateExists = products.some( (row) => row.productName?.trim().toLowerCase() === baseData.productName?.trim().toLowerCase() && row.nou === baseData.nou && row.uom === baseData.uom ); if (duplicateExists) { setMessageType('error'); setMessageData('Product Already exists'); setVoiceData(null); return; } setProducts((prev) => { // Find the first row with an empty product name (if any) const firstEmptyRowIndex = prev.findIndex( (row) => !row.productName || row.productName.trim() === '' ); if (firstEmptyRowIndex !== -1) { // Update existing row const updatedProducts = [...prev]; const rowToUpdate = { ...updatedProducts[firstEmptyRowIndex], ...baseData, }; // Move updated row to top updatedProducts.splice(firstEmptyRowIndex, 1); return [rowToUpdate, ...updatedProducts]; } else { // Create a new row at the top const newRow = createNewRow(baseData); return [newRow, ...prev]; } }); // Clear the voiceData after processing setVoiceData(null); }, [voiceData, createNewRow, selectOptions.uom, products]); useEffect(() => { if ( searchText !== '' && searchText !== null && searchText?.length !== 0 && searchText !== undefined ) { handleQrScan(searchText); } }, [searchText]); console.log(products, 'productsproducts'); useEffect(() => { debouncedHSNLookup.current = debounce(async (value) => { if (!value) { setHsnDetails([]); return; } try { const response = await dispatch(getHsnData(value)).unwrap(); if (response?.data?.statusCode === 1) { setHsnDetails(response?.data?.data || []); } else { setHsnDetails([]); } } catch { setHsnDetails([]); } }, 400); }, [dispatch]); // --- Error helper --- const getError = useCallback( (id, field) => errors[`${id}-${field}`], [errors] ); const handleFieldSetup = () => { setFieldSetup(true); }; const handleQrScan = async (value) => { const barcodeExists = products.some( (p) => p.barcode && p.barcode.toLowerCase() === value?.toLowerCase() ); if (barcodeExists) { setMessageType('error'); setMessageData('Barcode Already Exists'); stopScannerFromParent(); return; } const firstEmptyRowIndex = products?.findIndex( (row) => !row.productName || row.productName.trim() === '' ); if (firstEmptyRowIndex === -1) { addNewRow(); debouncedProductLookup.current(value, 0); } else { debouncedProductLookup.current(value, firstEmptyRowIndex); } dispatch(changeSearchedData(null)); stopScannerFromParent(); }; const handleFieldSetupSubmit = async () => { const postData = { AppId: AppId, CompId: CompId, BranchId: BranchId, Type: 'GB', FormType: 'Product', TypeId: categoryId, ConfigDtl: selectedFields?.map((field) => ({ ConfigId: field, Access: 'Y', })), CreatedBy: UserId, }; const response = await dispatch(postFieldSetup(postData))?.unwrap(); if (response?.data?.statusCode === 1) { setMessageType('success'); setMessageData(response?.data?.response); await getFieldSetup(); } else { setMessageType('error'); setMessageData('Failed to set up fields'); } }; const handleFieldSelect = (value) => { setSelectedFields((prev) => { if (prev.includes(value)) { return prev; } return [value, ...prev]; }); }; const handleFieldRemove = (id) => { setSelectedFields((prev) => prev.filter((field) => field !== id)); }; const handleHSNChange = (id, value) => { handleInputChange(id, 'hsnCode', value); setSelectedHsn(value); debouncedHSNLookup.current(value); }; const handleBrowseImages = async (productName, rowIndex) => { if (productName) { let response = await dispatch(onlineimages(productName)).unwrap(); setImageModal(true); setOnlineImageData(response?.data?.data); setImageModalRowIndex(rowIndex); setSelectedImage(null); } }; const handleOnlineImageModalClose = () => { setImageModal(false); setOnlineImage(null); }; const handleSubmitImage = async () => { let response = await fetch(selectedImage.image); let data = await response?.blob(); let metadata = { type: 'image/jpeg' }; let file = new File([data], 'image.jpg', metadata); let uploadImgData = await dispatch(uploadImage(file)).unwrap(); if (uploadImgData?.data?.status) { setOnlineImage(uploadImgData?.data?.image); } setProducts((prev) => prev.map((p, idx) => idx === imageModalRowIndex ? { ...p, productImage: uploadImgData?.data?.image } : p ) ); setImageModal(false); }; const handleQrcode = useCallback(() => { setOpenScanner(true); // directly open scanner }, []); const stopScannerFromParent = useCallback(() => { try { if (scannerRef?.current && scannerRef.current.isRunning) { scannerRef.current.stopScanner(); } } catch (err) { console.warn('Scanner not running, skip stop:', err); } setOpenScanner(false); }, []); const handleQrData = useCallback( (value, err) => { if (err && err?.error) { setMessageData(err?.error); setMessageType('error'); } dispatch(changeSearchedData(value)); stopScannerFromParent(); setOpenScanner(false); }, [dispatch, stopScannerFromParent] ); // --- Render --- return (
{ setMessageData(null); setMessageType(null); }} />
{/* Toolbar */}
setSearchTerm(e.target.value)} className="search-input" />
{isMobile && window.innerWidth <= 768 && ( // handleQrcode()} // style={{ cursor: 'pointer', marginLeft: '8px' }} // /> { changeSearchedData(value); }} /> )}
{/* Table */}
{hasField('WholeSale Price') && ( )} {hasField('Sub Category') && ( )} {hasField('Sub Category') && hasField('Brand') && ( )} {hasField('EMI Allowed') && } {hasField('Auto Generate QRCode') && ( )} {hasField('Auto Generate QRCode') && } {hasField('Stock Maintenance') && } {hasField('Stock Maintenance') && } {hasField('Token Maintenance') && } {hasField('Amount Per Piece') && ( <> )} {hasField('Product Type') && } {hasField('Tax') && ( )} {hasField('Cess') && } {hasField('Discount Type') && } {hasField('Discount Limit') && } {hasField('HSN') && } {hasField('Part Number') && } {hasField('Rack') && } {hasField('Batch Number') && } {hasField('Model Number') && } {hasField('IMEI 1') && } {hasField('IMEI 2') && } {hasField('Serial Number') && } {hasField('MacId') && } {hasField('Manufacture Date') && } {hasField('Expire Date') && } {hasField('Expiry Notification Days') && ( )} {hasField('Available From') && } {hasField('Available To') && } {hasField('Product Image') && } {/* {products.length === 0 ? ( ) : ( )} */} {filteredProducts?.map((product, rowIndex) => ( product[field] === null || product[field] === undefined || product[field] === '' ) ? 'incomplete-row' : '' } > {/* Row Number */} {/* Product Name */} {/* UOM */} {/* No. of Units */} {/* MRP */} {/* Sale Price */} {hasField('WholeSale Price') && ( )} {/* Category */} {/* Sub-Category */} {hasField('Sub Category') && ( )} {hasField('Brand') && hasField('Sub Category') && ( )} {/* EMI Allowed */} {hasField('EMI Allowed') && ( )} {/* Auto Generate QRCode */} {hasField('Auto Generate QRCode') && ( <> {/* Scanner/AddQrCode */} {product.autoGenerateQrcode === 'No' ? ( ) : ( )} )} {hasField('Stock Maintenance') && ( <> {/* Stock Maintenance */} {/* Low Stock Count */} {product.stockMaintenance === 'Yes' ? ( ) : ( )} )} {/* Token Maintenance */} {hasField('Token Maintenance') && ( )} {/* Amount Per Piece */} {hasField('Amount Per Piece') && ( <> {/* Per Piece Amount, Auto Generate One Piece Qrcode, Scanner/AddOnePcQrCode, No. of Piece Inside */} {product.amountPerPiece === 'Yes' ? ( <> {product.autoGenerateOnePcQrcode === 'No' ? ( ) : ( )} ) : ( <> )} )} {/* Product Type */} {hasField('Product Type') && ( )} {/* Tax */} {hasField('Tax') && ( )} {/* Cess */} {hasField('Cess') && ( )} {/* Percentage/Fixed */} {hasField('Discount Type') && ( )} {/* Discount Limit */} {hasField('Discount Limit') && ( )} {/* HSN Code */} {hasField('HSN') && ( )} {/* Part Number */} {hasField('Part Number') && ( )} {/* Rack */} {hasField('Rack') && ( )} {/* Batch Number */} {hasField('Batch Number') && ( )} {/* Model Number */} {hasField('Model Number') && ( )} {/* IMEI 1 */} {hasField('IMEI 1') && ( )} {/* IMEI 2 */} {hasField('IMEI 2') && ( )} {/* Serial Number */} {hasField('Serial Number') && ( )} {/* MacId */} {hasField('MacId') && ( )} {/* Manufacture Date */} {hasField('Manufacture Date') && ( )} {/* Expire Date */} {hasField('Expire Date') && ( )} {/* Expiry Notification Days */} {hasField('Expiry Notification Days') && ( )} {/* Available From */} {hasField('Available From') && ( )} {/* Available To */} {hasField('Available To') && ( )} {/* Product Image */} {/* {hasField('Product Image') && } */} {/* {hasField('Product Image') && } */} {hasField('Product Image') && ( )} {/* Delete */} ))}
# Product Name * UOM * No. of Units * MRP * Sale Price *Wholesale/Special Price Category * } /> Sub-Category 0 && selectedDefaultCategory ? `Selected Category : ${defaultCategories?.find((cat) => cat.value === selectedDefaultCategory)?.label}` : null } triggerModal={setOpenSubCategoryModal} Component={} /> Brand 0 && selectedDefaultSubCategory ? `Selected Sub-Category : ${defaultSubCategories?.find((subCat) => subCat.value === selectedDefaultSubCategory)?.label}` : null } currentDefault={selectedDefaultBrand} popOverHeading="Set Default Brand" field="brand" options={defaultBrands} onSubmit={handleDefaultSetting} checkBoxHeading={ 'Apply to all selected sub-category rows' } triggerModal={setOpenBrandModal} needAllSubCategories={true} Component={} /> EMI AllowedAuto Generate QRCodeScanner/AddQrCodeStock MaintenanceLow Stock CountToken MaintenanceAmount Per Piece Per Piece Amount Auto Generate One Piece Qrcode Scanner/AddOnePcQrCode No. of Piece InsideProduct Type Tax % * } /> CessDiscount TypeDiscount LimitHSN CodePart NumberRackBatch NumberModel NumberIMEI 1IMEI 2Serial NumberMacIdManufacture DateExpire DateExpiry Notification DaysAvailable FromAvailable ToProduct ImageDelete
No products yet. Click{" "} Add Row {" "} to start.
insertRowAbove(rowIndex)} title="Click to add row above" > {rowIndex + 1} {/*
{ const newValue = e.target.value?.trim(); const barcodeExists = products.some( p => p.barcode && p.barcode.toLowerCase() === newValue.toLowerCase() ); if (barcodeExists) { handleInputChange(product.id, "productName", ""); // clear field setMessageType("error"); setMessageData("Barcode Already Exists"); return; } handleInputChange(product.id, "productName", e.target.value) debouncedProductLookup.current(e.target.value, rowIndex); }} onFocus={() => setSelectedCell(`${rowIndex}-0`)} onKeyDown={e => handleKeyDown(e, rowIndex, `${rowIndex}-0`)} dataCell={`${rowIndex}-0`} dataBarcode={true} placeholder="Enter product name..." hasError={!!getError(product.id, "productName")} /> {getError(product.id, "productName") && {getError(product.id, "productName")}}
*/}
{ const newValue = e.target.value?.trim(); const barcodeExists = products.some( (p) => p.barcode && p.barcode.toLowerCase() === newValue?.toLowerCase() ); if (barcodeExists) { handleInputChange(product.id, 'productName', ''); // clear field setMessageType('error'); setMessageData('Barcode Already Exists'); return; } handleInputChange( product.id, 'productName', e.target.value ); debouncedProductLookup.current( e.target.value, rowIndex ); }} onFocus={() => setSelectedCell(`${rowIndex}-0`)} onKeyDown={(e) => handleKeyDown(e, rowIndex, `${rowIndex}-0`) } dataCell={`${rowIndex}-0`} dataBarcode={true} placeholder="Enter product name..." hasError={!!getError(product.id, 'productName')} />
{getError(product.id, 'productName') && ( {getError(product.id, 'productName')} )}
handleInputChange(product.id, 'uom', e.target.value) } onKeyDown={(e) => handleKeyDown(e, rowIndex, `${rowIndex}-1`) } dataCell={`${rowIndex}-1`} options={selectOptions.uom} hasError={!!getError(product.id, 'uom')} placeholder="Select UOM" onClick={() => setSelectedCell(`${rowIndex}-1`)} /> {getError(product.id, 'uom') && ( {getError(product.id, 'uom')} )}
handleInputChange(product.id, 'nou', e.target.value) } onKeyDown={(e) => { if ( !( (e.key >= '0' && e.key <= '9') || [ 'Backspace', 'Tab', 'ArrowLeft', 'ArrowRight', 'Delete', 'Home', 'End', 'Enter', 'Escape', 'Period', '.', ].includes(e.key) ) ) { e.preventDefault(); } handleKeyDown(e, rowIndex, `${rowIndex}-2`); }} onFocus={() => setSelectedCell(`${rowIndex}-2`)} dataCell={`${rowIndex}-2`} placeholder="0" hasError={!!getError(product.id, 'nou')} /> {getError(product.id, 'nou') && ( {getError(product.id, 'nou')} )}
handleInputChange(product.id, 'mrp', e.target.value) } onKeyDown={(e) => { if ( !( (e.key >= '0' && e.key <= '9') || [ 'Backspace', 'Tab', 'ArrowLeft', 'ArrowRight', 'Delete', 'Home', 'End', 'Enter', 'Escape', 'Period', '.', ].includes(e.key) ) ) { e.preventDefault(); } handleKeyDown(e, rowIndex, `${rowIndex}-3`); }} dataCell={`${rowIndex}-3`} onFocus={() => setSelectedCell(`${rowIndex}-3`)} placeholder="₹ 0.00" step="0.01" hasError={!!getError(product.id, 'mrp')} /> {getError(product.id, 'mrp') && ( {getError(product.id, 'mrp')} )}
handleInputChange( product.id, 'salePrice', e.target.value ) } onKeyDown={(e) => { if ( !( (e.key >= '0' && e.key <= '9') || [ 'Backspace', 'Tab', 'ArrowLeft', 'ArrowRight', 'Delete', 'Home', 'End', 'Enter', 'Escape', 'Period', '.', ].includes(e.key) ) ) { e.preventDefault(); } handleKeyDown(e, rowIndex, `${rowIndex}-4`); }} dataCell={`${rowIndex}-4`} onFocus={() => setSelectedCell(`${rowIndex}-4`)} placeholder="₹ 0.00" step="0.01" hasError={!!getError(product.id, 'salePrice')} /> {getError(product.id, 'salePrice') && ( {getError(product.id, 'salePrice')} )}
{ if ( !( (e.key >= '0' && e.key <= '9') || [ 'Backspace', 'Tab', 'ArrowLeft', 'ArrowRight', 'Delete', 'Home', 'End', 'Enter', 'Escape', 'Period', '.', ].includes(e.key) ) ) { e.preventDefault(); } }} value={product.whSalePrice} onChange={(e) => handleInputChange( product.id, 'whSalePrice', e.target.value ) } placeholder="Wholesale/Special Price" onFocus={() => setSelectedCell(`${rowIndex}-whSalePrice`) } />
handleCategoryChange(product.id, e.target.value) } onKeyDown={(e) => handleKeyDown(e, rowIndex, `${rowIndex}-5`) } dataCell={`${rowIndex}-5`} onClick={() => setSelectedCell(`${rowIndex}-5`)} options={selectOptions.categories} hasError={!!getError(product.id, 'category')} placeholder="Select Category" /> {getError(product.id, 'category') && ( {getError(product.id, 'category')} )}
handleSubCategoryChange(product.id, e.target.value) } onKeyDown={(e) => handleKeyDown(e, rowIndex, `${rowIndex}-6`) } dataCell={`${rowIndex}-6`} onClick={() => setSelectedCell(`${rowIndex}-6`)} options={product.availableSubCategories} hasError={!!getError(product.id, 'subCategory')} placeholder="Select Sub-Category" /> handleInputChange(product.id, 'brand', e.target.value) } options={product.availableBrands || []} // define your brand options placeholder="Select Brand" onClick={() => setSelectedCell(`${rowIndex}-brand`)} /> handleInputChange( product.id, 'emiAllowed', e.target.value ) } options={[ { label: 'Yes', value: 'Yes' }, { label: 'No', value: 'No' }, ]} optionType="button" buttonStyle="solid" /> { const val = e.target.value; handleInputChange( product.id, 'autoGenerateQrcode', val ); handleInputChange(product.id, 'qrcode', ''); }} options={[ { label: 'Yes', value: 'Yes' }, { label: 'No', value: 'No' }, ]} optionType="button" buttonStyle="solid" /> handleInputChange( product.id, 'qrcode', e.target.value ) } placeholder="Scan/Add QR" onFocus={() => setSelectedCell(`${rowIndex}-qrcode`) } /> - { const val = e.target.value; handleInputChange( product.id, 'stockMaintenance', val ); if (val === 'Yes') { handleInputChange(product.id, 'lowStockCount', 0); } else { handleInputChange( product.id, 'lowStockCount', undefined ); } }} options={[ { label: 'Yes', value: 'Yes' }, { label: 'No', value: 'No' }, ]} optionType="button" buttonStyle="solid" /> { if ( !( (e.key >= '0' && e.key <= '9') || [ 'Backspace', 'Tab', 'ArrowLeft', 'ArrowRight', 'Delete', 'Home', 'End', 'Enter', 'Escape', 'Period', '.', ].includes(e.key) ) ) { e.preventDefault(); } }} value={product?.lowStockCount} onChange={(e) => handleInputChange( product.id, 'lowStockCount', e.target.value ) } placeholder="0" onFocus={() => setSelectedCell(`${rowIndex}-lowStockCount`) } /> - handleInputChange( product.id, 'tokenMaintenance', e.target.value ) } options={[ { label: 'Yes', value: 'Yes' }, { label: 'No', value: 'No' }, ]} optionType="button" buttonStyle="solid" /> { const val = e.target.value; handleInputChange( product.id, 'amountPerPiece', val ); if (val === 'Yes') { handleInputChange( product.id, 'perPieceAmount', 0 ); } else { handleInputChange( product.id, 'perPieceAmount', undefined ); handleInputChange( product.id, 'autoGenerateOnePcQrcode', 'No' ); handleInputChange(product.id, 'onePcQrcode', ''); handleInputChange( product.id, 'noOfPieceInside', '' ); } }} options={[ { label: 'Yes', value: 'Yes' }, { label: 'No', value: 'No' }, ]} optionType="button" buttonStyle="solid" /> { if ( !( (e.key >= '0' && e.key <= '9') || [ 'Backspace', 'Tab', 'ArrowLeft', 'ArrowRight', 'Delete', 'Home', 'End', 'Enter', 'Escape', 'Period', '.', ].includes(e.key) ) ) { e.preventDefault(); } }} value={product.perPieceAmount} onChange={(e) => handleInputChange( product.id, 'perPieceAmount', e.target.value ) } placeholder="Amount" onFocus={() => setSelectedCell(`${rowIndex}-perPieceAmount`) } /> { const val = e.target.value; handleInputChange( product.id, 'autoGenerateOnePcQrcode', val ); handleInputChange( product.id, 'onePcQrcode', '' ); }} options={[ { label: 'Yes', value: 'Yes' }, { label: 'No', value: 'No' }, ]} optionType="button" buttonStyle="solid" /> handleInputChange( product.id, 'onePcQrcode', e.target.value ) } placeholder="Scan/Add OnePc QR" onFocus={() => setSelectedCell(`${rowIndex}-onePcQrcode`) } /> - { if ( !( (e.key >= '0' && e.key <= '9') || [ 'Backspace', 'Tab', 'ArrowLeft', 'ArrowRight', 'Delete', 'Home', 'End', 'Enter', 'Escape', 'Period', '.', ].includes(e.key) ) ) { e.preventDefault(); } }} value={product.noOfPieceInside} onChange={(e) => handleInputChange( product.id, 'noOfPieceInside', e.target.value ) } placeholder="No. of Piece" onFocus={() => setSelectedCell(`${rowIndex}-noOfPieceInside`) } /> - - - - handleInputChange( product.id, 'productType', e.target.value ) } options={productTypeData ?.filter((pt) => pt?.ConfigName !== 'Others') ?.map((pt) => ({ label: pt.ConfigName, value: pt.ConfigId, }))} placeholder="Select Type" onClick={() => setSelectedCell(`${rowIndex}-productType`) } />
handleInputChange(product.id, 'tax', e.target.value) } onKeyDown={(e) => handleKeyDown(e, rowIndex, 7)} dataCell={`${rowIndex}-7`} onClick={() => setSelectedCell(`${rowIndex}-7`)} options={selectOptions.tax} hasError={!!getError(product.id, 'tax')} placeholder="Select Tax" /> {getError(product.id, 'tax') && ( {getError(product.id, 'tax')} )}
{ if ( !( (e.key >= '0' && e.key <= '9') || [ 'Backspace', 'Tab', 'ArrowLeft', 'ArrowRight', 'Delete', 'Home', 'End', 'Enter', 'Escape', 'Period', '.', ].includes(e.key) ) ) { e.preventDefault(); } }} value={product.cess} onChange={(e) => handleInputChange(product.id, 'cess', e.target.value) } placeholder="Cess" onFocus={() => setSelectedCell(`${rowIndex}-cess`)} /> handleInputChange( product.id, 'discountType', e.target.value ) } options={[ { label: 'Percentage', value: 'P' }, { label: 'Fixed', value: 'F' }, ]} optionType="button" buttonStyle="solid" />
{ if ( !( (e.key >= '0' && e.key <= '9') || [ 'Backspace', 'Tab', 'ArrowLeft', 'ArrowRight', 'Delete', 'Home', 'End', 'Enter', 'Escape', 'Period', '.', ].includes(e.key) ) ) { e.preventDefault(); } }} value={product.discountLimit} onChange={(e) => handleInputChange( product.id, 'discountLimit', e.target.value ) } placeholder="Discount Limit" onFocus={() => setSelectedCell(`${rowIndex}-discountLimit`) } hasError={!!getError(product.id, 'discountLimit')} /> {getError(product.id, 'discountLimit') && ( {getError(product.id, 'discountLimit')} )}
handleHSNChange(product.id, e.target.value) } placeholder="HSN Code" onFocus={() => setSelectedCell(`${rowIndex}-hsnCode`)} list={`hsnList-${product.id}`} /> {hsnDetails?.length > 0 && ( {hsnDetails .filter((item) => { const search = (selectedHsn || '').toLowerCase(); return ( item.HSN_CD?.toLowerCase()?.includes(search) || item.Description?.toLowerCase()?.includes( search ) ); }) .map((item, index) => ( )} handleInputChange( product.id, 'partNumber', e.target.value ) } placeholder="Part Number" onFocus={() => setSelectedCell(`${rowIndex}-partNumber`) } /> handleInputChange(product.id, 'rack', e.target.value) } placeholder="Rack" onFocus={() => setSelectedCell(`${rowIndex}-rack`)} /> handleInputChange( product.id, 'batchNumber', e.target.value ) } placeholder="Batch Number" onFocus={() => setSelectedCell(`${rowIndex}-batchNumber`) } /> handleInputChange( product.id, 'modelNumber', e.target.value ) } placeholder="Model Number" onFocus={() => setSelectedCell(`${rowIndex}-modelNumber`) } />
handleInputChange( product.id, 'imei1', e.target.value ) } placeholder="IMEI 1" onFocus={() => setSelectedCell(`${rowIndex}-imei1`)} hasError={!!getError(product.id, 'imei1')} /> {getError(product.id, 'imei1') && ( {getError(product.id, 'imei1')} )}
handleInputChange( product.id, 'imei2', e.target.value ) } placeholder="IMEI 2" onFocus={() => setSelectedCell(`${rowIndex}-imei2`)} hasError={!!getError(product.id, 'imei2')} /> {getError(product.id, 'imei2') && ( {getError(product.id, 'imei2')} )}
handleInputChange( product.id, 'serialNumber', e.target.value ) } placeholder="Serial Number" onFocus={() => setSelectedCell(`${rowIndex}-serialNumber`) } />
handleInputChange( product.id, 'macId', e.target.value ) } placeholder="MacId" onFocus={() => setSelectedCell(`${rowIndex}-macId`)} hasError={!!getError(product.id, 'macId')} /> {getError(product.id, 'macId') && ( {getError(product.id, 'macId')} )}
{ handleInputChange( product.id, 'manufactureDate', dateString || null ); // If manufacture date changes, clear expire date handleInputChange(product.id, 'expireDate', null); }} size="small" style={{ width: 120 }} format="YYYY-MM-DD" placeholder="Manufacture" allowClear inputReadOnly /> handleInputChange( product.id, 'expireDate', dateString || null ) } size="small" style={{ width: 120 }} format="YYYY-MM-DD" placeholder="Expire" allowClear inputReadOnly disabledDate={(current) => { // Disable dates before manufactureDate (if set) if (product.manufactureDate) { return ( current && current.isBefore( dayjs(product.manufactureDate), 'day' ) ); } return false; }} /> handleInputChange( product.id, 'expiryNotificationDays', e.target.value ) } placeholder="0" min={0} onFocus={() => setSelectedCell(`${rowIndex}-expiryNotificationDays`) } onKeyDown={(e) => { if ( !( (e.key >= '0' && e.key <= '9') || [ 'Backspace', 'Tab', 'ArrowLeft', 'ArrowRight', 'Delete', 'Home', 'End', 'Enter', 'Escape', ].includes(e.key) ) ) { e.preventDefault(); } }} /> { handleInputChange( product.id, 'availableFrom', timeString || null ); handleInputChange(product.id, 'availableTo', null); }} size="small" style={{ width: 110 }} format="hh:mm a" use12Hours placeholder="From" allowClear inputReadOnly /> handleInputChange( product.id, 'availableTo', timeString || null ) } size="small" style={{ width: 110 }} format="hh:mm a" use12Hours placeholder="To" allowClear inputReadOnly />
{ setProducts(prev => prev.map((p, idx) => idx === rowIndex ? { ...p, productImage: url } : p ) ); }} />
{ setImageModalRowIndex(rowIndex); setUploadImageModal(true); setOnlineImage(null); setSelectedImage(null); }} />
{/* Stats */}
Total Products: {products?.length}
Showing: {filteredProducts?.length} of{' '} {products?.length}
• Click row numbers to insert • Tab/Enter to navigate
• Arrows to move • * = Required
{isMobile && screenWidth <= 768 && openScanner && ( { handleQrData(null); setOpenScanner(false); // close on cancel }} > )} setFieldSetup(false)} footer={false} title={'Select the fields you want to include in the table'} children={
{/*
*/}
!selectedFields?.some((s) => s === p.value) )} />
{selectedFields ?.map((id) => tableFieldPreferences?.find((p) => p.value === id) ) .filter(Boolean) .map((field) => (
{field.label}
handleFieldRemove(field.value)} >
))}
} htmlType={true} />
} />
{onlineImageData?.map((item, index) => (
setSelectedImage(item)} > no image
))}
} handleSubmit={handleSubmitImage} handleCancel={handleOnlineImageModalClose} >
{ setUploadImageModal(false); setOnlineImage(null); setImageModalRowIndex(null); setSelectedImage(null); }} children={
{ setProducts((prev) => prev.map((p, idx) => idx === imageModalRowIndex ? { ...p, productImage: url } : p ) ); }} />
} />
); }; export default TurboAddForm;