From d044cce054d253b7864e0b8c49cd795dea8c4b94 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Wed, 19 Feb 2025 10:31:56 -0800 Subject: [PATCH] Initial work for claims clerk. --- electron/claims-clerk/claims-clerk.js | 192 ++++++++++++++++++ electron/decoder/decoder.js | 9 +- .../default/tables/public_joblines.yaml | 10 + .../down.sql | 4 + .../up.sql | 2 + .../down.sql | 4 + .../up.sql | 2 + package.json | 2 +- .../jobs-claims-clerk.molecule.jsx | 47 +++++ .../jobs-lines-table.molecule.jsx | 70 +++---- .../reporting-jobs-list.molecule.jsx | 17 +- .../jobs-detail/jobs-detail.organism.jsx | 4 + src/graphql/jobs.queries.js | 1 + src/graphql/reporting.queries.js | 1 + src/ipc/suvs.json | 6 - src/redux/reporting/reporting.sagas.js | 14 ++ 16 files changed, 330 insertions(+), 55 deletions(-) create mode 100644 electron/claims-clerk/claims-clerk.js create mode 100644 hasura/migrations/default/1739917379114_alter_table_public_joblines_add_column_alerts/down.sql create mode 100644 hasura/migrations/default/1739917379114_alter_table_public_joblines_add_column_alerts/up.sql create mode 100644 hasura/migrations/default/1739985660715_alter_table_public_joblines_add_column_db_hrs/down.sql create mode 100644 hasura/migrations/default/1739985660715_alter_table_public_joblines_add_column_db_hrs/up.sql create mode 100644 src/components/molecules/jobs-claims-clerk/jobs-claims-clerk.molecule.jsx 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. + +
` + }; + } + + 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. + +
` + }; + } + + 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. + +
` + }; + } + + 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. + +
` + }; + } else { + return { + key: "Modified part price", + alert: `
+ Modified part price detected. + +
` + }; + } + } + + 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 + +
` + }; + } + + return null; + }, + ({ jobline, joblines }) => { + //Upgrade 6 + if (jobline.part_type && jobline.part_qty !== 1) { + return { + key: "Quantity changed", + alert: `
+ Quantity manual change detected. + +
` + }; + } + + 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,