Android_Retail/src/Pages/Product/TurboAddForm.jsx

2698 lines
106 KiB
React
Raw Normal View History

2026-01-27 18:27:29 +05:30
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 (
<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' }}
/>
)}
<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" }}>
<table className="excel-table">
<thead>
<tr>
<th>#</th>
<th style={{ width: "300px !important" }}>Product Name *</th>
<th>
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}
/>
</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}
/>
</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'}
/>
</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}
/>
</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" onClick={() => insertRowAbove(rowIndex)} title="Click to add row above">
{rowIndex + 1}
</td>
{/* Product Name */}
<td >
{/* <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>
<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;