Merged in v1.0.4 (pull request #6)

V1.0.4
This commit is contained in:
Patrick Fic
2020-10-26 22:15:09 +00:00
47 changed files with 27746 additions and 3934 deletions

View File

@@ -1,3 +1,3 @@
REACT_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyCXg148Ma82Qa7dK-2EL4sE0tJhKVnh1rY", "authDomain": "rps-prod-b53c8.firebaseapp.com", "databaseURL": "https://rps-prod-b53c8.firebaseio.com", "projectId": "rps-prod-b53c8", "storageBucket": "rps-prod-b53c8.appspot.com", "messagingSenderId": "361220226954", "appId": "1:361220226954:web:bf3a38d196e4fd8c921273", "measurementId": "G-W3BHH420EC"}
REACT_APP_GRAPHQL_ENDPOINT=https://rps.bodyshop.app/v1/graphql
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://rps.bodyshop.app/v1/graphql
REACT_APP_GRAPHQL_ENDPOINT=https://db.rps.imex.online/v1/graphql
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.rps.imex.online/v1/graphql

View File

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

View File

@@ -5,6 +5,7 @@ const store = new Store({
enableNotifications: true,
filePaths: [],
accepted_ins_co: [],
runWatcherOnStartup: true,
polling: {
enabled: false,
pollingInterval: 1000,

View File

@@ -2,7 +2,7 @@ const { ipcMain } = require("electron");
const Nucleus = require("nucleus-nodejs");
const ipcTypes = require("../../src/ipc.types");
const { ImportJob } = require("../decoder/decoder");
const { GetListOfEstimates } = require("./file-scan");
const { GetListOfEstimates, DeleteAllEms } = require("./file-scan");
ipcMain.on(
ipcTypes.default.fileScan.toMain.scanFilePaths,
@@ -22,3 +22,16 @@ ipcMain.on(
await ImportJob(filePath);
}
);
ipcMain.on(
ipcTypes.default.fileScan.toMain.deleteAllEms,
async (event, filePath) => {
Nucleus.track("DELETE_ALLEMS");
await DeleteAllEms();
const ret = await GetListOfEstimates();
event.reply(
ipcTypes.default.fileScan.toRenderer.scanFilePathsResponse,
ret
);
}
);

View File

@@ -6,6 +6,7 @@ const fsPromises = fs.promises;
const _ = require("lodash");
const { DecodeEstimate } = require("../decoder/decoder");
const Nucleus = require("nucleus-nodejs");
const { format } = require("path");
async function GetListOfEstimates() {
Nucleus.track("SCAN_ALL_ESTIMATES");
@@ -42,4 +43,32 @@ async function GetEnvFiles() {
return allFilePaths;
}
async function DeleteAllEms() {
try {
const filePaths = store.get("filePaths");
const allFilePaths = [];
await Promise.all(
filePaths.map(async (fp) => {
const allFilesinDir = await fsPromises.readdir(fp);
allFilesinDir.map((envFileName) => {
allFilePaths.push(path.join(fp, envFileName));
return null;
});
})
);
await Promise.all(
allFilePaths.map(async (file) => {
await fsPromises.unlink(file);
})
);
return true;
} catch (error) {
return false;
}
}
exports.GetListOfEstimates = GetListOfEstimates;
exports.DeleteAllEms = DeleteAllEms

View File

@@ -17,7 +17,10 @@ ipcMain.on(
);
ipcMain.on(ipcTypes.default.fileWatcher.toMain.start, async (event, arg) => {
StartWatcher();
if ((arg && arg.startup && store.get("runWatcherOnStartup")) || !arg) {
StartWatcher();
}
// event.sender.send(ipcTypes.default.fileWatcher.toRenderer.startSuccess);
event.sender.send(
ipcTypes.default.fileWatcher.toRenderer.filepathsList,

View File

@@ -94,14 +94,16 @@ function onWatcherReady() {
}
async function StopWatcher() {
await watcher.close();
log.info("Watcher stopped.");
const b = BrowserWindow.getAllWindows()[0];
b.webContents.send(ipcTypes.default.fileWatcher.toRenderer.stopSuccess);
NewNotification({
title: "RPS Watcher Stopped",
body: "Estimates will not be automatically uploaded.",
});
if (watcher) {
await watcher.close();
log.info("Watcher stopped.");
const b = BrowserWindow.getAllWindows()[0];
b.webContents.send(ipcTypes.default.fileWatcher.toRenderer.stopSuccess);
NewNotification({
title: "RPS Watcher Stopped",
body: "Estimates will not be automatically uploaded.",
});
}
}
exports.StartWatcher = StartWatcher;

23006
electron/licenses.txt Normal file

File diff suppressed because one or more lines are too long

View File

@@ -70,7 +70,10 @@ var menu = Menu.buildFromTemplate([
{
label: `Check for Updates (currently ${app.getVersion()})`,
click() {
autoUpdater.checkForUpdatesAndNotify();
autoUpdater.checkForUpdatesAndNotify({
title: "ImEX RPS Update Downloaded",
body: "Restart ImEX RPS to install.",
});
},
},
{
@@ -85,14 +88,20 @@ var menu = Menu.buildFromTemplate([
shell.openPath(path.join(app.getPath("appData"), "ImeX RPS\\logs"));
},
},
{
label: "Third Party Notices",
click() {
openNoticeWindow();
},
},
],
},
]);
let mainWindow;
let noticeWindow;
let tray = null;
function createWindow() {
makeSingleInstance();
// Create the browser window.
Menu.setApplicationMenu(menu);
mainWindow = new BrowserWindow({
@@ -109,44 +118,62 @@ function createWindow() {
preload: path.join(__dirname, "preload.js"), // use a preload script
},
});
// and load the index.html of the app.
// win.loadFile("index.html");
mainWindow.loadURL(
isDev
? "http://localhost:3000"
: `file://${path.join(__dirname, "/../build/index.html")}`
);
// mainWindow.on("close", function (event) {
// event.preventDefault();
// mainWindow.hide();
// tray = createTray();
// });
mainWindow.on("minimize", function (event) {
event.preventDefault();
mainWindow.hide();
tray = createTray();
});
ipcMain.on(ipcTypes.quit, (event, arg) => {
app.isQuiting = true;
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
});
} else {
app.on("second-instance", (event, commandLine, workingDirectory) => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
// Open the DevTools.
if (isDev) {
mainWindow.webContents.openDevTools({
// mode: "detach"
// and load the index.html of the app.
// win.loadFile("index.html");
mainWindow.loadURL(
isDev
? "http://localhost:3000"
: `file://${path.join(__dirname, "/../build/index.html")}`
);
// mainWindow.on("close", function (event) {
// event.preventDefault();
// mainWindow.hide();
// if (!tray) {
// tray = createTray();
// }
// });
mainWindow.on("minimize", function (event) {
event.preventDefault();
mainWindow.hide();
if (!tray) {
tray = createTray();
}
});
ipcMain.on(ipcTypes.quit, (event, arg) => {
app.isQuiting = true;
app.quit();
});
// Open the DevTools.
if (isDev) {
mainWindow.webContents.openDevTools({
// mode: "detach"
});
}
mainWindow.maximize();
autoUpdater.checkForUpdatesAndNotify();
globalShortcut.register("CommandOrControl+Shift+I", () => {
mainWindow.webContents.toggleDevTools();
});
}
mainWindow.maximize();
autoUpdater.checkForUpdatesAndNotify();
globalShortcut.register("CommandOrControl+Shift+I", () => {
mainWindow.webContents.toggleDevTools();
});
}
exports.mainWindow = mainWindow;
@@ -189,21 +216,6 @@ app.on("activate", () => {
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
function makeSingleInstance() {
if (process.mas) return;
app.requestSingleInstanceLock();
app.on("second-instance", () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
}
function createTray() {
let appIcon = new Tray(path.join(__dirname, "../src/assets/logo192.png"));
const contextMenu = Menu.buildFromTemplate([
@@ -231,23 +243,23 @@ function createTray() {
}
autoUpdater.on("checking-for-update", () => {
console.log("Checking for update...");
log.log("Checking for update...");
});
autoUpdater.on("update-available", (ev, info) => {
console.log("Update available.");
log.log("Update available.");
});
autoUpdater.on("update-not-available", (ev, info) => {
console.log("Update not available.");
log.log("Update not available.");
});
autoUpdater.on("error", (ev, err) => {
console.log("Error in auto-updater.");
log.log("Error in auto-updater.");
});
autoUpdater.on("download-progress", (ev, progressObj) => {
console.log("Download progress...");
});
autoUpdater.on("update-downloaded", (ev, info) => {
console.log("Update downloaded; will install in 5 seconds");
log.log("Download progress...");
});
// autoUpdater.on("update-downloaded", (ev, info) => {
// console.log("Update downloaded; will install in 5 seconds");
// });
autoUpdater.on("update-downloaded", (ev, info) => {
Nucleus.track("UPDATE_DOWNLOADED", info);
if (process.env.NODE_ENV === "production") {
@@ -270,3 +282,22 @@ autoUpdater.on("update-downloaded", (ev, info) => {
);
}
});
function openNoticeWindow() {
if (noticeWindow) {
noticeWindow.focus();
return;
}
noticeWindow = new BrowserWindow({
height: 600,
width: 800,
title: "ImEX RPS - Third Party Notices",
});
noticeWindow.loadURL("file://" + __dirname + "/licenses.txt");
noticeWindow.on("closed", function () {
noticeWindow = null;
});
}

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,6 @@
- args:
cascade: true
read_only: false
sql: "create trigger calculate_updated_job_line_insert\r\nbefore\r\ninsert\r\n
\ on\r\n public.joblines for each row execute function calculate_job_line();"
type: run_sql

7867
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,13 +3,12 @@
"productName": "ImEX RPS",
"author": "ImEX Systems Inc. <support@thinkimex.com>",
"description": "ImEX RPS",
"version": "1.0.3",
"version": "1.0.4",
"main": "electron/main.js",
"homepage": "./",
"dependencies": {
"@apollo/client": "^3.2.4",
"@fingerprintjs/fingerprintjs": "^2.1.4",
"antd": "^4.7.2",
"antd": "^4.7.3",
"apollo-link-logger": "^2.0.0",
"chokidar": "^3.4.2",
"dbffile": "^1.4.3",
@@ -21,20 +20,19 @@
"electron-store": "^6.0.1",
"electron-updater": "^4.3.5",
"firebase": "^7.24.0",
"graphql": "^15.3.0",
"graphql": "^15.4.0",
"lodash": "^4.17.20",
"logrocket": "^1.0.14",
"moment": "^2.29.1",
"node-notifier": "^8.0.0",
"node-sass": "^4.14.1",
"nucleus-nodejs": "^3.0.6",
"query-string": "^6.13.6",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-infinite-scroller": "^1.2.4",
"react-redux": "^7.2.1",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.3",
"react-scripts": "4.0.0",
"recharts": "^1.8.5",
"redux": "^4.0.5",
"redux-logger": "^3.0.6",
@@ -53,8 +51,8 @@
"pack": "electron-builder --dir",
"dist": "npm run build && electron-builder",
"distp": "npm run build && electron-builder --publish always",
"distpnb": "npm run build && electron-builder --publish always",
"postinstall": "npm run build && electron-builder install-app-deps"
"distpnb": "lectron-builder --publish always",
"postinstall": "electron-builder install-app-deps"
},
"eslintConfig": {
"extends": "react-app"
@@ -73,7 +71,7 @@
},
"devDependencies": {
"concurrently": "^5.3.0",
"electron": "^10.1.3",
"electron": "^10.1.5",
"electron-builder": "^22.9.1",
"electron-devtools-installer": "^3.1.1",
"enzyme": "^3.11.0",

View File

@@ -47,23 +47,23 @@ body {
overflow: hidden;
white-space: nowrap;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 0.2rem;
background-color: #f5f5f5;
}
// ::-webkit-scrollbar-track {
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
// border-radius: 0.2rem;
// background-color: #f5f5f5;
// }
::-webkit-scrollbar {
width: 0.25rem;
max-height: 0.25rem;
background-color: #f5f5f5;
}
// ::-webkit-scrollbar {
// width: 0.25rem;
// max-height: 0.25rem;
// background-color: #f5f5f5;
// }
::-webkit-scrollbar-thumb {
border-radius: 0.2rem;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
background-color: #188fff;
}
// ::-webkit-scrollbar-thumb {
// border-radius: 0.2rem;
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
// background-color: #188fff;
// }
.jobs-list-container {
height: 100%;
}

View File

@@ -0,0 +1,17 @@
import { Button, Popconfirm } from "antd";
import React from "react";
import ipcTypes from "../../../ipc.types";
const { ipcRenderer } = window;
export default function DeleteAllEmsAtom() {
return (
<Popconfirm
title="Are you sure you want to delete all EMS files? This cannot be undone."
onConfirm={() =>
ipcRenderer.send(ipcTypes.default.fileScan.toMain.deleteAllEms)
}
>
<Button>Delete All EMS</Button>
</Popconfirm>
);
}

View File

@@ -43,7 +43,7 @@ export function DeleteJobAtom({ setSelectedJobId, jobId }) {
onConfirm={handleDelete}
>
<Button loading={loading}>
<DeleteFilled />
<DeleteFilled /> <span>Delete</span>
</Button>
</Popconfirm>
</div>

View File

@@ -1,11 +1,19 @@
import { Button, Result } from "antd";
import React from "react";
import ipcTypes from "../../../ipc.types";
const { ipcRenderer } = window;
export default function ErrorResultAtom({
title,
errorMessage,
tryAgainCallback,
}) {
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "ERROR_RESULT_ATOM_DISPLAYED",
title,
errorMessage,
});
return (
<Result
status="500"

View File

@@ -9,15 +9,16 @@ export default function IgnoreJobLineAtom({ ignore, lineId, line_desc }) {
const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
const [loading, setLoading] = useState(false);
const handleChange = async (checked) => {
console.log("handleChange -> checked", checked);
setLoading(true);
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "TOGGLE_IGNORE_LINE",
line_desc: line_desc,
ignore: checked,
ignore: !checked,
});
const result = await updateJobLine({
variables: { lineId: lineId, line: { ignore: checked } },
variables: { lineId: lineId, line: { ignore: !checked } },
});
if (result.errors) {
message.error("Error updating line.");
@@ -27,5 +28,5 @@ export default function IgnoreJobLineAtom({ ignore, lineId, line_desc }) {
setLoading(false);
};
return <Switch checked={ignore} onChange={handleChange} loading={loading} />;
return <Switch checked={!ignore} onChange={handleChange} loading={loading} />;
}

View File

@@ -48,9 +48,6 @@ export default function JobPartsGraphAtom({
alignItems: "center",
}}
>
<Typography.Title level={4}>
{price === "act_price" ? "Actual Price" : "Database Price"}
</Typography.Title>
<ResponsiveContainer>
<PieChart>
<Pie
@@ -71,6 +68,9 @@ export default function JobPartsGraphAtom({
</Pie>
</PieChart>
</ResponsiveContainer>
<Typography.Title level={4}>
{price === "act_price" ? "Actual Price" : "Database Price"}
</Typography.Title>
</div>
);
}

View File

@@ -15,8 +15,13 @@ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function WatcherPollingMolecule({ appSettings }) {
export function NotificationsToggleAtom({ appSettings }) {
const handleChange = (val) => {
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "TOGGLE_NOTIFICATION",
enabled: val,
});
ipcRenderer.send(ipcTypes.default.store.set, {
enableNotifications: val,
});
@@ -36,4 +41,4 @@ export function WatcherPollingMolecule({ appSettings }) {
export default connect(
mapStateToProps,
mapDispatchToProps
)(WatcherPollingMolecule);
)(NotificationsToggleAtom);

View File

@@ -1,4 +1,4 @@
export default (part_type) => {
const converter = (part_type) => {
switch (part_type) {
case "PAA":
return "A/M";
@@ -19,3 +19,5 @@ export default (part_type) => {
return part_type;
}
};
export default converter;

View File

@@ -0,0 +1,17 @@
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectUpdateAvailable } from "../../../redux/application/application.selectors";
const mapStateToProps = createStructuredSelector({
//scanLoading: selectScanLoading,
updateAvailable: selectUpdateAvailable,
});
const mapDispatchToProps = (dispatch) => ({});
export function UpdateAvailableAtom({ available }) {
return <div>Update Available!</div>;
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(UpdateAvailableAtom);

View File

@@ -0,0 +1,41 @@
import { Switch } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import ipcTypes from "../../../ipc.types";
import { selectSettings } from "../../../redux/application/application.selectors";
import DataLabel from "../../atoms/data-label/data-label.atom";
const { ipcRenderer } = window;
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
appSettings: selectSettings,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function WatcherStartupAtom({ appSettings }) {
const handleChange = (val) => {
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "TOGGLE_WATCHER_ON_STARTUP",
enabled: val,
});
ipcRenderer.send(ipcTypes.default.store.set, {
runWatcherOnStartup: val,
});
};
return (
<div>
<DataLabel label="Run Watcher on Startup?">
<Switch
onChange={handleChange}
checked={appSettings && appSettings.runWatcherOnStartup}
/>
</DataLabel>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(WatcherStartupAtom);

View File

@@ -3,8 +3,9 @@ import { DatePicker, message, Spin } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { UPDATE_JOB } from "../../../graphql/jobs.queries";
import ipcTypes from "../../../ipc.types";
import { DateFormat } from "../../../util/constants";
const { ipcRenderer } = window;
export default function CloseDateDisplayMolecule({ jobId, close_date }) {
const [editMode, setEditMode] = useState(false);
const [value, setValue] = useState(moment(close_date));
@@ -12,6 +13,9 @@ export default function CloseDateDisplayMolecule({ jobId, close_date }) {
const [updateJob] = useMutation(UPDATE_JOB);
const handleChange = async (newDate) => {
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "SET_CLOSED_DATE",
});
setLoading(true);
setValue(newDate);
const result = await updateJob({

View File

@@ -54,7 +54,7 @@ export function JobGroupMolecule({ bodyshop, jobId, group, job }) {
<Dropdown overlay={menu} trigger={["click"]}>
<a href=" #" onClick={(e) => e.preventDefault()}>
{group}
<DownOutlined />
<DownOutlined style={{ marginLeft: ".2rem" }} />
{loading && <LoadingOutlined />}
</a>
</Dropdown>

View File

@@ -5,6 +5,7 @@ import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import TimeAgoFormatter from "../../atoms/time-ago-formatter/time-ago-formatter.atom";
import CloseDateDisplayMolecule from "../close-date-display/close-date-display.molecule";
import JobGroupMolecule from "../job-group/job-group.molecule";
import DeleteJobAtom from "../../atoms/delete-job/delete-job.atom";
export default function JobsDetailDescriptionMolecule({ loading, job }) {
if (loading) return <Skeleton active />;
@@ -13,7 +14,12 @@ export default function JobsDetailDescriptionMolecule({ loading, job }) {
return (
<div>
<PageHeader ghost={false} title={job.clm_no} subTitle={job.ins_co_nm}>
<PageHeader
ghost={false}
title={job.clm_no}
subTitle={job.ins_co_nm}
extra={[<DeleteJobAtom key="delete" jobId={job.id} />]}
>
<Descriptions column={{ xxl: 5, xl: 4, lg: 3, md: 3, sm: 2, xs: 1 }}>
<Descriptions.Item label="Owner">{`${job.ownr_fn} ${job.ownr_ln}`}</Descriptions.Item>
<Descriptions.Item label="Vehicle">{`${job.v_model_yr} ${job.v_makedesc} ${job.v_model} (${job.v_type})`}</Descriptions.Item>

View File

@@ -1,3 +1,4 @@
import { CalculatorOutlined } from "@ant-design/icons";
import { Input, Table } from "antd";
import React, { useState } from "react";
import ipcTypes from "../../../ipc.types";
@@ -6,11 +7,11 @@ import CurrencyFormatterAtom from "../../atoms/currency-formatter/currency-forma
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";
const { ipcRenderer } = window;
export default function JobLinesTableMolecule({ loading, job }) {
const [searchText, setSearchText] = useState("");
const [filters, setFilters] = useState({ ignore: ["false"] });
const { joblines } = job;
const columns = [
@@ -19,23 +20,27 @@ export default function JobLinesTableMolecule({ loading, job }) {
dataIndex: "line_no",
key: "line_no",
sorter: (a, b) => a.line_no - b.line_no,
width: "5%",
},
{
title: "S#",
dataIndex: "line_ind",
key: "line_ind",
width: "5%",
sorter: (a, b) => alphaSort(a.line_ind, b.line_ind),
},
{
title: "Line Description",
dataIndex: "line_desc",
key: "line_desc",
width: "25%",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
},
{
title: "Part Type",
dataIndex: "part_type",
key: "part_type",
width: "5%",
sorter: (a, b) => alphaSort(a.part_type, b.part_type),
render: (text, record) => partTypeConverterAtom(text),
},
@@ -43,12 +48,14 @@ export default function JobLinesTableMolecule({ loading, job }) {
title: "Part Number",
dataIndex: "oem_partno",
key: "oem_partno",
width: "15%",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
},
{
title: "Database Price",
dataIndex: "db_price",
key: "db_price",
width: "10%",
sorter: (a, b) => a.db_price - b.db_price,
render: (text, record) => (
<CurrencyFormatterAtom>{record.db_price}</CurrencyFormatterAtom>
@@ -58,6 +65,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
title: "Actual Price",
dataIndex: "act_price",
key: "act_price",
width: "10%",
sorter: (a, b) => a.act_price - b.act_price,
render: (text, record) => (
<CurrencyFormatterAtom>{record.act_price}</CurrencyFormatterAtom>
@@ -67,6 +75,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
title: "Price Diff.",
dataIndex: "price_diff",
key: "price_diff",
width: "10%",
sorter: (a, b) => a.price_diff - b.price_diff,
render: (text, record) => (
<CurrencyFormatterAtom>{record.price_diff}</CurrencyFormatterAtom>
@@ -76,6 +85,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
title: "Price Diff. %",
dataIndex: "price_diff_pc",
key: "price_diff_pc",
width: "10%",
sorter: (a, b) => a.price_diff_pc - b.price_diff_pc,
render: (text, record) => (
<PriceDiffPcFormatterAtom
@@ -86,13 +96,15 @@ export default function JobLinesTableMolecule({ loading, job }) {
),
},
{
title: "Ignore?",
title: <CalculatorOutlined />,
dataIndex: "ignore",
key: "ignore",
filters: [
{ text: "True", value: true },
{ text: "False", value: false },
{ text: "Eligible for RPS Calculation", value: false },
{ text: "Ineligible RPS Calculation", value: true },
],
width: "5%",
filteredValue: filters.ignore || null,
onFilter: (value, record) => value === record.ignore,
render: (text, record) => (
<IgnoreJobLine
@@ -111,33 +123,50 @@ export default function JobLinesTableMolecule({ loading, job }) {
)
: joblines;
const handleChange = (pagination, filters, sorter) => {
console.log("Various parameters", pagination, filters, sorter);
setFilters(filters);
};
return (
<div>
<Input.Search
placeholder="Search"
onSearch={(val) => {
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "JOB_LINES_SEARCH",
query: val,
});
setSearchText(val);
}}
enterButton
allowClear
/>
<Table
title={() => (
<Input.Search
placeholder="Search"
onSearch={(val) => {
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "JOB_LINES_SEARCH",
query: val,
});
setSearchText(val);
}}
enterButton
allowClear
/>
)}
columns={columns}
rowKey="id"
loading={loading}
size="small"
pagination={false}
dataSource={data}
onChange={handleChange}
scroll={{
x: true,
y: "20rem",
}}
// summary={
// () => (
// <Table.Summary.Row>
// <Table.Summary.Cell index={0}>Summary</Table.Summary.Cell>
// <Table.Summary.Cell index={5}>
// This is a summary content
// </Table.Summary.Cell>
// <Table.Summary.Cell index={6}>
// This is a summary content
// </Table.Summary.Cell>
// </Table.Summary.Row>
// )
// }
/>
</div>
);

View File

@@ -1,22 +1,21 @@
.jobs-list-item {
padding: 0;
margin: 0;
.jobs-list-item-content {
&-selected {
border-left: 3px solid #1890ff;
}
display: inline;
margin: 0.5rem;
padding: 0.5rem;
width: 100%;
}
cursor: pointer;
&:hover {
background-color: #e6f7ff;
padding: 0.1rem !important;
margin: 0;
border-bottom: 0.8rem solid #f0f0f0 !important;
.jobs-list-item-content {
&-selected {
border-left: 3px solid #1890ff;
}
display: inline;
margin: 0.5rem;
padding: 0.5rem;
width: 100%;
}
cursor: pointer;
&:hover {
background-color: #e6f7ff;
}
}

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";
@@ -25,14 +25,16 @@ export function JobsTargetsStatsMolecule({
job,
selectedJobTargetPc,
}) {
// eslint-disable-next-line react-hooks/exhaustive-deps
const { actPriceSum, jobRpsDollars } = useCallback(
CalculateJobRpsDollars(job, true),
[job]
[job, CalculateJobRpsDollars]
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const { dbPriceSum, jobRpsPc } = useCallback(
CalculateJobRpsPc(job, jobRpsDollars, true),
[job, jobRpsDollars]
[job, jobRpsDollars, CalculateJobRpsPc]
);
if (loading) return <Skeleton active />;
@@ -43,30 +45,52 @@ export function JobsTargetsStatsMolecule({
display: "flex",
alignItems: "center",
justifyContent: "space-around",
marginTop: "1rem",
marginTop: "2rem",
marginBottom: "1rem",
}}
>
<Statistic
title="Target RPS %"
value={(selectedJobTargetPc * 100).toFixed(1)}
suffix="%"
/>
<Statistic
title="Current RPS %"
valueStyle={{
color: selectedJobTargetPc > (jobRpsPc || 0) ? "tomato" : "seagreen",
}}
value={((jobRpsPc || 0) * 100).toFixed(1)}
suffix="%"
/>
<Statistic
title="Target RPS $"
value={actPriceSum.percentage(selectedJobTargetPc * 100).toFormat()}
/>
<Statistic title="Current RPS $" value={jobRpsDollars.toFormat()} />
<Statistic title="DB Price Total" value={dbPriceSum.toFormat()} />
<Statistic title="Actual Price Total" value={actPriceSum.toFormat()} />
<div style={{ display: "flex" }}>
<Statistic
title="Target RPS %"
value={(selectedJobTargetPc * 100).toFixed(1)}
suffix="%"
style={{ margin: "0rem .5rem" }}
/>
<Statistic
title="Current RPS %"
style={{ margin: "0rem .5rem" }}
valueStyle={{
color:
selectedJobTargetPc > (jobRpsPc || 0) ? "tomato" : "seagreen",
}}
value={((jobRpsPc || 0) * 100).toFixed(1)}
suffix="%"
/>
</div>
<div style={{ display: "flex" }}>
<Statistic
title="Target RPS $"
style={{ margin: "0rem .5rem" }}
value={actPriceSum.percentage(selectedJobTargetPc * 100).toFormat()}
/>
<Statistic
title="Current RPS $"
style={{ margin: "0rem .5rem" }}
value={jobRpsDollars.toFormat()}
/>
</div>
<div style={{ display: "flex" }}>
<Statistic
title="DB Price Total"
style={{ margin: "0rem .5rem" }}
value={dbPriceSum.toFormat()}
/>
<Statistic
title="Actual Price Total"
style={{ margin: "0rem .5rem" }}
value={actPriceSum.toFormat()}
/>
</div>
</div>
);
}

View File

@@ -3,11 +3,16 @@ import React, { useState } from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import ipcTypes from "../../../ipc.types";
import { setSelectedJobId } from "../../../redux/application/application.actions";
import {
selectReportData,
selectReportLoading,
} from "../../../redux/reporting/reporting.selectors";
const { ipcRenderer } = window;
const mapStateToProps = createStructuredSelector({
reportingLoading: selectReportLoading,
reportData: selectReportData,
@@ -123,6 +128,10 @@ export function ReportingJobsListMolecule({
<Input.Search
placeholder="Search"
onSearch={(val) => {
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "REPORTS_LIST_SEARCH",
query: val,
});
setSearchText(val);
}}
enterButton

View File

@@ -1,4 +1,4 @@
import { Button, Input, Table } from "antd";
import { Button, Input, message, Table } from "antd";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -8,6 +8,7 @@ import {
selectScanLoading,
} from "../../../redux/scan/scan.selectors";
import { alphaSort } from "../../../util/sorters";
import DeleteAllEmsAtom from "../../atoms/delete-all-ems/delete-all-ems.atom";
import LastScannedAtom from "../../atoms/last-scanned/last-scanned.atom";
import ScanRefreshAtom from "../../atoms/scan-refresh/scan-refresh.atom";
@@ -64,12 +65,13 @@ export function ScanEstimateListMolecule({ scanLoading, estimates }) {
key: "import",
render: (text, record) => (
<Button
onClick={() =>
onClick={() => {
message.info("Attempting to import job...");
ipcRenderer.send(
ipcTypes.default.fileScan.toMain.importJob,
record.filepath
)
}
);
}}
>
Import
</Button>
@@ -96,6 +98,7 @@ export function ScanEstimateListMolecule({ scanLoading, estimates }) {
<div className="imex-table-header">
<ScanRefreshAtom />
<LastScannedAtom />
<DeleteAllEmsAtom />
<Input.Search
className="imex-table-header__search"
placeholder="Search"

View File

@@ -1,12 +1,11 @@
import { useQuery } from "@apollo/client";
import { Result } from "antd";
import { Card, Result } from "antd";
import React, { useEffect } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_JOB_BY_PK } from "../../../graphql/jobs.queries";
import { setSelectedJobTargetPc } from "../../../redux/application/application.actions";
import { selectSelectedJobId } from "../../../redux/application/application.selectors";
import DeleteJobAtom from "../../atoms/delete-job/delete-job.atom";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import JobsPartsGraphAtom from "../../atoms/jobs-parts-graph/jobs-parts-graph.atom";
import JobsDetailDescriptionMolecule from "../../molecules/jobs-detail-description/jobs-detail-description.molecule";
@@ -37,7 +36,19 @@ export function JobsDetailOrganism({ selectedJobId, setSelectedJobTargetPc }) {
});
}, [data, setSelectedJobTargetPc]);
if (!selectedJobId) return <Result title="No job selected." />;
if (!selectedJobId)
return (
<div
style={{
display: "flex",
height: "100%",
justifyContent: "center",
alignItems: "center",
}}
>
<Result title="No job selected." />
</div>
);
if (error)
return (
<ErrorResultAtom
@@ -48,37 +59,42 @@ export function JobsDetailOrganism({ selectedJobId, setSelectedJobTargetPc }) {
return (
<div className="jobs-detail-container">
<JobsDetailDescriptionMolecule
loading={loading}
job={data ? data.jobs_by_pk : null}
/>
<JobsLinesTableMolecule
loading={loading}
job={data ? data.jobs_by_pk : {}}
/>
<JobsTargetsStatsMolecule
loading={loading}
job={data ? data.jobs_by_pk : null}
/>
<div
style={{
display: "flex",
justifyContent: "space-evenly",
minHeight: "20rem",
width: "100%",
}}
>
<JobsPartsGraphAtom
job={data ? data.jobs_by_pk : null}
<Card>
<JobsDetailDescriptionMolecule
loading={loading}
price="db_price"
/>
<JobsPartsGraphAtom
job={data ? data.jobs_by_pk : null}
loading={loading}
/>
</div>
<DeleteJobAtom jobId={data ? data.jobs_by_pk.id : null} />
</Card>
<Card>
<JobsLinesTableMolecule
loading={loading}
job={data ? data.jobs_by_pk : {}}
/>
</Card>
<Card>
<JobsTargetsStatsMolecule
loading={loading}
job={data ? data.jobs_by_pk : null}
/>
<div
style={{
display: "flex",
justifyContent: "space-evenly",
minHeight: "20rem",
width: "100%",
}}
>
<JobsPartsGraphAtom
job={data ? data.jobs_by_pk : null}
loading={loading}
price="db_price"
/>
<JobsPartsGraphAtom
job={data ? data.jobs_by_pk : null}
loading={loading}
/>
</div>
</Card>
</div>
);
}

View File

@@ -1,4 +1,8 @@
.jobs-detail-container {
height: 100%;
overflow-y: auto;
background-color: rgb(244, 244, 244);
& > * {
margin: 0.7rem;
}
}

View File

@@ -4,10 +4,13 @@ import { Dropdown, List, Menu, Spin } from "antd";
import React, { useState } from "react";
import InfiniteScroll from "react-infinite-scroller";
import { SEARCH_JOBS_PAGINATED } from "../../../graphql/jobs.queries";
import ipcTypes from "../../../ipc.types";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import JobsListItemMolecule from "../../molecules/jobs-list-item/jobs-list-item.molecule";
import JobsSearchFieldsMolecule from "../../molecules/jobs-search-fields/jobs-search-fields.molecule";
const { ipcRenderer } = window;
const limit = 20;
export default function JobsTableOrganism() {
const [state, setState] = useState({ hasMore: true });
@@ -32,7 +35,10 @@ export default function JobsTableOrganism() {
);
const handleInfiniteOnLoad = (page) => {
if (fetchMore)
if (fetchMore) {
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "FETCH_MORE_JOBS",
});
fetchMore({
variables: {
offset: limit * page,
@@ -59,6 +65,7 @@ export default function JobsTableOrganism() {
return newCache;
},
});
}
};
if (error)

View File

@@ -5,7 +5,6 @@ import JobsListOrganism from "../../organisms/jobs-list-latest/jobs-list-latest.
import JobsListSearchOrganism from "../../organisms/jobs-list-search/jobs-list-search.organism";
export default function JobsPage() {
console.log("Jobs Page Rerender");
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];

View File

@@ -22,7 +22,7 @@ export function RoutesPage({ bodyshop }) {
errorMessage="You do not currently have access to any shop. Please reach out to technical support."
/>
);
console.log("routes render");
return (
<Layout style={{ background: "#fff", height: "100vh" }} hasSider>
<Layout.Sider
@@ -33,7 +33,7 @@ export function RoutesPage({ bodyshop }) {
<SiderMenuOrganism />
</Layout.Sider>
<Layout style={{ background: "#fff" }}>
<Layout.Content style={{ margin: "1rem", height: "100%" }}>
<Layout.Content style={{ marginLeft: "1rem", height: "100%" }}>
<Route exact path="/settings" component={SettingsPage} />
<Route exact path="/reporting" component={ReportingPage} />
<Route exact path="/scan" component={ScanPage} />

View File

@@ -2,6 +2,7 @@ import { Col, Row } from "antd";
import React, { useEffect } from "react";
import ipcTypes from "../../../ipc.types";
import NotificationsToggleAtom from "../../atoms/notifications-toggle/notifications-toggle.atom";
import WatcherStartupAtom from "../../atoms/watcher-startup/watcher-startup.atom";
import WatcherPollingMolecule from "../../molecules/watcher-polling/watcher-polling.molecule";
import FilePathsListOrganism from "../../organisms/filepaths-list/filepaths-list.organism";
import ShopSettingsOrganism from "../../organisms/shop-settings/shop-settings.organism";
@@ -23,6 +24,7 @@ export default function SettingsPage() {
<WatcherManagerOrganism />
<WatcherPollingMolecule />
<NotificationsToggleAtom />
<WatcherStartupAtom />
</Col>
</Row>

View File

@@ -1,48 +0,0 @@
import { Button, Layout } from "antd";
import React, { useEffect } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import ipcTypes from "../ipc.types";
const { ipcRenderer } = window.require("electron");
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({});
export function App() {
useEffect(() => {
ipcRenderer.on("not a real channel", (event, obj) => {
console.log("not a real channel", obj);
});
// Cleanup the listener events so that memory leaks are avoided.
return function cleanup() {
ipcRenderer.removeAllListeners("not a real channel");
};
}, []);
return (
<Layout>
<Layout.Header>
<div> Header</div>
</Layout.Header>
<Layout.Content>
<div>Welcome to your new react app. asdas sd</div>
<Button
onClick={() => {
ipcRenderer.send("test", { test: true });
}}
>
TEST Generic IPC
</Button>
<Button
onClick={() => {
ipcRenderer.send(ipcTypes.default.filewatcher.start);
}}
>
Start Watcher
</Button>
</Layout.Content>
</Layout>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(App);

View File

@@ -9,7 +9,7 @@ import App from "./App/App";
import "./index.css";
import { persistor, store } from "./redux/store";
require("dotenv").config();
LogRocket.init("imex/rps");
if (process.env.NODE_ENV === "production") LogRocket.init("imex/rps");
ReactDOM.render(
<Provider store={store}>
@@ -21,3 +21,4 @@ ReactDOM.render(
</Provider>,
document.getElementById("root")
);
console.log("Connecting to endpoint: ", process.env.REACT_APP_GRAPHQL_ENDPOINT);

View File

@@ -21,6 +21,7 @@ exports.default = {
toMain: {
scanFilePaths: "fileScan__scanFilePaths",
importJob: "fileScan__importJob",
deleteAllEms: "filescan_deleteAllEms",
},
toRenderer: {
scanFilePathsResponse: "fileScan__scanFilePathsResponse",

View File

@@ -1,3 +1,4 @@
import { message } from "antd";
import gql from "graphql-tag";
import _ from "lodash";
import moment from "moment";
@@ -5,7 +6,7 @@ import client from "../graphql/GraphQLClient";
import {
INSERT_NEW_JOB,
QUERY_JOB_BY_CLM_NO,
UPDATE_JOB
UPDATE_JOB,
} from "../graphql/jobs.queries";
import { QUERY_GROUPS_BY_MAKE_TYPE } from "../graphql/veh_group.queries";
import { store } from "../redux/store";
@@ -56,7 +57,7 @@ export async function UpsertEstimate(job) {
} else {
logger.info("Attemping to insert job record.");
const result = await client.mutate({
await client.mutate({
mutation: INSERT_NEW_JOB,
variables: {
job: { ...job, bodyshopid: shopId },
@@ -64,9 +65,8 @@ export async function UpsertEstimate(job) {
refetchQueries: ["QUERY_ALL_JOBS_PAGINATED"],
});
logger.info("Job inserted succesfully.");
console.log("UpsertEstimate -> result", result);
}
message.success("Job uploaded successfully!");
}
export const GetSupplementDelta = async (jobId, existingLinesO, newLines) => {

View File

@@ -38,7 +38,13 @@ export const setSelectedJobTargetPcSuccess = (pct) => ({
type: ApplicationActionTypes.SET_SELECTED_JOB_TARGET_PC_SUCCESS,
payload: pct,
});
export const setSettings = (settingsObj) => ({
type: ApplicationActionTypes.SET_SETTINGS,
payload: settingsObj,
});
export const setUpdateAvailable = (available) => ({
type: ApplicationActionTypes.SET_UPDATE_AVAILABLE,
payload: available,
});

View File

@@ -1,3 +1,4 @@
import ipcTypes from "../../ipc.types";
import ApplicationActionTypes from "./application.types";
const INITIAL_STATE = {
watcherStatus: "Not Started",
@@ -6,8 +7,11 @@ const INITIAL_STATE = {
selectedJobId: null,
selectedJobTargetPc: 0,
settings: {},
updateAvailable: false,
};
const { ipcRenderer } = window;
const applicationReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case ApplicationActionTypes.SET_WATCHED_PATHS:
@@ -16,11 +20,17 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
watchedPaths: action.payload,
};
case ApplicationActionTypes.ADD_WATCHED_PATH:
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "ADD_WATCHED_PATH",
});
return {
...state,
watchedPaths: [...state.watchedPaths, action.payload],
};
case ApplicationActionTypes.REMOVE_WATCHED_PATH:
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "REMOVE_WATCHED_PATH",
});
return {
...state,
watchedPaths: state.watchedPaths.filter((p) => p !== action.payload),
@@ -31,6 +41,10 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
watcherStatus: action.payload,
};
case ApplicationActionTypes.SET_WATCHER_ERROR:
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "WATCHER_ERROR",
error: action.payload,
});
return {
...state,
watcherError: action.payload,
@@ -41,10 +55,14 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
selectedJobTargetPc: action.payload,
};
case ApplicationActionTypes.SET_SELECTED_JOB_ID:
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "SET_SELECTED_JOB",
});
return { ...state, selectedJobId: action.payload };
case ApplicationActionTypes.SET_SETTINGS:
return { ...state, settings: { ...state.settings, ...action.payload } };
case ApplicationActionTypes.SET_UPDATE_AVAILABLE:
return { ...state, updateAvailable: action.payload };
default:
return state;
}

View File

@@ -31,3 +31,8 @@ export const selectSettings = createSelector(
[selectApplication],
(application) => application.settings
);
export const selectUpdateAvailable = createSelector(
[selectApplication],
(application) => application.updateAvailable
);

View File

@@ -8,5 +8,6 @@ const ApplicationActionTypes = {
SET_SELECTED_JOB_TARGET_PC: "SET_SELECTED_JOB_TARGET_PC",
SET_SELECTED_JOB_TARGET_PC_SUCCESS: "SET_SELECTED_JOB_TARGET_PC_SUCCESS",
SET_SETTINGS: "SET_SETTINGS",
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE",
};
export default ApplicationActionTypes;

View File

@@ -30,5 +30,5 @@ sagaMiddleWare.run(rootSaga);
export const persistor = persistStore(store);
export default { store, persistStore };
const epxortObj = { store, persistStore };
export default epxortObj;

View File

@@ -92,6 +92,9 @@ export function* onSignOutStart() {
export function* signOutStart() {
try {
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "SIGN_OUT",
});
ipcRenderer.send(ipcTypes.default.fileWatcher.toMain.stop);
yield auth.signOut();
yield put(signOutSuccess());
@@ -137,7 +140,9 @@ export function* signInSuccessSaga({ payload }) {
ipcTypes.default.app.toMain.setAcceptableInsCoNm,
shop.data.bodyshops[0].accepted_ins_co
);
ipcRenderer.send(ipcTypes.default.fileWatcher.toMain.start);
ipcRenderer.send(ipcTypes.default.fileWatcher.toMain.start, {
startup: true,
});
} else {
console.log("No bodyshop has been associated.");
yield put(setBodyshop(false));
@@ -155,6 +160,10 @@ export function* onSendPasswordResetStart() {
}
export function* sendPasswordResetEmail({ payload }) {
try {
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "RESET_PASSWORD",
email: payload,
});
yield auth.sendPasswordResetEmail(payload);
yield put(sendPasswordResetSuccess());