IO-3515 Refactor button to separate component.

This commit is contained in:
Patrick Fic
2026-02-18 10:32:44 -08:00
parent 6d94265081
commit e5f930b8c8
2 changed files with 166 additions and 120 deletions

View File

@@ -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);

View File

@@ -6,6 +6,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
@@ -21,13 +22,12 @@ import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import confirmDialog from "../../utils/asyncConfirm";
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 { CalculateBillTotal } from "../bill-form/bill-form.totals.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 RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import axios from "axios";
import { handleUpload } from "../documents-upload/documents-upload.utility";
const mapStateToProps = createStructuredSelector({
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(() => {
if (enterAgain) form.submit();
}, [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(() => {
if (billEnterModal.open) {
form.setFieldsValue(formValues);
@@ -497,72 +450,14 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
}}
footer={
<Space>
<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..."
});
// 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 = "";
}}
<BillEnterAiScan
fileInputRef={fileInputRef}
form={form}
pollingIntervalRef={pollingIntervalRef}
setPollingIntervalRef={setPollingIntervalRef}
scanLoading={scanLoading}
setScanLoading={setScanLoading}
/>
<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)}>
{t("bills.labels.generatepartslabel")}
</Checkbox>