Merged in v1.0.9 (pull request #12)

V1.0.9
This commit is contained in:
Patrick Fic
2020-11-17 23:17:18 +00:00
35 changed files with 407 additions and 66 deletions

View File

@@ -1,3 +1,6 @@
Creating Release Notes Tools:
https://onlinestringtools.com/json-stringify-string
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts

13
WIP Changelog.txt Normal file
View File

@@ -0,0 +1,13 @@
New Features:
- Vehicles with mileage under 20,000kms will now be included in Watcher filtering criteria.
- Savings on OEM parts will always default to the user enter price and override the estimating system price.
- Wheel related lines will no longer be automatically ignored.
- Glass related lines will no longer be automatically ignored.
- Added 'Variance %' statistic to reporting totals.
- Automatically ignore any lines which have invalid prices from estimating system.
- Force line inclusion/exclusion - Using “ /rps-exclude” or “/rps” in the Part Number field to force inclusion or exclusion of lines for RPS calculation.
- Added automatic update checks every 30 minutes.
Bug Fixes:
- Resolved an issue where the updater would not show update progress to some users.
- Fixed a UI bug during job search that would cause the 'no close date' alert to be incorrectly shown.

7
changelog.json Normal file
View File

@@ -0,0 +1,7 @@
{
"1.0.9": {
"title": "Release Notes for 1.0.9",
"date": "11/16/2020",
"notes": "New Features: \n- Vehicles with mileage under 20,000kms will now be included in Watcher filtering criteria.\n- Savings on OEM parts will always default to the user enter price and override the estimating system price.\n- Wheel related lines will no longer be automatically ignored.\n- Glass related lines will no longer be automatically ignored.\n- Added 'Variance %' statistic to reporting totals.\n- Automatically ignore any lines which have invalid prices from estimating system.\n- Force line inclusion/exclusion - Using “ /rps-exclude” or “/rps” in the Part Number field to force inclusion or exclusion of lines for RPS calculation.\n- Added automatic update checks every 30 minutes.\n\nBug Fixes: \n- Resolved an issue where the updater would not show update progress to some users.\n- Fixed a UI bug during job search that would cause the 'no close date' alert to be incorrectly shown."
}
}

View File

@@ -7,6 +7,7 @@ const { default: ipcTypes } = require("../src/ipc.types");
Nucleus.init("5f91b569b95bac34eefdb63a", {
disableInDev: true,
debug: false,
version: app.getVersion(),
});
Nucleus.setProps({

View File

@@ -57,9 +57,12 @@ async function DecodeEstimate(filePath, includeFilePathInReturnJob = false) {
const accepted_ins_co = store.get("accepted_ins_co");
let returnValue;
if (job.V_MILEAGE <= 20000) {
returnValue = { ERROR: "Vehicle mileage is less than 20,000kms." };
} else if (!accepted_ins_co.includes(job.INS_CO_NM)) {
//Removed as a part of RPS-40.
// if (job.V_MILEAGE <= 20000) {
// returnValue = { ERROR: "Vehicle mileage is less than 20,000kms." };
// } else
if (!accepted_ins_co.includes(job.INS_CO_NM)) {
returnValue = {
ERROR: `Insurance Company Name is not valid for RPS. (${
job.INS_CO_NM || "No name set"
@@ -351,51 +354,68 @@ async function DecodeLinFile(extensionlessFilePath) {
// jobline.glass_flag === false
// )
.map((jobline) => {
//Removed as a result of conversation with Norm.
// Appears you are calculating based on N.A. part values in the Mitchell database.
// Reminder if Mitchell DB has $0.00 price updates can be tracked, if it has N.A. it cannot calculate RPS value.
// if (
// (jobline.db_price === null || jobline.db_price === 0) &&
// !!jobline.act_price &&
// jobline.act_price > 0
// ) {
// // log.info(
// // "DB Price null/lower than act price",
// // jobline.line_desc,
// // jobline.db_price,
// // jobline.act_price
// // );
// jobline.db_price = jobline.act_price;
// }
//RPS-39 - OEM ON OEM SAVINGS
if (
(jobline.db_price === null || jobline.db_price === 0) &&
!!jobline.act_price &&
jobline.act_price > 0
jobline.part_type === "PAN" &&
jobline.db_price !== jobline.act_price
) {
// log.info(
// "DB Price null/lower than act price",
// jobline.line_desc,
// jobline.db_price,
// jobline.act_price
// );
jobline.db_price = jobline.act_price;
}
if (
jobline.db_price &&
jobline.act_price &&
!!jobline.db_price &&
jobline.db_price > 0 &&
!!jobline.act_price &&
jobline.act_price > jobline.db_price
) {
// log.info(
// "Act price higher than existing db price",
// jobline.line_desc,
// jobline.db_price,
// jobline.act_price
// );
//Actual price should never be higher than the DB Price.
jobline.db_price = jobline.act_price;
}
//Logic Based Exclusions.
if (
!jobline.part_type ||
jobline.db_ref.startsWith("900") ||
jobline.line_desc.toLowerCase().startsWith("urethane") ||
jobline.line_desc.toLowerCase().includes("wheel") ||
//jobline.line_desc.toLowerCase().includes("wheel") || Removed as a part of RPS-41
jobline.line_desc.toLowerCase().includes("tire") ||
jobline.line_desc.toLowerCase().startsWith("hazardous") ||
jobline.line_desc.toLowerCase().startsWith("detail") ||
jobline.line_desc.toLowerCase().startsWith("clean") ||
jobline.part_type.toUpperCase() === "PAG" ||
// jobline.part_type.toUpperCase() === "PAG" ||Removed for RPS-43.
jobline.part_type.toUpperCase() === "PAS" ||
jobline.part_type.toUpperCase() === "PASL" ||
jobline.part_type.toUpperCase() === "PAE" ||
jobline.glass_flag === true
//jobline.glass_flag === true //Removed for RPS-43.
jobline.db_price === 0 //Added as a part of RPS-46.
)
jobline.ignore = true;
//RPS-42 Dynamic Inclusion or Exclusion of Lines based on RPS-EXCLUDE or RPS.
if (jobline.oem_partno.toLowerCase().includes("/rps-exclude")) {
jobline.ignore = true;
} else if (jobline.oem_partno.toLowerCase().includes("/rps")) {
jobline.ignore = false;
}
delete jobline.glass_flag;
return jobline;
});

View File

@@ -2,13 +2,14 @@ const Store = require("electron-store");
const store = new Store({
defaults: {
showChangeLog: true,
enableNotifications: true,
filePaths: [],
accepted_ins_co: [],
runWatcherOnStartup: true,
polling: {
enabled: false,
pollingInterval: 1000,
pollingInterval: 30000,
},
},
});

View File

@@ -1,4 +1,5 @@
const { ipcMain } = require("electron");
const { ipcMain, app: electronApp } = require("electron");
const { app } = require("firebase");
const { default: ipcTypes } = require("../src/ipc.types");
const { store } = require("./electron-store");
//Import Ipc Handlers
@@ -32,3 +33,13 @@ ipcMain.on(ipcTypes.store.getAll, (event, obj) => {
const val = store.get();
event.sender.send(ipcTypes.store.response, val);
});
ipcMain.on(ipcTypes.app.toMain.getReleaseNotes, (event, obj) => {
const showNotes = store.get("showChangeLog");
if (showNotes) {
const rn = require("../changelog.json")[electronApp.getVersion()];
event.sender.send(ipcTypes.app.toRenderer.setReleaseNotes, rn);
} else {
event.sender.send(ipcTypes.app.toRenderer.setReleaseNotes, null);
}
});

View File

@@ -81,6 +81,15 @@ var menu = Menu.buildFromTemplate([
checkForUpdates();
},
},
{
label: `Show Release Notes`,
click() {
mainWindow.webContents.send(
ipcTypes.app.toRenderer.setReleaseNotes,
require("../changelog.json")[app.getVersion()]
);
},
},
{
label: "Open Config File",
click() {
@@ -195,6 +204,10 @@ app.whenReady().then(() => {
.catch((error) => console.log(`An error occurred: , ${error}`));
}
setInterval(() => {
checkForUpdates();
}, 1000 * 60 * 30); //Added auto update check for RPS-38
// ipcMain.on(ipcTypes.default.webcontent, (event, ...obj) => {
// console.log("event", event);
// mainWindow.webContents.send(event, obj);
@@ -246,9 +259,6 @@ function createTray() {
return appIcon;
}
// autoUpdater.on("checking-for-update", () => {
// log.log("Checking for update...");
// });
autoUpdater.on("update-available", (ev) => {
log.log("Update available.", ev);
mainWindow.webContents.send(ipcTypes.app.toRenderer.updateAvailable, ev);
@@ -261,10 +271,6 @@ autoUpdater.on("error", (ev, err) => {
log.log("Error in auto-updater.", ev, err);
});
// // autoUpdater.on("update-downloaded", (ev, info) => {
// // console.log("Update downloaded; will install in 5 seconds");
// // });
function openNoticeWindow() {
if (noticeWindow) {
noticeWindow.focus();
@@ -318,6 +324,7 @@ autoUpdater.on("update-downloaded", (ev, info) => {
if (buttonIndex === 0) {
const isSilent = true;
const isForceRunAfter = true;
store.set("showChangeLog", true);
autoUpdater.quitAndInstall(isSilent, isForceRunAfter);
} else {
logger.error("Error");

View File

@@ -1,2 +1 @@
#endpoint: https://rps.bodyshop.app
endpoint: https://db.rps.imex.online
endpoint: https://rps.bodyshop.app

View File

@@ -0,0 +1,6 @@
- args:
cascade: false
read_only: false
sql: "alter table \"public\".\"associations\"\n add constraint \"associations_pkey\"
\n primary key ( \"bodyshopid\", \"email\" );"
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: alter table "public"."associations" drop constraint "associations_pkey";
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."associations" DROP COLUMN "id";
type: run_sql

View File

@@ -0,0 +1,11 @@
- args:
cascade: false
read_only: false
sql: CREATE EXTENSION IF NOT EXISTS pgcrypto;
type: run_sql
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."associations" ADD COLUMN "id" uuid NULL UNIQUE DEFAULT
gen_random_uuid();
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: alter table "public"."associations" drop constraint "associations_pkey";
type: run_sql

View File

@@ -0,0 +1,6 @@
- args:
cascade: false
read_only: false
sql: "alter table \"public\".\"associations\"\n add constraint \"associations_pkey\"
\n primary key ( \"id\" );"
type: run_sql

View File

@@ -0,0 +1,22 @@
- args:
role: user
table:
name: associations
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- email
- bodyshopid
computed_fields: []
filter:
user:
authid:
_eq: X-Hasura-User-Id
role: user
table:
name: associations
schema: public
type: create_select_permission

View File

@@ -0,0 +1,23 @@
- args:
role: user
table:
name: associations
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- bodyshopid
- email
- id
computed_fields: []
filter:
user:
authid:
_eq: X-Hasura-User-Id
role: user
table:
name: associations
schema: public
type: create_select_permission

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."users" DROP COLUMN "id";
type: run_sql

View File

@@ -0,0 +1,10 @@
- args:
cascade: false
read_only: false
sql: CREATE EXTENSION IF NOT EXISTS pgcrypto;
type: run_sql
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."users" ADD COLUMN "id" uuid NULL UNIQUE DEFAULT gen_random_uuid();
type: run_sql

View File

@@ -0,0 +1,23 @@
- args:
role: user
table:
name: users
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- authid
- email
- created_at
- updated_at
computed_fields: []
filter:
authid:
_eq: X-Hasura-User-Id
role: user
table:
name: users
schema: public
type: create_select_permission

View File

@@ -0,0 +1,24 @@
- args:
role: user
table:
name: users
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- authid
- created_at
- email
- id
- updated_at
computed_fields: []
filter:
authid:
_eq: X-Hasura-User-Id
role: user
table:
name: users
schema: public
type: create_select_permission

View File

@@ -14,8 +14,9 @@ tables:
- role: user
permission:
columns:
- email
- bodyshopid
- email
- id
filter:
user:
authid:
@@ -316,8 +317,9 @@ tables:
permission:
columns:
- authid
- email
- created_at
- email
- id
- updated_at
filter:
authid:

View File

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

View File

@@ -0,0 +1,44 @@
import { Modal } from "antd";
import React, { useEffect } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import ipcTypes from "../../../ipc.types";
import { setReleaseNotes } from "../../../redux/application/application.actions";
import { selectReleaseNotes } from "../../../redux/application/application.selectors";
const mapStateToProps = createStructuredSelector({
releaseNotes: selectReleaseNotes,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
setReleaseNotes: (notes) => dispatch(setReleaseNotes(notes)),
});
const { ipcRenderer } = window;
export function ReleaseNotes({ releaseNotes, setReleaseNotes }) {
console.log("ReleaseNotes -> releaseNotes", releaseNotes);
useEffect(() => {
ipcRenderer.send(ipcTypes.default.app.toMain.getReleaseNotes);
}, []);
const handleOk = () => {
ipcRenderer.send(ipcTypes.default.store.set, { showChangeLog: false });
setReleaseNotes(null);
};
return (
<Modal
visible={!!releaseNotes}
onOk={handleOk}
onCancel={handleOk}
cancelButtonProps={{ style: { display: "none" } }}
title={releaseNotes && releaseNotes.title}
>
<div>{releaseNotes && releaseNotes.date}</div>
<div style={{ whiteSpace: "pre-line" }}>
{releaseNotes && releaseNotes.notes}
</div>
</Modal>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ReleaseNotes);

View File

@@ -1,5 +1,11 @@
import { Card, Skeleton, Typography, Tooltip as AntdToolTip } from "antd";
import React from "react";
import {
Card,
Radio,
Skeleton,
Tooltip as AntdToolTip,
Typography,
} from "antd";
import React, { useState } from "react";
import { connect } from "react-redux";
import {
CartesianGrid,
@@ -24,19 +30,23 @@ const mapStateToProps = createStructuredSelector({
reportingLoading: selectReportLoading,
scoreCard: selectScorecard,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
const mapDispatchToProps = (dispatch) => ({});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ReportingScatterChartMolecule);
export function ReportingScatterChartMolecule({ reportingLoading, scoreCard }) {
const [type, setType] = useState("percent");
if (reportingLoading) return <Skeleton active />;
if (!scoreCard)
return <ErrorResultAtom title="Error displaying score card data." />;
const handleTypeChange = (e) => {
setType(e.target.value);
};
return (
<div>
<div
@@ -47,24 +57,60 @@ export function ReportingScatterChartMolecule({ reportingLoading, scoreCard }) {
height: "30rem",
}}
>
<AntdToolTip
placement="bottomLeft"
title="Calculated as Actual Job RPS % less Target Job RPS %. E.g. 10% - 8% = 2%"
>
<Typography.Title level={4}>% Variance from Target</Typography.Title>
</AntdToolTip>
<div style={{ display: "flex", flexDirection: "row" }}>
{type === "percent" && (
<AntdToolTip
placement="bottomLeft"
title="Calculated as Actual Job RPS % less Target Job RPS %. E.g. 10% - 8% = 2%"
>
<Typography.Title level={4}>
% Variance from Target
</Typography.Title>
</AntdToolTip>
)}
{type === "dollars" && (
<AntdToolTip
placement="bottomLeft"
title="Calculated as Actual Job RPS $ less Target Job RPS $."
>
<Typography.Title level={4}>
$ Variance from Target
</Typography.Title>
</AntdToolTip>
)}
<Radio.Group
onChange={handleTypeChange}
value={type}
placeholder="Variance Type"
style={{ marginLeft: "auto" }}
>
<Radio.Button value="percent">% - Percent</Radio.Button>
<Radio.Button value="dollars">$ - Dollars</Radio.Button>
</Radio.Group>
</div>
<ResponsiveContainer>
<ScatterChart margin={{ top: 20, right: 20, bottom: 10, left: 10 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="deviation"
type="number"
ticks={[-100, -75, -50, -25, 0, 25, 50, 75, 100]}
domain={[-100, 100]}
name="Deviation"
unit="%"
/>
{type === "percent" && (
<XAxis
dataKey="deviationPc"
type="number"
ticks={[-100, -75, -50, -25, 0, 25, 50, 75, 100]}
domain={[-100, 100]}
name="Deviation"
unit="%"
/>
)}
{type === "dollars" && (
<XAxis
dataKey="deviationDollars"
type="number"
name="Deviation ($)"
/>
)}
<YAxis type="number" dataKey="age" name="Age" unit="Years" />
<ZAxis dataKey="dbPriceSumAmt" name="DB Price Total" />
<Tooltip
@@ -73,8 +119,6 @@ export function ReportingScatterChartMolecule({ reportingLoading, scoreCard }) {
if (!item) return null;
return (
<Card title={item.clm_no}>
<div></div>
<div></div>
<DataLabelAtom label="Owner">{item.owner}</DataLabelAtom>
<DataLabelAtom label="Vehicle">
{item.vehicle}
@@ -88,6 +132,12 @@ export function ReportingScatterChartMolecule({ reportingLoading, scoreCard }) {
<DataLabelAtom label="DB Price">
{item.dbPriceSum.toFormat()}
</DataLabelAtom>
<DataLabelAtom label="Variance %">
{`${item.deviationPc}%`}
</DataLabelAtom>
<DataLabelAtom label="Variance $">
${item.deviationDollars}
</DataLabelAtom>
</Card>
);
}}

View File

@@ -54,6 +54,15 @@ export function ReportingTotalsStatsMolecule({ reportingLoading, scoreCard }) {
value={((scoreCard.currentRpsPc || 0) * 100).toFixed(1)}
suffix="%"
/>
<Statistic
title="RPS Variance %"
style={{ margin: "0rem .5rem" }}
valueStyle={{
color: scoreCard.variancePc < 0 ? "tomato" : "seagreen",
}}
value={(scoreCard.variancePc * 100).toFixed(2)}
suffix="%"
/>
</div>
<div style={{ display: "flex" }}>
<Statistic

View File

@@ -5,6 +5,7 @@ import { Route } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../../redux/user/user.selectors";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import ReleaseNotes from "../../molecules/release-notes/release-notes.molecule";
import SiderMenuOrganism from "../../organisms/sider-menu/sider-menu.organism";
import UpdateManagerOrganism from "../../organisms/update-manager/update-manager.organism";
import JobsPage from "../jobs/jobs.page";
@@ -40,7 +41,7 @@ export function RoutesPage({ bodyshop }) {
<Route exact path="/scan" component={ScanPage} />
<Route exact path="/" component={JobsPage} />
</Layout.Content>
<ReleaseNotes />
<UpdateManagerOrganism />
</Layout>
</Layout>

View File

@@ -60,6 +60,7 @@ export const SEARCH_JOBS_PAGINATED = gql`
ins_co_nm
clm_no
updated_at
close_date
}
search_jobs_aggregate(
args: { enddate: $endDate, search: $search, startdate: $startDate }

View File

@@ -12,11 +12,13 @@ exports.default = {
checkForUpdates: "app_checkForUpdates",
downloadUpdates: "app_downloadUpdates",
installUpdates: "app_installupdates",
getReleaseNotes: "app_getReleaseNotes",
},
toRenderer: {
updateAvailable: "app_updateAvailable",
downloadProgress: "app_downloadProgress",
signOut: "app_signOut",
setReleaseNotes: "app_setReleaseNotes",
},
},
store: {

View File

@@ -1,5 +1,6 @@
import ipcTypes from "../ipc.types";
import {
setReleaseNotes,
setSettings,
setUpdateAvailable,
setUpdateProgress,
@@ -89,3 +90,10 @@ ipcRenderer.on(
store.dispatch(signOutStart());
}
);
ipcRenderer.on(
ipcTypes.default.app.toRenderer.setReleaseNotes,
async (event, releaseNotes) => {
store.dispatch(setReleaseNotes(releaseNotes));
}
);

View File

@@ -52,3 +52,8 @@ export const setUpdateProgress = (progress) => ({
type: ApplicationActionTypes.SET_UPDATE_PROGRESS,
payload: progress,
});
export const setReleaseNotes = (releaseNotes) => ({
type: ApplicationActionTypes.SET_RELEASE_NOTES,
payload: releaseNotes,
});

View File

@@ -9,6 +9,7 @@ const INITIAL_STATE = {
settings: {},
updateAvailable: false,
updateProgress: null,
releaseNotes: null,
};
const { ipcRenderer } = window;
@@ -66,6 +67,8 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
return { ...state, updateAvailable: action.payload };
case ApplicationActionTypes.SET_UPDATE_PROGRESS:
return { ...state, updateProgress: action.payload };
case ApplicationActionTypes.SET_RELEASE_NOTES:
return { ...state, releaseNotes: action.payload };
default:
return state;
}

View File

@@ -36,7 +36,13 @@ export const selectUpdateAvailable = createSelector(
[selectApplication],
(application) => application.updateAvailable
);
export const selectUpdateProgress = createSelector(
[selectApplication],
(application) => application.updateProgress
);
export const selectReleaseNotes = createSelector(
[selectApplication],
(application) => application.releaseNotes
);

View File

@@ -10,6 +10,6 @@ const ApplicationActionTypes = {
SET_SETTINGS: "SET_SETTINGS",
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE",
SET_UPDATE_PROGRESS: "SET_UPDATE_PROGRESS",
SET_RELEASE_NOTES: "SET_RELEASE_NOTES",
};
export default ApplicationActionTypes;

View File

@@ -102,8 +102,13 @@ export function* handleCalculateScoreCard({ payload: jobs }) {
actPriceSum
);
const deviationPc = Math.round((jobRpsPc - jobTarget) * 1000) / 10;
scoreCard.scatterChart[job.group].push({
deviation: Math.round((jobRpsPc - jobTarget) * 1000) / 10,
deviationPc: isNaN(deviationPc) ? -100 : deviationPc,
deviationDollars: (
jobRpsDollars.subtract(expectedRpsDollars).getAmount() / 100
).toFixed(2),
age: job.v_age,
dbPriceSum,
dbPriceSumAmt: dbPriceSum.getAmount() / 100,
@@ -112,7 +117,7 @@ export function* handleCalculateScoreCard({ payload: jobs }) {
vehicle: `${job.v_model_yr} ${job.v_makedesc} ${job.v_model} (${job.v_type}) - ${job.group}`,
clm_no: job.clm_no,
jobRpsDollars,
jobRpsPc,
jobRpsPc: isNaN(jobRpsPc) ? -1 : jobRpsPc,
});
//sum db price * percentage expected.
@@ -131,16 +136,14 @@ export function* handleCalculateScoreCard({ payload: jobs }) {
scoreCard.shopRpsExpectedDollars
);
scoreCard.variancePc =
scoreCard.varianceDollars.getAmount() /
scoreCard.shopRpsExpectedDollars.getAmount();
scoreCard.currentRpsPc =
scoreCard.shopRpsTotalDollars.getAmount() /
scoreCard.allJobsSumDbPrice.getAmount();
scoreCard.targetRpsPc =
scoreCard.shopRpsExpectedDollars.getAmount() /
scoreCard.allJobsSumDbPrice.getAmount();
scoreCard.variancePc = scoreCard.currentRpsPc - scoreCard.targetRpsPc;
//Set the data.
yield put(setScoreCard(scoreCard));
yield put(setReportingData(jobs));