diff --git a/electron/claims-clerk/claims-clerk.js b/electron/claims-clerk/claims-clerk.js
new file mode 100644
index 0000000..9f9585e
--- /dev/null
+++ b/electron/claims-clerk/claims-clerk.js
@@ -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: `
+ Manually entered line detected.
+
+ This part will NOT count towards your RPS.
+ You will need to supply an invoice to MPI for this part.
+ Always ensure part is not in CEG before creating a manual entry line.
+
+
`
+ };
+ }
+
+ 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: `
+ Labor time manually changed from original CEG time.
+
+ This could possibly be denied by MPI.
+ Ensure labor time is accurate & justified.
+ Add an explanation line if needed.
+
+
`
+ };
+ }
+
+ return null;
+ },
+ ({ jobline, joblines }) => {
+ //Upgrade 3
+ if (jobline.db_ref === "900500" && jobline.mod_lb_hrs !== jobline.db_hrs) {
+ return {
+ key: "Manual Labor Line",
+ alert: `
+ Manually entered labor line detected.
+
+ Always ensure the labor operation you manually entered was not available in CEG.
+ Make sure there are no overlaps with other labor lines to consider.
+
+
`
+ };
+ }
+
+ 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: `
+ Modified part price detected.
+
+
+ You will need to supply MPI with an invoice for this part showing the retail price you manually
+ entered.
+
+
+ Your manually entered price is LOWER than the database price, consider reverting back to database
+ price.
+
+ If you chose to leave the manually entered price, you will need to:
+ Supply MPI with an invoice for this part showing the retail price you manually entered
+ NOTE: You do not need to show MPI your cost on this part, only retail.
+
+
`
+ };
+ } else {
+ return {
+ key: "Modified part price",
+ alert: `
+ Modified part price detected.
+
+
+ You will need to supply MPI with an invoice for this part showing the retail price you manually
+ entered.
+
+ NOTE: You do not need to show MPI your cost on this part, only retail.
+
+
`
+ };
+ }
+ }
+
+ 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: `In that explanation line (900501) we are looking for the words “pricematch” or “price” & “match”.
`
+ };
+ }
+ }
+
+ 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: `
+ Modified part # and Price detected
+
+
+ Try searching that part # in CEG to find the correct lines rather than modifying a different part line.
+
+ If that part # does not exist in CEG, create a manual entry line and erase the one you modified.
+ Since this part is not ins CEG, it will not be considered for RPS.
+
+
`
+ };
+ }
+
+ return null;
+ },
+ ({ jobline, joblines }) => {
+ //Upgrade 6
+ if (jobline.part_type && jobline.part_qty !== 1) {
+ return {
+ key: "Quantity changed",
+ alert: `
+ Quantity manual change detected.
+
+
+ MPI Estimating Standard outline that every part should have 1 line rather than manually changing the
+ quantity.
+
+ Leaving the quantity modified MAY affect your RPS score.
+
+ Consider creating manual entry lines for each part which will also disqualify those parts from your RPS
+ calculation.
+
+
+
`
+ };
+ }
+
+ return null;
+ }
+];
diff --git a/electron/decoder/decoder.js b/electron/decoder/decoder.js
index 8972b78..4651df2 100644
--- a/electron/decoder/decoder.js
+++ b/electron/decoder/decoder.js
@@ -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") ||
diff --git a/hasura/metadata/databases/default/tables/public_joblines.yaml b/hasura/metadata/databases/default/tables/public_joblines.yaml
index 3a37afe..552095d 100644
--- a/hasura/metadata/databases/default/tables/public_joblines.yaml
+++ b/hasura/metadata/databases/default/tables/public_joblines.yaml
@@ -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
diff --git a/hasura/migrations/default/1739917379114_alter_table_public_joblines_add_column_alerts/down.sql b/hasura/migrations/default/1739917379114_alter_table_public_joblines_add_column_alerts/down.sql
new file mode 100644
index 0000000..23e79bc
--- /dev/null
+++ b/hasura/migrations/default/1739917379114_alter_table_public_joblines_add_column_alerts/down.sql
@@ -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();
diff --git a/hasura/migrations/default/1739917379114_alter_table_public_joblines_add_column_alerts/up.sql b/hasura/migrations/default/1739917379114_alter_table_public_joblines_add_column_alerts/up.sql
new file mode 100644
index 0000000..bff7024
--- /dev/null
+++ b/hasura/migrations/default/1739917379114_alter_table_public_joblines_add_column_alerts/up.sql
@@ -0,0 +1,2 @@
+alter table "public"."joblines" add column "alerts" jsonb
+ null default jsonb_build_array();
diff --git a/hasura/migrations/default/1739985660715_alter_table_public_joblines_add_column_db_hrs/down.sql b/hasura/migrations/default/1739985660715_alter_table_public_joblines_add_column_db_hrs/down.sql
new file mode 100644
index 0000000..c6341e9
--- /dev/null
+++ b/hasura/migrations/default/1739985660715_alter_table_public_joblines_add_column_db_hrs/down.sql
@@ -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;
diff --git a/hasura/migrations/default/1739985660715_alter_table_public_joblines_add_column_db_hrs/up.sql b/hasura/migrations/default/1739985660715_alter_table_public_joblines_add_column_db_hrs/up.sql
new file mode 100644
index 0000000..e20ecaa
--- /dev/null
+++ b/hasura/migrations/default/1739985660715_alter_table_public_joblines_add_column_db_hrs/up.sql
@@ -0,0 +1,2 @@
+alter table "public"."joblines" add column "db_hrs" numeric
+ null;
diff --git a/package.json b/package.json
index cc9cc38..78ee4f3 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"productName": "ImEX RPS",
"author": "ImEX Systems Inc. ",
"description": "ImEX RPS",
- "version": "1.3.5",
+ "version": "1.4.0-alpha.1",
"main": "electron/main.js",
"homepage": "./",
"dependencies": {
diff --git a/src/components/molecules/jobs-claims-clerk/jobs-claims-clerk.molecule.jsx b/src/components/molecules/jobs-claims-clerk/jobs-claims-clerk.molecule.jsx
new file mode 100644
index 0000000..6aed331
--- /dev/null
+++ b/src/components/molecules/jobs-claims-clerk/jobs-claims-clerk.molecule.jsx
@@ -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 ;
+ if (!job)
+ return ;
+
+ 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:
+
+ // style: {
+ // backgroundColor: token.colorErrorBgHover
+ //
+ }))
+ )
+ .flat();
+
+ return (
+
+ Claims Clerk AI
+
+ }
+ bordered={false}
+ >
+
+
+ );
+}
diff --git a/src/components/molecules/jobs-lines-table/jobs-lines-table.molecule.jsx b/src/components/molecules/jobs-lines-table/jobs-lines-table.molecule.jsx
index d21db4c..fdf01a1 100644
--- a/src/components/molecules/jobs-lines-table/jobs-lines-table.molecule.jsx
+++ b/src/components/molecules/jobs-lines-table/jobs-lines-table.molecule.jsx
@@ -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 (
-
- );
+ return ;
}
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) => (
+
+ {record.line_desc}
+ {record.alerts &&
+ record.alerts.length > 0 &&
+ record.alerts.map((alert) => {alert.key} )}
+
+ )
},
{
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) => (
- {record.db_price}
- ),
+ render: (text, record) => {record.db_price}
},
{
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) => (
- {record.act_price}
- ),
+ render: (text, record) => {record.act_price}
},
{
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) => (
- {record.price_diff}
- ),
+ render: (text, record) => {record.price_diff}
},
{
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) => (
-
- ),
+
+ )
},
{
title: ,
@@ -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) => (
-
- ),
- },
+ render: (text, record) =>
+ }
];
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"
}}
/>
diff --git a/src/components/molecules/reporting-jobs-list/reporting-jobs-list.molecule.jsx b/src/components/molecules/reporting-jobs-list/reporting-jobs-list.molecule.jsx
index 4a8e89b..d69a88a 100644
--- a/src/components/molecules/reporting-jobs-list/reporting-jobs-list.molecule.jsx
+++ b/src/components/molecules/reporting-jobs-list/reporting-jobs-list.molecule.jsx
@@ -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
setSelectedJobId(record.id)} to={"/"}>
{text}
+
+ {record.alerts && record.alerts.length > 0 && (
+
+
+
+ )}
@@ -174,6 +181,10 @@ export function ReportingJobsListMolecule({ scoreCard, reportingLoading, reportD
size="small"
pagination={false}
dataSource={data}
+ expandable={{
+ expandedRowRender: (record) => ,
+ rowExpandable: (record) => record.alerts && record.alerts.length > 0
+ }}
scroll={{
x: true
}}
diff --git a/src/components/organisms/jobs-detail/jobs-detail.organism.jsx b/src/components/organisms/jobs-detail/jobs-detail.organism.jsx
index a6fc532..8895be4 100644
--- a/src/components/organisms/jobs-detail/jobs-detail.organism.jsx
+++ b/src/components/organisms/jobs-detail/jobs-detail.organism.jsx
@@ -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 }) {
+
+
+
+ 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,