IO-3515 set po context, update confidence UI showing

This commit is contained in:
Patrick Fic
2026-02-18 11:57:56 -08:00
parent d4bbdd7383
commit 5d53d09af9
9 changed files with 77 additions and 42 deletions

View File

@@ -5,6 +5,7 @@ import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext"; import { useNotification } from "../../contexts/Notifications/notificationContext";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors"; import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { FaWandMagicSparkles } from "react-icons/fa6";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal, billEnterModal: selectBillEnterModal,
@@ -19,7 +20,8 @@ function BillEnterAiScan({
form, form,
fileInputRef, fileInputRef,
scanLoading, scanLoading,
setScanLoading setScanLoading,
setIsAiScan
}) { }) {
const notification = useNotification(); const notification = useNotification();
@@ -59,8 +61,6 @@ function BillEnterAiScan({
} }
// If status is IN_PROGRESS, continue polling // If status is IN_PROGRESS, continue polling
} catch (error) { } catch (error) {
console.error("Error polling job status:", error);
// Stop polling on error // Stop polling on error
if (pollingIntervalRef.current) { if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current); clearInterval(pollingIntervalRef.current);
@@ -86,11 +86,12 @@ function BillEnterAiScan({
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
setScanLoading(true); setScanLoading(true);
setIsAiScan(true);
const formdata = new FormData(); const formdata = new FormData();
formdata.append("billScan", file); formdata.append("billScan", file);
formdata.append("jobid", billEnterModal.context.job.id); formdata.append("jobid", billEnterModal.context.job.id);
formdata.append("bodyshopid", bodyshop.id); formdata.append("bodyshopid", bodyshop.id);
formdata.append("partsorderid", "3dd26419-a139-4399-af4e-43eeb6f0dbad"); formdata.append("partsorderid", billEnterModal.context.parts_order?.id);
//formdata.append("skipTextract", "true"); // For testing purposes //formdata.append("skipTextract", "true"); // For testing purposes
axios axios
.post("/ai/bill-ocr", formdata) .post("/ai/bill-ocr", formdata)
@@ -123,7 +124,6 @@ function BillEnterAiScan({
} }
}) })
.catch((error) => { .catch((error) => {
console.error("*** ~ BillEnterModalContainer ~ error:", error);
setScanLoading(false); setScanLoading(false);
notification.error({ notification.error({
title: "AI Scan Failed", title: "AI Scan Failed",
@@ -137,9 +137,9 @@ function BillEnterAiScan({
/> />
<Button <Button
onClick={() => { onClick={() => {
console.log("Fields Object", form.getFieldsValue());
fileInputRef.current?.click(); fileInputRef.current?.click();
}} }}
icon={<FaWandMagicSparkles />}
loading={scanLoading} loading={scanLoading}
disabled={scanLoading} disabled={scanLoading}
> >

View File

@@ -52,6 +52,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES); const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [scanLoading, setScanLoading] = useState(false); const [scanLoading, setScanLoading] = useState(false);
const [isAiScan, setIsAiScan] = useState(false);
const client = useApolloClient(); const client = useApolloClient();
const [generateLabel, setGenerateLabel] = useLocalStorage("enter_bill_generate_label", false); const [generateLabel, setGenerateLabel] = useLocalStorage("enter_bill_generate_label", false);
const notification = useNotification(); const notification = useNotification();
@@ -59,12 +60,13 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
const pollingIntervalRef = useRef(null); const pollingIntervalRef = useRef(null);
const { const {
treatments: { Enhanced_Payroll, Imgproxy } treatments: { Enhanced_Payroll, Imgproxy, Bill_OCR_AI }
} = useTreatmentsWithConfig({ } = useTreatmentsWithConfig({
attributes: {}, attributes: {},
names: ["Enhanced_Payroll", "Imgproxy"], names: ["Enhanced_Payroll", "Imgproxy", "Bill_OCR_AI"],
splitKey: bodyshop.imexshopid splitKey: bodyshop.imexshopid
}); });
console.log("*** ~ BillEnterModalContainer ~ Bill_OCR_AI:", Bill_OCR_AI);
const formValues = useMemo(() => { const formValues = useMemo(() => {
return { return {
@@ -382,6 +384,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
vendorid: values.vendorid, vendorid: values.vendorid,
billlines: [] billlines: []
}); });
setIsAiScan(false);
// form.resetFields(); // form.resetFields();
} else { } else {
toggleModalVisible(); toggleModalVisible();
@@ -398,6 +401,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
pollingIntervalRef.current = null; pollingIntervalRef.current = null;
} }
setScanLoading(false); setScanLoading(false);
setIsAiScan(false);
toggleModalVisible(); toggleModalVisible();
} }
}; };
@@ -422,6 +426,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
pollingIntervalRef.current = null; pollingIntervalRef.current = null;
} }
setScanLoading(false); setScanLoading(false);
setIsAiScan(false);
} }
}, [billEnterModal.open, form, formValues]); }, [billEnterModal.open, form, formValues]);
@@ -437,7 +442,22 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
return ( return (
<Modal <Modal
title={t("bills.labels.new")} title={
<Space size="large">
{t("bills.labels.new")}
{Bill_OCR_AI.treatment === "on" && (
<BillEnterAiScan
fileInputRef={fileInputRef}
form={form}
pollingIntervalRef={pollingIntervalRef}
setPollingIntervalRef={setPollingIntervalRef}
scanLoading={scanLoading}
setScanLoading={setScanLoading}
setIsAiScan={setIsAiScan}
/>
)}
</Space>
}
width={"98%"} width={"98%"}
open={billEnterModal.open} open={billEnterModal.open}
okText={t("general.actions.save")} okText={t("general.actions.save")}
@@ -450,14 +470,6 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
}} }}
footer={ footer={
<Space> <Space>
<BillEnterAiScan
fileInputRef={fileInputRef}
form={form}
pollingIntervalRef={pollingIntervalRef}
setPollingIntervalRef={setPollingIntervalRef}
scanLoading={scanLoading}
setScanLoading={setScanLoading}
/>
<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>
@@ -491,7 +503,11 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
}} }}
> >
<RbacWrapper action="bills:enter"> <RbacWrapper action="bills:enter">
<BillFormContainer form={form} disableInvNumber={billEnterModal.context.disableInvNumber} /> <BillFormContainer
form={form}
isAiScan={isAiScan}
disableInvNumber={billEnterModal.context.disableInvNumber}
/>
</RbacWrapper> </RbacWrapper>
</Form> </Form>
</Modal> </Modal>

