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 { bulkpostdata, getBrandData, getFieldSetupData, getHsnData, getProdCatData, getProdSubCatData, getProductTypeData, getQrcodeData, getUomData, onlineimages, postFieldSetup, prodCatDataSelector, prodSubCatDataSelector, 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"; 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 taxData = useSelector(taxSelector); const scannerRef = useRef(); console.log(taxData, "taxData") 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 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 [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]); // 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' }} /> )}
{/* 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} /> 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'} /> 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;