3738 lines
131 KiB
JavaScript
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;
|