Merged in v1.0.5 (pull request #7)

V1.0.5
This commit is contained in:
Patrick Fic
2020-10-28 13:33:58 +00:00
29 changed files with 578 additions and 255 deletions

3
Xdev-app-update.yml Normal file
View File

@@ -0,0 +1,3 @@
provider: s3
bucket: rps-updater
region: ca-central-1

View File

@@ -80,163 +80,190 @@ async function DecodeEstimate(filePath, includeFilePathInReturnJob = false) {
}
async function DecodeAd1File(extensionlessFilePath) {
let dbf = await DBFFile.open(`${extensionlessFilePath}A.AD1`);
let records = await dbf.readRecords(1);
return _.pick(records[0], [
// "INS_CO_ID",
"INS_CO_NM",
// "INS_ADDR1",
// "INS_ADDR2",
// "INS_CITY",
// "INS_ST",
// "INS_ZIP",
// "INS_CTRY",
let dbf;
try {
dbf = await DBFFile.open(`${extensionlessFilePath}A.AD1`);
} catch (error) {
log.error("Error opening AD1 File.", error);
dbf = await DBFFile.open(`${extensionlessFilePath}.AD1`);
dbf && log.log("Found AD1 file using regular CIECA Id.");
} finally {
if (!dbf) return {};
let records = await dbf.readRecords(1);
return _.pick(records[0], [
// "INS_CO_ID",
"INS_CO_NM",
// "INS_ADDR1",
// "INS_ADDR2",
// "INS_CITY",
// "INS_ST",
// "INS_ZIP",
// "INS_CTRY",
// "INS_EA",
// "POLICY_NO",
// "DED_AMT",
// "DED_STATUS",
// "ASGN_NO",
//"ASGN_DATE",
// "ASGN_TYPE",
"CLM_NO",
// "CLM_OFC_ID",
// "CLM_OFC_NM",
// "CLM_ADDR1",
// "CLM_ADDR2",
// "CLM_CITY",
// "CLM_ST",
// "CLM_ZIP",
// "CLM_CTRY",
// "CLM_PH1",
// "CLM_PH1X",
// "CLM_PH2",
// "CLM_PH2X",
// "CLM_FAX",
// "CLM_FAXX",
// "CLM_CT_LN",
// "CLM_CT_FN",
// "CLM_TITLE",
// "CLM_CT_PH",
// "CLM_CT_PHX",
// "CLM_EA",
// "PAYEE_NMS",
// "PAY_TYPE",
// "PAY_DATE",
// "PAY_CHKNM",
// "PAY_AMT",
// "AGT_CO_ID",
// "AGT_CO_NM",
// "AGT_ADDR1",
// "AGT_ADDR2",
// "AGT_CITY",
// "AGT_ST",
// "AGT_ZIP",
// "AGT_CTRY",
// "AGT_PH1",
// "AGT_PH1X",
// "AGT_PH2",
// "AGT_PH2X",
// "AGT_FAX",
// "AGT_FAXX",
// "AGT_CT_LN",
// "AGT_CT_FN",
// "AGT_CT_PH",
// "AGT_CT_PHX",
// "AGT_EA",
// "AGT_LIC_NO",
"LOSS_DATE",
// "LOSS_TYPE",
// "LOSS_DESC",
// "THEFT_IND",
// "CAT_NO",
// "TLOS_IND",
// "CUST_PR",
// "INSD_LN",
// "INSD_FN",
// "INSD_TITLE",
// "INSD_CO_NM",
// "INSD_ADDR1",
// "INSD_ADDR2",
// "INSD_CITY",
// "INSD_ST",
// "INSD_ZIP",
// "INSD_CTRY",
// "INSD_PH1",
// "INSD_PH1X",
// "INSD_PH2",
// "INSD_PH2X",
// "INSD_FAX",
// "INSD_FAXX",
// "INSD_EA",
"OWNR_LN",
"OWNR_FN",
// "OWNR_TITLE",
// "OWNR_CO_NM",
// "OWNR_ADDR1",
// "OWNR_ADDR2",
// "OWNR_CITY",
// "OWNR_ST",
// "OWNR_ZIP",
// "OWNR_CTRY",
// "OWNR_PH1",
// "OWNR_PH1X",
// "OWNR_PH2",
// "OWNR_PH2X",
// "OWNR_FAX",
// "OWNR_FAXX",
// "OWNR_EA",
// "INS_PH1",
// "INS_PH1X",
// "INS_PH2",
// "INS_PH2X",
// "INS_FAX",
// "INS_FAXX",
// "INS_CT_LN",
// "INS_CT_FN",
// "INS_TITLE",
// "INS_CT_PH",
// "INS_CT_PHX",
// "LOSS_CAT",
]);
// "INS_EA",
// "POLICY_NO",
// "DED_AMT",
// "DED_STATUS",
// "ASGN_NO",
//"ASGN_DATE",
// "ASGN_TYPE",
"CLM_NO",
// "CLM_OFC_ID",
// "CLM_OFC_NM",
// "CLM_ADDR1",
// "CLM_ADDR2",
// "CLM_CITY",
// "CLM_ST",
// "CLM_ZIP",
// "CLM_CTRY",
// "CLM_PH1",
// "CLM_PH1X",
// "CLM_PH2",
// "CLM_PH2X",
// "CLM_FAX",
// "CLM_FAXX",
// "CLM_CT_LN",
// "CLM_CT_FN",
// "CLM_TITLE",
// "CLM_CT_PH",
// "CLM_CT_PHX",
// "CLM_EA",
// "PAYEE_NMS",
// "PAY_TYPE",
// "PAY_DATE",
// "PAY_CHKNM",
// "PAY_AMT",
// "AGT_CO_ID",
// "AGT_CO_NM",
// "AGT_ADDR1",
// "AGT_ADDR2",
// "AGT_CITY",
// "AGT_ST",
// "AGT_ZIP",
// "AGT_CTRY",
// "AGT_PH1",
// "AGT_PH1X",
// "AGT_PH2",
// "AGT_PH2X",
// "AGT_FAX",
// "AGT_FAXX",
// "AGT_CT_LN",
// "AGT_CT_FN",
// "AGT_CT_PH",
// "AGT_CT_PHX",
// "AGT_EA",
// "AGT_LIC_NO",
"LOSS_DATE",
// "LOSS_TYPE",
// "LOSS_DESC",
// "THEFT_IND",
// "CAT_NO",
// "TLOS_IND",
// "CUST_PR",
// "INSD_LN",
// "INSD_FN",
// "INSD_TITLE",
// "INSD_CO_NM",
// "INSD_ADDR1",
// "INSD_ADDR2",
// "INSD_CITY",
// "INSD_ST",
// "INSD_ZIP",
// "INSD_CTRY",
// "INSD_PH1",
// "INSD_PH1X",
// "INSD_PH2",
// "INSD_PH2X",
// "INSD_FAX",
// "INSD_FAXX",
// "INSD_EA",
"OWNR_LN",
"OWNR_FN",
// "OWNR_TITLE",
// "OWNR_CO_NM",
// "OWNR_ADDR1",
// "OWNR_ADDR2",
// "OWNR_CITY",
// "OWNR_ST",
// "OWNR_ZIP",
// "OWNR_CTRY",
// "OWNR_PH1",
// "OWNR_PH1X",
// "OWNR_PH2",
// "OWNR_PH2X",
// "OWNR_FAX",
// "OWNR_FAXX",
// "OWNR_EA",
// "INS_PH1",
// "INS_PH1X",
// "INS_PH2",
// "INS_PH2X",
// "INS_FAX",
// "INS_FAXX",
// "INS_CT_LN",
// "INS_CT_FN",
// "INS_TITLE",
// "INS_CT_PH",
// "INS_CT_PHX",
// "LOSS_CAT",
]);
}
}
async function DecodeAd2File(extensionlessFilePath) {
let dbf = await DBFFile.open(`${extensionlessFilePath}B.AD2`);
let records = await dbf.readRecords(1);
return _.pick(records[0], ["CLMT_LN", "CLMT_FN"]);
let dbf;
try {
dbf = await DBFFile.open(`${extensionlessFilePath}B.AD2`);
} catch (error) {
log.error("Error opening AD2 File.", error);
dbf = await DBFFile.open(`${extensionlessFilePath}.AD2`);
dbf && log.log("Found AD2 file using regular CIECA Id.");
} finally {
if (!dbf) return {};
let records = await dbf.readRecords(1);
return _.pick(records[0], ["CLMT_LN", "CLMT_FN"]);
}
}
async function DecodeVehFile(extensionlessFilePath) {
let dbf = await DBFFile.open(`${extensionlessFilePath}V.VEH`);
let records = await dbf.readRecords(1);
return _.pick(records[0], [
// "IMPACT_1",
// "IMPACT_2",
// "DB_V_CODE",
// "PLATE_NO",
// "PLATE_ST",
"V_VIN",
// "V_COND",
// "V_PROD_DT",
"V_MODEL_YR",
// "V_MAKECODE",
"V_MAKEDESC",
"V_MODEL",
"V_TYPE",
"V_MILEAGE",
// "V_BSTYLE",
// "V_TRIMCODE",
// "TRIM_COLOR",
// "V_MLDGCODE",
// "V_ENGINE",
// "V_COLOR",
// "V_TONE",
// "V_STAGE",
// "PAINT_CD1",
// "PAINT_CD2",
// "PAINT_CD3",
]);
let dbf;
try {
dbf = await DBFFile.open(`${extensionlessFilePath}V.VEH`);
} catch (error) {
log.error("Error opening VEH File.", error);
dbf = await DBFFile.open(`${extensionlessFilePath}.VEH`);
dbf && log.log("Found VEH file using regular CIECA Id.");
} finally {
if (!dbf) return {};
let records = await dbf.readRecords(1);
return _.pick(records[0], [
// "IMPACT_1",
// "IMPACT_2",
// "DB_V_CODE",
// "PLATE_NO",
// "PLATE_ST",
"V_VIN",
// "V_COND",
// "V_PROD_DT",
"V_MODEL_YR",
// "V_MAKECODE",
"V_MAKEDESC",
"V_MODEL",
"V_TYPE",
"V_MILEAGE",
// "V_BSTYLE",
// "V_TRIMCODE",
// "TRIM_COLOR",
// "V_MLDGCODE",
// "V_ENGINE",
// "V_COLOR",
// "V_TONE",
// "V_STAGE",
// "PAINT_CD1",
// "PAINT_CD2",
// "PAINT_CD3",
]);
}
}
async function DecodeTtlFile(extensionlessFilePath) {
let dbf = await DBFFile.open(`${extensionlessFilePath}.TTL`);
@@ -356,7 +383,8 @@ async function DecodeLinFile(extensionlessFilePath) {
!jobline.part_type ||
jobline.db_ref.startsWith("900") ||
jobline.line_desc.toLowerCase().startsWith("urethane") ||
jobline.line_desc.toLowerCase().startsWith("wheel") ||
jobline.line_desc.toLowerCase().includes("wheel") ||
jobline.line_desc.toLowerCase().includes("tire") ||
jobline.line_desc.toLowerCase().startsWith("hazardous") ||
jobline.line_desc.toLowerCase().startsWith("detail") ||
jobline.line_desc.toLowerCase().startsWith("clean") ||

View File

@@ -16,6 +16,11 @@ async function GetListOfEstimates() {
const FilteredListOfSummarizedEstimates = ListOfSummarizedEstimates.filter(
(j) => !j.ERROR
);
log.log(
"Number of estimates filtered on file scan due to error: ",
ListOfSummarizedEstimates.length - FilteredListOfSummarizedEstimates.length
);
return FilteredListOfSummarizedEstimates;
}
@@ -71,4 +76,4 @@ async function DeleteAllEms() {
}
exports.GetListOfEstimates = GetListOfEstimates;
exports.DeleteAllEms = DeleteAllEms
exports.DeleteAllEms = DeleteAllEms;

View File

@@ -19,6 +19,8 @@ const Nucleus = require("nucleus-nodejs");
require("./ipc-main-handler");
require("./analytics");
autoUpdater.autoDownload = false;
autoUpdater.logger = log;
autoUpdater.logger.transports.file.level = "info";
log.info("App starting...", app.getVersion());
@@ -49,6 +51,12 @@ var menu = Menu.buildFromTemplate([
mainWindow.webContents.session.clearStorageData();
},
},
{
label: "Sign Out",
click() {
mainWindow.webContents.send(ipcTypes.app.toRenderer.signOut);
},
},
{
label: "Exit",
click() {
@@ -70,10 +78,7 @@ var menu = Menu.buildFromTemplate([
{
label: `Check for Updates (currently ${app.getVersion()})`,
click() {
autoUpdater.checkForUpdatesAndNotify({
title: "ImEX RPS Update Downloaded",
body: "Restart ImEX RPS to install.",
});
checkForUpdates();
},
},
{
@@ -168,7 +173,6 @@ function createWindow() {
}
mainWindow.maximize();
autoUpdater.checkForUpdatesAndNotify();
globalShortcut.register("CommandOrControl+Shift+I", () => {
mainWindow.webContents.toggleDevTools();
@@ -242,46 +246,22 @@ function createTray() {
return appIcon;
}
autoUpdater.on("checking-for-update", () => {
log.log("Checking for update...");
});
autoUpdater.on("update-available", (ev, info) => {
log.log("Update available.");
});
autoUpdater.on("update-not-available", (ev, info) => {
log.log("Update not available.");
});
autoUpdater.on("error", (ev, err) => {
log.log("Error in auto-updater.");
});
autoUpdater.on("download-progress", (ev, progressObj) => {
log.log("Download progress...");
});
// autoUpdater.on("update-downloaded", (ev, info) => {
// console.log("Update downloaded; will install in 5 seconds");
// autoUpdater.on("checking-for-update", () => {
// log.log("Checking for update...");
// });
autoUpdater.on("update-downloaded", (ev, info) => {
Nucleus.track("UPDATE_DOWNLOADED", info);
if (process.env.NODE_ENV === "production") {
dialog.showMessageBox(
{
type: "info",
title: "Found Updates",
message: "Found updates, do you want update now?",
buttons: ["Sure", "No"],
},
(buttonIndex) => {
if (buttonIndex === 0) {
const isSilent = true;
const isForceRunAfter = true;
autoUpdater.quitAndInstall(isSilent, isForceRunAfter);
} else {
logger.warn("Error");
}
}
);
}
});
// autoUpdater.on("update-available", (ev, info) => {
// log.log("Update available.");
// });
// autoUpdater.on("update-not-available", (ev, info) => {
// log.log("Update not available.");
// });
// autoUpdater.on("error", (ev, err) => {
// log.log("Error in auto-updater.");
// });
// // autoUpdater.on("update-downloaded", (ev, info) => {
// // console.log("Update downloaded; will install in 5 seconds");
// // });
function openNoticeWindow() {
if (noticeWindow) {
@@ -301,3 +281,57 @@ function openNoticeWindow() {
noticeWindow = null;
});
}
ipcMain.on(ipcTypes.app.toMain.checkForUpdates, (event, args) => {
checkForUpdates();
});
ipcMain.on(ipcTypes.app.toMain.downloadUpdates, (event, args) => {
autoUpdater.downloadUpdate();
});
ipcMain.on(ipcTypes.app.toMain.installUpdates, (event, args) => {
const isSilent = true;
const isForceRunAfter = true;
autoUpdater.quitAndInstall(isSilent, isForceRunAfter);
});
autoUpdater.on("download-progress", (ev) => {
mainWindow.webContents.send(ipcTypes.app.toRenderer.downloadProgress, ev);
});
autoUpdater.on("update-downloaded", (ev, info) => {
Nucleus.track("UPDATE_DOWNLOADED", info);
// if (process.env.NODE_ENV === "production") {
dialog.showMessageBox(
{
type: "info",
title: "ImeX RPS Update Manager",
message: `ImEX RPS is ready to update to Version ${ev.version}. It is highly recommended that you update immediately. Would you like to update now? RPS will automatically restart.`,
buttons: ["Yes", "No"],
},
(buttonIndex) => {
if (buttonIndex === 0) {
const isSilent = true;
const isForceRunAfter = true;
autoUpdater.quitAndInstall(isSilent, isForceRunAfter);
} else {
logger.error("Error");
}
}
);
});
async function checkForUpdates() {
try {
log.info("Checking for updates.");
const result = await autoUpdater.checkForUpdates();
const { updateInfo } = result;
mainWindow.webContents.send(
ipcTypes.app.toRenderer.updateAvailable,
updateInfo
);
} catch (error) {
log.error("Error while checking for update", error);
}
}

View File

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

View File

@@ -78,3 +78,13 @@ body {
.ant-tabs-content {
height: 100%;
}
.blink_me {
animation: blinker 1s linear infinite;
}
@keyframes blinker {
50% {
opacity: 0;
}
}

View File

@@ -25,6 +25,8 @@ export function DeleteJobAtom({ setSelectedJobId, jobId }) {
});
const result = await deleteJob({
variables: { jobId: jobId },
refetchQueries: ["QUERY_ALL_JOBS_PAGINATED"],
awaitRefetchQueries: true,
});
if (result.errors) {

View File

@@ -37,7 +37,13 @@ export default function JobPartsGraphAtom({
}, [job, price]);
if (loading) return <Skeleton active />;
if (!job) return <ErrorResultAtom title="Error displaying job data." />;
if (!job)
return (
<ErrorResultAtom
title="Error displaying parts graphs."
errorMessage="It looks like this job doesn't exist."
/>
);
return (
<div

View File

@@ -1,9 +0,0 @@
.blink_me {
animation: blinker 1s linear infinite;
}
@keyframes blinker {
50% {
opacity: 0;
}
}

View File

@@ -1,3 +1,4 @@
import { WarningOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { DatePicker, message, Spin } from "antd";
import moment from "moment";
@@ -43,7 +44,14 @@ export default function CloseDateDisplayMolecule({ jobId, close_date }) {
return (
<div style={{ cursor: "pointer" }} onClick={() => setEditMode(true)}>
{value && value.isValid() ? value.format(DateFormat) : "No date set"}
{value && value.isValid() ? (
value.format(DateFormat)
) : (
<div>
<span>No date set.</span>
<WarningOutlined style={{ marginLeft: ".5rem", color: "orange" }} />
</div>
)}
</div>
);
}

View File

@@ -35,7 +35,7 @@ export function JobGroupMolecule({ bodyshop, jobId, group, job }) {
});
if (!result.errors) {
message.success("Close date updated.");
message.success("Vehicle group updated.");
} else {
message.error("Error updating job.");
}

View File

@@ -10,7 +10,13 @@ import DeleteJobAtom from "../../atoms/delete-job/delete-job.atom";
export default function JobsDetailDescriptionMolecule({ loading, job }) {
if (loading) return <Skeleton active />;
if (!job) return <ErrorResultAtom title="Error displaying job data." />;
if (!job)
return (
<ErrorResultAtom
title="Error displaying job header data."
errorMessage="It looks like this job doesn't exist."
/>
);
return (
<div>

View File

@@ -4,6 +4,7 @@ import React, { useState } from "react";
import ipcTypes from "../../../ipc.types";
import { alphaSort } from "../../../util/sorters";
import CurrencyFormatterAtom from "../../atoms/currency-formatter/currency-formatter.atom";
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";
@@ -13,6 +14,14 @@ export default function JobLinesTableMolecule({ loading, job }) {
const [searchText, setSearchText] = useState("");
const [filters, setFilters] = useState({ ignore: ["false"] });
if (!job) {
return (
<ErrorResultAtom
title="Error Displaying Job Lines"
errorMessage="It looks like this job doesn't exist."
/>
);
}
const { joblines } = job;
const columns = [
{

View File

@@ -6,6 +6,7 @@ import { setSelectedJobId } from "../../../redux/application/application.actions
import { selectSelectedJobId } from "../../../redux/application/application.selectors";
import TimeAgoFormatter from "../../atoms/time-ago-formatter/time-ago-formatter.atom";
import "./jobs-list-item.styles.scss";
import { WarningOutlined } from "@ant-design/icons";
const mapStateToProps = createStructuredSelector({
selectedJobId: selectSelectedJobId,
@@ -41,7 +42,15 @@ export function JobsListItemMolecule({
justifyContent: "space-between",
}}
>
<strong>{item.clm_no || "No Claim Number"}</strong>
<strong>
{item.clm_no || "No Claim Number"}
{!item.close_date && (
<WarningOutlined
title="No close date has been set for this job."
style={{ marginLeft: ".5rem", color: "orange" }}
/>
)}
</strong>
<span style={{ fontStyle: "italic" }}>
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
</span>

View File

@@ -5,7 +5,7 @@ import { createStructuredSelector } from "reselect";
import { selectSelectedJobTargetPc } from "../../../redux/application/application.selectors";
import {
CalculateJobRpsDollars,
CalculateJobRpsPc
CalculateJobRpsPc,
} from "../../../util/CalculateJobRps";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
@@ -38,7 +38,13 @@ export function JobsTargetsStatsMolecule({
);
if (loading) return <Skeleton active />;
if (!job) return <ErrorResultAtom title="Error displaying job data." />;
if (!job)
return (
<ErrorResultAtom
title="Error displaying job totals."
errorMessage="It looks like this job doesn't exist."
/>
);
return (
<div
style={{
@@ -71,7 +77,7 @@ export function JobsTargetsStatsMolecule({
<Statistic
title="Target RPS $"
style={{ margin: "0rem .5rem" }}
value={actPriceSum.percentage(selectedJobTargetPc * 100).toFormat()}
value={dbPriceSum.percentage(selectedJobTargetPc * 100).toFormat()}
/>
<Statistic
title="Current RPS $"

View File

@@ -9,6 +9,7 @@ import { setSelectedJobId } from "../../../redux/application/application.actions
import {
selectReportData,
selectReportLoading,
selectScorecard,
} from "../../../redux/reporting/reporting.selectors";
const { ipcRenderer } = window;
@@ -16,12 +17,14 @@ const { ipcRenderer } = window;
const mapStateToProps = createStructuredSelector({
reportingLoading: selectReportLoading,
reportData: selectReportData,
scoreCard: selectScorecard,
});
const mapDispatchToProps = (dispatch) => ({
setSelectedJobId: (id) => dispatch(setSelectedJobId(id)),
});
export function ReportingJobsListMolecule({
scoreCard,
reportingLoading,
reportData,
setSelectedJobId,
@@ -147,6 +150,24 @@ export function ReportingJobsListMolecule({
scroll={{
x: true,
}}
summary={() => (
<Table.Summary.Row>
<Table.Summary.Cell index={0}></Table.Summary.Cell>
<Table.Summary.Cell index={1}></Table.Summary.Cell>
<Table.Summary.Cell index={2}></Table.Summary.Cell>
<Table.Summary.Cell index={3}></Table.Summary.Cell>
<Table.Summary.Cell index={4}></Table.Summary.Cell>
<Table.Summary.Cell index={5}>
{scoreCard && scoreCard.allJobsSumDbPrice.toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell index={6}>
{" "}
{scoreCard && scoreCard.allJobsSumActPrice.toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell index={7}></Table.Summary.Cell>
<Table.Summary.Cell index={8}></Table.Summary.Cell>
</Table.Summary.Row>
)}
/>
</div>
);

View File

@@ -35,38 +35,47 @@ export function ReportingTotalsStatsMolecule({ reportingLoading, scoreCard }) {
marginBottom: "1rem",
}}
>
<Statistic
title="RPS Total"
value={scoreCard.shopRpsTotalDollars.toFormat()}
/>
<Statistic
title="RPS Expectation"
value={scoreCard.shopRpsExpectedDollars.toFormat()}
/>
<Statistic
title="RPS Variance $"
valueStyle={{
color:
scoreCard.varianceDollars.getAmount() < 0 ? "tomato" : "seagreen",
}}
value={scoreCard.varianceDollars.toFormat()}
/>
<Statistic
title="Current RPS %"
valueStyle={{
color:
(scoreCard.currentRpsPc || 0) <= (scoreCard.targetRpsPc || 0)
? "tomato"
: "seagreen",
}}
value={((scoreCard.currentRpsPc || 0) * 100).toFixed(1)}
suffix="%"
/>
<Statistic
title="Target RPS %"
value={((scoreCard.targetRpsPc || 0) * 100).toFixed(1)}
suffix="%"
/>
<div style={{ display: "flex" }}>
<Statistic
title="Target RPS %"
style={{ margin: "0rem .5rem" }}
value={((scoreCard.targetRpsPc || 0) * 100).toFixed(1)}
suffix="%"
/>
<Statistic
title="Current RPS %"
style={{ margin: "0rem .5rem" }}
valueStyle={{
color:
(scoreCard.currentRpsPc || 0) <= (scoreCard.targetRpsPc || 0)
? "tomato"
: "seagreen",
}}
value={((scoreCard.currentRpsPc || 0) * 100).toFixed(1)}
suffix="%"
/>
</div>
<div style={{ display: "flex" }}>
<Statistic
title="Target RPS $"
style={{ margin: "0rem .5rem" }}
value={scoreCard.shopRpsExpectedDollars.toFormat()}
/>
<Statistic
title="Current RPS $"
style={{ margin: "0rem .5rem" }}
value={scoreCard.shopRpsTotalDollars.toFormat()}
/>
<Statistic
title="RPS Variance $"
style={{ margin: "0rem .5rem" }}
valueStyle={{
color:
scoreCard.varianceDollars.getAmount() < 0 ? "tomato" : "seagreen",
}}
value={scoreCard.varianceDollars.toFormat()}
/>
</div>
</div>
);
}

View File

@@ -35,18 +35,20 @@ export function ScanEstimateListMolecule({ scanLoading, estimates }) {
key: "ins_co_nm",
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
},
{
title: "Last Name",
dataIndex: "ownr_ln",
key: "ownr_ln",
defaultSortOrder: "ascend",
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
},
{
title: "First Name",
dataIndex: "ownr_fn",
key: "ownr_fn",
sorter: (a, b) => alphaSort(a.ownr_fn, b.ownr_fn),
},
{
title: "Last Name",
dataIndex: "ownr_ln",
key: "ownr_ln",
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
},
{
title: "Vehicle",
dataIndex: "vehicle",

View File

@@ -29,10 +29,10 @@ export function JobsDetailOrganism({ selectedJobId, setSelectedJobTargetPc }) {
});
useEffect(() => {
if (data)
if (data && data.jobs_by_pk)
setSelectedJobTargetPc({
group: data.jobs_by_pk.group,
v_age: data.jobs_by_pk.v_age,
group: data.jobs_by_pk && data.jobs_by_pk.group,
v_age: data.jobs_by_pk && data.jobs_by_pk.v_age,
});
}, [data, setSelectedJobTargetPc]);

View File

@@ -0,0 +1,124 @@
import { AlertFilled } from "@ant-design/icons";
import { Button, Layout, Progress } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import ipcTypes from "../../../ipc.types";
import {
selectUpdateAvailable,
selectUpdateProgress,
} from "../../../redux/application/application.selectors";
const { ipcRenderer } = window;
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
updateAvailable: selectUpdateAvailable,
updateProgress: selectUpdateProgress,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function UpdateManagerOrganism({ updateAvailable, updateProgress }) {
if (!updateAvailable) return null;
return (
<Layout.Footer>
{updateAvailable && !updateProgress && (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<div>
<AlertFilled
style={{ marginRight: ".5rem", color: "tomato" }}
className="blink_me"
/>
<span>{`An update to ImEX RPS is available. (Version ${updateAvailable.version})`}</span>
</div>
<Button
type="primary"
style={{ margin: "0rem .5rem" }}
onClick={() =>
ipcRenderer.send(ipcTypes.default.app.toMain.downloadUpdates)
}
>
Download
</Button>
</div>
)}
{updateAvailable && updateProgress && (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Progress
style={{ flex: 1, margin: "0rem .5rem" }}
status={updateProgress.percent === 100 ? "success" : "active"}
percent={updateProgress.percent.toFixed(1)}
/>
{updateProgress.percent === 100 ? (
<div tyle={{ margin: "0rem .5rem" }}>
<span
style={{ margin: "0rem .5rem" }}
>{`Updated downloaded.`}</span>
<Button
type="primary"
style={{ margin: "0rem .5rem" }}
onClick={() =>
ipcRenderer.send(ipcTypes.default.app.toMain.installUpdates)
}
>
Install
</Button>
</div>
) : (
<span
style={{ margin: "0rem .5rem" }}
>{`Downloading update at ${formatBytes(
updateProgress.bytesPerSecond
)})`}</span>
)}
</div>
)}
</Layout.Footer>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(UpdateManagerOrganism);
// download Info
// {
// // bytesPerSecond: 6633258;
// // delta: 2479242;
// // percent: 100;
// // total: 95651575;
// // transferred: 95651575;
// }
function formatBytes(bytes) {
var marker = 1024; // Change to 1000 if required
var decimal = 1; // Change as required
var kiloBytes = marker; // One Kilobyte is 1024 bytes
var megaBytes = marker * marker; // One MB is 1024 KB
var gigaBytes = marker * marker * marker; // One GB is 1024 MB
//var teraBytes = marker * marker * marker * marker; // One TB is 1024 GB
// return bytes if less than a KB
if (bytes < kiloBytes) return bytes + " Bytes/sec";
// return KB if less than a MB
else if (bytes < megaBytes)
return (bytes / kiloBytes).toFixed(decimal) + " KB/sec";
// return MB if less than a GB
else if (bytes < gigaBytes)
return (bytes / megaBytes).toFixed(decimal) + " MB/sec";
// return GB if less than a TB
else return (bytes / gigaBytes).toFixed(decimal) + " GB/sec";
}

View File

@@ -6,6 +6,7 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../../redux/user/user.selectors";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import SiderMenuOrganism from "../../organisms/sider-menu/sider-menu.organism";
import UpdateManagerOrganism from "../../organisms/update-manager/update-manager.organism";
import JobsPage from "../jobs/jobs.page";
import ReportingPage from "../reporting/reporting.page";
import ScanPage from "../scan/scan.page";
@@ -39,6 +40,8 @@ export function RoutesPage({ bodyshop }) {
<Route exact path="/scan" component={ScanPage} />
<Route exact path="/" component={JobsPage} />
</Layout.Content>
<UpdateManagerOrganism />
</Layout>
</Layout>
);

View File

@@ -26,6 +26,7 @@ export const QUERY_ALL_JOBS_PAGINATED = gql`
id
ins_co_nm
clm_no
close_date
updated_at
}
jobs_aggregate {

View File

@@ -9,6 +9,14 @@ exports.default = {
setAcceptableInsCoNm: "setAcceptableInsCoNm",
setUserName: "setUserName",
track: "analytics_track",
checkForUpdates: "app_checkForUpdates",
downloadUpdates: "app_downloadUpdates",
installUpdates: "app_installupdates",
},
toRenderer: {
updateAvailable: "app_updateAvailable",
downloadProgress: "app_downloadProgress",
signOut: "app_signOut",
},
},
store: {

View File

@@ -1,12 +1,15 @@
import ipcTypes from "../ipc.types";
import {
setSettings,
setUpdateAvailable,
setUpdateProgress,
setWatchedPaths,
setWatcherStatus,
} from "../redux/application/application.actions";
import { store } from "../redux/store";
import { UpsertEstimate } from "./ipc-estimate-utils";
import { setScanEstimateList } from "../redux/scan/scan.actions";
import { signOutStart } from "../redux/user/user.actions";
const { ipcRenderer } = window;
console.log("----Initializing IPC Listeners in React App.");
@@ -65,3 +68,24 @@ ipcRenderer.on(
store.dispatch(setScanEstimateList(listOfEstimates));
}
);
ipcRenderer.on(
ipcTypes.default.app.toRenderer.updateAvailable,
async (event, updateInfo) => {
store.dispatch(setUpdateAvailable(updateInfo));
}
);
ipcRenderer.on(
ipcTypes.default.app.toRenderer.downloadProgress,
async (event, progress) => {
store.dispatch(setUpdateProgress(progress));
}
);
ipcRenderer.on(
ipcTypes.default.app.toRenderer.signOut,
async (event, progress) => {
store.dispatch(signOutStart());
}
);

View File

@@ -48,3 +48,7 @@ export const setUpdateAvailable = (available) => ({
type: ApplicationActionTypes.SET_UPDATE_AVAILABLE,
payload: available,
});
export const setUpdateProgress = (progress) => ({
type: ApplicationActionTypes.SET_UPDATE_PROGRESS,
payload: progress,
});

View File

@@ -8,6 +8,7 @@ const INITIAL_STATE = {
selectedJobTargetPc: 0,
settings: {},
updateAvailable: false,
updateProgress: null,
};
const { ipcRenderer } = window;
@@ -63,6 +64,8 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
return { ...state, settings: { ...state.settings, ...action.payload } };
case ApplicationActionTypes.SET_UPDATE_AVAILABLE:
return { ...state, updateAvailable: action.payload };
case ApplicationActionTypes.SET_UPDATE_PROGRESS:
return { ...state, updateProgress: action.payload };
default:
return state;
}

View File

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

View File

@@ -9,5 +9,7 @@ const ApplicationActionTypes = {
SET_SELECTED_JOB_TARGET_PC_SUCCESS: "SET_SELECTED_JOB_TARGET_PC_SUCCESS",
SET_SETTINGS: "SET_SETTINGS",
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE",
SET_UPDATE_PROGRESS: "SET_UPDATE_PROGRESS",
};
export default ApplicationActionTypes;

View File

@@ -128,6 +128,7 @@ export function* signInSuccessSaga({ payload }) {
LogRocket.identify(payload.email, {
email: payload.email,
});
ipcRenderer.send(ipcTypes.default.app.toMain.checkForUpdates);
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "SIGN_IN_SUCCESS",