Added file scanning module.

This commit is contained in:
Patrick Fic
2020-10-21 14:33:45 -07:00
parent ee3136a3ac
commit 34e244783c
24 changed files with 435 additions and 61 deletions

View File

@@ -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.",
}).show();
} else {
log.info(`Ignored job. ${newJob.ERROR}`);
NewNotification({
title: "Job Ignored",
body: newJob.ERROR,
}).show();
}
}
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);
@@ -297,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;
}
@@ -311,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;
}
@@ -344,3 +374,4 @@ async function DecodeLinFile(extensionlessFilePath) {
}
exports.DecodeEstimate = DecodeEstimate;
exports.ImportJob = ImportJob;

View 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);
}
);

View 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;

View File

@@ -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 {
@@ -36,16 +36,7 @@ async function StartWatcher() {
watcher = chokidar.watch(filePaths, {
ignored: (fp, stats) => {
const p = path.parse(fp);
log.log(
"Checking if should ignore.",
fp,
p,
p.ext !== "" && p.ext !== ".ENV" && p.ext !== ".env"
);
// prettier-ignore
// const ignore = RegExp("^.*(?<!\.env)(?<!\.ENV)$").test(fp);
//console.log("StartWatcher ->", fp, "Ignore?", ignore);
return (p.ext !== "" && p.ext !== ".ENV" && p.ext !== ".env" );
return p.ext !== "" && p.ext.toUpperCase() !== ".ENV";
},
usePolling: store.get("polling").enabled || false,
interval: store.get("polling").pollingInterval || 1000,
@@ -116,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);
}

View File

@@ -1,16 +1,14 @@
const { ipcMain } = require("electron");
const { default: ipcTypes } = require("../src/ipc.types");
const { store } = require("./electron-store");
const { watcher } = require("./file-watcher/file-watcher");
//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");
event.reply("test-toRenderer", { status: 0, message: null });
});

View File

@@ -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%;

View 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);

View 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);

View File

@@ -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()}
{timestampString}
</Tooltip>
) : null;
}

View File

@@ -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;

View File

@@ -0,0 +1,116 @@
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 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",
},
{
title: "Ins Co.",
dataIndex: "ins_co_nm",
key: "ins_co_nm",
},
{
title: "First Name",
dataIndex: "ownr_fn",
key: "ownr_fn",
},
{
title: "Last Name",
dataIndex: "ownr_ln",
key: "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: "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);

View File

@@ -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>

View File

@@ -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>

View 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>
);
}

View File

@@ -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",

View File

@@ -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 = [];

View File

@@ -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));
}
);

View File

@@ -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);

View File

@@ -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),
]);
}

View 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,
});

View 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;

View 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)]);
}

View 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
);

View 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;