diff --git a/electron/decoder/decoder.js b/electron/decoder/decoder.js
index 2bd8a2f..597eb57 100644
--- a/electron/decoder/decoder.js
+++ b/electron/decoder/decoder.js
@@ -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;
diff --git a/electron/file-scan/file-scan-ipc.js b/electron/file-scan/file-scan-ipc.js
new file mode 100644
index 0000000..4f5d84a
--- /dev/null
+++ b/electron/file-scan/file-scan-ipc.js
@@ -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);
+ }
+);
diff --git a/electron/file-scan/file-scan.js b/electron/file-scan/file-scan.js
new file mode 100644
index 0000000..01e65e1
--- /dev/null
+++ b/electron/file-scan/file-scan.js
@@ -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;
diff --git a/electron/file-watcher/file-watcher.js b/electron/file-watcher/file-watcher.js
index 9331c3d..ff7866d 100644
--- a/electron/file-watcher/file-watcher.js
+++ b/electron/file-watcher/file-watcher.js
@@ -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("^.*(?", 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);
}
diff --git a/electron/ipc-main-handler.js b/electron/ipc-main-handler.js
index 2fa6e25..54f9858 100644
--- a/electron/ipc-main-handler.js
+++ b/electron/ipc-main-handler.js
@@ -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 });
});
diff --git a/src/App/App.styles.scss b/src/App/App.styles.scss
index 7a09eba..e7a0b20 100644
--- a/src/App/App.styles.scss
+++ b/src/App/App.styles.scss
@@ -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%;
diff --git a/src/components/atoms/last-scanned/last-scanned.atom.jsx b/src/components/atoms/last-scanned/last-scanned.atom.jsx
new file mode 100644
index 0000000..61e7457
--- /dev/null
+++ b/src/components/atoms/last-scanned/last-scanned.atom.jsx
@@ -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 && (
+
+ Last scanned
+ {lastScanned}
+
+ )
+ );
+}
+export default connect(mapStateToProps, mapDispatchToProps)(LastScannedAtom);
diff --git a/src/components/atoms/scan-refresh/scan-refresh.atom.jsx b/src/components/atoms/scan-refresh/scan-refresh.atom.jsx
new file mode 100644
index 0000000..b43ff03
--- /dev/null
+++ b/src/components/atoms/scan-refresh/scan-refresh.atom.jsx
@@ -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 (
+
+ );
+}
+export default connect(mapStateToProps, mapDispatchToProps)(ScanRefreshAtom);
diff --git a/src/components/atoms/time-ago-formatter/time-ago-formatter.atom.jsx b/src/components/atoms/time-ago-formatter/time-ago-formatter.atom.jsx
index 87e874b..d7d5217 100644
--- a/src/components/atoms/time-ago-formatter/time-ago-formatter.atom.jsx
+++ b/src/components/atoms/time-ago-formatter/time-ago-formatter.atom.jsx
@@ -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 ? (
- {m.fromNow()}
+ {timestampString}
) : null;
}
diff --git a/src/components/molecules/reporting-jobs-list/reporting-jobs-list.molecule.jsx b/src/components/molecules/reporting-jobs-list/reporting-jobs-list.molecule.jsx
index 9e3fa8c..16fa7ca 100644
--- a/src/components/molecules/reporting-jobs-list/reporting-jobs-list.molecule.jsx
+++ b/src/components/molecules/reporting-jobs-list/reporting-jobs-list.molecule.jsx
@@ -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;
diff --git a/src/components/molecules/scan-estimate-list/scan-estimate-list.molecule.jsx b/src/components/molecules/scan-estimate-list/scan-estimate-list.molecule.jsx
new file mode 100644
index 0000000..f86f68e
--- /dev/null
+++ b/src/components/molecules/scan-estimate-list/scan-estimate-list.molecule.jsx
@@ -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) => (
+
+ ),
+ },
+ ];
+
+ 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 (
+
+
(
+
+
+
+ {
+ setSearchText(val);
+ }}
+ enterButton
+ allowClear
+ />
+
+ )}
+ columns={columns}
+ rowKey="filepath"
+ loading={scanLoading}
+ size="small"
+ pagination={false}
+ dataSource={data}
+ scroll={{
+ x: true,
+ }}
+ />
+
+ );
+}
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(ScanEstimateListMolecule);
diff --git a/src/components/organisms/sider-menu/sider-menu.organism.jsx b/src/components/organisms/sider-menu/sider-menu.organism.jsx
index e82410e..1bd3ef1 100644
--- a/src/components/organisms/sider-menu/sider-menu.organism.jsx
+++ b/src/components/organisms/sider-menu/sider-menu.organism.jsx
@@ -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() {
}>
Jobs
+ }>
+ File Scan
+
}>
Reporting
diff --git a/src/components/pages/routes/routes.page.jsx b/src/components/pages/routes/routes.page.jsx
index 1f523b6..94a5c5b 100644
--- a/src/components/pages/routes/routes.page.jsx
+++ b/src/components/pages/routes/routes.page.jsx
@@ -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 }) {
+
diff --git a/src/components/pages/scan/scan.page.jsx b/src/components/pages/scan/scan.page.jsx
new file mode 100644
index 0000000..60eeb46
--- /dev/null
+++ b/src/components/pages/scan/scan.page.jsx
@@ -0,0 +1,10 @@
+import React from "react";
+import ScanEstimateListMolecule from "../../molecules/scan-estimate-list/scan-estimate-list.molecule";
+
+export default function ScanPage() {
+ return (
+
+
+
+ );
+}
diff --git a/src/ipc.types.js b/src/ipc.types.js
index 27ceace..6ee0fa0 100644
--- a/src/ipc.types.js
+++ b/src/ipc.types.js
@@ -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",
diff --git a/src/ipc/ipc-estimate-utils.js b/src/ipc/ipc-estimate-utils.js
index 8b684e9..0de15fc 100644
--- a/src/ipc/ipc-estimate-utils.js
+++ b/src/ipc/ipc-estimate-utils.js
@@ -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 = [];
diff --git a/src/ipc/ipc-renderer-handler.js b/src/ipc/ipc-renderer-handler.js
index 6fecb22..b14dbdf 100644
--- a/src/ipc/ipc-renderer-handler.js
+++ b/src/ipc/ipc-renderer-handler.js
@@ -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));
+ }
+);
diff --git a/src/redux/root.reducer.js b/src/redux/root.reducer.js
index eaeda26..0a39bd0 100644
--- a/src/redux/root.reducer.js
+++ b/src/redux/root.reducer.js
@@ -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);
diff --git a/src/redux/root.saga.js b/src/redux/root.saga.js
index d348a07..5ce2551 100644
--- a/src/redux/root.saga.js
+++ b/src/redux/root.saga.js
@@ -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),
+ ]);
}
diff --git a/src/redux/scan/scan.actions.js b/src/redux/scan/scan.actions.js
new file mode 100644
index 0000000..5be37cd
--- /dev/null
+++ b/src/redux/scan/scan.actions.js
@@ -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,
+});
diff --git a/src/redux/scan/scan.reducer.js b/src/redux/scan/scan.reducer.js
new file mode 100644
index 0000000..ffd5948
--- /dev/null
+++ b/src/redux/scan/scan.reducer.js
@@ -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;
diff --git a/src/redux/scan/scan.sagas.js b/src/redux/scan/scan.sagas.js
new file mode 100644
index 0000000..4a0a134
--- /dev/null
+++ b/src/redux/scan/scan.sagas.js
@@ -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)]);
+}
diff --git a/src/redux/scan/scan.selectors.js b/src/redux/scan/scan.selectors.js
new file mode 100644
index 0000000..ed119fb
--- /dev/null
+++ b/src/redux/scan/scan.selectors.js
@@ -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
+);
diff --git a/src/redux/scan/scan.types.js b/src/redux/scan/scan.types.js
new file mode 100644
index 0000000..6f8c4f3
--- /dev/null
+++ b/src/redux/scan/scan.types.js
@@ -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;