Add impact and JSON results showing for ES.

This commit is contained in:
Patrick Fic
2025-08-26 14:21:56 -07:00
parent be12e94234
commit 58670d208b
21 changed files with 266 additions and 11 deletions

View File

@@ -255,7 +255,7 @@ async function DecodeAd2File(extensionlessFilePath) {
}
}
async function DecodeVehFile(extensionlessFilePath) {
async function DecodePfhFile(extensionlessFilePath) {
let dbf;
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFH`);
@@ -270,7 +270,7 @@ async function DecodeVehFile(extensionlessFilePath) {
}
}
async function DecodePfhFile(extensionlessFilePath) {
async function DecodeVehFile(extensionlessFilePath) {
let dbf;
try {
dbf = await DBFFile.open(`${extensionlessFilePath}V.VEH`);
@@ -282,8 +282,8 @@ async function DecodePfhFile(extensionlessFilePath) {
if (!dbf) return {};
let records = await dbf.readRecords(1);
return _.pick(records[0], [
// "IMPACT_1",
// "IMPACT_2",
"IMPACT_1",
"IMPACT_2",
// "DB_V_CODE",
// "PLATE_NO",
// "PLATE_ST",

View File

@@ -82,7 +82,7 @@ async function ScrubEstimate({ job }) {
return lowercasedTotal;
});
}
console.log("*** ~ ScrubEstimate ~ job:", job);
const fileName = `RPSTest-${job.id}-${Date.now()}`;
// Write job object to logs subfolder
@@ -104,7 +104,6 @@ async function ScrubEstimate({ job }) {
},
headers: formData.getHeaders ? formData.getHeaders() : {}
});
console.log("*** ~ ScrubEstimate ~ result:", result);
const resultPDFUrl = result?.data?.report_link

View File

@@ -34,6 +34,8 @@ insert_permissions:
- group_verified
- id
- id_pro_nam
- impact_1
- impact_2
- ins_co_nm
- loss_date
- loss_desc
@@ -71,6 +73,8 @@ select_permissions:
- group_verified
- id
- id_pro_nam
- impact_1
- impact_2
- ins_co_nm
- loss_date
- loss_desc
@@ -115,6 +119,8 @@ update_permissions:
- group_verified
- id
- id_pro_nam
- impact_1
- impact_2
- ins_co_nm
- loss_date
- loss_desc

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."jobs" add column "impact1" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."jobs" add column "impact1" text
null;

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."jobs" add column "impact2" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."jobs" add column "impact2" text
null;

View File

@@ -0,0 +1 @@
alter table "public"."jobs" rename column "impact_1" to "impact1";

View File

@@ -0,0 +1 @@
alter table "public"."jobs" rename column "impact1" to "impact_1";

View File

@@ -0,0 +1 @@
alter table "public"."jobs" rename column "impact_2" to "impact2";

View File

@@ -0,0 +1 @@
alter table "public"."jobs" rename column "impact2" to "impact_2";

View File

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

View File

@@ -0,0 +1,197 @@
import { InfoCircleOutlined, LinkOutlined, SearchOutlined } from "@ant-design/icons";
import { Badge, Collapse, Input, Space, Table, Tag, Typography } from "antd";
import _ from "lodash";
import { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectEsResults } from "../../../redux/application/application.selectors";
import { selectBodyshop } from "../../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
esResults: selectEsResults
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(EstimateScrubberResults);
const { Panel } = Collapse;
const { Title, Text, Link } = Typography;
export function EstimateScrubberResults({ bodyshop, jobid, job, esResults }) {
const [searchText, setSearchText] = useState("");
// Filter items based on search text
const filteredItems = esResults?.items
? esResults.items.filter(item => {
if (!searchText.trim()) return true;
const searchLower = searchText.toLowerCase();
return (
(item.L && item.L.toLowerCase().includes(searchLower)) ||
(item.R && item.R.toLowerCase().includes(searchLower))
);
})
: [];
// Group filtered items by category
const groupedItems = filteredItems.length
? _.groupBy(
filteredItems.filter((item) => item.Category !== "Display Group"),
"Category"
)
: {};
// Define category colors and priorities
const categoryConfig = {
"Association Items": { color: "blue", priority: 1, icon: "🔗" },
"Guidelines Items": { color: "orange", priority: 2, icon: "📋" }
//"Display Group": { color: "green", priority: 3, icon: "📊" }
};
// Sort categories by priority
const sortedCategories = Object.keys(groupedItems).sort((a, b) => {
const priorityA = categoryConfig[a]?.priority || 999;
const priorityB = categoryConfig[b]?.priority || 999;
return priorityA - priorityB;
});
// Define table columns
const columns = [
{
title: "Item",
dataIndex: "L",
key: "item",
width: "25%",
render: (text, record) => (
<Space direction="vertical" size="small">
<Text strong>{text}</Text>
{record.LinkText && record.Anchor && (
<Link href={record.Anchor} target="_blank" rel="noopener noreferrer">
<LinkOutlined /> Learn more
</Link>
)}
</Space>
)
},
{
title: "Description",
dataIndex: "R",
key: "description",
width: "75%",
render: (text) => <Text style={{}}>{text}</Text>
}
];
if (!esResults?.items?.length || job?.id !== esResults?.jobid) {
return (
<div style={{ padding: "24px", textAlign: "center" }}>
<InfoCircleOutlined style={{ fontSize: "48px", color: "#d9d9d9", marginBottom: "16px" }} />
<Title level={4} type="secondary">
Estimate not yet scrubbed.
</Title>
<Text type="secondary">Run the estimate scrubber to see results here.</Text>
</div>
);
}
// Show empty search results state
if (searchText && !filteredItems.length) {
return (
<div style={{ padding: "16px" }}>
<Input
placeholder="Search items and descriptions..."
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
allowClear
style={{ maxWidth: 400, marginBottom: "24px" }}
/>
<div style={{ padding: "24px", textAlign: "center" }}>
<SearchOutlined style={{ fontSize: "48px", color: "#d9d9d9", marginBottom: "16px" }} />
<Title level={4} type="secondary">
No items match your search
</Title>
<Text type="secondary">Try different keywords or clear the search to see all items.</Text>
</div>
</div>
);
}
return (
<div style={{ padding: "16px" }}>
<Space direction="vertical" size="large" style={{ width: "100%" }}>
{/* Search Input */}
<div>
<Input
placeholder="Search items and descriptions..."
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>
)}
</div>
{/* 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>
{/* Summary */}
<Title level={5}>Summary</Title>
<Space wrap>
{Object.entries(groupedItems).map(([category, items]) => {
const config = categoryConfig[category] || { color: "default" };
return (
<Tag key={category} color={config.color}>
{category}: {items.length}
</Tag>
);
})}
</Space>
</Space>
</div>
);
}

View File

@@ -1,6 +1,6 @@
import { useQuery } from "@apollo/client";
import { Card, Result } from "antd";
import React, { useEffect, useRef } from "react";
import { useEffect, useRef } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_JOB_BY_PK } from "../../../graphql/jobs.queries";
@@ -8,12 +8,12 @@ import { setSelectedJobTargetPc } from "../../../redux/application/application.a
import { selectSelectedJobId } from "../../../redux/application/application.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 JobsClaimClerk from "../../molecules/jobs-claims-clerk/jobs-claims-clerk.molecule";
import { EstimateScrubberButton } from "../../molecules/estimate-scrubber-button/estimate-scrubber-button.molecule";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -93,6 +93,18 @@ export function JobsDetailOrganism({ selectedJobId, setSelectedJobTargetPc }) {
>
<JobsLinesTableMolecule loading={loading} job={data ? data.jobs_by_pk : {}} />
</Card>
<Card
title="Estimate Scrubber Results"
extra={[
<EstimateScrubberButton
key="es"
jobid={data ? data.jobs_by_pk?.id : null}
job={data ? data.jobs_by_pk : null}
/>
]}
>
<EstimateScrubberResultsMolecule loading={loading} job={data ? data.jobs_by_pk : {}} />
</Card>
<Card title="Parts Breakdown">
<div
style={{

View File

@@ -235,6 +235,8 @@ export const QUERY_JOB_ESTIMATE_SCRUBBER = gql`
ownr_ph1
v_stage
id_pro_nam
impact_1
impact_2
supp_amt
g_bett_amt
bodyshop {

View File

@@ -1,6 +1,7 @@
import ipcTypes from "../ipc.types";
import {
setReleaseNotes,
setScrubResults,
setSettings,
setUpdateAvailable,
setUpdateProgress,
@@ -84,3 +85,6 @@ ipcRenderer.on(ipcTypes.audit.toRenderer.auditClaimsArray, async (event, claimsA
ipcRenderer.on(ipcTypes.audit.toRenderer.auditError, async (event, error) => {
store.dispatch(setAuditError(error));
});
ipcRenderer.on(ipcTypes.app.toRenderer.scrubResults, async (event, results) => {
store.dispatch(setScrubResults(results));
});

View File

@@ -57,3 +57,7 @@ export const setReleaseNotes = (releaseNotes) => ({
type: ApplicationActionTypes.SET_RELEASE_NOTES,
payload: releaseNotes
});
export const setScrubResults = (scrubResults) => ({
type: ApplicationActionTypes.SET_ES_RESULTS,
payload: scrubResults
});

View File

@@ -10,6 +10,10 @@ const INITIAL_STATE = {
updateAvailable: false,
updateProgress: null,
releaseNotes: null,
esResults: {
jobid: null,
identified_items: []
}
};
const { ipcRenderer } = window;
@@ -69,6 +73,8 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
return { ...state, updateProgress: action.payload };
case ApplicationActionTypes.SET_RELEASE_NOTES:
return { ...state, releaseNotes: action.payload };
case ApplicationActionTypes.SET_ES_RESULTS:
return { ...state, esResults: action.payload };
default:
return state;
}

View File

@@ -46,3 +46,7 @@ export const selectReleaseNotes = createSelector(
[selectApplication],
(application) => application.releaseNotes
);
export const selectEsResults = createSelector(
[selectApplication],
(application) => application.esResults
);

View File

@@ -11,5 +11,6 @@ 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"
};
export default ApplicationActionTypes;

View File

@@ -186,6 +186,10 @@ export function* handleCalculateScoreCard({ payload: queriedJobs }) {
const simpleJobAlerts = [];
job.joblines.forEach((jobline) => {
if (jobline.ignore) {
return;
}
if (jobline.part_qty > 1) {
simpleJobAlerts.push({
key: `line-${jobline.id}-q`,
@@ -206,7 +210,7 @@ export function* handleCalculateScoreCard({ payload: queriedJobs }) {
}
});
const jobAlerts = [...simpleJobAlerts,
const jobAlerts = [...simpleJobAlerts,
// ...job.joblines
// .map((jobline) =>
// jobline.alerts?.map((alert, idx) => ({