diff --git a/client/src/components/bill-enter-ai-scan/bill-enter-ai-scan.component.jsx b/client/src/components/bill-enter-ai-scan/bill-enter-ai-scan.component.jsx
new file mode 100644
index 000000000..38c7802e4
--- /dev/null
+++ b/client/src/components/bill-enter-ai-scan/bill-enter-ai-scan.component.jsx
@@ -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 (
+ <>
+ {
+ 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 = "";
+ }}
+ />
+
+ >
+ );
+}
+export default connect(mapStateToProps, null)(BillEnterAiScan);
diff --git a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx
index 6f8d7ca0d..84249e2aa 100644
--- a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx
+++ b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx
@@ -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={
- {
- 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 = "";
- }}
+
-
setGenerateLabel(e.target.checked)}>
{t("bills.labels.generatepartslabel")}