@@ -3,8 +3,37 @@ const path = require("path");
|
||||
const _ = require("lodash");
|
||||
const log = require("electron-log");
|
||||
const { store } = require("../electron-store");
|
||||
const { BrowserWindow } = require("electron");
|
||||
const ipcTypes = require("../../src/ipc.types");
|
||||
const {
|
||||
NewNotification,
|
||||
} = require("../notification-wrapper/notification-wrapper");
|
||||
|
||||
async function DecodeEstimate(filePath) {
|
||||
async function ImportJob(path) {
|
||||
const b = BrowserWindow.getAllWindows()[0];
|
||||
b.webContents.send(ipcTypes.default.estimate.toRenderer.estimateDecodeStart);
|
||||
const newJob = await DecodeEstimate(path);
|
||||
|
||||
if (newJob && !newJob.ERROR) {
|
||||
b.webContents.send(
|
||||
ipcTypes.default.estimate.toRenderer.estimateDecodeSuccess,
|
||||
newJob
|
||||
);
|
||||
log.info(`Sent job for upload. ${newJob.clm_no}`);
|
||||
NewNotification({
|
||||
title: "Job Uploaded",
|
||||
body: "A new job has been uploaded.",
|
||||
});
|
||||
} else {
|
||||
log.info(`Ignored job. ${newJob.ERROR}`);
|
||||
NewNotification({
|
||||
title: "Job Ignored",
|
||||
body: newJob.ERROR,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function DecodeEstimate(filePath, includeFilePathInReturnJob = false) {
|
||||
const parsedFilePath = path.parse(filePath);
|
||||
let extensionlessFilePath = path.join(
|
||||
parsedFilePath.dir,
|
||||
@@ -15,6 +44,7 @@ async function DecodeEstimate(filePath) {
|
||||
...(await DecodeVehFile(extensionlessFilePath)),
|
||||
...(await DecodeTtlFile(extensionlessFilePath)),
|
||||
...(await DecodeLinFile(extensionlessFilePath)),
|
||||
...(includeFilePathInReturnJob ? { filePath } : {}),
|
||||
};
|
||||
|
||||
const ad2 = await DecodeAd2File(extensionlessFilePath);
|
||||
@@ -217,56 +247,6 @@ async function DecodeLinFile(extensionlessFilePath) {
|
||||
let records = await dbf.readRecords();
|
||||
let joblines = records
|
||||
.map((record) => {
|
||||
console.log(
|
||||
"object",
|
||||
_.pick(record, [
|
||||
"LINE_NO",
|
||||
"LINE_IND",
|
||||
// "LINE_REF",
|
||||
// "TRAN_CODE",
|
||||
"DB_REF",
|
||||
"UNQ_SEQ",
|
||||
// "WHO_PAYS",
|
||||
"LINE_DESC",
|
||||
"PART_TYPE",
|
||||
// "PART_DESCJ",
|
||||
"GLASS_FLAG",
|
||||
"OEM_PARTNO",
|
||||
// "PRICE_INC",
|
||||
// "ALT_PART_I",
|
||||
// "TAX_PART",
|
||||
"DB_PRICE",
|
||||
"ACT_PRICE",
|
||||
// "PRICE_J",
|
||||
// "CERT_PART",
|
||||
"PART_QTY",
|
||||
// "ALT_CO_ID",
|
||||
// "ALT_PARTNO",
|
||||
// "ALT_OVERRD",
|
||||
// "ALT_PARTM",
|
||||
// "PRT_DSMK_P",
|
||||
// "PRT_DSMK_M",
|
||||
// "MOD_LBR_TY",
|
||||
// "DB_HRS",
|
||||
// "MOD_LB_HRS",
|
||||
// "LBR_INC",
|
||||
// "LBR_OP",
|
||||
// "LBR_HRS_J",
|
||||
// "LBR_TYP_J",
|
||||
// "LBR_OP_J",
|
||||
// "PAINT_STG",
|
||||
// "PAINT_TONE",
|
||||
// "LBR_TAX",
|
||||
// "LBR_AMT",
|
||||
// "MISC_AMT",
|
||||
// "MISC_SUBLT",
|
||||
// "MISC_TAX",
|
||||
// "BETT_TYPE",
|
||||
// "BETT_PCTG",
|
||||
// "BETT_AMT",
|
||||
// "BETT_TAX",
|
||||
])
|
||||
);
|
||||
return _.transform(
|
||||
_.pick(record, [
|
||||
"LINE_NO",
|
||||
@@ -318,7 +298,7 @@ async function DecodeLinFile(extensionlessFilePath) {
|
||||
]),
|
||||
function (result, val, key) {
|
||||
//Required because unq_seq gets pulled as a numeric instaed of a string.
|
||||
console.log("key", key);
|
||||
|
||||
if (key === "UNQ_SEQ") {
|
||||
return (result[key.toLowerCase()] = val.toString());
|
||||
}
|
||||
@@ -347,12 +327,12 @@ async function DecodeLinFile(extensionlessFilePath) {
|
||||
!!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
|
||||
);
|
||||
// log.info(
|
||||
// "DB Price null/lower than act price",
|
||||
// jobline.line_desc,
|
||||
// jobline.db_price,
|
||||
// jobline.act_price
|
||||
// );
|
||||
jobline.db_price = jobline.act_price;
|
||||
}
|
||||
|
||||
@@ -361,12 +341,12 @@ async function DecodeLinFile(extensionlessFilePath) {
|
||||
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
|
||||
);
|
||||
// log.info(
|
||||
// "Act price higher than existing db price",
|
||||
// jobline.line_desc,
|
||||
// jobline.db_price,
|
||||
// jobline.act_price
|
||||
// );
|
||||
jobline.db_price = jobline.act_price;
|
||||
}
|
||||
|
||||
@@ -394,3 +374,4 @@ async function DecodeLinFile(extensionlessFilePath) {
|
||||
}
|
||||
|
||||
exports.DecodeEstimate = DecodeEstimate;
|
||||
exports.ImportJob = ImportJob;
|
||||
|
||||
@@ -2,11 +2,12 @@ const Store = require("electron-store");
|
||||
|
||||
const store = new Store({
|
||||
defaults: {
|
||||
enableNotifications: true,
|
||||
filePaths: [],
|
||||
accepted_ins_co: [],
|
||||
polling: {
|
||||
enabled: false,
|
||||
pollingInterval: 100,
|
||||
pollingInterval: 1000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
22
electron/file-scan/file-scan-ipc.js
Normal file
22
electron/file-scan/file-scan-ipc.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const { ipcMain } = require("electron");
|
||||
const ipcTypes = require("../../src/ipc.types");
|
||||
const { ImportJob } = require("../decoder/decoder");
|
||||
const { GetListOfEstimates } = require("./file-scan");
|
||||
|
||||
ipcMain.on(
|
||||
ipcTypes.default.fileScan.toMain.scanFilePaths,
|
||||
async (event, object) => {
|
||||
const ret = await GetListOfEstimates();
|
||||
event.reply(
|
||||
ipcTypes.default.fileScan.toRenderer.scanFilePathsResponse,
|
||||
ret
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.on(
|
||||
ipcTypes.default.fileScan.toMain.importJob,
|
||||
async (event, filePath) => {
|
||||
await ImportJob(filePath);
|
||||
}
|
||||
);
|
||||
42
electron/file-scan/file-scan.js
Normal file
42
electron/file-scan/file-scan.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { store } = require("../electron-store");
|
||||
const log = require("electron-log");
|
||||
const fsPromises = fs.promises;
|
||||
const _ = require("lodash");
|
||||
const { DecodeEstimate } = require("../decoder/decoder");
|
||||
|
||||
async function GetListOfEstimates() {
|
||||
const ListOfEnvFiles = await GetEnvFiles();
|
||||
const ListOfSummarizedEstimates = await ReadAllEstimates(ListOfEnvFiles);
|
||||
const FilteredListOfSummarizedEstimates = ListOfSummarizedEstimates.filter(
|
||||
(j) => !j.ERROR
|
||||
);
|
||||
return FilteredListOfSummarizedEstimates;
|
||||
}
|
||||
|
||||
async function ReadAllEstimates(ListOfEnvFiles) {
|
||||
return await Promise.all(
|
||||
ListOfEnvFiles.map(async (e) => await DecodeEstimate(e, true))
|
||||
);
|
||||
}
|
||||
|
||||
async function GetEnvFiles() {
|
||||
const filePaths = store.get("filePaths");
|
||||
const allFilePaths = [];
|
||||
await Promise.all(
|
||||
filePaths.map(async (fp) => {
|
||||
const envFilesInDir = (await fsPromises.readdir(fp)).filter((p) =>
|
||||
p.toUpperCase().includes(".ENV")
|
||||
);
|
||||
|
||||
envFilesInDir.map((envFileName) => {
|
||||
allFilePaths.push(path.join(fp, envFileName));
|
||||
return null;
|
||||
});
|
||||
})
|
||||
);
|
||||
return allFilePaths;
|
||||
}
|
||||
|
||||
exports.GetListOfEstimates = GetListOfEstimates;
|
||||
@@ -1,7 +1,7 @@
|
||||
const chokidar = require("chokidar");
|
||||
const ipcTypes = require("../../src/ipc.types");
|
||||
const path = require("path");
|
||||
const { DecodeEstimate } = require("../decoder/decoder");
|
||||
const { ImportJob } = require("../decoder/decoder");
|
||||
const { BrowserWindow } = require("electron");
|
||||
const { store } = require("../electron-store");
|
||||
const {
|
||||
@@ -11,16 +11,13 @@ const log = require("electron-log");
|
||||
var watcher;
|
||||
|
||||
async function StartWatcher() {
|
||||
const filePaths =
|
||||
store.get("filePaths").map((fp) => path.join(fp, "**.[eE][nN][vV]")) || [];
|
||||
log.info("StartWatcher -> filePaths", filePaths);
|
||||
const filePaths = store.get("filePaths") || [];
|
||||
log.info("Use polling? ", store.get("polling").enabled);
|
||||
|
||||
if (filePaths.length === 0) {
|
||||
NewNotification({
|
||||
title: "RPS Watcher cannot start",
|
||||
body: "Please set the appropriate file paths and try again.",
|
||||
}).show();
|
||||
});
|
||||
log.warn("Cannot start watcher. No file paths set.");
|
||||
return [];
|
||||
}
|
||||
@@ -37,12 +34,16 @@ async function StartWatcher() {
|
||||
}
|
||||
|
||||
watcher = chokidar.watch(filePaths, {
|
||||
//ignored: /[\/\\]\./,
|
||||
usePolling: store.get("polling").enabled,
|
||||
ignored: (fp, stats) => {
|
||||
const p = path.parse(fp);
|
||||
return p.ext !== "" && p.ext.toUpperCase() !== ".ENV";
|
||||
},
|
||||
usePolling: store.get("polling").enabled || false,
|
||||
interval: store.get("polling").pollingInterval || 1000,
|
||||
persistent: true,
|
||||
ignoreInitial: true,
|
||||
awaitWriteFinish: {
|
||||
pollInterval: 100,
|
||||
pollInterval: 500,
|
||||
stabilityThreshold: 2000,
|
||||
},
|
||||
});
|
||||
@@ -86,7 +87,8 @@ function onWatcherReady() {
|
||||
NewNotification({
|
||||
title: "RPS Watcher Started",
|
||||
body: "Newly exported estimates will be automatically uploaded.",
|
||||
}).show();
|
||||
});
|
||||
console.log("Confirmed watched paths:", watcher.getWatched());
|
||||
}
|
||||
|
||||
async function StopWatcher() {
|
||||
@@ -97,7 +99,7 @@ async function StopWatcher() {
|
||||
NewNotification({
|
||||
title: "RPS Watcher Stopped",
|
||||
body: "Estimates will not be automatically uploaded.",
|
||||
}).show();
|
||||
});
|
||||
}
|
||||
|
||||
exports.StartWatcher = StartWatcher;
|
||||
@@ -105,25 +107,5 @@ exports.StopWatcher = StopWatcher;
|
||||
exports.watcher = watcher;
|
||||
|
||||
async function HandleNewFile(path) {
|
||||
const b = BrowserWindow.getAllWindows()[0];
|
||||
b.webContents.send(ipcTypes.default.estimate.toRenderer.estimateDecodeStart);
|
||||
const newJob = await DecodeEstimate(path);
|
||||
|
||||
if (newJob && !newJob.ERROR) {
|
||||
b.webContents.send(
|
||||
ipcTypes.default.estimate.toRenderer.estimateDecodeSuccess,
|
||||
newJob
|
||||
);
|
||||
log.info(`Sent job for upload. ${newJob.clm_no}`);
|
||||
NewNotification({
|
||||
title: "Job Uploaded",
|
||||
body: "A new job has been uploaded.",
|
||||
}).show();
|
||||
} else {
|
||||
log.info(`Ignored job. ${newJob.ERROR}`);
|
||||
NewNotification({
|
||||
title: "Job Ignored",
|
||||
body: newJob.ERROR,
|
||||
}).show();
|
||||
}
|
||||
await ImportJob(path);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
const { ipcMain } = require("electron");
|
||||
const { default: ipcTypes } = require("../src/ipc.types");
|
||||
const { store } = require("./electron-store");
|
||||
const { mainWindow } = require("./main");
|
||||
//Import Ipc Handlers
|
||||
require("./file-watcher/file-watcher-ipc");
|
||||
require("./file-scan/file-scan-ipc");
|
||||
|
||||
console.log("*** Added IPC Handlers ***");
|
||||
|
||||
ipcMain.on("test", async (event, object) => {
|
||||
console.log("Received test IPC Command");
|
||||
//const job = await DecodeEstimate("C:\\VPS\\EMS\\687_3_A.AD1");
|
||||
console.log(mainWindow);
|
||||
event.reply("test-toRenderer", { status: 0, message: null });
|
||||
});
|
||||
|
||||
|
||||
@@ -72,11 +72,17 @@ var menu = Menu.buildFromTemplate([
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Open Log File",
|
||||
label: "Open Config File",
|
||||
click() {
|
||||
shell.openPath(store.path);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Open Log File",
|
||||
click() {
|
||||
shell.openPath(path.join(app.getPath("appData"), "ImeX RPS\\logs"));
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
const { Notification } = require("electron");
|
||||
const path = require("path");
|
||||
const { store } = require("../electron-store");
|
||||
|
||||
function NewNotification(config) {
|
||||
return Notification({
|
||||
|
||||
icon: path.join(__dirname, "../../src/assets/logo512.png"),
|
||||
...config,
|
||||
});
|
||||
const enableNotifications = store.get("enableNotifications");
|
||||
if (enableNotifications) {
|
||||
Notification({
|
||||
icon: path.join(__dirname, "../../src/assets/logo512.png"),
|
||||
...config,
|
||||
}).show();
|
||||
}
|
||||
}
|
||||
exports.NewNotification = NewNotification;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
- args:
|
||||
cascade: false
|
||||
read_only: false
|
||||
sql: ALTER TABLE "public"."bodyshops" DROP COLUMN "groups";
|
||||
type: run_sql
|
||||
@@ -0,0 +1,6 @@
|
||||
- args:
|
||||
cascade: false
|
||||
read_only: false
|
||||
sql: ALTER TABLE "public"."bodyshops" ADD COLUMN "groups" jsonb NOT NULL DEFAULT
|
||||
jsonb_build_array();
|
||||
type: run_sql
|
||||
@@ -0,0 +1,27 @@
|
||||
- args:
|
||||
role: user
|
||||
table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
type: drop_select_permission
|
||||
- args:
|
||||
permission:
|
||||
allow_aggregations: false
|
||||
columns:
|
||||
- accepted_ins_co
|
||||
- created_at
|
||||
- id
|
||||
- shopname
|
||||
- targets
|
||||
- updated_at
|
||||
computed_fields: []
|
||||
filter:
|
||||
associations:
|
||||
user:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
role: user
|
||||
table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
type: create_select_permission
|
||||
@@ -0,0 +1,28 @@
|
||||
- args:
|
||||
role: user
|
||||
table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
type: drop_select_permission
|
||||
- args:
|
||||
permission:
|
||||
allow_aggregations: false
|
||||
columns:
|
||||
- accepted_ins_co
|
||||
- created_at
|
||||
- groups
|
||||
- id
|
||||
- shopname
|
||||
- targets
|
||||
- updated_at
|
||||
computed_fields: []
|
||||
filter:
|
||||
associations:
|
||||
user:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
role: user
|
||||
table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
type: create_select_permission
|
||||
@@ -0,0 +1,23 @@
|
||||
- args:
|
||||
role: user
|
||||
table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
type: drop_update_permission
|
||||
- args:
|
||||
permission:
|
||||
columns:
|
||||
- accepted_ins_co
|
||||
- shopname
|
||||
- targets
|
||||
filter:
|
||||
associations:
|
||||
user:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
set: {}
|
||||
role: user
|
||||
table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
type: create_update_permission
|
||||
@@ -0,0 +1,24 @@
|
||||
- args:
|
||||
role: user
|
||||
table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
type: drop_update_permission
|
||||
- args:
|
||||
permission:
|
||||
columns:
|
||||
- accepted_ins_co
|
||||
- groups
|
||||
- shopname
|
||||
- targets
|
||||
filter:
|
||||
associations:
|
||||
user:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
set: {}
|
||||
role: user
|
||||
table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
type: create_update_permission
|
||||
@@ -0,0 +1,6 @@
|
||||
- args:
|
||||
role: user
|
||||
table:
|
||||
name: jobs
|
||||
schema: public
|
||||
type: drop_delete_permission
|
||||
@@ -0,0 +1,14 @@
|
||||
- args:
|
||||
permission:
|
||||
backend_only: false
|
||||
filter:
|
||||
bodyshop:
|
||||
associations:
|
||||
user:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
role: user
|
||||
table:
|
||||
name: jobs
|
||||
schema: public
|
||||
type: create_delete_permission
|
||||
@@ -44,6 +44,7 @@ tables:
|
||||
columns:
|
||||
- accepted_ins_co
|
||||
- created_at
|
||||
- groups
|
||||
- id
|
||||
- shopname
|
||||
- targets
|
||||
@@ -58,6 +59,7 @@ tables:
|
||||
permission:
|
||||
columns:
|
||||
- accepted_ins_co
|
||||
- groups
|
||||
- shopname
|
||||
- targets
|
||||
filter:
|
||||
@@ -277,6 +279,15 @@ tables:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
check: null
|
||||
delete_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
filter:
|
||||
bodyshop:
|
||||
associations:
|
||||
user:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
- table:
|
||||
schema: public
|
||||
name: users
|
||||
|
||||
3175
package-lock.json
generated
3175
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
"productName": "ImEX RPS",
|
||||
"author": "ImEX Systems Inc. <support@thinkimex.com>",
|
||||
"description": "ImEX RPS",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"main": "electron/main.js",
|
||||
"homepage": "./",
|
||||
"dependencies": {
|
||||
@@ -11,7 +11,7 @@
|
||||
"@fingerprintjs/fingerprintjs": "^2.1.4",
|
||||
"antd": "^4.7.2",
|
||||
"apollo-link-logger": "^2.0.0",
|
||||
"chokidar": "^3.4.3",
|
||||
"chokidar": "^3.4.2",
|
||||
"dbffile": "^1.4.3",
|
||||
"dinero.js": "^1.8.1",
|
||||
"dotenv": "^8.2.0",
|
||||
|
||||
@@ -3,6 +3,20 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.imex-table-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
&__search {
|
||||
flex: 1;
|
||||
}
|
||||
& > * {
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.imex-flex-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
@@ -60,8 +74,6 @@ body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Required for the tab with infinite loading
|
||||
.ant-tabs-content {
|
||||
height: 100%;
|
||||
|
||||
23
src/components/atoms/last-scanned/last-scanned.atom.jsx
Normal file
23
src/components/atoms/last-scanned/last-scanned.atom.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Typography } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectScanLastScanned } from "../../../redux/scan/scan.selectors";
|
||||
import TimeAgoFormatter from "../../atoms/time-ago-formatter/time-ago-formatter.atom";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
lastScanned: selectScanLastScanned,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export function LastScannedAtom({ lastScanned }) {
|
||||
return (
|
||||
lastScanned && (
|
||||
<Typography.Title level={5}>
|
||||
<span>Last scanned </span>
|
||||
<TimeAgoFormatter>{lastScanned}</TimeAgoFormatter>
|
||||
</Typography.Title>
|
||||
)
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LastScannedAtom);
|
||||
@@ -0,0 +1,39 @@
|
||||
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 WatcherPollingMolecule({ appSettings }) {
|
||||
const handleChange = (val) => {
|
||||
ipcRenderer.send(ipcTypes.default.store.set, {
|
||||
enableNotifications: val,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DataLabel label="Notifications Enabled?">
|
||||
<Switch
|
||||
onChange={handleChange}
|
||||
checked={appSettings && appSettings.enableNotifications}
|
||||
/>
|
||||
</DataLabel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(WatcherPollingMolecule);
|
||||
@@ -1,15 +1,20 @@
|
||||
export default (part_type) => {
|
||||
switch (part_type) {
|
||||
case "PAA":
|
||||
case "PAL":
|
||||
case "PAC":
|
||||
return "A/M";
|
||||
case "PAE":
|
||||
return "Exist.";
|
||||
return "Ex.";
|
||||
case "PAN":
|
||||
case "PAP":
|
||||
return "OEM";
|
||||
case "PAP":
|
||||
return "OEMP";
|
||||
case "PAL":
|
||||
return "LKQ";
|
||||
|
||||
case "PAC":
|
||||
return "A/M (PAC)";
|
||||
case "PAR":
|
||||
return "A/M (PAR)";
|
||||
default:
|
||||
return part_type;
|
||||
}
|
||||
|
||||
22
src/components/atoms/scan-refresh/scan-refresh.atom.jsx
Normal file
22
src/components/atoms/scan-refresh/scan-refresh.atom.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Button } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { scanStart } from "../../../redux/scan/scan.actions";
|
||||
import { selectScanLoading } from "../../../redux/scan/scan.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
scanLoading: selectScanLoading,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
scanStart: () => dispatch(scanStart()),
|
||||
});
|
||||
|
||||
export function ScanRefreshAtom({ scanLoading, scanStart }) {
|
||||
return (
|
||||
<Button onClick={() => scanStart()} loading={scanLoading}>
|
||||
Refresh
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScanRefreshAtom);
|
||||
@@ -1,12 +1,19 @@
|
||||
import { Tooltip } from "antd";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
export default function TimeAgoFormatter(props) {
|
||||
const [timestampString, setTimestampString] = useState("");
|
||||
const m = moment(props.children);
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setTimestampString(m.fromNow()), 15000);
|
||||
setTimestampString(m.fromNow());
|
||||
return () => clearInterval(timer);
|
||||
}, [m]);
|
||||
|
||||
return props.children ? (
|
||||
<Tooltip placement="top" title={m.format("MM/DD/YYY hh:mm A")}>
|
||||
{m.fromNow()}
|
||||
<Tooltip placement="top" title={m.format("MM/DD/YYYY hh:mm:ss A")}>
|
||||
{timestampString}
|
||||
</Tooltip>
|
||||
) : null;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
selectWatcherStatus,
|
||||
} from "../../../redux/application/application.selectors";
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
watcherStatus: selectWatcherStatus,
|
||||
watcherError: selectWatcherError,
|
||||
@@ -14,7 +16,10 @@ const mapStateToProps = createStructuredSelector({
|
||||
|
||||
export function WatcherStatusAtom({ watcherStatus, watcherError }) {
|
||||
return (
|
||||
<div style={{ color: watcherStatus === "Started" ? "green" : "tomato" }}>
|
||||
<div
|
||||
onDoubleClick={() => ipcRenderer.send("test")}
|
||||
style={{ color: watcherStatus === "Started" ? "green" : "tomato" }}
|
||||
>
|
||||
<strong>{watcherStatus}</strong>
|
||||
{watcherError && <Alert message={watcherError} />}
|
||||
</div>
|
||||
|
||||
53
src/components/molecules/job-group/job-group.molecule.jsx
Normal file
53
src/components/molecules/job-group/job-group.molecule.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { DownOutlined, LoadingOutlined } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Dropdown, Menu, message } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { UPDATE_JOB } from "../../../graphql/jobs.queries";
|
||||
import { selectBodyshop } from "../../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobGroupMolecule);
|
||||
|
||||
export function JobGroupMolecule({ bodyshop, jobId, group }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [updateJob] = useMutation(UPDATE_JOB);
|
||||
|
||||
const handleMenuClick = async (value) => {
|
||||
setLoading(true);
|
||||
const result = await updateJob({
|
||||
variables: { jobId: jobId, job: { group: value.key } },
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
message.success("Close date updated.");
|
||||
} else {
|
||||
message.error("Error updating job.");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<Menu onClick={handleMenuClick}>
|
||||
{bodyshop.groups.map((g, idx) => (
|
||||
<Menu.Item key={g}>{g}</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown overlay={menu} trigger={["click"]}>
|
||||
<a href=" #" onClick={(e) => e.preventDefault()}>
|
||||
{group}
|
||||
<DownOutlined />
|
||||
{loading && <LoadingOutlined />}
|
||||
</a>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -2,8 +2,9 @@ import { Descriptions, PageHeader, Skeleton } from "antd";
|
||||
import React from "react";
|
||||
import CurrencyFormatterAtom from "../../atoms/currency-formatter/currency-formatter.atom";
|
||||
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
|
||||
import CloseDateDisplayMolecule from "../close-date-display/close-date-display.molecule";
|
||||
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";
|
||||
|
||||
export default function JobsDetailDescriptionMolecule({ loading, job }) {
|
||||
if (loading) return <Skeleton active />;
|
||||
@@ -15,11 +16,13 @@ export default function JobsDetailDescriptionMolecule({ loading, job }) {
|
||||
<PageHeader ghost={false} title={job.clm_no} subTitle={job.ins_co_nm}>
|
||||
<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}`}</Descriptions.Item>
|
||||
<Descriptions.Item label="Vehicle">{`${job.v_model_yr} ${job.v_makedesc} ${job.v_model} (${job.v_type})`}</Descriptions.Item>
|
||||
<Descriptions.Item label="Claim Total">
|
||||
<CurrencyFormatterAtom>{job.clm_total}</CurrencyFormatterAtom>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Group">{job.group}</Descriptions.Item>
|
||||
<Descriptions.Item label="Group">
|
||||
<JobGroupMolecule jobId={job.id} group={job.group} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Age">{job.v_age}</Descriptions.Item>
|
||||
<Descriptions.Item label="Close Date">
|
||||
<CloseDateDisplayMolecule
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Input, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { alphaSort } from "../../../util/sorters";
|
||||
import CurrencyFormatterAtom from "../../atoms/currency-formatter/currency-formatter.atom";
|
||||
import IgnoreJobLine from "../../atoms/ignore-job-line/ignore-job-line.atom";
|
||||
import partTypeConverterAtom from "../../atoms/part-type-converter/part-type-converter.atom";
|
||||
@@ -14,33 +15,38 @@ export default function JobLinesTableMolecule({ loading, job }) {
|
||||
title: "#",
|
||||
dataIndex: "line_no",
|
||||
key: "line_no",
|
||||
sorter: (a, b) => a.line_no - b.line_no,
|
||||
},
|
||||
{
|
||||
title: "S#",
|
||||
dataIndex: "line_ind",
|
||||
key: "line_ind",
|
||||
sorter: (a, b) => alphaSort(a.line_ind, b.line_ind),
|
||||
},
|
||||
{
|
||||
title: "Line Description",
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
},
|
||||
{
|
||||
title: "Part Type",
|
||||
dataIndex: "part_type",
|
||||
key: "part_type",
|
||||
sorter: (a, b) => alphaSort(a.part_type, b.part_type),
|
||||
render: (text, record) => partTypeConverterAtom(text),
|
||||
},
|
||||
{
|
||||
title: "Part Number",
|
||||
dataIndex: "oem_partno",
|
||||
key: "oem_partno",
|
||||
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
|
||||
},
|
||||
{
|
||||
title: "Database Price",
|
||||
dataIndex: "db_price",
|
||||
key: "db_price",
|
||||
|
||||
sorter: (a, b) => a.db_price - b.db_price,
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatterAtom>{record.db_price}</CurrencyFormatterAtom>
|
||||
),
|
||||
@@ -49,7 +55,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
|
||||
title: "Actual Price",
|
||||
dataIndex: "act_price",
|
||||
key: "act_price",
|
||||
|
||||
sorter: (a, b) => a.act_price - b.act_price,
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatterAtom>{record.act_price}</CurrencyFormatterAtom>
|
||||
),
|
||||
@@ -58,7 +64,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
|
||||
title: "Price Diff.",
|
||||
dataIndex: "price_diff",
|
||||
key: "price_diff",
|
||||
|
||||
sorter: (a, b) => a.price_diff - b.price_diff,
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatterAtom>{record.price_diff}</CurrencyFormatterAtom>
|
||||
),
|
||||
@@ -67,7 +73,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
|
||||
title: "Price Diff. %",
|
||||
dataIndex: "price_diff_pc",
|
||||
key: "price_diff_pc",
|
||||
|
||||
sorter: (a, b) => a.price_diff_pc - b.price_diff_pc,
|
||||
render: (text, record) => (
|
||||
<PriceDiffPcFormatterAtom
|
||||
price_diff_pc={record.price_diff_pc}
|
||||
|
||||
@@ -25,16 +25,18 @@ export function JobsTargetsStatsMolecule({
|
||||
job,
|
||||
selectedJobTargetPc,
|
||||
}) {
|
||||
const currentRpsDollars = useCallback(CalculateJobRpsDollars(job), [job]);
|
||||
const { actPriceSum, jobRpsDollars } = useCallback(
|
||||
CalculateJobRpsDollars(job, true),
|
||||
[job]
|
||||
);
|
||||
|
||||
const currentRpsPc = useCallback(CalculateJobRpsPc(job, currentRpsDollars), [
|
||||
job,
|
||||
currentRpsDollars,
|
||||
]);
|
||||
const { dbPriceSum, jobRpsPc } = useCallback(
|
||||
CalculateJobRpsPc(job, jobRpsDollars, true),
|
||||
[job, jobRpsDollars]
|
||||
);
|
||||
|
||||
if (loading) return <Skeleton active />;
|
||||
if (!job) return <ErrorResultAtom title="Error displaying job data." />;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -53,12 +55,18 @@ export function JobsTargetsStatsMolecule({
|
||||
<Statistic
|
||||
title="Current RPS %"
|
||||
valueStyle={{
|
||||
color: selectedJobTargetPc > currentRpsPc ? "tomato" : "seagreen",
|
||||
color: selectedJobTargetPc > (jobRpsPc || 0) ? "tomato" : "seagreen",
|
||||
}}
|
||||
value={(currentRpsPc * 100).toFixed(1)}
|
||||
value={((jobRpsPc || 0) * 100).toFixed(1)}
|
||||
suffix="%"
|
||||
/>
|
||||
<Statistic title="Current RPS $" value={currentRpsDollars.toFormat()} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Input, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setSelectedJobId } from "../../../redux/application/application.actions";
|
||||
import {
|
||||
selectReportData,
|
||||
selectReportLoading,
|
||||
} from "../../../redux/reporting/reporting.selectors";
|
||||
import { setSelectedJobId } from "../../../redux/application/application.actions";
|
||||
import { Link } from "react-router-dom";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
reportingLoading: selectReportLoading,
|
||||
reportData: selectReportData,
|
||||
@@ -108,9 +108,11 @@ export function ReportingJobsListMolecule({
|
||||
searchText !== ""
|
||||
? reportData.filter(
|
||||
(j) =>
|
||||
j.v_makedesc.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
j.v_model.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
j.ownr_fn.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
j.ownr_ln.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
j.ownr_clm_no.toLowerCase().includes(searchText.toLowerCase())
|
||||
j.clm_no.toLowerCase().includes(searchText.toLowerCase())
|
||||
)
|
||||
: reportData;
|
||||
|
||||
|
||||
@@ -55,16 +55,16 @@ export function ReportingTotalsStatsMolecule({ reportingLoading, scoreCard }) {
|
||||
title="Current RPS %"
|
||||
valueStyle={{
|
||||
color:
|
||||
scoreCard.currentRpsPc <= scoreCard.targetRpsPc
|
||||
(scoreCard.currentRpsPc || 0) <= (scoreCard.targetRpsPc || 0)
|
||||
? "tomato"
|
||||
: "seagreen",
|
||||
}}
|
||||
value={(scoreCard.currentRpsPc * 100).toFixed(1)}
|
||||
value={((scoreCard.currentRpsPc || 0) * 100).toFixed(1)}
|
||||
suffix="%"
|
||||
/>
|
||||
<Statistic
|
||||
title="Target RPS %"
|
||||
value={(scoreCard.targetRpsPc * 100).toFixed(1)}
|
||||
value={((scoreCard.targetRpsPc || 0) * 100).toFixed(1)}
|
||||
suffix="%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Button, Input, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import ipcTypes from "../../../ipc.types";
|
||||
import {
|
||||
selectScanEstimates,
|
||||
selectScanLoading,
|
||||
} from "../../../redux/scan/scan.selectors";
|
||||
import { alphaSort } from "../../../util/sorters";
|
||||
import LastScannedAtom from "../../atoms/last-scanned/last-scanned.atom";
|
||||
import ScanRefreshAtom from "../../atoms/scan-refresh/scan-refresh.atom";
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
scanLoading: selectScanLoading,
|
||||
estimates: selectScanEstimates,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export function ScanEstimateListMolecule({ scanLoading, estimates }) {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "Claim No.",
|
||||
dataIndex: "clm_no",
|
||||
key: "clm_no",
|
||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
},
|
||||
{
|
||||
title: "Ins Co.",
|
||||
dataIndex: "ins_co_nm",
|
||||
key: "ins_co_nm",
|
||||
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
|
||||
},
|
||||
{
|
||||
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",
|
||||
key: "vehicle",
|
||||
render: (text, record) =>
|
||||
`${record.v_model_yr} ${record.v_makedesc} ${record.v_model} (${record.v_type})`,
|
||||
},
|
||||
{
|
||||
title: "File Path",
|
||||
dataIndex: "filepath",
|
||||
key: "filepath",
|
||||
},
|
||||
{
|
||||
title: "Import",
|
||||
dataIndex: "import",
|
||||
key: "import",
|
||||
render: (text, record) => (
|
||||
<Button
|
||||
onClick={() =>
|
||||
ipcRenderer.send(
|
||||
ipcTypes.default.fileScan.toMain.importJob,
|
||||
record.filepath
|
||||
)
|
||||
}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const data =
|
||||
searchText !== ""
|
||||
? estimates.filter(
|
||||
(j) =>
|
||||
j.v_makedesc.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
j.v_model.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
j.ownr_fn.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
j.ownr_ln.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
j.clm_no.toLowerCase().includes(searchText.toLowerCase())
|
||||
)
|
||||
: estimates;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table
|
||||
title={() => (
|
||||
<div className="imex-table-header">
|
||||
<ScanRefreshAtom />
|
||||
<LastScannedAtom />
|
||||
<Input.Search
|
||||
className="imex-table-header__search"
|
||||
placeholder="Search"
|
||||
onSearch={(val) => {
|
||||
setSearchText(val);
|
||||
}}
|
||||
enterButton
|
||||
allowClear
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
columns={columns}
|
||||
rowKey="filepath"
|
||||
loading={scanLoading}
|
||||
size="small"
|
||||
pagination={false}
|
||||
dataSource={data}
|
||||
scroll={{
|
||||
x: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ScanEstimateListMolecule);
|
||||
@@ -1,10 +1,18 @@
|
||||
import { Button, Input, Form, Select, InputNumber, Typography } from "antd";
|
||||
import FormListMoveArrows from "../../atoms/form-list-move-arrows/form-list-move-arrows.atom";
|
||||
import React from "react";
|
||||
import LayoutFormRow from "../../atoms/layout-form-row/layout-form-row.atom";
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { Button, Form, Input, InputNumber, Select, Typography } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import FormListMoveArrows from "../../atoms/form-list-move-arrows/form-list-move-arrows.atom";
|
||||
import LayoutFormRow from "../../atoms/layout-form-row/layout-form-row.atom";
|
||||
|
||||
export default function ShopSettingsFormMolecule({ form, saveLoading }) {
|
||||
const [groupOptions, setGroupOptions] = useState(
|
||||
form.getFieldValue("groups") || []
|
||||
);
|
||||
const handleBlur = () => {
|
||||
console.log(form.getFieldValue("groups") || []);
|
||||
setGroupOptions(form.getFieldValue("groups") || []);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title>Shop Settings</Typography.Title>
|
||||
@@ -40,6 +48,19 @@ export default function ShopSettingsFormMolecule({ form, saveLoading }) {
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
label="Available Groupings (Must match below)"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
|
||||
type: "array",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" onBlur={handleBlur} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Typography.Title level={4}>Group Definitions</Typography.Title>
|
||||
<Form.List name={["targets"]}>
|
||||
@@ -59,7 +80,13 @@ export default function ShopSettingsFormMolecule({ form, saveLoading }) {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
<Select>
|
||||
{groupOptions.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 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";
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
SettingFilled,
|
||||
CloseOutlined,
|
||||
BarChartOutlined,
|
||||
FileAddFilled,
|
||||
} from "@ant-design/icons";
|
||||
import { Menu } from "antd";
|
||||
import React from "react";
|
||||
@@ -20,6 +21,9 @@ export default function SiderMenuOrganism() {
|
||||
<Menu.Item key="/" icon={<PieChartOutlined />}>
|
||||
<Link to="/">Jobs</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="/scan" icon={<FileAddFilled />}>
|
||||
<Link to="/scan">File Scan</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="/reporting" icon={<BarChartOutlined />}>
|
||||
<Link to="/reporting">Reporting</Link>
|
||||
</Menu.Item>
|
||||
|
||||
@@ -8,6 +8,7 @@ import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
|
||||
import SiderMenuOrganism from "../../organisms/sider-menu/sider-menu.organism";
|
||||
import JobsPage from "../jobs/jobs.page";
|
||||
import ReportingPage from "../reporting/reporting.page";
|
||||
import ScanPage from "../scan/scan.page";
|
||||
import SettingsPage from "../settings/settings.page";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop });
|
||||
@@ -35,6 +36,7 @@ export function RoutesPage({ bodyshop }) {
|
||||
<Layout.Content style={{ margin: "1rem", height: "100%" }}>
|
||||
<Route exact path="/settings" component={SettingsPage} />
|
||||
<Route exact path="/reporting" component={ReportingPage} />
|
||||
<Route exact path="/scan" component={ScanPage} />
|
||||
<Route exact path="/" component={JobsPage} />
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
|
||||
10
src/components/pages/scan/scan.page.jsx
Normal file
10
src/components/pages/scan/scan.page.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import ScanEstimateListMolecule from "../../molecules/scan-estimate-list/scan-estimate-list.molecule";
|
||||
|
||||
export default function ScanPage() {
|
||||
return (
|
||||
<div>
|
||||
<ScanEstimateListMolecule />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,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 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";
|
||||
@@ -21,6 +22,7 @@ export default function SettingsPage() {
|
||||
<Col span={6}>
|
||||
<WatcherManagerOrganism />
|
||||
<WatcherPollingMolecule />
|
||||
<NotificationsToggleAtom />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@ export const QUERY_BODYSHOP = gql`
|
||||
shopname
|
||||
targets
|
||||
accepted_ins_co
|
||||
groups
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_SHOP = gql`
|
||||
mutation UPDATE_SHOP($id: uuid, $shop: bodyshops_set_input!) {
|
||||
update_bodyshops(where: { id: { _eq: $id } }, _set: $shop) {
|
||||
@@ -17,6 +19,7 @@ export const UPDATE_SHOP = gql`
|
||||
shopname
|
||||
targets
|
||||
accepted_ins_co
|
||||
groups
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ export const QUERY_JOB_BY_PK = gql`
|
||||
updated_at
|
||||
group
|
||||
v_age
|
||||
v_type
|
||||
loss_date
|
||||
close_date
|
||||
updated_at
|
||||
|
||||
@@ -15,6 +15,15 @@ exports.default = {
|
||||
set: "store_set",
|
||||
response: "store_response",
|
||||
},
|
||||
fileScan: {
|
||||
toMain: {
|
||||
scanFilePaths: "fileScan__scanFilePaths",
|
||||
importJob: "fileScan__importJob",
|
||||
},
|
||||
toRenderer: {
|
||||
scanFilePathsResponse: "fileScan__scanFilePathsResponse",
|
||||
},
|
||||
},
|
||||
fileWatcher: {
|
||||
toMain: {
|
||||
filepathsGet: "filewatcher__filepathsget",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import gql from "graphql-tag";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import client from "../graphql/GraphQLClient";
|
||||
import {
|
||||
INSERT_NEW_JOB,
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
} from "../graphql/jobs.queries";
|
||||
import { QUERY_GROUPS_BY_MAKE_TYPE } from "../graphql/veh_group.queries";
|
||||
import { store } from "../redux/store";
|
||||
import moment from "moment";
|
||||
const { logger } = window;
|
||||
|
||||
export async function UpsertEstimate(job) {
|
||||
@@ -76,8 +76,8 @@ export async function UpsertEstimate(job) {
|
||||
export const GetSupplementDelta = async (jobId, existingLinesO, newLines) => {
|
||||
const existingLines = _.cloneDeep(existingLinesO);
|
||||
|
||||
console.log("GetSupplementDelta -> newLines", newLines);
|
||||
console.log("GetSupplementDelta -> existingLines", existingLines);
|
||||
//console.log("GetSupplementDelta -> newLines", newLines);
|
||||
//console.log("GetSupplementDelta -> existingLines", existingLines);
|
||||
const linesToInsert = [];
|
||||
const linesToUpdate = [];
|
||||
|
||||
@@ -89,7 +89,10 @@ export const GetSupplementDelta = async (jobId, existingLinesO, newLines) => {
|
||||
//Found a relevant matching line. Add it to lines to update.
|
||||
linesToUpdate.push({
|
||||
id: existingLines[matchingIndex].id,
|
||||
newData: newLine,
|
||||
newData: {
|
||||
...newLine,
|
||||
ignore: existingLines[matchingIndex].ignore,
|
||||
},
|
||||
});
|
||||
//Splice out item we found for performance.
|
||||
existingLines.splice(matchingIndex, 1);
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "../redux/application/application.actions";
|
||||
import { store } from "../redux/store";
|
||||
import { UpsertEstimate } from "./ipc-estimate-utils";
|
||||
|
||||
import { setScanEstimateList } from "../redux/scan/scan.actions";
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
console.log("----Initializing IPC Listeners in React App.");
|
||||
@@ -57,3 +57,11 @@ ipcRenderer.on(
|
||||
ipcRenderer.on(ipcTypes.default.store.response, (event, obj) => {
|
||||
store.dispatch(setSettings(obj));
|
||||
});
|
||||
|
||||
//FileScan Section
|
||||
ipcRenderer.on(
|
||||
ipcTypes.default.fileScan.toRenderer.scanFilePathsResponse,
|
||||
async (event, listOfEstimates) => {
|
||||
store.dispatch(setScanEstimateList(listOfEstimates));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ const INITIAL_STATE = {
|
||||
watchedPaths: [],
|
||||
watcherError: null,
|
||||
selectedJobId: null,
|
||||
selectedJobTargetPc: 100,
|
||||
selectedJobTargetPc: 0,
|
||||
settings: {},
|
||||
};
|
||||
|
||||
|
||||
@@ -4,17 +4,19 @@ import storage from "redux-persist/lib/storage";
|
||||
import applicationReducer from "./application/application.reducer";
|
||||
import userReducer from "./user/user.reducer";
|
||||
import reportingReducer from "./reporting/reporting.reducer";
|
||||
import scanReducer from './scan/scan.reducer'
|
||||
|
||||
const persistConfig = {
|
||||
key: "root",
|
||||
storage,
|
||||
blacklist: ["application", "user", "reporting"],
|
||||
blacklist: ["application", "user", "reporting", "scan"],
|
||||
};
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
application: applicationReducer,
|
||||
user: userReducer,
|
||||
reporting: reportingReducer,
|
||||
scan: scanReducer
|
||||
});
|
||||
|
||||
export default persistReducer(persistConfig, rootReducer);
|
||||
|
||||
@@ -2,6 +2,12 @@ import { all, call } from "redux-saga/effects";
|
||||
import { applicationSagas } from "./application/application.sagas";
|
||||
import { userSagas } from "./user/user.sagas";
|
||||
import { reportingSagas } from "./reporting/reporting.sagas";
|
||||
import { scanSagas } from "./scan/scan.sagas";
|
||||
export default function* rootSaga() {
|
||||
yield all([call(applicationSagas), call(userSagas), call(reportingSagas)]);
|
||||
yield all([
|
||||
call(applicationSagas),
|
||||
call(userSagas),
|
||||
call(reportingSagas),
|
||||
call(scanSagas),
|
||||
]);
|
||||
}
|
||||
|
||||
18
src/redux/scan/scan.actions.js
Normal file
18
src/redux/scan/scan.actions.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import ScanActionTypes from "./scan.types";
|
||||
|
||||
export const setScanLoading = () => ({
|
||||
type: ScanActionTypes.SET_SCAN_LOADING,
|
||||
});
|
||||
|
||||
export const setScanEstimateList = (listOfEstimates) => ({
|
||||
type: ScanActionTypes.SET_LIST_OF_ESTIMATES,
|
||||
payload: listOfEstimates,
|
||||
});
|
||||
|
||||
export const clearScanEstimateList = () => ({
|
||||
type: ScanActionTypes.CLEAR_LIST_OF_ESTIMATES,
|
||||
});
|
||||
|
||||
export const scanStart = () => ({
|
||||
type: ScanActionTypes.SCAN_START,
|
||||
});
|
||||
27
src/redux/scan/scan.reducer.js
Normal file
27
src/redux/scan/scan.reducer.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import ScanActionTypes from "./scan.types";
|
||||
|
||||
const INITIAL_STATE = {
|
||||
loading: false,
|
||||
lastScanned: null,
|
||||
estimates: [],
|
||||
};
|
||||
|
||||
const applicationReducer = (state = INITIAL_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case ScanActionTypes.SET_LIST_OF_ESTIMATES:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
estimates: action.payload,
|
||||
lastScanned: new Date(),
|
||||
};
|
||||
case ScanActionTypes.SCAN_START:
|
||||
return { ...state, loading: true };
|
||||
case ScanActionTypes.CLEAR_LIST_OF_ESTIMATES:
|
||||
return { ...state, estimates: [], lastScanned: null };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default applicationReducer;
|
||||
18
src/redux/scan/scan.sagas.js
Normal file
18
src/redux/scan/scan.sagas.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { all, call, takeLatest } from "redux-saga/effects";
|
||||
import ipcTypes from "../../ipc.types";
|
||||
import ScanActionTypes from "./scan.types";
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
export function* onScanStart() {
|
||||
yield takeLatest(ScanActionTypes.SCAN_START, handleScanStart);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-yield
|
||||
export function* handleScanStart() {
|
||||
ipcRenderer.send(ipcTypes.default.fileScan.toMain.scanFilePaths);
|
||||
}
|
||||
|
||||
export function* scanSagas() {
|
||||
yield all([call(onScanStart)]);
|
||||
}
|
||||
17
src/redux/scan/scan.selectors.js
Normal file
17
src/redux/scan/scan.selectors.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createSelector } from "reselect";
|
||||
|
||||
const selectScan = (state) => state.scan;
|
||||
|
||||
export const selectScanLoading = createSelector(
|
||||
[selectScan],
|
||||
(scan) => scan.loading
|
||||
);
|
||||
export const selectScanEstimates = createSelector(
|
||||
[selectScan],
|
||||
(scan) => scan.estimates
|
||||
);
|
||||
|
||||
export const selectScanLastScanned = createSelector(
|
||||
[selectScan],
|
||||
(scan) => scan.lastScanned
|
||||
);
|
||||
7
src/redux/scan/scan.types.js
Normal file
7
src/redux/scan/scan.types.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const ScanActionTypes = {
|
||||
SET_LIST_OF_ESTIMATES: "SET_LIST_OF_ESTIMATES",
|
||||
SET_SCAN_LOADING: "SET_SCAN_LOADING",
|
||||
CLEAR_LIST_OF_ESTIMATES: "CLEAR_LIST_OF_ESTIMATES",
|
||||
SCAN_START: "SCAN_START",
|
||||
};
|
||||
export default ScanActionTypes;
|
||||
12
src/util/sorters.js
Normal file
12
src/util/sorters.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export function alphaSort(a, b) {
|
||||
let A;
|
||||
let B;
|
||||
|
||||
A = a ? a.toLowerCase() : "";
|
||||
B = b ? b.toLowerCase() : "";
|
||||
if (A < B)
|
||||
//sort string ascending
|
||||
return -1;
|
||||
if (A > B) return 1;
|
||||
return 0; //default return value (no sorting)
|
||||
}
|
||||
Reference in New Issue
Block a user