Check for existing scrub results & add beta terms.

This commit is contained in:
Patrick Fic
2025-10-10 09:57:08 -07:00
parent 700da26112
commit 84028f19dd
9 changed files with 142 additions and 104 deletions

View File

@@ -203,5 +203,10 @@
"title": "Release Notes for 1.4.2-beta.4",
"date": "09/16/2025",
"notes": "New Features:\r\n* Added part quantity toggle to shop setup. This will use the part quantity from the job lines when calculating RPS instead of assuming 1 per MPI guidelines.\r\n\r\nImprovements:\r\n* Updated ES integration to use newly specified API keys."
},
"1.5.0": {
"title": "Release Notes for 1.5.0",
"date": "10/14/2025",
"notes": "New Features:\r\n* Direct integration to Estimate Scrubber is now available in Public Beta. Existing customers should automatically have access to this feature. If you encounter any issues, please reach out to support.\r\n* Added a quantity toggle to use line quantity instead of assuming 1 per MPI guidelines for RPS calculations.\r\n* Added date shortcuts to the report page for ease of use.\r\n* Added a floating shortcut to run 1 month and 3 month reports.\r\n\r\nImprovements:\r\n* Added better indicators for quantities and negative RPS values."
}
}

View File

@@ -3,7 +3,7 @@
"productName": "ImEX RPS",
"author": "ImEX Systems Inc. <support@thinkimex.com>",
"description": "ImEX RPS",
"version": "1.4.2-beta.8",
"version": "1.5.0-alpha.1",
"main": "electron/main.js",
"homepage": "./",
"dependencies": {

View File

@@ -1,12 +1,11 @@
import { Button, message } from "antd";
import { useApolloClient } from "@apollo/client";
import { Button, message, Tooltip } from "antd";
import { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_JOB_ESTIMATE_SCRUBBER } from "../../../graphql/jobs.queries";
import ipcTypes from "../../../ipc.types";
import { selectBodyshop } from "../../../redux/user/user.selectors";
import { useApolloClient } from "@apollo/client";
import { QUERY_JOB_ESTIMATE_SCRUBBER } from "../../../graphql/jobs.queries";
import _ from "lodash";
const { ipcRenderer } = window;
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -61,9 +60,17 @@ export function EstimateScrubberButton({ bodyshop, jobid, job }) {
setLoading(false);
};
return (
const buttonDisabled = job?.g_bett_amt == null;
return buttonDisabled ? (
<Tooltip title="Additional information is required to scrub this estimate. Please reimport the estimate to enable scrubbing.">
<Button disabled={job?.g_bett_amt == null} loading={loading}>
Scrub Estimate with Estimate Scrubber (BETA)
</Button>
</Tooltip>
) : (
<Button onClick={handleScrub} disabled={job?.g_bett_amt == null} loading={loading}>
Scrub Estimate with Estimate Scrubber
Scrub Estimate with Estimate Scrubber (BETA)
</Button>
);
}

View File

@@ -23,6 +23,7 @@ const { Title, Text, Link } = Typography;
export function EstimateScrubberResults({ bodyshop, jobid, job, esResults }) {
const [searchText, setSearchText] = useState("");
const buttonDisabled = job?.g_bett_amt == null;
// Filter items based on search text
const filteredItems = esResults?.items
@@ -91,9 +92,13 @@ export function EstimateScrubberResults({ bodyshop, jobid, job, esResults }) {
if (!esResults?.items?.length || job?.id !== esResults?.jobid) {
return (
<Result
status={"info"}
title="Estimate not yet scrubbed."
subTitle="Run the estimate scrubber to see results here."
status={buttonDisabled ? "error" : "info"}
title={buttonDisabled ? "Unable to scrub." : "Estimate Not Scrubbed"}
subTitle={
buttonDisabled
? "Additional information is required to scrub this estimate. Please reimport the estimate to enable scrubbing."
: "Run the estimate scrubber to see results here."
}
extra={<EstimateScrubberButton key="es" jobid={job ? job.id : null} job={job ? job : null} />}
/>
@@ -133,91 +138,94 @@ export function EstimateScrubberResults({ bodyshop, jobid, job, esResults }) {
return (
<div style={{ padding: "16px" }}>
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<Space>
<Input
placeholder="Search"
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
allowClear
style={{ maxWidth: 400 }}
/>
{searchText && (
<div style={{ marginTop: "8px" }}>
<Text type="secondary">
Showing {filteredItems.length} of {esResults.items.length} items
</Text>
</div>
)}
{Object.keys(groupedItems).length === 0 ? (
<Result title="Estimate Scrubber did not find any issues." status="success" />
) : (
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<Space>
<Input
placeholder="Search"
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
allowClear
style={{ maxWidth: 400 }}
/>
{searchText && (
<div style={{ marginTop: "8px" }}>
<Text type="secondary">
Showing {filteredItems.length} of {esResults.items.length} items
</Text>
</div>
)}
<Button
style={{ float: "right" }}
type="primary"
href={esResults?.pdfUrl}
target="_blank"
disabled={!esResults?.pdfUrl}
>
View PDF
</Button>
<Button
style={{ float: "right" }}
type="link"
href={esResults?.reportIssueUrl}
target="_blank"
disabled={!esResults?.reportIssueUrl}
>
Report Scrubbing Issue
</Button>
<Button
style={{ float: "right" }}
type="primary"
href={esResults?.pdfUrl}
target="_blank"
disabled={!esResults?.pdfUrl}
>
View PDF
</Button>
<Button
style={{ float: "right" }}
type="link"
href={esResults?.reportIssueUrl}
target="_blank"
disabled={!esResults?.reportIssueUrl}
>
Report Scrubbing Issue
</Button>
</Space>
<Space wrap size={"small"}>
{Object.entries(groupedItems).map(([category, items]) => {
const config = categoryConfig[category] || { color: "default" };
return (
<Tag key={category} color={config.color}>
{category}: {items.length}
</Tag>
);
})}
</Space>
<Collapse defaultActiveKey={sortedCategories} expandIconPosition="right" size="large">
{sortedCategories.map((category) => {
const items = groupedItems[category];
const config = categoryConfig[category] || { color: "default", icon: "📄" };
return (
<Panel
header={
<Space>
{/* <span style={{ fontSize: "18px" }}>{config.icon}</span> */}
<Text strong>{category}</Text>
<Badge
count={items.length}
style={{
backgroundColor:
config.color === "blue" ? "#1890ff" : config.color === "orange" ? "#fa8c16" : "#52c41a"
}}
/>
</Space>
}
key={category}
>
<Table
dataSource={items}
columns={columns}
pagination={false}
size="middle"
rowKey={(record, index) => `${category}-${index}`}
style={{ marginTop: "12px" }}
scroll={{}}
/>
</Panel>
);
})}
</Collapse>
</Space>
<Space wrap size={"small"}>
{Object.entries(groupedItems).map(([category, items]) => {
const config = categoryConfig[category] || { color: "default" };
return (
<Tag key={category} color={config.color}>
{category}: {items.length}
</Tag>
);
})}
</Space>
{/* Grouped Results */}
<Collapse defaultActiveKey={sortedCategories} expandIconPosition="right" size="large">
{sortedCategories.map((category) => {
const items = groupedItems[category];
const config = categoryConfig[category] || { color: "default", icon: "📄" };
return (
<Panel
header={
<Space>
{/* <span style={{ fontSize: "18px" }}>{config.icon}</span> */}
<Text strong>{category}</Text>
<Badge
count={items.length}
style={{
backgroundColor:
config.color === "blue" ? "#1890ff" : config.color === "orange" ? "#fa8c16" : "#52c41a"
}}
/>
</Space>
}
key={category}
>
<Table
dataSource={items}
columns={columns}
pagination={false}
size="middle"
rowKey={(record, index) => `${category}-${index}`}
style={{ marginTop: "12px" }}
scroll={{}}
/>
</Panel>
);
})}
</Collapse>
</Space>
)}
</div>
);
}

View File

@@ -1,20 +1,19 @@
import { useQuery } from "@apollo/client";
import { Card, Result } from "antd";
import { Badge, Card, Result } from "antd";
import { useEffect, useRef } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_JOB_BY_PK } from "../../../graphql/jobs.queries";
import { setSelectedJobTargetPc } from "../../../redux/application/application.actions";
import { selectSelectedJobId } from "../../../redux/application/application.selectors";
import { selectBodyshop } from "../../../redux/user/user.selectors";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import JobsPartsGraphAtom from "../../atoms/jobs-parts-graph/jobs-parts-graph.atom";
import EstimateScrubberButton from "../../molecules/estimate-scrubber-button/estimate-scrubber-button.molecule";
import EstimateScrubberResultsMolecule from "../../molecules/estimate-scruber-results/estimate-scrubber-results.molecule";
import JobsDetailDescriptionMolecule from "../../molecules/jobs-detail-description/jobs-detail-description.molecule";
import JobsLinesTableMolecule from "../../molecules/jobs-lines-table/jobs-lines-table.molecule";
import JobsTargetsStatsMolecule from "../../molecules/jobs-targets-stats/jobs-targets-stats.molecule";
import "./jobs-detail.organism.styles.scss";
import { selectBodyshop } from "../../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -87,9 +86,11 @@ export function JobsDetailOrganism({ bodyshop, selectedJobId, setSelectedJobTarg
<JobsLinesTableMolecule loading={loading} job={data ? data.jobs_by_pk : {}} />
</Card>
{bodyshop.es_api_key && (
<Card id="es-results-card" title="Estimate Scrubber Results" extra={[]}>
<EstimateScrubberResultsMolecule loading={loading} job={data ? data.jobs_by_pk : {}} />
</Card>
<Badge.Ribbon text="BETA" color="red">
<Card id="es-results-card" title="Estimate Scrubber Results" extra={[]}>
<EstimateScrubberResultsMolecule loading={loading} job={data ? data.jobs_by_pk : {}} />
</Card>
</Badge.Ribbon>
)}
<Card title="Parts Breakdown">
<div

View File

@@ -4,14 +4,16 @@ import _ from "lodash";
import client from "../graphql/GraphQLClient";
import { INSERT_NEW_JOB, QUERY_CLOSE_DATE_BY_CLM_NO, QUERY_JOB_BY_CLM_NO, UPDATE_JOB } from "../graphql/jobs.queries";
import { QUERY_GROUPS_BY_MAKE_TYPE } from "../graphql/veh_group.queries";
import ipcTypes from "../ipc.types";
import { clearEsResults } from "../redux/application/application.actions.js";
import { store } from "../redux/store";
import TrucksList from "./trucks.json";
import { WhichRulesetToApply } from "../util/constants.js";
import dayjs from "../util/day.js";
import CargoVanList from "./cargovans.json";
import PassengerVanList from "./passengervans.json";
import SuvList from "./suvs.json";
import ipcTypes from "../ipc.types";
import dayjs from "../util/day.js";
import { WhichRulesetToApply } from "../util/constants.js";
import TrucksList from "./trucks.json";
const { logger } = window;
const { ipcRenderer } = window;
@@ -46,6 +48,7 @@ export async function GetR4PDateWithClaim(clm_no) {
export async function UpsertEstimate(job) {
const shopId = store.getState().user.bodyshop.id;
//logger.info("Beginning Upserting job from Renderer.");
ipcRenderer.send(ipcTypes.app.toMain.log.info, "Beginning Upserting job from Renderer.");
const existingJobs = await client.query({
@@ -88,6 +91,12 @@ export async function UpsertEstimate(job) {
//This will now only preserve theg roup if it didn't require a re-import. If it did, reset the group.
}
const currentScrubbedjobId = store.getState().application.esResults.jobid;
if (currentScrubbedjobId && currentScrubbedjobId === existingJobs.data.jobs[0].id) {
store.dispatch(clearEsResults());
}
logger.info("Attemping to update job.");
await client.mutate({
mutation: UPDATE_JOB,

View File

@@ -61,3 +61,8 @@ export const setScrubResults = (scrubResults) => ({
type: ApplicationActionTypes.SET_ES_RESULTS,
payload: scrubResults
});
export const clearEsResults = () => ({
type: ApplicationActionTypes.CLEAR_ES_RESULTS
});

View File

@@ -75,6 +75,8 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
return { ...state, releaseNotes: action.payload };
case ApplicationActionTypes.SET_ES_RESULTS:
return { ...state, esResults: action.payload };
case ApplicationActionTypes.CLEAR_ES_RESULTS:
return { ...state, esResults: { jobid: null, identified_items: [] } };
default:
return state;
}

View File

@@ -11,6 +11,7 @@ const ApplicationActionTypes = {
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE",
SET_UPDATE_PROGRESS: "SET_UPDATE_PROGRESS",
SET_RELEASE_NOTES: "SET_RELEASE_NOTES",
SET_ES_RESULTS: "SET_ES_RESULTS"
SET_ES_RESULTS: "SET_ES_RESULTS",
CLEAR_ES_RESULTS: "CLEAR_ES_RESULTS"
};
export default ApplicationActionTypes;