Android_Retail/src/Pages/Product/TurboAddForm.jsx

3738 lines
131 KiB
JavaScript

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 (
<div className="product-form-container">
<Messages
messageType={messageType}
messageData={messageData}
onComplete={() => {
setMessageData(null);
setMessageType(null);
}}
/>
<div className="excel-container">
{/* Toolbar */}
<div className="toolbar">
<div className="toolbar-row">
<div className="toolbar-left">
<div className="search-box">
<Search size={16} className="search-icon" />
<input
type="text"
placeholder="Search products..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
</div>
</div>
<div className="toolbar-right">
{isMobile && window.innerWidth <= 768 && (
// <BiBarcodeReader
// // className="search-icon barcode-scanner-icon"
// onClick={() => handleQrcode()}
// style={{ cursor: 'pointer', marginLeft: '8px' }}
// />
<BarCodeScan
style={{ FontSize: '12px', marginLeft: '8px' }}
onScan={(value) => {
changeSearchedData(value);
}}
/>
)}
<EnhancedVoiceForm
setVoiceData={setVoiceData}
uomNames={['kg', 'g', 'ltr', 'pcs', 'kgs']}
/>
<Tooltip title="Field Setup">
<div className="btn btn--info" onClick={handleFieldSetup}>
<MdOutlineAppRegistration size={19} />
</div>
</Tooltip>
<button
onClick={resetAll}
className="btn btn--warning"
disabled={isSubmitting}
>
<RotateCcw size={14} /> Reset
</button>
<button
onClick={addNewRow}
className="btn btn--success"
disabled={isSubmitting}
>
<Plus size={14} /> Add Row
</button>
<button
onClick={handleSubmit}
className="btn btn--primary"
disabled={isSubmitting || products?.length === 0}
>
<Save size={14} />
{isSubmitting
? 'Submitting...'
: `Submit (${products?.length})`}
</button>
</div>
</div>
</div>
{/* Table */}
<div
className="table-container"
ref={tableRef}
style={{ height: '72vh', overflowX: 'auto' }}
>
<table
className="excel-table"
style={{ minWidth: '100%', position: 'relative' }}
>
<thead>
<tr>
<th className="sticky-left-1">#</th>
<th
className="sticky-left-2"
style={{ width: '300px !important' }}
>
Product Name *
</th>
<th className="sticky-left-3">
UOM *
<DefaultSettingPopover
popOverHeading="Set Default UOM"
field="uom"
options={selectOptions.uom}
onSubmit={handleDefaultSetting}
currentDefault={defaults.uom}
/>
</th>
<th>No. of Units *</th>
<th>MRP *</th>
<th>Sale Price *</th>
{hasField('WholeSale Price') && (
<th>Wholesale/Special Price</th>
)}
<th>
Category *
<DefaultSettingPopover
currentDefault={selectedDefaultCategory}
popOverHeading="Set Default Category"
field="category"
options={defaultCategories}
onSubmit={handleDefaultSetting}
triggerModal={setOpenCategoryModal}
Component={<AddCategoryModal
ref={categoryRef}
open={openCategoryModal}
close={setOpenCategoryModal}
/>}
/>
</th>
{hasField('Sub Category') && (
<th>
Sub-Category
<DefaultSettingPopover
currentDefault={selectedDefaultSubCategory}
popOverHeading="Set Default Sub-Category"
field="subCategory"
options={defaultSubCategories}
onSubmit={handleDefaultSetting}
checkBoxHeading={'Apply to all selected category rows'}
showField={
defaultCategories?.length > 0 && selectedDefaultCategory
? `Selected Category : ${defaultCategories?.find((cat) => cat.value === selectedDefaultCategory)?.label}`
: null
}
triggerModal={setOpenSubCategoryModal}
Component={<AddSubCategoryModal
open={openSubCategoryModal}
close={setOpenSubCategoryModal}
categoryData={prodCategories}
products={products}
ref={subCategoryRef}
setProducts={setProducts}
/>}
/>
</th>
)}
{hasField('Sub Category') && hasField('Brand') && (
<th>
Brand
<DefaultSettingPopover
showField={
defaultSubCategories?.length > 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={<AddBrandModal
open={openBrandModal}
close={setOpenBrandModal}
subCategories={allProdSubCategories}
products={products}
setProducts={setProducts}
ref={brandRef}
/>}
/>
</th>
)}
{hasField('EMI Allowed') && <th>EMI Allowed</th>}
{hasField('Auto Generate QRCode') && (
<th>Auto Generate QRCode</th>
)}
{hasField('Auto Generate QRCode') && <th>Scanner/AddQrCode</th>}
{hasField('Stock Maintenance') && <th>Stock Maintenance</th>}
{hasField('Stock Maintenance') && <th>Low Stock Count</th>}
{hasField('Token Maintenance') && <th>Token Maintenance</th>}
{hasField('Amount Per Piece') && (
<>
<th>Amount Per Piece</th>
<th>Per Piece Amount</th>
<th>Auto Generate One Piece Qrcode</th>
<th>Scanner/AddOnePcQrCode</th>
<th>No. of Piece Inside</th>
</>
)}
{hasField('Product Type') && <th>Product Type</th>}
{hasField('Tax') && (
<th>
Tax % *
<DefaultSettingPopover
popOverHeading="Set Default Tax"
field="tax"
options={selectOptions.tax}
onSubmit={handleDefaultSetting}
currentDefault={defaults.tax}
triggerModal={setOpenTaxModal}
Component={<AddTaxModal
open={openTaxModal}
close={setOpenTaxModal}
prodTaxData={prodTaxData}
ref={taxRef}
flag={true}
/>}
/>
</th>
)}
{hasField('Cess') && <th>Cess</th>}
{hasField('Discount Type') && <th>Discount Type</th>}
{hasField('Discount Limit') && <th>Discount Limit</th>}
{hasField('HSN') && <th>HSN Code</th>}
{hasField('Part Number') && <th>Part Number</th>}
{hasField('Rack') && <th>Rack</th>}
{hasField('Batch Number') && <th>Batch Number</th>}
{hasField('Model Number') && <th>Model Number</th>}
{hasField('IMEI 1') && <th>IMEI 1</th>}
{hasField('IMEI 2') && <th>IMEI 2</th>}
{hasField('Serial Number') && <th>Serial Number</th>}
{hasField('MacId') && <th>MacId</th>}
{hasField('Manufacture Date') && <th>Manufacture Date</th>}
{hasField('Expire Date') && <th>Expire Date</th>}
{hasField('Expiry Notification Days') && (
<th>Expiry Notification Days</th>
)}
{hasField('Available From') && <th>Available From</th>}
{hasField('Available To') && <th>Available To</th>}
{hasField('Product Image') && <th>Product Image</th>}
<th>Delete</th>
</tr>
</thead>
<tbody>
{/* {products.length === 0 ? (
<tr>
<td colSpan={document.querySelectorAll("thead tr th").length} className="no-products-message">
<div style={{ textAlign: 'left' }}>No products yet. Click{" "}
<strong onClick={addNewRow} style={{ cursor: "pointer", color: "#007bff" }}>
Add Row
</strong>{" "}
to start.</div>
</td>
</tr>
) : (
)} */}
{filteredProducts?.map((product, rowIndex) => (
<tr
key={product.id}
className={
['productName', 'nou', 'uom'].some(
(field) =>
product[field] === null ||
product[field] === undefined ||
product[field] === ''
)
? 'incomplete-row'
: ''
}
>
{/* Row Number */}
<td
className="row-number sticky-left-1"
onClick={() => insertRowAbove(rowIndex)}
title="Click to add row above"
>
{rowIndex + 1}
</td>
{/* Product Name */}
<td className="sticky-left-2">
{/* <div className="error-text-wrap">
<CellInput
value={product.productName}
onChange={e => {
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") && <span className="error-text">{getError(product.id, "productName")}</span>}
</div> */}
<div className="error-text-wrap">
<div className="input-with-scanner">
<CellInput
value={product.productName}
onChange={(e) => {
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')}
/>
</div>
{getError(product.id, 'productName') && (
<span className="error-text">
{getError(product.id, 'productName')}
</span>
)}
</div>
</td>
{/* UOM */}
<td className="sticky-left-3">
<div className="error-text-wrap">
<CellSelect
value={product.uom}
onChange={(e) =>
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') && (
<span className="error-text">
{getError(product.id, 'uom')}
</span>
)}
</div>
</td>
{/* No. of Units */}
<td>
<div className="error-text-wrap">
<CellInput
type="number"
value={product.nou}
onChange={(e) =>
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') && (
<span className="error-text">
{getError(product.id, 'nou')}
</span>
)}
</div>
</td>
{/* MRP */}
<td>
<div className="error-text-wrap">
<CellInput
type="number"
value={product.mrp}
onChange={(e) =>
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') && (
<span className="error-text">
{getError(product.id, 'mrp')}
</span>
)}
</div>
</td>
{/* Sale Price */}
<td>
<div className="error-text-wrap">
<CellInput
type="number"
value={product.salePrice}
onChange={(e) =>
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') && (
<span className="error-text">
{getError(product.id, 'salePrice')}
</span>
)}
</div>
</td>
{hasField('WholeSale Price') && (
<td>
<CellInput
type="number"
onKeyDown={(e) => {
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`)
}
/>
</td>
)}
{/* Category */}
<td>
<div className="error-text-wrap">
<CellSelect
value={product.category}
onChange={(e) =>
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') && (
<span className="error-text">
{getError(product.id, 'category')}
</span>
)}
</div>
</td>
{/* Sub-Category */}
{hasField('Sub Category') && (
<td>
<CellSelect
value={product.subCategory}
onChange={(e) =>
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"
/>
</td>
)}
{hasField('Brand') && hasField('Sub Category') && (
<td>
<CellSelect
value={product.brand || ''}
onChange={(e) =>
handleInputChange(product.id, 'brand', e.target.value)
}
options={product.availableBrands || []} // define your brand options
placeholder="Select Brand"
onClick={() => setSelectedCell(`${rowIndex}-brand`)}
/>
</td>
)}
{/* EMI Allowed */}
{hasField('EMI Allowed') && (
<td>
<Radio.Group
value={product.emiAllowed}
onChange={(e) =>
handleInputChange(
product.id,
'emiAllowed',
e.target.value
)
}
options={[
{ label: 'Yes', value: 'Yes' },
{ label: 'No', value: 'No' },
]}
optionType="button"
buttonStyle="solid"
/>
</td>
)}
{/* Auto Generate QRCode */}
{hasField('Auto Generate QRCode') && (
<>
<td>
<Radio.Group
value={product.autoGenerateQrcode}
onChange={(e) => {
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"
/>
</td>
{/* Scanner/AddQrCode */}
{product.autoGenerateQrcode === 'No' ? (
<td>
<CellInput
value={product.qrcode}
onChange={(e) =>
handleInputChange(
product.id,
'qrcode',
e.target.value
)
}
placeholder="Scan/Add QR"
onFocus={() =>
setSelectedCell(`${rowIndex}-qrcode`)
}
/>
</td>
) : (
<td
style={{
textAlign: 'center',
color: '#999',
cursor: 'not-allowed',
}}
>
-
</td>
)}
</>
)}
{hasField('Stock Maintenance') && (
<>
{/* Stock Maintenance */}
<td>
<Radio.Group
value={product.stockMaintenance}
onChange={(e) => {
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"
/>
</td>
{/* Low Stock Count */}
{product.stockMaintenance === 'Yes' ? (
<td>
<CellInput
type="number"
onKeyDown={(e) => {
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`)
}
/>
</td>
) : (
<td
style={{
textAlign: 'center',
color: '#999',
cursor: 'not-allowed',
}}
>
-
</td>
)}
</>
)}
{/* Token Maintenance */}
{hasField('Token Maintenance') && (
<td>
<Radio.Group
value={product.tokenMaintenance}
onChange={(e) =>
handleInputChange(
product.id,
'tokenMaintenance',
e.target.value
)
}
options={[
{ label: 'Yes', value: 'Yes' },
{ label: 'No', value: 'No' },
]}
optionType="button"
buttonStyle="solid"
/>
</td>
)}
{/* Amount Per Piece */}
{hasField('Amount Per Piece') && (
<>
<td>
<Radio.Group
value={product.amountPerPiece}
onChange={(e) => {
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"
/>
</td>
{/* Per Piece Amount, Auto Generate One Piece Qrcode, Scanner/AddOnePcQrCode, No. of Piece Inside */}
{product.amountPerPiece === 'Yes' ? (
<>
<td>
<CellInput
type="number"
onKeyDown={(e) => {
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`)
}
/>
</td>
<td>
<Radio.Group
value={product.autoGenerateOnePcQrcode}
onChange={(e) => {
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"
/>
</td>
{product.autoGenerateOnePcQrcode === 'No' ? (
<td>
<CellInput
value={product.onePcQrcode}
onChange={(e) =>
handleInputChange(
product.id,
'onePcQrcode',
e.target.value
)
}
placeholder="Scan/Add OnePc QR"
onFocus={() =>
setSelectedCell(`${rowIndex}-onePcQrcode`)
}
/>
</td>
) : (
<td
style={{
textAlign: 'center',
color: '#999',
cursor: 'not-allowed',
}}
>
-
</td>
)}
<td>
<CellInput
type="number"
onKeyDown={(e) => {
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`)
}
/>
</td>
</>
) : (
<>
<td
style={{
textAlign: 'center',
color: '#999',
cursor: 'not-allowed',
}}
>
-
</td>
<td
style={{
textAlign: 'center',
color: '#999',
cursor: 'not-allowed',
}}
>
-
</td>
<td
style={{
textAlign: 'center',
color: '#999',
cursor: 'not-allowed',
}}
>
-
</td>
<td
style={{
textAlign: 'center',
color: '#999',
cursor: 'not-allowed',
}}
>
-
</td>
</>
)}
</>
)}
{/* Product Type */}
{hasField('Product Type') && (
<td>
<CellSelect
value={product.productType}
onChange={(e) =>
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`)
}
/>
</td>
)}
{/* Tax */}
{hasField('Tax') && (
<td>
<div className="error-text-wrap">
<CellSelect
value={product.tax}
onChange={(e) =>
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') && (
<span className="error-text">
{getError(product.id, 'tax')}
</span>
)}
</div>
</td>
)}
{/* Cess */}
{hasField('Cess') && (
<td>
<CellInput
type="number"
onKeyDown={(e) => {
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`)}
/>
</td>
)}
{/* Percentage/Fixed */}
{hasField('Discount Type') && (
<td>
<Radio.Group
value={product.discountType}
onChange={(e) =>
handleInputChange(
product.id,
'discountType',
e.target.value
)
}
options={[
{ label: 'Percentage', value: 'P' },
{ label: 'Fixed', value: 'F' },
]}
optionType="button"
buttonStyle="solid"
/>
</td>
)}
{/* Discount Limit */}
{hasField('Discount Limit') && (
<td>
<div className="error-text-wrap">
<CellInput
type="number"
onKeyDown={(e) => {
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') && (
<span className="error-text">
{getError(product.id, 'discountLimit')}
</span>
)}
</div>
</td>
)}
{/* HSN Code */}
{hasField('HSN') && (
<td>
<CellInput
value={product.hsnCode}
onChange={(e) =>
handleHSNChange(product.id, e.target.value)
}
placeholder="HSN Code"
onFocus={() => setSelectedCell(`${rowIndex}-hsnCode`)}
list={`hsnList-${product.id}`}
/>
{hsnDetails?.length > 0 && (
<datalist id={`hsnList-${product.id}`}>
{hsnDetails
.filter((item) => {
const search = (selectedHsn || '').toLowerCase();
return (
item.HSN_CD?.toLowerCase()?.includes(search) ||
item.Description?.toLowerCase()?.includes(
search
)
);
})
.map((item, index) => (
<option
key={index}
value={item.HSN_CD}
label={item.Description}
/>
))}
</datalist>
)}
</td>
)}
{/* Part Number */}
{hasField('Part Number') && (
<td>
<CellInput
value={product.partNumber}
onChange={(e) =>
handleInputChange(
product.id,
'partNumber',
e.target.value
)
}
placeholder="Part Number"
onFocus={() =>
setSelectedCell(`${rowIndex}-partNumber`)
}
/>
</td>
)}
{/* Rack */}
{hasField('Rack') && (
<td>
<CellInput
value={product.rack}
onChange={(e) =>
handleInputChange(product.id, 'rack', e.target.value)
}
placeholder="Rack"
onFocus={() => setSelectedCell(`${rowIndex}-rack`)}
/>
</td>
)}
{/* Batch Number */}
{hasField('Batch Number') && (
<td>
<CellInput
value={product.batchNumber}
onChange={(e) =>
handleInputChange(
product.id,
'batchNumber',
e.target.value
)
}
placeholder="Batch Number"
onFocus={() =>
setSelectedCell(`${rowIndex}-batchNumber`)
}
/>
</td>
)}
{/* Model Number */}
{hasField('Model Number') && (
<td>
<CellInput
value={product.modelNumber}
onChange={(e) =>
handleInputChange(
product.id,
'modelNumber',
e.target.value
)
}
placeholder="Model Number"
onFocus={() =>
setSelectedCell(`${rowIndex}-modelNumber`)
}
/>
</td>
)}
{/* IMEI 1 */}
{hasField('IMEI 1') && (
<td>
<div className="error-text-wrap">
<CellInput
value={product.imei1}
onChange={(e) =>
handleInputChange(
product.id,
'imei1',
e.target.value
)
}
placeholder="IMEI 1"
onFocus={() => setSelectedCell(`${rowIndex}-imei1`)}
hasError={!!getError(product.id, 'imei1')}
/>
{getError(product.id, 'imei1') && (
<span className="error-text">
{getError(product.id, 'imei1')}
</span>
)}
</div>
</td>
)}
{/* IMEI 2 */}
{hasField('IMEI 2') && (
<td>
<div className="error-text-wrap">
<CellInput
value={product.imei2}
onChange={(e) =>
handleInputChange(
product.id,
'imei2',
e.target.value
)
}
placeholder="IMEI 2"
onFocus={() => setSelectedCell(`${rowIndex}-imei2`)}
hasError={!!getError(product.id, 'imei2')}
/>
{getError(product.id, 'imei2') && (
<span className="error-text">
{getError(product.id, 'imei2')}
</span>
)}
</div>
</td>
)}
{/* Serial Number */}
{hasField('Serial Number') && (
<td>
<CellInput
value={product.serialNumber}
onChange={(e) =>
handleInputChange(
product.id,
'serialNumber',
e.target.value
)
}
placeholder="Serial Number"
onFocus={() =>
setSelectedCell(`${rowIndex}-serialNumber`)
}
/>
</td>
)}
{/* MacId */}
{hasField('MacId') && (
<td>
<div className="error-text-wrap">
<CellInput
value={product.macId}
onChange={(e) =>
handleInputChange(
product.id,
'macId',
e.target.value
)
}
placeholder="MacId"
onFocus={() => setSelectedCell(`${rowIndex}-macId`)}
hasError={!!getError(product.id, 'macId')}
/>
{getError(product.id, 'macId') && (
<span className="error-text">
{getError(product.id, 'macId')}
</span>
)}
</div>
</td>
)}
{/* Manufacture Date */}
{hasField('Manufacture Date') && (
<td>
<DatePicker
value={
product.manufactureDate
? dayjs(product.manufactureDate)
: null
}
onChange={(_, dateString) => {
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
/>
</td>
)}
{/* Expire Date */}
{hasField('Expire Date') && (
<td>
<DatePicker
value={
product.expireDate ? dayjs(product.expireDate) : null
}
onChange={(_, dateString) =>
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;
}}
/>
</td>
)}
{/* Expiry Notification Days */}
{hasField('Expiry Notification Days') && (
<td>
<CellInput
type="number"
value={product.expiryNotificationDays}
onChange={(e) =>
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();
}
}}
/>
</td>
)}
{/* Available From */}
{hasField('Available From') && (
<td>
<TimePicker
value={
product.availableFrom
? dayjs(product.availableFrom, 'hh:mm a')
: null
}
onChange={(_, timeString) => {
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
/>
</td>
)}
{/* Available To */}
{hasField('Available To') && (
<td>
<TimePicker
value={
product.availableTo
? dayjs(product.availableTo, 'hh:mm a')
: null
}
onChange={(_, timeString) =>
handleInputChange(
product.id,
'availableTo',
timeString || null
)
}
size="small"
style={{ width: 110 }}
format="hh:mm a"
use12Hours
placeholder="To"
allowClear
inputReadOnly
/>
</td>
)}
{/* Product Image */}
{/* {hasField('Product Image') &&
<td style={{ verticalAlign: "bottom" }}>
<div className="imageuploadSction">
<EnhancedCropUpload
onlineImage={imageData.onlineImage}
ImageLink={imageData.imageUrl}
updateImageUrl={updateImageUrl}
isProductList={isProductList}
/>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<button
className="BrowseimgaeBTN"
onClick={handleBrowseImages}
disabled={!product.productName}
loading={loading}
>
Browse Images
</button>
</div>
</div>
</td>} */}
{/* {hasField('Product Image') &&
<td style={{ verticalAlign: "bottom" }}>
<div className="imageuploadSction">
<CropUpload
key={rowIndex}
onlineImage={rowIndex === imageModalRowIndex ? onlineImage : null}
ImageLink={product.productImage}
setOnlineImage={setOnlineImage}
updateImageUrl={url => {
setProducts(prev =>
prev.map((p, idx) =>
idx === rowIndex ? { ...p, productImage: url } : p
)
);
}}
/>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<button
className="BrowseimgaeBTN"
onClick={() => handleBrowseImages(product?.productName, rowIndex)}
disabled={!product.productName}
>
Browse
</button>
</div>
</div>
</td>} */}
{hasField('Product Image') && (
<td
style={{ verticalAlign: 'middle', textAlign: 'center' }}
>
<div className="imageuploadSction1">
<FaRegEye
style={{ background: 'none', cursor: 'pointer' }}
size={25}
color={product?.productImage && '#1F9B3A'}
onClick={() => {
setImageModalRowIndex(rowIndex);
setUploadImageModal(true);
setOnlineImage(null);
setSelectedImage(null);
}}
/>
</div>
</td>
)}
{/* Delete */}
<td style={{ textAlign: 'center' }}>
<button
onClick={() => deleteRow(product.id)}
className="action-btn action-btn--delete"
disabled={isSubmitting}
>
<Trash2 size={12} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Stats */}
<div className="stats-bar">
<div className="stats-item">
Total Products: <strong>{products?.length}</strong>
</div>
<div className="stats-item">
Showing: <strong>{filteredProducts?.length}</strong> of{' '}
<strong>{products?.length}</strong>
</div>
<div className="stats-help">
<div>
<span> Click row numbers to insert </span>
<span> Tab/Enter to navigate </span>
</div>
<div>
<span> Arrows to move </span>
<span> * = Required</span>
</div>
</div>
</div>
</div>
{isMobile && screenWidth <= 768 && openScanner && (
<DefaultModal
title="Barcode Reader"
width={800}
open={true}
footer={false}
handleCancel={() => {
handleQrData(null);
setOpenScanner(false); // close on cancel
}}
>
<BarcodeScannerTurbo
ref={scannerRef}
openScanner={openScanner} // same state for both modal + scanner
setOpenScanner={setOpenScanner}
handleQrData={handleQrData}
type="Search"
/>
</DefaultModal>
)}
<DefaultModal
open={fieldSetup}
handleCancel={() => setFieldSetup(false)}
footer={false}
title={'Select the fields you want to include in the table'}
children={
<div className="field-setup-container">
{/* <div className="field-setup-header">
<FormHeader title={'Select the fields you want to include in the table'} />
</div> */}
<div className="field-setup-content">
<Form onFinish={handleFieldSetupSubmit} ref={formRef}>
<Form.Item name="fields">
<DropDowns
label="Select fields"
// valueData={selectedFields}
onChangeFunction={handleFieldSelect}
options={tableFieldPreferences?.filter(
(p) => !selectedFields?.some((s) => s === p.value)
)}
/>
</Form.Item>
<div className="field-setup-selected-fields">
{selectedFields
?.map((id) =>
tableFieldPreferences?.find((p) => p.value === id)
)
.filter(Boolean)
.map((field) => (
<div className="selected-field" key={field.value}>
<div>{field.label}</div>
<div
className="close-icon"
onClick={() => handleFieldRemove(field.value)}
>
<IoClose size={15} />
</div>
</div>
))}
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Buttons
buttonText="SUBMIT"
color="901D77"
icon={<ArrowRightOutlined />}
htmlType={true}
/>
</div>
</Form>
</div>
</div>
}
/>
<DefaultModal
title="ONLINE IMAGE UPLOADER"
width={800}
open={imageModal}
footer={
selectedImage === null || selectedImage === undefined ? false : true
}
buttonText="Submit"
children={
<Form className="">
<div className="productonlineImg">
{onlineImageData?.map((item, index) => (
<div
className={`singleimg ${selectedImage === item ? 'selected' : ''}`}
key={index}
onClick={() => setSelectedImage(item)}
>
<img
src={item.image}
style={{ height: '100%', width: '100%' }}
alt="no image"
/>
</div>
))}
</div>
</Form>
}
handleSubmit={handleSubmitImage}
handleCancel={handleOnlineImageModalClose}
></DefaultModal>
<DefaultModal
title="Product Image"
width={500}
open={uploadImageModal}
footer={false}
handleCancel={() => {
setUploadImageModal(false);
setOnlineImage(null);
setImageModalRowIndex(null);
setSelectedImage(null);
}}
children={
<div className="imageuploadSction">
<CropUpload
key={products?.[imageModalRowIndex]?.id}
onlineImage={onlineImage}
ImageLink={products?.[imageModalRowIndex]?.productImage}
updateImageUrl={(url) => {
setProducts((prev) =>
prev.map((p, idx) =>
idx === imageModalRowIndex ? { ...p, productImage: url } : p
)
);
}}
/>
<Tooltip
title={
products?.[imageModalRowIndex]?.productName
? ''
: 'Please enter product name first'
}
>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<button
className="BrowseimgaeBTN"
onClick={() =>
handleBrowseImages(
products?.[imageModalRowIndex]?.productName,
imageModalRowIndex
)
}
disabled={!products?.[imageModalRowIndex]?.productName}
>
Browse
</button>
</div>
</Tooltip>
</div>
}
/>
</div>
);
};
export default TurboAddForm;