View File

@@ -43,7 +43,8 @@ export function BillFormComponent({
loadOutstandingReturns, loadOutstandingReturns,
loadInventory, loadInventory,
preferredMake, preferredMake,
disableInHouse disableInHouse,
isAiScan
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
@@ -452,6 +453,7 @@ export function BillFormComponent({
responsibilityCenters={responsibilityCenters} responsibilityCenters={responsibilityCenters}
disabled={disabled} disabled={disabled}
billEdit={billEdit} billEdit={billEdit}
isAiScan={isAiScan}
/> />
)} )}
<Divider titlePlacement="left" style={{ display: billEdit ? "none" : null }}> <Divider titlePlacement="left" style={{ display: billEdit ? "none" : null }}>

View File

@@ -15,7 +15,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber, disableInHouse }) { export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber, disableInHouse,isAiScan }) {
const { const {
treatments: { Simple_Inventory } treatments: { Simple_Inventory }
} = useTreatmentsWithConfig({ } = useTreatmentsWithConfig({
@@ -50,6 +50,7 @@ export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableI
loadOutstandingReturns={loadOutstandingReturns} loadOutstandingReturns={loadOutstandingReturns}
loadInventory={loadInventory} loadInventory={loadInventory}
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null} preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
isAiScan={isAiScan}
/> />
{!billEdit && <BillCmdReturnsTableComponent form={form} returnLoading={returnLoading} returnData={returnData} />} {!billEdit && <BillCmdReturnsTableComponent form={form} returnLoading={returnLoading} returnData={returnData} />}
{Simple_Inventory.treatment === "on" && ( {Simple_Inventory.treatment === "on" && (

View File

@@ -30,7 +30,8 @@ export function BillEnterModalLinesComponent({
discount, discount,
form, form,
responsibilityCenters, responsibilityCenters,
billEdit billEdit,
isAiScan
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form; const { setFieldsValue, getFieldsValue, getFieldValue } = form;
@@ -140,6 +141,29 @@ export function BillEnterModalLinesComponent({
const columns = (remove) => { const columns = (remove) => {
return [ return [
...(isAiScan
? [
{
title: t("billlines.fields.confidence"),
dataIndex: "confidence",
editable: true,
width: "5rem",
formItemProps: (field) => ({
key: `${field.index}confidence`,
name: [field.name, "confidence"],
label: t("billlines.fields.confidence")
}),
formInput: (record) => {
const confidenceValue = getFieldValue(["billlines", record.name, "confidence"]);
return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
<ConfidenceDisplay value={confidenceValue} />
</div>
);
}
}
]
: []),
{ {
title: t("billlines.fields.jobline"), title: t("billlines.fields.jobline"),
dataIndex: "joblineid", dataIndex: "joblineid",
@@ -213,25 +237,7 @@ export function BillEnterModalLinesComponent({
}), }),
formInput: () => <Input.TextArea disabled={disabled} autoSize tabIndex={0} /> formInput: () => <Input.TextArea disabled={disabled} autoSize tabIndex={0} />
}, },
{
title: t("billlines.fields.confidence"),
dataIndex: "confidence",
editable: true,
width: "5rem",
formItemProps: (field) => ({
key: `${field.index}confidence`,
name: [field.name, "confidence"],
label: t("billlines.fields.confidence")
}),
formInput: (record) => {
const confidenceValue = getFieldValue(["billlines", record.name, "confidence"]);
return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
<ConfidenceDisplay value={confidenceValue} />
</div>
);
}
},
{ {
title: t("billlines.fields.quantity"), title: t("billlines.fields.quantity"),
dataIndex: "quantity", dataIndex: "quantity",

View File

@@ -187,6 +187,7 @@ export function PartsOrderListTableDrawerComponent({
actions: { refetch: refetch }, actions: { refetch: refetch },
context: { context: {
job: job, job: job,
parts_order: { id: record.id },
bill: { bill: {
vendorid: record.vendor.id, vendorid: record.vendor.id,
is_credit_memo: record.return, is_credit_memo: record.return,

View File

@@ -162,6 +162,7 @@ export function PartsOrderListTableComponent({
actions: { refetch: refetch }, actions: { refetch: refetch },
context: { context: {
job: job, job: job,
parts_order: { id: record.id },
bill: { bill: {
vendorid: record.vendor.id, vendorid: record.vendor.id,
is_credit_memo: record.return, is_credit_memo: record.return,

View File

@@ -10,8 +10,14 @@ const { Option } = Select;
const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, preferredMake, showPhone, ref }) => { const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, preferredMake, showPhone, ref }) => {
const [option, setOption] = useState(value); const [option, setOption] = useState(value);
// Sync internal state when value prop changes (e.g., from form.setFieldsValue)
useEffect(() => {
if (value !== option) {
setOption(value);
}
}, [value]);
useEffect(() => { useEffect(() => {
console.log("*** ~ VendorSearchSelect ~ USEEFFECT:", value, option);
if (value !== option && onChange) { if (value !== option && onChange) {
if (value && !option) { if (value && !option) {
onChange(value); onChange(value);

View File

@@ -4,4 +4,6 @@ Required Infrastructure setup
3. Created 2 roles for SNS. The textract role is the right one, the other was created manually based on incorrect instructions. 3. Created 2 roles for SNS. The textract role is the right one, the other was created manually based on incorrect instructions.
TODO: TODO:
* Create a rome bucket for uploads, or move to the regular spot. * Create a rome bucket for uploads, or move to the regular spot.
* How to implement this across environments.
* How to prevent polling for a job that may have errored.