Android_Retail/src/Pages/StockTransfer/CreateTransfer.jsx

756 lines
25 KiB
React
Raw Normal View History

2026-01-27 18:27:29 +05:30
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { Form, Input, AutoComplete, Select, Tooltip, Modal } from 'antd';
import { BsUpcScan } from 'react-icons/bs';
import { ArrowRightOutlined, DeleteFilled } from '@ant-design/icons';
import { useDispatch, useSelector } from 'react-redux';
import FormHeader from '../PageComponents/FormHeader.jsx';
import FloatLabel from '../../Components/Forms/FloatLabel/index.jsx';
import { Messages } from '../../Components/Notifications/Messages.jsx';
import Buttons from '../../Components/Forms/Buttons.jsx';
import './CreateTransfer.scss';
import { getPurProdvaraiantdata } from '../../Features/PurchaseOrder/PurchaseOrder.js';
import { getProductDataPageNo } from '../../Features/ProductPage/ProductPage.js';
import { DropDowns } from '../../Components/Forms/DropDown.jsx';
import { getSession } from '../../Services/Others.js';
import { getCompBranchData } from '../../Features/BrachLogin/BranchLogin.js';
import { changeBreadCrumb } from '../../Features/AppPage/CenterPage.js';
import { useLocation, useNavigate } from 'react-router-dom';
import { postRequestStock } from '../../Features/RequestStock/RequestStock.js';
2026-02-20 15:31:31 +05:30
import BarCodeScan from '../../Services/BarCodeScan.jsx';
import { isMobile } from 'react-device-detect';
2026-01-27 18:27:29 +05:30
const subDirectory = import.meta.env.BASE_URL;
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
const CreateTransfer = () => {
const dispatch = useDispatch();
const formProductRef = useRef();
const productInputRef = useRef(null);
const location = useLocation();
const state = location?.state;
const status = state.transform;
const navigate = useNavigate();
const CompId = getSession('CompId');
const BranchId = getSession('BranchId');
const AppId = getSession('AppId');
const UserId = getSession('UserId');
const [messageType, setMessageType] = useState(null);
const [messageData, setMessageData] = useState(null);
const [productSearchText, setProductSearchText] = useState('');
const [searchText, setSearchText] = useState('');
const [tableData, setTableData] = useState([]);
const [scanner, setScanner] = useState(false);
const [productdata, setProductData] = useState([]);
const [variantDetails, setVariantDetails] = useState([]);
const [showVariantModal, setShowVariantModal] = useState(false);
const [selectedProductForVariant, setSelectedProductForVariant] =
useState(null);
const [selectedVariants, setSelectedVariants] = useState([]);
const [isProcessing, setIsProcessing] = useState(false);
const [isManualSelection, setIsManualSelection] = useState(false);
const [branchData, setBranchData] = useState();
const [selectedBranchId, setSelectedBranchId] = useState();
const [selectedComponent, setselectedComponent] = useState('Create');
const items = [
{
name: 'Home',
link: `${subDirectory}app-page/home`,
},
{
name: 'Transfer List',
link: `${subDirectory}setting/stock-transferlist`,
},
{
name: status === 'Create' ? 'Create' : 'Request ',
link: null,
},
];
useEffect(() => {
dispatch(changeBreadCrumb({ items: items }));
fetchbranchData();
fetchProductData();
}, []);
const onComplete = useCallback(() => {
setMessageType(null);
setMessageData(null);
}, []);
const productOptions = useMemo(
() =>
productdata?.map((p) => ({
value: p.ProdId,
label: p.ProdName,
})) || [],
[productdata]
);
const filteredTableData = useMemo(() => {
if (!searchText) return tableData;
return tableData.filter(
(item) =>
item.ProdName?.toLowerCase().includes(searchText.toLowerCase()) ||
item.ProdVariantName?.toLowerCase().includes(searchText.toLowerCase())
);
}, [tableData, searchText]);
console.log(filteredTableData, 'AaADaAadaadAD');
const fetchbranchData = async () => {
const data = {
AppId: AppId,
CompId: CompId,
};
const res = await dispatch(getCompBranchData(data)).unwrap();
if (res?.data?.statusCode === 1) {
const filteredBranches = res?.data?.data?.filter(
(branch) => branch.BrId !== BranchId
);
setBranchData(filteredBranches);
}
};
const fetchProductData = async () => {
try {
const ProdData = {
AppId: AppId,
CompId: CompId,
BranchId: BranchId,
};
const res = await dispatch(getProductDataPageNo(ProdData)).unwrap();
if (res?.data?.statusCode === 1) {
setProductData(res?.data?.data || []);
} else {
setMessageType('error');
setMessageData('Failed to load products');
}
} catch (error) {
console.error('Error fetching products:', error);
setMessageType('error');
setMessageData('Error loading products');
}
};
const branchDropDownChange = async (e) => {
setSelectedBranchId(e);
};
const addProductToTable = useCallback((product, variant = null) => {
const newProduct = {
...product,
...(variant && {
ProdVariantId: variant.ProdVariantId,
ProdVariantName: variant.ProdVariantName,
InwardDtlId: variant.StockDetails?.[0]?.InwardDtlId,
MRP: variant.StockDetails?.[0]?.MRP,
SellPrice: variant.StockDetails?.[0]?.SellPrice,
OverAllQty: variant.OverAllQty,
}),
};
let success = false;
setTableData((prev) => {
const safePrev = Array.isArray(prev) ? prev : [];
const exists = safePrev.some(
(p) =>
p.ProdId === newProduct.ProdId &&
(variant
? p.ProdVariantId === variant.ProdVariantId
: !p.ProdVariantId)
);
if (exists) {
return safePrev;
}
success = true;
// return [...safePrev, newProduct];
return [newProduct, ...safePrev];
});
if (!success) {
setMessageType('warning');
setMessageData(
variant ? `Already added in the table` : 'This product is already Added'
);
}
2026-01-29 09:40:03 +05:30
if (scanner) {
setTimeout(() => {
productInputRef.current?.focus();
setProductSearchText('');
}, 100);
}
2026-01-27 18:27:29 +05:30
formProductRef?.current?.resetFields();
2026-01-29 09:40:03 +05:30
if (!scanner) {
2026-01-27 18:27:29 +05:30
setProductSearchText('');
2026-01-29 09:40:03 +05:30
}
2026-01-27 18:27:29 +05:30
return success;
2026-01-29 09:40:03 +05:30
}, [scanner]);
2026-01-27 18:27:29 +05:30
const focusProductInput = () => {
// give React a tick to close modal/input blur before refocusing
setTimeout(() => productInputRef.current?.focus(), 0);
};
const handleProductSearch = (value) => {
setProductSearchText(value);
if (scanner && value.length >= 3 && !isManualSelection) {
debouncedScannerSearch(value);
}
if (isManualSelection) {
setIsManualSelection(false);
}
};
const debouncedScannerSearch = useCallback(
debounce((value) => {
handleScannerResult(value);
}, 300),
[productdata]
);
const handleScannerResult = async (qrCode) => {
if (isProcessing) return; // Prevent multiple simultaneous processing
setIsProcessing(true);
try {
const selectedProduct = productdata.find(
(p) => p.QRCode === qrCode || p.ProdId === qrCode
);
if (!selectedProduct) {
setMessageType('error');
setMessageData('Product not found for scanned code');
setProductSearchText('');
return;
}
await processSelectedProduct(selectedProduct, true); // Pass true to indicate scanner mode
} finally {
setIsProcessing(false);
}
};
const handleProductSelect = async (prodId, option) => {
if (isProcessing) return;
setIsManualSelection(true);
setIsProcessing(true);
try {
const selectedProduct = productdata.find((p) => p.ProdId === prodId);
if (!selectedProduct) {
setMessageType('error');
setMessageData('Product not found');
return;
}
await processSelectedProduct(selectedProduct, false);
} finally {
setIsProcessing(false);
}
};
const processSelectedProduct = async (selectedProduct, isScanner = false) => {
if (!selectedProduct) {
setMessageType('error');
setMessageData('Product not found');
return;
}
formProductRef.current?.setFieldsValue({ ProdId: selectedProduct.ProdId });
setProductSearchText(selectedProduct.ProdName);
setSelectedProductForVariant(selectedProduct);
try {
const data = {
CompId,
AppId,
BranchId,
prodName: selectedProduct.ProdName,
};
const variantValues = await dispatch(
getPurProdvaraiantdata(data)
).unwrap();
if (variantValues?.data?.statusCode === 1) {
const variants =
variantValues?.data?.data?.filter(
(prod) =>
prod?.ProductDetail?.[0]?.ProdId === selectedProduct?.ProdId
)?.[0]?.ProductDetail?.[0]?.ProdVariantDetails || [];
// Add unique identifier to each variant if not present
const variantsWithId = variants.map((variant, index) => ({
...variant,
ProdVariantId:
variant.ProdVariantId ||
variant.StockDetails?.[0]?.InwardDtlId ||
`${selectedProduct.ProdId}_${variant.ProdVariantName}_${index}`,
}));
if (variantsWithId.length > 1) {
setVariantDetails(variantsWithId);
setSelectedVariants([]); // Reset selected variants
setShowVariantModal(true);
} else if (variantsWithId.length === 1) {
addProductToTable(selectedProduct, variantsWithId[0]);
} else {
addProductToTable(selectedProduct);
}
} else {
addProductToTable(selectedProduct);
}
} catch (error) {
console.error('Error fetching variants:', error);
addProductToTable(selectedProduct);
}
};
const handleFieldChange = (field, index, value) => {
setTableData((prev) => {
const updated = [...prev];
const actualItem = filteredTableData[index];
const actualIndex = prev?.findIndex(
(p) =>
p.ProdId === actualItem.ProdId &&
p.ProdVariantId === actualItem.ProdVariantId
);
if (actualIndex !== -1) {
updated[actualIndex][field] = value;
}
return updated;
});
};
const handleVariantCheckboxChange = (variantId, checked) => {
setSelectedVariants((prev) => {
if (checked) {
return [...prev, variantId];
} else {
return prev.filter((id) => id !== variantId);
}
});
};
const handleAddSelectedVariants = () => {
if (selectedVariants.length === 0) {
setMessageType('warning');
setMessageData('Please select at least one variant');
return;
}
if (selectedProductForVariant) {
let addedCount = 0;
selectedVariants.forEach((variantId) => {
const variant = variantDetails.find(
(v) => v.ProdVariantId === variantId
);
if (variant) {
const success = addProductToTable(selectedProductForVariant, variant);
if (success) addedCount++;
}
if (addedCount > 0) {
setMessageType('success');
setMessageData(
`${variant.ProdVariantName} added to transfer successfully`
);
}
});
}
setShowVariantModal(false);
setVariantDetails([]);
setSelectedProductForVariant(null);
setSelectedVariants([]);
2026-01-29 09:40:03 +05:30
if (scanner) {
setTimeout(() => {
productInputRef.current?.focus();
setProductSearchText('');
}, 100);
}
2026-01-27 18:27:29 +05:30
};
const handleDeleteRow = (index) => {
const productToDelete = filteredTableData[index];
setTableData((prev) =>
prev?.filter(
(p) =>
!(
p.ProdId === productToDelete.ProdId &&
p.ProdVariantId === productToDelete.ProdVariantId
)
)
);
setMessageType('success');
setMessageData(`${productToDelete.ProdName} removed successfully`);
};
const onFinish = async () => {
if (tableData.length === 0) {
setMessageType('warning');
setMessageData('Please add at least one product');
return;
}
const invalidRow = tableData.find(
(item) => !item.ReceivedQty || Number(item.ReceivedQty) <= 0
);
if (invalidRow) {
setMessageType('warning');
setMessageData('Required Qty must be greater than 0 for all products');
return;
}
if (!selectedBranchId) {
setMessageType('warning');
setMessageData('Please select To Location (Branch)');
return;
}
const payload = {
CompId: CompId,
BranchId: BranchId,
AppId: AppId,
FromLocationId:
selectedComponent === 'Create' ? selectedBranchId : BranchId,
ToLocationId:
selectedComponent === 'Create' ? BranchId : selectedBranchId,
RequestStatus: selectedComponent,
RequestStock: tableData?.map((item) => ({
ProdId: item.ProdId,
InwardDtlId: item.InwardDtlId,
RequestedQty: Number(item.ReceivedQty),
})),
CreatedBy: UserId,
};
const response = await dispatch(postRequestStock(payload)).unwrap();
if (response?.data?.statusCode === 1) {
setMessageType('success');
setMessageData(response?.data?.response);
setTableData('');
setSelectedBranchId('');
navigate(`${subDirectory}setting/stock-transferlist`);
} else {
setMessageType('error');
setMessageData(response?.data?.response);
}
};
useEffect(() => {
if (status && status !== selectedComponent) {
setselectedComponent(status);
}
}, [status]);
const ComponentSelect = (value) => {
setselectedComponent(value);
};
const handleScannerToggle = () => {
setScanner((prev) => {
const next = !prev;
if (next) {
setIsManualSelection(false);
setTimeout(() => productInputRef.current?.focus(), 0);
}
return next;
});
};
return (
<div className="pageOverAll CreateTransferMaster">
<div className="userPage">
<div className="userPageContent">
<Messages
messageType={messageType}
messageData={messageData}
onComplete={onComplete}
/>
{/* <FormHeader title="Create Transfer" /> */}
{/* <div
className="defaultandCustomisedTheme"
>
{status === 'Create' && <div
className={selectedComponent === 'Create' ? 'selected' : ''}
onClick={() => ComponentSelect(status)}
>
{status} Transfer
</div>}
{status === 'Request' && <div
className={selectedComponent === 'Request' ? 'selected' : ''}
onClick={() => ComponentSelect(status)}
>
{status} Transfer
</div>}
</div> */}
<Form ref={formProductRef} layout="vertical">
<div style={{ display: 'flex', gap: '10px' }}>
<div className="product-selection-row">
<div className="product-name-wrapper">
<FloatLabel label="Product Name" value={productSearchText}>
<AutoComplete
ref={productInputRef}
options={productOptions}
value={productSearchText}
onSelect={(value, option) =>
handleProductSelect(value, option)
}
onChange={handleProductSearch}
filterOption={(input, option) =>
option.label.toLowerCase().includes(input.toLowerCase())
}
disabled={isProcessing}
/>
</FloatLabel>
2026-02-20 15:31:31 +05:30
{isMobile ? (
<div className="IsmoBileBarcode">
<BarCodeScan
onScan={(value) => {
debouncedScannerSearch(value);
}}
/>
</div>
) : (
2026-01-27 18:27:29 +05:30
<Tooltip title="BarCode / QR Code">
<BsUpcScan
className={`product-scan-icon ${scanner ? 'active' : ''}`}
onClick={handleScannerToggle}
/>
</Tooltip>
2026-02-20 15:31:31 +05:30
)}
2026-01-27 18:27:29 +05:30
</div>
</div>
<div>
<DropDowns
label={<label className="required">Branch</label>}
className="field-DropDown"
options={branchData?.map((option) => ({
value: option.BrId,
label: option.BrName,
}))}
onChangeFunction={(e) => branchDropDownChange(e)}
valueData={selectedBranchId}
/>
</div>
{/* <div>
<Input
placeholder="Search products in table..."
value={searchText}
allowClear
onChange={(e) => setSearchText(e.target.value)}
className="transfer-search-input"
/>
</div> */}
</div>
</Form>
<Form onFinish={onFinish} className="transfer-form">
<div className="transfer-table-container">
<table className="transfer-table">
<thead className="transfer-table-head">
<tr>
<th style={{ width: '60px' }}>S.No</th>
<th style={{ textAlign: 'left' }}>Product Name</th>
<th>Product Variant</th>
<th>UOM Name</th>
{selectedComponent === 'Create' && <th>Balance Qty</th>}
<th>Required Qty</th>
<th style={{ width: '100px' }}>Action</th>
</tr>
</thead>
<tbody className="transfer-table-body">
{filteredTableData.length === 0 ? (
<tr>
<td colSpan="7" className="transfer-no-data">
No products added yet.
</td>
</tr>
) : (
filteredTableData?.map((record, index) => (
<tr
key={`${record.ProdId}-${record.ProdVariantId || 'no-variant'}`}
>
<td>{index + 1}</td>
<td style={{ textAlign: 'left' }}>{record.ProdName}</td>
<td>{record.ProdVariantName || 'N/A'}</td>
<td>{record.UomName}</td>
{selectedComponent === 'Create' && (
<td>{record.OverAllQty}</td>
)}
<td>
<Input
className="transfer-input"
// value={record.ReceivedQty}
placeholder="0"
onChange={(e) =>
handleFieldChange(
'ReceivedQty',
index,
e.target.value.replace(/[^0-9.]/g, '')
)
}
/>
</td>
<td className="transfer-action">
<DeleteFilled
style={{
color: '#FF4D4F',
cursor: 'pointer',
fontSize: '16px',
}}
onClick={() => handleDeleteRow(index)}
/>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
<div className="buttonStyle">
<Buttons
icon={<ArrowRightOutlined />}
buttonText={status}
color="901D77"
htmlType="submit"
/>
</div>
</Form>
<Modal
title="Select Product Variant"
open={showVariantModal}
onCancel={() => {
setShowVariantModal(false);
setVariantDetails([]);
setSelectedProductForVariant(null);
setSelectedVariants([]);
2026-01-29 09:40:03 +05:30
if (scanner) {
setTimeout(() => {
productInputRef.current?.focus();
setProductSearchText('');
}, 100);
}
2026-01-27 18:27:29 +05:30
}}
onOk={handleAddSelectedVariants}
okText="Add Selected Variants"
cancelText="Cancel"
width={500}
>
<div className="variant-modal-content">
{variantDetails && variantDetails.length > 0 ? (
<div>
<p style={{ marginBottom: '1rem', fontWeight: 500 }}>
Select variant(s) for{' '}
<strong>{selectedProductForVariant?.ProdName}</strong>
</p>
{variantDetails.map((variant) => (
<div
key={variant.ProdVariantId}
className="variant-option"
style={{
padding: '12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
marginBottom: '8px',
cursor: 'pointer',
transition: 'all 0.3s',
backgroundColor: selectedVariants.includes(
variant.ProdVariantId
)
? '#f0f5ff'
: 'transparent',
}}
onClick={() =>
handleVariantCheckboxChange(
variant.ProdVariantId,
!selectedVariants.includes(variant.ProdVariantId)
)
}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
}}
>
<input
type="checkbox"
checked={selectedVariants.includes(
variant.ProdVariantId
)}
onChange={(e) =>
handleVariantCheckboxChange(
variant.ProdVariantId,
e.target.checked
)
}
onClick={(e) => e.stopPropagation()}
style={{
width: '18px',
height: '18px',
cursor: 'pointer',
accentColor: '#901D77',
}}
/>
<div style={{ flex: 1 }}>
<p style={{ margin: '0 0 4px 0', fontWeight: 500 }}>
{variant.ProdVariantName}
</p>
<p
style={{
margin: '0',
fontSize: '12px',
color: '#666',
}}
>
MRP: {variant.StockDetails?.[0]?.MRP || 0} | Sell
Price: {variant.StockDetails?.[0]?.SellPrice || 0}
</p>
</div>
</div>
</div>
))}
<p
style={{
marginTop: '12px',
fontSize: '12px',
color: '#666',
}}
>
Selected: {selectedVariants.length} variant(s)
</p>
</div>
) : (
<p>No variants available</p>
)}
</div>
</Modal>
</div>
</div>
</div>
);
};
export default CreateTransfer;