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