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