From e5f930b8c85f49aa5dc2b50dcac371db218805c4 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Wed, 18 Feb 2026 10:32:44 -0800 Subject: [PATCH] IO-3515 Refactor button to separate component. --- .../bill-enter-ai-scan.component.jsx | 151 ++++++++++++++++++ .../bill-enter-modal.container.jsx | 135 ++-------------- 2 files changed, 166 insertions(+), 120 deletions(-) create mode 100644 client/src/components/bill-enter-ai-scan/bill-enter-ai-scan.component.jsx 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")}