Initial work for claims clerk.

This commit is contained in:
Patrick Fic
2025-02-19 10:31:56 -08:00
parent 4e663977b3
commit d044cce054
16 changed files with 330 additions and 55 deletions

View File

@@ -0,0 +1,192 @@
const { DBFFile } = require("dbffile");
const path = require("path");
const _ = require("lodash");
const log = require("electron-log");
const { store } = require("../electron-store");
const { BrowserWindow } = require("electron");
const ipcTypes = require("../../src/ipc.types.commonjs");
//Return the jobline. Modification happens in place.
exports.claimsClerk = ({ jobline, joblines }) => {
const alerts = rules
.map((rule) => rule({ jobline, joblines })) //If it should be ignored, skip it.
.filter((rule) => rule !== null);
return alerts;
};
const rules = [
({ jobline, joblines }) => {
//Upgrade 1
if (
jobline.db_ref === "900500" &&
jobline.db_price === 0 &&
jobline.db_hrs === 0 &&
jobline.mod_lb_hrs !== jobline.db_hrs
) {
return {
key: "Manual Line",
alert: `<div>
Manually entered line detected.
<ul>
<li>This part will NOT count towards your RPS.</li>
<li>You will need to supply an invoice to MPI for this part.</li>
<li>Always ensure part is not in CEG before creating a manual entry line.</li>
</ul>
</div>`
};
}
return null;
},
({ jobline, joblines }) => {
//Upgrade 2
if (joblines.db_hrs !== 0 && jobline.mod_lb_hrs !== jobline.db_hrs) {
return {
key: "Manual labor Time Change",
alert: `<div>
Labor time manually changed from original CEG time.
<ul>
<li>This could possibly be denied by MPI.</li>
<li> Ensure labor time is accurate & justified.</li>
<li>Add an explanation line if needed.</li>
</ul>
</div>`
};
}
return null;
},
({ jobline, joblines }) => {
//Upgrade 3
if (jobline.db_ref === "900500" && jobline.mod_lb_hrs !== jobline.db_hrs) {
return {
key: "Manual Labor Line",
alert: `<div>
Manually entered labor line detected.
<ul>
<li>Always ensure the labor operation you manually entered was not available in CEG.</li>
<li>Make sure there are no overlaps with other labor lines to consider.</li>
</ul>
</div>`
};
}
return null;
},
({ jobline, joblines }) => {
//Upgrade 4
if (
jobline.db_ref !== "900500" &&
jobline.part_type &&
jobline.oem_partno &&
jobline.price_j //TODO Requires verification per Norm's email.
) {
if (jobline.act_price < jobline.db_price) {
//TODO: Verify what should happen here when the two values are the same?
return {
key: "Modified part price",
alert: `<div>
Modified part price detected.
<ul>
<li>
You will need to supply MPI with an invoice for this part showing the retail price you manually
entered.
</li>
<li>
Your manually entered price is <strong>LOWER</strong> than the database price, consider reverting back to database
price.
</li>
<li>If you chose to leave the manually entered price, you will need to:</li>
<li>Supply MPI with an invoice for this part showing the retail price you manually entered</li>
<li>NOTE: You do not need to show MPI your cost on this part, only retail.</li>
</ul>
</div>`
};
} else {
return {
key: "Modified part price",
alert: `<div>
Modified part price detected.
<ul>
<li>
You will need to supply MPI with an invoice for this part showing the retail price you manually
entered.
</li>
<li>NOTE: You do not need to show MPI your cost on this part, only retail.</li>
</ul>
</div>`
};
}
}
return null;
},
({ jobline, joblines }) => {
// In this update we want to identify OEM part lines where the part # and price have been changed.
if (
(jobline.part_type === "PAA" || jobline.part_type === "PAL") &&
jobline.alt_partno &&
jobline.act_price < jobline.db_price //TODO: Verify the equals than case.
) {
//Need to find a 900501 line that is right after it indicating it has the words pricematch
const lineIndex = joblines.findIndex((line) => line.line_no === jobline.line_no);
const nextLine = joblines[lineIndex + 1];
console.log("*** ~ nextLine:", nextLine);
if (!nextLine?.line_desc.includes("price")) {
return {
key: "Missing pricematch explanation",
alert: `<div>In that explanation line (900501) we are looking for the words “pricematch” or “price” & “match”.</div>`
};
}
}
return null;
},
({ jobline, joblines }) => {
//Upgrade 5
//TODO: LIN Files did not seem accurate for this. Perhaps one was created and it doesn't work right.
if (false) {
return {
key: "Modified part # & price",
alert: `<div>
Modified part # and Price detected
<ul>
<li>
Try searching that part # in CEG to find the correct lines rather than modifying a different part line.
</li>
<li>If that part # does not exist in CEG, create a manual entry line and erase the one you modified.</li>
<li>Since this part is not ins CEG, it will not be considered for RPS.</li>
</ul>
</div>`
};
}
return null;
},
({ jobline, joblines }) => {
//Upgrade 6
if (jobline.part_type && jobline.part_qty !== 1) {
return {
key: "Quantity changed",
alert: `<div>
Quantity manual change detected.
<ul>
<li>
MPI Estimating Standard outline that every part should have 1 line rather than manually changing the
quantity.
</li>
<li>Leaving the quantity modified <strong>MAY</strong> affect your RPS score.</li>
<li>
Consider creating manual entry lines for each part which will also disqualify those parts from your RPS
calculation.
</li>
</ul>
</div>`
};
}
return null;
}
];

