Add admin feature, extended SUVs and correct age 7 vehicles.

This commit is contained in:
Patrick Fic
2024-10-16 12:34:31 -07:00
parent a48b5e7245
commit 80c14094e7
12 changed files with 534 additions and 188 deletions

View File

@@ -37,27 +37,60 @@ ipcMain.on(ipcTypes.default.audit.toMain.runAudit, async (event, { sheetName })
const detailSheet = obj.find((sheet) => sheet.name === sheetName);
const claimsArray = [];
let foundHeaderRow, foundTotalRow;
let clmIndex,
close_dateIndex,
v_model_yrIndex,
v_makedescIndex,
v_modelIndex,
under20kmilesIndex,
pan_totalIndex,
paa_totalIndex,
pal_totalIndex,
pam_totalIndex,
eligible_db_price_totalIndex,
eligible_act_price_totalIndex,
expected_rps_dollarsIndex,
actual_rps_dollarsIndex;
detailSheet.data.forEach((line) => {
//Check the first element. If it's claim number, we have our header row. the next one is important.
if (!foundHeaderRow && line[0] === "Claim Number") {
foundHeaderRow = true;
//Set all of the indexes to match the titles.
clmIndex = line.findIndex((l) => l === "Claim Number");
close_dateIndex = line.findIndex((l) => l === "Ready for Pay Date");
v_model_yrIndex = line.findIndex((l) => l === "Vehicle Year");
v_makedescIndex = line.findIndex((l) => l === "Vehicle Make");
v_modelIndex = line.findIndex((l) => l === "Vehicle Model");
under20kmilesIndex = line.findIndex((l) => l === "Under 20K");
pan_totalIndex = line.findIndex((l) => l === "OE Part Prices");
paa_totalIndex = line.findIndex((l) => l === "AM Part Prices");
pal_totalIndex = line.findIndex((l) => l === "Recycled Part Prices");
pam_totalIndex = line.findIndex((l) => l === "Reman & Other Part Prices");
eligible_db_price_totalIndex = line.findIndex((l) => l === "(a) Eligible OEM Part Prices");
eligible_act_price_totalIndex = line.findIndex((l) => l === "(b) Eligible Actual Part Prices");
expected_rps_dollarsIndex = line.findIndex((l) => l === "(e) Expected RPS $ ");
actual_rps_dollarsIndex = line.findIndex((l) => l === "(f) Actual RPS $");
} else if (foundHeaderRow && !foundTotalRow && line[0] && line[0] !== "Grand Total") {
//Add it to the array
const row = {
clm_no: line[0].startsWith("00") ? line[0].slice(2) : line[0],
close_date: line[1],
v_model_yr: line[3],
v_makedesc: line[4],
v_model: line[5],
under20kmiles: line[6],
pan_total: line[7],
paa_total: line[8],
pal_total: line[9],
pam_total: line[10],
eligible_db_price_total: Math.round((line[11] + Number.EPSILON) * 100) / 100,
eligible_act_price_total: Math.round((line[12] + Number.EPSILON) * 100) / 100,
expected_rps_dollars: Math.round((line[15] + Number.EPSILON) * 100) / 100,
actual_rps_dollars: Math.round((line[16] + Number.EPSILON) * 100) / 100
clm_no: line[clmIndex].startsWith("00") ? line[clmIndex].slice(2) : line[clmIndex],
close_date: line[close_dateIndex],
v_model_yr: line[v_model_yrIndex],
v_makedesc: line[v_makedescIndex],
v_model: line[v_modelIndex],
under20kmiles: line[under20kmilesIndex],
pan_total: line[pan_totalIndex],
paa_total: line[paa_totalIndex],
pal_total: line[pal_totalIndex],
pam_total: line[pam_totalIndex],
eligible_db_price_total: Math.round((line[eligible_db_price_totalIndex] + Number.EPSILON) * 100) / 100,
eligible_act_price_total: Math.round((line[eligible_act_price_totalIndex] + Number.EPSILON) * 100) / 100,
expected_rps_dollars: Math.round((line[expected_rps_dollarsIndex] + Number.EPSILON) * 100) / 100,
actual_rps_dollars: Math.round((line[actual_rps_dollarsIndex] + Number.EPSILON) * 100) / 100
};
claimsArray.push(row);
} else {

View File

@@ -183,5 +183,10 @@
"title": "Release Notes for 1.3.4",
"date": "10/11/2024",
"notes": "Bug Fix\n*Fix typos on application labels."
},
"1.3.5": {
"title": "Release Notes for 1.3.5",
"date": "TBD",
"notes": "Bug Fix\n*Fix typos on application labels."
}
}

View File

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

View File

@@ -1,12 +1,12 @@
import { WarningOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { DatePicker, message, notification, Spin } from "antd";
import dayjs from '../../../util/day.js';
import { DatePicker, message, notification, Space, Spin } from "antd";
import React, { useState } from "react";
import { UPDATE_JOB } from "../../../graphql/jobs.queries";
import ipcTypes from "../../../ipc.types";
import { CalculateVehicleAge } from "../../../ipc/ipc-estimate-utils";
import { ChangeOfRuleSet, DateFormat } from "../../../util/constants";
import dayjs from "../../../util/day.js";
const { ipcRenderer } = window;
export default function CloseDateDisplayMolecule({ job, jobId, close_date }) {
@@ -16,19 +16,19 @@ export default function CloseDateDisplayMolecule({ job, jobId, close_date }) {
const handleChange = async (newDate) => {
ipcRenderer.send(ipcTypes.app.toMain.track, {
event: "SET_CLOSED_DATE",
event: "SET_CLOSED_DATE"
});
setLoading(true);
setValue(newDate);
const requires_reimport = ChangeOfRuleSet({
prevDateMoment: job.close_date ? dayjs(job.close_date) : dayjs(job.created_at),
newDateMoment: newDate ? newDate : dayjs(),
newDateMoment: newDate ? newDate : dayjs()
});
if (requires_reimport) {
notification.open({
type: "warning",
message:
"Changing the R4P date has changed the applicable ruleset. Please re-import the job for accurate scoring.",
"Changing the R4P date has changed the applicable ruleset. Please re-import the job for accurate scoring."
});
}
@@ -38,9 +38,9 @@ export default function CloseDateDisplayMolecule({ job, jobId, close_date }) {
job: {
close_date: newDate,
v_age: CalculateVehicleAge({ ...job, close_date: newDate }),
requires_reimport: job.requires_reimport || requires_reimport,
},
},
requires_reimport: job.requires_reimport || requires_reimport
}
}
});
if (!result.errors) {
@@ -53,17 +53,13 @@ export default function CloseDateDisplayMolecule({ job, jobId, close_date }) {
return (
<div>
<DatePicker
value={value && value.isValid() ? value : null}
onChange={handleChange}
format={DateFormat}
/>
<DatePicker value={value && value.isValid() ? value : null} onChange={handleChange} format={DateFormat} />
{loading && <Spin size="small" />}
{value && !value.isValid() && (
<div>
<Space size="small">
<WarningOutlined className="blink_me" style={{ color: "tomato" }} />
<span>No date set.</span>
<WarningOutlined style={{ marginLeft: ".5rem", color: "orange" }} />
</div>
</Space>
)}
</div>
);

View File

@@ -11,6 +11,7 @@ import TimeAgoFormatter from "../../atoms/time-ago-formatter/time-ago-formatter.
import VehicleGroupAlertAtom from "../../atoms/vehicle-group-alert/vehicle-group-alert.atom";
import CloseDateDisplayMolecule from "../close-date-display/close-date-display.molecule";
import JobGroupMolecule from "../job-group/job-group.molecule";
import moment from "moment";
export default function JobsDetailDescriptionMolecule({ loading, job }) {
const hasQuantityGreaterThan1 = useMemo(() => {
@@ -67,7 +68,9 @@ export default function JobsDetailDescriptionMolecule({ loading, job }) {
<CloseDateDisplayMolecule job={job} jobId={job.id} close_date={job.close_date} />
</Descriptions.Item>
<Descriptions.Item label="Last Updated">
<TimeAgoFormatter>{job.updated_at}</TimeAgoFormatter>
<Tooltip title={`First Created ${moment(job.created_at).format("MM/DD/YY")}`}>
<TimeAgoFormatter>{job.updated_at}</TimeAgoFormatter>
</Tooltip>
</Descriptions.Item>
<Descriptions.Item label="# RPS Eligible Parts">
{job && job.joblines.filter((i) => !i.ignore && i.db_ref !== "900511").length}

View File

@@ -47,7 +47,8 @@ export function JobsListItemMolecule({ selectedJobId, setSelectedJobId, item, re
{!item.close_date && (
<WarningOutlined
title="No Ready for Payment Date has been set for this job."
style={{ marginLeft: ".5rem", color: "orange" }}
className="blink_me"
style={{ marginLeft: ".5rem", color: "tomato" }}
/>
)}
{item.requires_reimport && (

View File

@@ -5,7 +5,8 @@ import {
FileAddFilled,
LogoutOutlined,
PieChartOutlined,
SettingFilled
SettingFilled,
AlertOutlined
} from "@ant-design/icons";
import { Menu } from "antd";
import React from "react";
@@ -66,7 +67,17 @@ export default function SiderMenuOrganism() {
Quit
</span>
)
}
},
// ...(process.env.NODE_ENV !== "production"
// ? [
// {
// key: "/admin",
// icon: <AlertOutlined />,
// label: <Link to="/admin">ADMIN</Link>
// }
// ]
// : [])
]}
/>
);

View File

@@ -0,0 +1,143 @@
import { ApolloClient, gql, InMemoryCache, useApolloClient } from "@apollo/client";
import { HttpLink } from "@apollo/client/link/http"; //"apollo-link-http";
import { Button, Table } from "antd";
import React, { useState } from "react";
import { LOCAL_DOESNOTUpsertEstimate } from "../../../ipc/ipc-estimate-utils";
import _ from "lodash";
import moment from "moment/moment";
import { dateSort } from "../../../util/sorters";
const httpLink = new HttpLink({
uri: import.meta.env.VITE_APP_GRAPHQL_ENDPOINT,
headers: {
"x-hasura-admin-secret": `ImEXRPSDataBase`
}
});
const localCLient = new ApolloClient({ link: httpLink, cache: new InMemoryCache({}) });
export default function AdminPage() {
const client = useApolloClient();
const clientToUse = client;
const [wrongGroups, setWrongGroups] = useState([]);
const handleQuery = async () => {
//TODO: MAKE SURE THIS USES THE ADMIN SECRET INSTEAD.
const {
data: { jobs }
} = await clientToUse.query({
variables: { createdAt: "2024-01-01" },
query: gql`
query ADMIN_GET_JOBS($createdAt: timestamptz) {
jobs(
where: {
_and: [
{ created_at: { _gte: $createdAt } }
{ _or: [{ close_date: { _is_null: true } }, { close_date: { _gte: "2024-09-01" } }] }
]
}
) {
ownr_ln
ownr_fn
ins_co_nm
group
group_verified
clm_total
clm_no
close_date
id
loss_date
updated_at
created_at
v_age
v_makedesc
v_model
v_model_yr
v_vin
v_type
requires_reimport
v_mileage
joblines {
act_price
db_price
part_qty
part_type
price_diff
price_diff_pc
updated_at
oem_partno
line_no
line_ind
line_desc
ignore
id
db_ref
unq_seq
}
}
}
`
});
const NoDateSetJobs = jobs.filter((j) => !j.close_date);
//For the jobs that DO have a date, let's see their group.
const JobsInWrongGroup = [];
const AllJobResults = [];
for (const job of jobs.filter((j) => true || j.close_date)) {
const initialJob = { ...job };
const calcedGroup = await LOCAL_DOESNOTUpsertEstimate(initialJob);
console.log("🚀 ~ handleQuery ~ calcedGroup:", calcedGroup);
AllJobResults.push({
close_date: job.close_date,
make: initialJob.v_makedesc,
model: initialJob.v_model,
type: initialJob.v_type,
calced_v_type: calcedGroup.v_type,
job_group: initialJob.group,
calced_group: calcedGroup.group
});
if (initialJob.group !== calcedGroup.group) {
JobsInWrongGroup.push({ ...job, calced_group: calcedGroup.group, calced_v_type: calcedGroup.v_type });
}
// break;
}
console.table(AllJobResults);
console.log(`Found ${JobsInWrongGroup.length} jobs that are in the wrong group.`);
console.table(JobsInWrongGroup.map((j) => _.pick(j, ["v_makedesc", "v_model", "group", "calced_group"])));
setWrongGroups(JobsInWrongGroup);
const jobsToMarkForReimport = [...NoDateSetJobs, ...JobsInWrongGroup];
};
return (
<div style={{ height: "100%" }}>
<Button onClick={handleQuery}>Run the query</Button>
<Table
title={() => "Jobs with the wrong group set."}
dataSource={wrongGroups}
columns={[
{
key: "created_at",
dataIndex: "created_at",
title: "Created",
sorter: (a, b) => dateSort(a.created_at, b.created_at),
render: (record) => moment(record).format("YYYY-MM-DD")
},
{ key: "close_date", dataIndex: "close_date", title: "Close Date" },
{ key: "clm_no", dataIndex: "clm_no", title: "Claim" },
{ key: "v_makedesc", dataIndex: "v_makedesc", title: "Make" },
{ key: "v_model", dataIndex: "v_model", title: "Model" },
{ key: "v_age", dataIndex: "v_age", title: "Age" },
{ key: "v_type", dataIndex: "v_type", title: "Type" },
{ key: "calced_v_type", dataIndex: "calced_v_type", title: "CALCED Type" },
{ key: "group", dataIndex: "group", title: "Group" },
{ key: "calced_group", dataIndex: "calced_group", title: "CALCED Group" }
]}
pagination={{ pageSize: 20 }}
rowKey={"id"}
/>
</div>
);
}

View File

@@ -15,6 +15,7 @@ import ReportingPage from "../reporting/reporting.page";
import ScanPage from "../scan/scan.page";
import SettingsPage from "../settings/settings.page";
import AuditPage from "../audit/audit.page";
import AdminPage from "../admin/admin.page";
const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop });
const mapDispatchToProps = (dispatch) => ({});
@@ -41,6 +42,7 @@ export function RoutesPage({ bodyshop }) {
<Route exact path="/reporting" element={<ReportingPage />} />
<Route exact path="/audit" element={<AuditPage />} />
<Route exact path="/scan" element={<ScanPage />} />
<Route exact path="/admin" element={<AdminPage />} />
<Route exact path="/" element={<JobsPage />} />
</Routes>
</Layout.Content>

File diff suppressed because it is too large Load Diff

View File

@@ -27,5 +27,7 @@
"VENTURE",
"SAFARI",
"VANAGON",
"WINDSTAR"
"WINDSTAR",
"TOWN&COUNTRY",
"ROUTAN"
]

View File

@@ -289,6 +289,7 @@
"CAYENNE",
"XC90 PLUG-IN",
"MODEL X",
"MODEL Y",
"GLC300",
"SANTA FE HYBRID",
"G63",
@@ -358,5 +359,133 @@
"CX-70",
"SANTA FE XL",
"RENEGADE",
"QX50"
"QX50",
"ECLIPSE CROSS",
"QX80",
"X5",
"X3",
"X1",
"X4",
"ENCLAVE",
"ENCORE GX",
"CAYENNE HYBRID",
"SOUL",
"GX 460",
"UX 250H",
"XT5",
"GLE53",
"XT4",
"SQ7",
"NX 350H",
"GLK350",
"GLE350",
"NX 300H",
"NX 200T",
"RANGE ROVER EVOQUE",
"GLS450",
"TERRAIN DENALI",
"GRAND CHEROKEE L",
"GLE400",
"TUCSON PLUG-IN",
"BLAZER",
"ASCENT",
"HIGHLANDER HYBRID",
"ATLAS CROSS SPORT",
"XC40",
"VENZA HYBRID",
"GLA45",
"GLB250",
"GRAND HIGHLANDER",
"GV70",
"NIRO",
"NIRO EV",
"GLA250",
"ESCAPE PLUG-IN",
"WAGONEER",
"CX-30",
"QX60",
"GRAND CHEROKEE 4XE",
"SPORTAGE HYBRID",
"EV6",
"TONALE PLUG-IN",
"GLC43 COUPE",
"X2",
"RX 350L",
"HORNET",
"ENVISTA",
"LEVANTE S",
"SPORTAGE PLUG-IN",
"ORLANDO",
"X5 M",
"EXPLORER HYBRID",
"FREESTYLE",
"CORSAIR",
"K1500 YUKON XL",
"RANGE ROVER",
"SUV W/O LABOR",
"ID.4",
"CX-90",
"X7",
"CORSAIR PLUG-IN",
"ESCALADE EXT",
"QX55",
"DISCOVERY",
"BOLT EUV",
"C40 ELECTRIC",
"LR4",
"GRAND WAGONEER",
"XC60 PLUG-IN",
"LR2",
"EQE350 SUV",
"COROLLA CROSS HYBRID",
"SOUL EV",
"GRECALE",
"SUV W/O LABOR",
"QX30",
"SQ5",
"NIRO PLUG-IN",
"BORREGO",
"CX-90 PLUG-IN",
"XL-7",
"SUV W/O LABOR",
"SUV W/O LABOR",
"I-PACE",
"HORNET PLUG-IN",
"UX 300H",
"ML320 CDI",
"VERACRUZ",
"SQ8",
"GLE53 COUPE",
"ZDX",
"9-7X",
"ARIYA",
"ASPEN",
"AVIATOR PLUG-IN",
"B9 TRIBECA",
"BRAVADA",
"ENVOY XL",
"EQB350",
"EQB350 SUV",
"ESCALADE-V",
"E-TRON",
"FX37",
"GL320 CDI",
"GLADIATOR",
"GLC43",
"GLE450 COUPE",
"GLE63",
"GV60",
"MKT TOWN CAR",
"ML350",
"ML550",
"ML63",
"NX 250",
"Q4 E-TRON",
"Q8 E-TRON SPORTBACK",
"QX4",
"QX56",
"SANTA FE PLUG-IN",
"UX 200",
"WAGONEER L",
"XB"
]