IO-3515 Refactor button to separate component.
This commit is contained in:
@@ -0,0 +1,151 @@
|
|||||||
|
import { Button } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext";
|
||||||
|
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
billEnterModal: selectBillEnterModal,
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
function BillEnterAiScan({
|
||||||
|
billEnterModal,
|
||||||
|
bodyshop,
|
||||||
|
pollingIntervalRef,
|
||||||
|
setPollingIntervalRef,
|
||||||
|
form,
|
||||||
|
fileInputRef,
|
||||||
|
scanLoading,
|
||||||
|
setScanLoading
|
||||||
|
}) {
|
||||||
|
const notification = useNotification();
|
||||||
|
|
||||||
|
// Polling function for multipage PDF status
|
||||||
|
const pollJobStatus = async (jobId) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`/ai/bill-ocr/status/${jobId}`);
|
||||||
|
|
||||||
|
if (data.status === "COMPLETED") {
|
||||||
|
// Stop polling
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
setPollingIntervalRef(null);
|
||||||
|
}
|
||||||
|
setScanLoading(false);
|
||||||
|
|
||||||
|
// Update form with the extracted data
|
||||||
|
if (data.data && data.data.billForm) {
|
||||||
|
form.setFieldsValue(data.data.billForm);
|
||||||
|
notification.success({
|
||||||
|
title: "AI Scan Complete",
|
||||||
|
message: "Invoice data has been extracted successfully"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (data.status === "FAILED") {
|
||||||
|
// Stop polling on failure
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
setPollingIntervalRef(null);
|
||||||
|
}
|
||||||
|
setScanLoading(false);
|
||||||
|
|
||||||
|
notification.error({
|
||||||
|
title: "AI Scan Failed",
|
||||||
|
message: data.error || "Failed to process the invoice"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// If status is IN_PROGRESS, continue polling
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error polling job status:", error);
|
||||||
|
|
||||||
|
// Stop polling on error
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
setPollingIntervalRef(null);
|
||||||
|
}
|
||||||
|
setScanLoading(false);
|
||||||
|
|
||||||
|
notification.error({
|
||||||
|
title: "AI Scan Error",
|
||||||
|
message: error.response?.data?.message || error.message || "Failed to check scan status"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*,application/pdf"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
setScanLoading(true);
|
||||||
|
const formdata = new FormData();
|
||||||
|
formdata.append("billScan", file);
|
||||||
|
formdata.append("jobid", billEnterModal.context.job.id);
|
||||||
|
formdata.append("bodyshopid", bodyshop.id);
|
||||||
|
formdata.append("partsorderid", "3dd26419-a139-4399-af4e-43eeb6f0dbad");
|
||||||
|
//formdata.append("skipTextract", "true"); // For testing purposes
|
||||||
|
axios
|
||||||
|
.post("/ai/bill-ocr", formdata)
|
||||||
|
.then(({ data, status }) => {
|
||||||
|
if (status === 202) {
|
||||||
|
// Multipage PDF - start polling
|
||||||
|
notification.info({
|
||||||
|
title: "Processing Invoice",
|
||||||
|
message: "This is a multipage document. Processing may take a few moments..."
|
||||||
|
});
|
||||||
|
|
||||||
|
//Workaround needed to bypass react-compiler error about manipulating refs in child components. Refactor may be needed in the future to clean this up.
|
||||||
|
setPollingIntervalRef(
|
||||||
|
setInterval(() => {
|
||||||
|
pollJobStatus(data.jobId);
|
||||||
|
}, 3000)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initial poll
|
||||||
|
pollJobStatus(data.jobId);
|
||||||
|
} else if (status === 200) {
|
||||||
|
// Single page - immediate response
|
||||||
|
setScanLoading(false);
|
||||||
|
|
||||||
|
form.setFieldsValue(data.data.billForm);
|
||||||
|
notification.success({
|
||||||
|
title: "AI Scan Complete",
|
||||||
|
message: "Invoice data has been extracted successfully"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("*** ~ BillEnterModalContainer ~ error:", error);
|
||||||
|
setScanLoading(false);
|
||||||
|
notification.error({
|
||||||
|
title: "AI Scan Failed",
|
||||||
|
message: error.response?.data?.message || error.message || "Failed to process invoice"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Reset the input so the same file can be selected again
|
||||||
|
e.target.value = "";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
console.log("Fields Object", form.getFieldsValue());
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
}}
|
||||||
|
loading={scanLoading}
|
||||||
|
disabled={scanLoading}
|
||||||
|
>
|
||||||
|
{scanLoading ? "Processing Invoice..." : "AI Scan"}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(mapStateToProps, null)(BillEnterAiScan);
|
||||||
@@ -6,6 +6,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
|
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
|
||||||
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
|
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
|
||||||
@@ -21,13 +22,12 @@ import { GenerateDocument } from "../../utils/RenderTemplate";
|
|||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import confirmDialog from "../../utils/asyncConfirm";
|
import confirmDialog from "../../utils/asyncConfirm";
|
||||||
import useLocalStorage from "../../utils/useLocalStorage";
|
import useLocalStorage from "../../utils/useLocalStorage";
|
||||||
|
import BillEnterAiScan from "../bill-enter-ai-scan/bill-enter-ai-scan.component.jsx";
|
||||||
import BillFormContainer from "../bill-form/bill-form.container";
|
import BillFormContainer from "../bill-form/bill-form.container";
|
||||||
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
||||||
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
|
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
|
||||||
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
|
||||||
import { handleUpload as handleUploadToImageProxy } from "../documents-upload-imgproxy/documents-upload-imgproxy.utility";
|
import { handleUpload as handleUploadToImageProxy } from "../documents-upload-imgproxy/documents-upload-imgproxy.utility";
|
||||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
billEnterModal: selectBillEnterModal,
|
billEnterModal: selectBillEnterModal,
|
||||||
@@ -402,62 +402,15 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//Workaround needed to bypass react-compiler error about manipulating refs in child components. Refactor may be needed in the future to clean this up.
|
||||||
|
const setPollingIntervalRef = (func) => {
|
||||||
|
pollingIntervalRef.current = func;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (enterAgain) form.submit();
|
if (enterAgain) form.submit();
|
||||||
}, [enterAgain, form]);
|
}, [enterAgain, form]);
|
||||||
|
|
||||||
// Polling function for multipage PDF status
|
|
||||||
const pollJobStatus = async (jobId) => {
|
|
||||||
try {
|
|
||||||
const { data } = await axios.get(`/ai/bill-ocr/status/${jobId}`);
|
|
||||||
|
|
||||||
if (data.status === 'COMPLETED') {
|
|
||||||
// Stop polling
|
|
||||||
if (pollingIntervalRef.current) {
|
|
||||||
clearInterval(pollingIntervalRef.current);
|
|
||||||
pollingIntervalRef.current = null;
|
|
||||||
}
|
|
||||||
setScanLoading(false);
|
|
||||||
|
|
||||||
// Update form with the extracted data
|
|
||||||
if (data.data && data.data.billForm) {
|
|
||||||
form.setFieldsValue(data.data.billForm);
|
|
||||||
notification.success({
|
|
||||||
title: "AI Scan Complete",
|
|
||||||
message: "Invoice data has been extracted successfully"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (data.status === 'FAILED') {
|
|
||||||
// Stop polling on failure
|
|
||||||
if (pollingIntervalRef.current) {
|
|
||||||
clearInterval(pollingIntervalRef.current);
|
|
||||||
pollingIntervalRef.current = null;
|
|
||||||
}
|
|
||||||
setScanLoading(false);
|
|
||||||
|
|
||||||
notification.error({
|
|
||||||
title: "AI Scan Failed",
|
|
||||||
message: data.error || "Failed to process the invoice"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// If status is IN_PROGRESS, continue polling
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error polling job status:", error);
|
|
||||||
|
|
||||||
// Stop polling on error
|
|
||||||
if (pollingIntervalRef.current) {
|
|
||||||
clearInterval(pollingIntervalRef.current);
|
|
||||||
pollingIntervalRef.current = null;
|
|
||||||
}
|
|
||||||
setScanLoading(false);
|
|
||||||
|
|
||||||
notification.error({
|
|
||||||
title: "AI Scan Error",
|
|
||||||
message: error.response?.data?.message || error.message || "Failed to check scan status"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (billEnterModal.open) {
|
if (billEnterModal.open) {
|
||||||
form.setFieldsValue(formValues);
|
form.setFieldsValue(formValues);
|
||||||
@@ -497,72 +450,14 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
}}
|
}}
|
||||||
footer={
|
footer={
|
||||||
<Space>
|
<Space>
|
||||||
<input
|
<BillEnterAiScan
|
||||||
ref={fileInputRef}
|
fileInputRef={fileInputRef}
|
||||||
type="file"
|
form={form}
|
||||||
accept="image/*,application/pdf"
|
pollingIntervalRef={pollingIntervalRef}
|
||||||
style={{ display: "none" }}
|
setPollingIntervalRef={setPollingIntervalRef}
|
||||||
onChange={(e) => {
|
scanLoading={scanLoading}
|
||||||
const file = e.target.files?.[0];
|
setScanLoading={setScanLoading}
|
||||||
if (file) {
|
|
||||||
setScanLoading(true);
|
|
||||||
const formdata = new FormData();
|
|
||||||
formdata.append("billScan", file);
|
|
||||||
formdata.append("jobid", billEnterModal.context.job.id);
|
|
||||||
formdata.append("bodyshopid", bodyshop.id);
|
|
||||||
formdata.append("partsorderid", "3dd26419-a139-4399-af4e-43eeb6f0dbad");
|
|
||||||
//formdata.append("skipTextract", "true"); // For testing purposes
|
|
||||||
axios
|
|
||||||
.post("/ai/bill-ocr", formdata)
|
|
||||||
.then(({ data, status }) => {
|
|
||||||
if (status === 202) {
|
|
||||||
// Multipage PDF - start polling
|
|
||||||
notification.info({
|
|
||||||
title: "Processing Invoice",
|
|
||||||
message: "This is a multipage document. Processing may take a few moments..."
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start polling every 3 seconds
|
|
||||||
pollingIntervalRef.current = setInterval(() => {
|
|
||||||
pollJobStatus(data.jobId);
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
// Initial poll
|
|
||||||
pollJobStatus(data.jobId);
|
|
||||||
} else if (status === 200) {
|
|
||||||
// Single page - immediate response
|
|
||||||
setScanLoading(false);
|
|
||||||
|
|
||||||
form.setFieldsValue(data.data.billForm);
|
|
||||||
notification.success({
|
|
||||||
title: "AI Scan Complete",
|
|
||||||
message: "Invoice data has been extracted successfully"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("*** ~ BillEnterModalContainer ~ error:", error);
|
|
||||||
setScanLoading(false);
|
|
||||||
notification.error({
|
|
||||||
title: "AI Scan Failed",
|
|
||||||
message: error.response?.data?.message || error.message || "Failed to process invoice"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Reset the input so the same file can be selected again
|
|
||||||
e.target.value = "";
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
console.log("Fields Object", form.getFieldsValue());
|
|
||||||
fileInputRef.current?.click();
|
|
||||||
}}
|
|
||||||
loading={scanLoading}
|
|
||||||
disabled={scanLoading}
|
|
||||||
>
|
|
||||||
{scanLoading ? "Processing Invoice..." : "AI Scan"}
|
|
||||||
</Button>
|
|
||||||
<Checkbox checked={generateLabel} onChange={(e) => setGenerateLabel(e.target.checked)}>
|
<Checkbox checked={generateLabel} onChange={(e) => setGenerateLabel(e.target.checked)}>
|
||||||
{t("bills.labels.generatepartslabel")}
|
{t("bills.labels.generatepartslabel")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|||||||
Reference in New Issue
Block a user