View File

@@ -7,6 +7,7 @@ const { BrowserWindow } = require("electron");
const ipcTypes = require("../../src/ipc.types.commonjs");
const { NewNotification } = require("../notification-wrapper/notification-wrapper");
const { WhichRulesetToApply } = require("./constants");
const { claimsClerk } = require("../claims-clerk/claims-clerk");
//const Nucleus = require("nucleus-nodejs");
async function ImportJob(filepath) {
@@ -326,7 +327,7 @@ async function DecodeLinFile(extensionlessFilePath, close_date) {
"PRT_DSMK_P",
"MOD_LBR_TY",
// "DB_HRS",
"DB_HRS",
"MOD_LB_HRS",
// "LBR_INC",
// "LBR_OP",
@@ -379,6 +380,8 @@ async function DecodeLinFile(extensionlessFilePath, close_date) {
break;
}
jobline.alerts = claimsClerk({ jobline, joblines });
//Moved from V1 function as they may be needed later.
delete jobline.prt_dsmk_m; //Delete price markup for wheel repair
delete jobline.prt_dsmk_p;
@@ -448,7 +451,9 @@ function V1Ruleset(jobline, joblines) {
jobline.line_desc.toLowerCase().startsWith("urethane") ||
jobline.line_desc.toLowerCase().startsWith("w/shield adhesive") ||
//jobline.line_desc.toLowerCase().includes("wheel") || Removed as a part of RPS-41
(jobline.line_desc.toLowerCase().includes("tire") && !jobline.line_desc.toLowerCase().includes("sensor")&& !jobline.line_desc.toLowerCase().includes("label")) ||
(jobline.line_desc.toLowerCase().includes("tire") &&
!jobline.line_desc.toLowerCase().includes("sensor") &&
!jobline.line_desc.toLowerCase().includes("label")) ||
jobline.line_desc.toLowerCase().startsWith("hazardous") ||
jobline.line_desc.toLowerCase().startsWith("detail") ||
jobline.line_desc.toLowerCase().startsWith("clean") ||

View File

@@ -17,7 +17,9 @@ insert_permissions:
_eq: X-Hasura-User-Id
columns:
- act_price
- alerts
- created_at
- db_hrs
- db_price
- db_ref
- id
@@ -42,15 +44,21 @@ select_permissions:
permission:
columns:
- act_price
- alerts
- created_at
- db_hrs
- db_price
- db_ref
- id
- ignore
- jobid
- lbr_amt
- line_desc
- line_ind
- line_no
- misc_amt
- mod_lb_hrs
- mod_lbr_ty
- oem_partno
- part_qty
- part_type
@@ -70,7 +78,9 @@ update_permissions:
permission:
columns:
- act_price
- alerts
- created_at
- db_hrs
- db_price
- db_ref
- id

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"."joblines" add column "alerts" jsonb
-- null default jsonb_build_array();

View File

@@ -0,0 +1,2 @@
alter table "public"."joblines" add column "alerts" jsonb
null default jsonb_build_array();

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"."joblines" add column "db_hrs" numeric
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."joblines" add column "db_hrs" numeric
null;

View File

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

View File

@@ -0,0 +1,47 @@
import { Badge, Card, Collapse, Skeleton, Space } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsClaimClerk);
export function JobsClaimClerk({ loading, job }) {
// const { token } = theme.useToken();
if (loading) return <Skeleton active />;
if (!job)
return <ErrorResultAtom title="Error displaying job." errorMessage="It looks like this job doesn't exist." />;
const alertData = job.joblines
.map((jobline) =>
jobline.alerts?.map((alert, idx) => ({
key: `${jobline.line_no}-${alert.key}-${idx}`,
label: `Line ${jobline.line_no}: ${alert.key}`,
children: <div dangerouslySetInnerHTML={{ __html: alert.alert }} />
// style: {
// backgroundColor: token.colorErrorBgHover
//
}))
)
.flat();
return (
<Card
title={
<Space>
Claims Clerk AI <Badge count={alertData.length}></Badge>
</Space>
}
bordered={false}
>
<Collapse items={alertData} bordered={false} />
</Card>
);
}

View File

@@ -1,5 +1,5 @@
import { CalculatorOutlined } from "@ant-design/icons";
import { Input, Table } from "antd";
import { Input, Space, Table, Tag } from "antd";
import React, { useState } from "react";
import ipcTypes from "../../../ipc.types";
import { alphaSort } from "../../../util/sorters";
@@ -8,6 +8,7 @@ import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import IgnoreJobLine from "../../atoms/ignore-job-line/ignore-job-line.atom";
import partTypeConverterAtom from "../../atoms/part-type-converter/part-type-converter.atom";
import PriceDiffPcFormatterAtom from "../../atoms/price-diff-pc-formatter/price-diff-pc-formatter.atom";
import { render } from "sass";
const { ipcRenderer } = window;
export default function JobLinesTableMolecule({ loading, job }) {
@@ -15,12 +16,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
const [filters, setFilters] = useState({ ignore: ["false"] });
if (!job) {
return (
<ErrorResultAtom
title="Error Displaying Job Lines"
errorMessage="It looks like this job doesn't exist."
/>
);
return <ErrorResultAtom title="Error Displaying Job Lines" errorMessage="It looks like this job doesn't exist." />;
}
const { joblines } = job;
const columns = [
@@ -29,14 +25,14 @@ export default function JobLinesTableMolecule({ loading, job }) {
dataIndex: "line_no",
key: "line_no",
sorter: (a, b) => a.line_no - b.line_no,
width: "5%",
width: "5%"
},
{
title: "S#",
dataIndex: "line_ind",
key: "line_ind",
width: "5%",
sorter: (a, b) => alphaSort(a.line_ind, b.line_ind),
sorter: (a, b) => alphaSort(a.line_ind, b.line_ind)
},
{
title: "Line Description",
@@ -44,6 +40,14 @@ export default function JobLinesTableMolecule({ loading, job }) {
key: "line_desc",
width: "25%",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
render: (text, record) => (
<Space wrap size={"small"}>
{record.line_desc}
{record.alerts &&
record.alerts.length > 0 &&
record.alerts.map((alert) => <Tag color="red">{alert.key}</Tag>)}
</Space>
)
},
{
title: "Part Type",
@@ -51,21 +55,21 @@ export default function JobLinesTableMolecule({ loading, job }) {
key: "part_type",
width: "5%",
sorter: (a, b) => alphaSort(a.part_type, b.part_type),
render: (text, record) => partTypeConverterAtom(text),
render: (text, record) => partTypeConverterAtom(text)
},
{
title: "Part Number",
dataIndex: "oem_partno",
key: "oem_partno",
width: "15%",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno)
},
{
title: "Qty.",
dataIndex: "part_qty",
key: "part_qty",
width: "5%",
sorter: (a, b) => a.part_qty - b.part_qty,
sorter: (a, b) => a.part_qty - b.part_qty
},
{
title: "Database Price",
@@ -73,9 +77,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
key: "db_price",
width: "10%",
sorter: (a, b) => a.db_price - b.db_price,
render: (text, record) => (
<CurrencyFormatterAtom>{record.db_price}</CurrencyFormatterAtom>
),
render: (text, record) => <CurrencyFormatterAtom>{record.db_price}</CurrencyFormatterAtom>
},
{
title: "Actual Price",
@@ -83,9 +85,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
key: "act_price",
width: "10%",
sorter: (a, b) => a.act_price - b.act_price,
render: (text, record) => (
<CurrencyFormatterAtom>{record.act_price}</CurrencyFormatterAtom>
),
render: (text, record) => <CurrencyFormatterAtom>{record.act_price}</CurrencyFormatterAtom>
},
{
title: "Price Diff.",
@@ -93,9 +93,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
key: "price_diff",
width: "10%",
sorter: (a, b) => a.price_diff - b.price_diff,
render: (text, record) => (
<CurrencyFormatterAtom>{record.price_diff}</CurrencyFormatterAtom>
),
render: (text, record) => <CurrencyFormatterAtom>{record.price_diff}</CurrencyFormatterAtom>
},
{
title: "Price Diff. %",
@@ -104,12 +102,8 @@ export default function JobLinesTableMolecule({ loading, job }) {
width: "10%",
sorter: (a, b) => a.price_diff_pc - b.price_diff_pc,
render: (text, record) => (
<PriceDiffPcFormatterAtom
price_diff_pc={record.price_diff_pc}
v_age={job.v_age}
group={job.group}
/>
),
<PriceDiffPcFormatterAtom price_diff_pc={record.price_diff_pc} v_age={job.v_age} group={job.group} />
)
},
{
title: <CalculatorOutlined />,
@@ -117,27 +111,17 @@ export default function JobLinesTableMolecule({ loading, job }) {
key: "ignore",
filters: [
{ text: "Eligible for RPS Calculation", value: false },
{ text: "Ineligible for RPS Calculation", value: true },
{ text: "Ineligible for RPS Calculation", value: true }
],
width: "5%",
filteredValue: filters.ignore || null,
onFilter: (value, record) => value === record.ignore,
render: (text, record) => (
<IgnoreJobLine
lineId={record.id}
ignore={record.ignore}
line_desc={record.line_desc}
/>
),
},
render: (text, record) => <IgnoreJobLine lineId={record.id} ignore={record.ignore} line_desc={record.line_desc} />
}
];
const data =
searchText !== ""
? joblines.filter((j) =>
j.line_desc.toLowerCase().includes(searchText.toLowerCase())
)
: joblines;
searchText !== "" ? joblines.filter((j) => j.line_desc.toLowerCase().includes(searchText.toLowerCase())) : joblines;
const handleChange = (pagination, filters, sorter) => {
setFilters(filters);
@@ -150,7 +134,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
onSearch={(val) => {
ipcRenderer.send(ipcTypes.app.toMain.track, {
event: "JOB_LINES_SEARCH",
query: val,
query: val
});
setSearchText(val);
}}
@@ -167,7 +151,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
onChange={handleChange}
scroll={{
x: true,
y: "20rem",
y: "20rem"
}}
/>
</div>

View File

@@ -1,5 +1,5 @@
import { CloudUploadOutlined } from "@ant-design/icons";
import { Alert, Input, Space, Table } from "antd";
import { ExclamationCircleOutlined } from "@ant-design/icons";
import { Alert, Badge, Input, Space, Table, Tooltip } from "antd";
import React, { useMemo, useState } from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
@@ -9,9 +9,10 @@ import { setSelectedJobId } from "../../../redux/application/application.actions
import { selectReportData, selectReportLoading, selectScorecard } from "../../../redux/reporting/reporting.selectors";
import dayjs from "../../../util/day.js";
import { alphaSort } from "../../../util/sorters";
import RequiresReimportDisplay from "../../atoms/requires-reimport/requires-reimport.atom.jsx";
import VehicleGroupAlertAtom from "../../atoms/vehicle-group-alert/vehicle-group-alert.atom";
import GroupVerifySwitch from "../group-verify-switch/group-verify-switch.component";
import RequiresReimportDisplay from "../../atoms/requires-reimport/requires-reimport.atom.jsx";
import JobsClaimsClerkMolecule from "../jobs-claims-clerk/jobs-claims-clerk.molecule.jsx";
const { ipcRenderer } = window;
@@ -36,6 +37,12 @@ export function ReportingJobsListMolecule({ scoreCard, reportingLoading, reportD
<Link onClick={() => setSelectedJobId(record.id)} to={"/"}>
<Space>
{text}
{record.alerts && record.alerts.length > 0 && (
<Tooltip title="Claims Clerk AI has detected possible issues with this estimate. Review the estimate to ensure you are following MPI guidelines.">
<Badge count={record.alerts.length} size="small" />
</Tooltip>
)}
<RequiresReimportDisplay job={record} />
</Space>
</Link>
@@ -174,6 +181,10 @@ export function ReportingJobsListMolecule({ scoreCard, reportingLoading, reportD
size="small"
pagination={false}
dataSource={data}
expandable={{
expandedRowRender: (record) => <JobsClaimsClerkMolecule job={record} />,
rowExpandable: (record) => record.alerts && record.alerts.length > 0
}}
scroll={{
x: true
}}

View File

@@ -12,6 +12,7 @@ import JobsDetailDescriptionMolecule from "../../molecules/jobs-detail-descripti
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";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -62,6 +63,9 @@ export function JobsDetailOrganism({ selectedJobId, setSelectedJobTargetPc }) {
<Card>
<JobsLinesTableMolecule loading={loading} job={data ? data.jobs_by_pk : {}} />
</Card>
<Card>
<JobsClaimClerk loading={loading} job={data ? data.jobs_by_pk : null} />
</Card>
<Card>
<JobsTargetsStatsMolecule loading={loading} job={data ? data.jobs_by_pk : null} />
<div

View File

@@ -117,6 +117,7 @@ export const QUERY_JOB_BY_PK = gql`
price_diff_pc
ignore
db_ref
alerts
}
}
}

View File

@@ -47,6 +47,7 @@ export const REPORTING_GET_JOBS = gql`
id
db_ref
unq_seq
alerts
}
}
}

View File

@@ -12,12 +12,6 @@
"EXCURSION",
"EXPLORER LIMITED",
"EXPLORER PLATINUM ECOBOOST",
"EXPLORER SPORT TRAC",
"EXPLORER SPORT TRAC ADRENAL V8",
"EXPLORER SPORT TRAC LIMITED",
"EXPLORER SPORT TRAC LIMITED V8",
"EXPLORER SPORT TRAC XLT",
"EXPLORER SPORT TRAC XLT V8",
"EXPLORER XLT",
"FLEX",
"FLEX SE",

View File

@@ -168,9 +168,23 @@ export function* handleCalculateScoreCard({ payload: jobs }) {
jobRpsPc: isNaN(jobRpsPc) ? -1 : jobRpsPc
});
const jobAlerts = job.joblines
.map((jobline) =>
jobline.alerts?.map((alert, idx) => ({
key: idx,
label: `Line ${jobline.line_no}: ${alert.key}`,
children: alert.alert
// style: {
// backgroundColor: token.colorErrorBgHover
// }
}))
)
.flat();
//sum db price * percentage expected.
return {
...job,
alerts: jobAlerts,
actPriceSum,
jobRpsDollars,
dbPriceSum,